<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\library;
use seekquarry\yioop\configs as C;
/**
* Pure helper logic for importing an outside discussion board into a
* Yioop group or issue tracker. These are the parts that do no database or
* model work, kept apart from the model-driven import in GroupWikiTool so
* they can be tested on their own. For now they are the helpers for the
* MantisBT bug tracker: reading a mysqldump's INSERT rows, deciding whether
* a MantisBT username can be reused, building an issue's body and a
* fallback reporter's byline, turning a stored attachment back into its
* bytes, and writing the wiki references for a post's attachments. Helpers
* for other boards can join them here as they are added.
*
* @author Chris Pollett
*/
class DiscussionImport
{
/**
* Parses the rows of a mysqldump INSERT for one table into a list of
* value arrays. Works on the output of
* mysqldump --no-create-info, where each table's data arrives as one or
* more INSERT INTO `table` VALUES (..),(..); statements. Reads the values
* a character at a time so quoted strings that contain commas, parentheses,
* doubled '' quotes, or backslash escapes are kept whole. An unquoted NULL
* becomes a PHP null; every other value is returned as a string, positioned
* the same as its column in the table.
*
* @param string $sql the whole dump text
* @param string $table name of the table whose rows are wanted
* @return array a list of rows, each row a list of column values
*/
public static function parseInserts($sql, $table)
{
$rows = [];
$marker = "INSERT INTO `$table` VALUES ";
$length = strlen($sql);
$offset = 0;
while (($start = strpos($sql, $marker, $offset)) !== false) {
$pos = $start + strlen($marker);
$current = [];
$value = "";
$quoted = false;
$in_string = false;
while ($pos < $length) {
$char = $sql[$pos];
if ($in_string) {
if ($char === "\\" && $pos + 1 < $length) {
$escapes = ["n" => "\n", "r" => "\r", "t" => "\t",
"0" => "\0", "b" => "\x08", "Z" => "\x1a"];
$value .= $escapes[$sql[$pos + 1]] ?? $sql[$pos + 1];
$pos += 2;
continue;
}
if ($char === "'" && ($sql[$pos + 1] ?? "") === "'") {
$value .= "'";
$pos += 2;
continue;
}
if ($char === "'") {
$in_string = false;
$pos++;
continue;
}
$value .= $char;
$pos++;
continue;
}
if ($char === "'") {
$in_string = true;
$quoted = true;
$pos++;
continue;
}
if ($char === "(") {
$current = [];
$value = "";
$quoted = false;
$pos++;
continue;
}
if ($char === "," || $char === ")") {
if ($quoted) {
$current[] = $value;
} else {
$trimmed = trim($value);
$current[] = ($trimmed === "NULL") ? null : $trimmed;
}
$value = "";
$quoted = false;
if ($char === ")") {
$rows[] = $current;
$pos++;
while ($pos < $length &&
strpos(" \n\r\t", $sql[$pos]) !== false) {
$pos++;
}
if (($sql[$pos] ?? "") === ";") {
break;
}
if (($sql[$pos] ?? "") === ",") {
$pos++;
}
} else {
$pos++;
}
continue;
}
$value .= $char;
$pos++;
}
$offset = $pos;
}
return $rows;
}
/**
* Decides whether a MantisBT username can be reused as a Yioop username.
* A compatible name is non-empty, no longer than Yioop allows, and made
* only of letters, digits, and the few punctuation marks Yioop keeps in a
* name, so it can be stored and shown without cleaning changing it.
*
* @param string $username the MantisBT username to weigh
* @return bool whether Yioop can take the name as is
*/
public static function usernameCompatible($username)
{
$trimmed = trim($username);
if ($trimmed === "" || strlen($trimmed) > C\NAME_LEN) {
return false;
}
return preg_match('/^[a-zA-Z0-9_.\-]+$/', $trimmed) === 1;
}
/**
* Makes a readable byline for a MantisBT user who could not be given their
* own Yioop account, so their name and email are not lost when the shared
* bot-sender account owns their issue or comment. Prefers the real name,
* falls back to the username, and adds the email when there is one.
*
* @param array $mantis the MantisBT user's username, real name, and email
* @return string the byline, empty when nothing is known
*/
public static function fallbackByline($mantis)
{
$name = trim($mantis["realname"] ?? "");
if ($name === "") {
$name = trim($mantis["username"] ?? "");
}
$email = trim($mantis["email"] ?? "");
if ($name === "" && $email === "") {
return "";
}
if ($email === "") {
return $name;
}
return ($name !== "") ? "$name <$email>" : $email;
}
/**
* Builds the body text of an imported issue from the bug's original
* description, steps to reproduce, and additional information. When the
* reporter could not be given their own Yioop account, a short footer
* records who originally reported it so their name is not lost; nothing
* else is added, since the issue's own fields already carry its number,
* priority, and the rest.
*
* @param array $text the description, steps, and additional-information
* strings for the bug
* @param string $reporter_note byline for the original reporter, added
* only when they were folded onto the shared bot account; empty
* otherwise
* @return string the issue body
*/
public static function issueBody($text, $reporter_note)
{
$body = trim($text[0] ?? "");
if (trim($text[1] ?? "") !== "") {
$body .= "\n\nSteps to reproduce:\n" . trim($text[1]);
}
if (trim($text[2] ?? "") !== "") {
$body .= "\n\nAdditional information:\n" . trim($text[2]);
}
if ($reporter_note !== "") {
$body .= "\n\n----\nOriginally reported by " . $reporter_note . ".";
}
return $body;
}
/**
* Turns the stored bytes of a MantisBT attachment, as they appear in a
* mysqldump, back into the raw file. A dump made with the hex-blob option
* writes the bytes as a 0x-prefixed hexadecimal literal, which is decoded
* here; otherwise the value is already the raw bytes, unescaped by the
* INSERT parser, and is returned unchanged. An empty value, which happens
* for an attachment MantisBT kept on disk rather than in the database,
* yields an empty string.
*
* @param string $raw the content column's value from the dump
* @return string the attachment's bytes, empty when the dump holds none
*/
public static function fileContent($raw)
{
if ($raw === null || $raw === "") {
return "";
}
if (preg_match('/^0x[0-9a-fA-F]+$/', $raw)) {
return hex2bin(substr($raw, 2));
}
return $raw;
}
/**
* Builds the wiki resource references for a list of imported MantisBT
* files, one per file that has bytes, to add to a post's body so each file
* shows as a link or image when the post is read. This is done before the
* post is made, since the reference names the file, while the bytes are
* stored afterwards once the post has an id.
*
* @param array $files the file rows, each with a filename and bytes
* @return string the resource references, each on its own line
*/
public static function fileReferences($files)
{
$references = "";
foreach ($files as $file) {
if ($file["content"] === "") {
continue;
}
$references .= "\n\n((resource:" . $file["filename"] . "|" .
$file["filename"] . "))";
}
return $references;
}}