99b844e04a
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 2s
Universal: PR Check / Require Docs Update (pull_request) Failing after 8s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 13s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 28s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 46s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 50s
Generic: Project CI / Tests (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Security (P0): - #169 mask FTP password in AjaxController maskSecrets()/mergeExistingSecrets() - #187 stop leaking absolute_path in API item view (JsonapiView) - #182 SftpUploader StrictHostKeyChecking=no -> accept-new (MITM) Data integrity / correctness (P1): - #179 DatabaseDumper: real newline after DROP TABLE (was literal backslash-n) - #180 profile-picker forms ORDER BY id (the 'ordering' column is dropped on upgraded sites, breaking the query) — run_profile.xml + plg_content xml - #181 uninstall.mysql.sql: add missing DROP for snapshots table - #184 CLI snapshot delete uses data_file column (file_path does not exist, so snapshot files were never deleted) - #186 remove duplicate pre-update backup: content plugin no longer subscribes to onExtensionBeforeUpdate (owned by the system plugin); keeps pre-install - #188 DatabaseImporter now collects non-fatal statement errors (getErrors()/hasErrors()) so restores are no longer silent and callers can surface a warning status Also folds in the .mokogitea/CLAUDE.md mokoplatform -> mokocli fix (stale repo name after the rename). Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
159 lines
3.8 KiB
PHP
159 lines
3.8 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteBackup
|
|
* @subpackage com_mokosuitebackup
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*
|
|
* Imports a SQL dump file created by DatabaseDumper.
|
|
* Handles #__ prefix replacement, multi-statement execution,
|
|
* and DROP TABLE before CREATE TABLE for clean restores.
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
|
|
class DatabaseImporter
|
|
{
|
|
/**
|
|
* Non-fatal per-statement errors collected during the last import().
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private array $errors = [];
|
|
|
|
/**
|
|
* Import a SQL dump file into the database.
|
|
*
|
|
* @param string $sqlFile Absolute path to the SQL dump file
|
|
*
|
|
* @return int Number of statements executed
|
|
*
|
|
* @throws \RuntimeException On import failure
|
|
*/
|
|
public function import(string $sqlFile): int
|
|
{
|
|
$this->errors = [];
|
|
|
|
if (!is_file($sqlFile) || !is_readable($sqlFile)) {
|
|
throw new \RuntimeException('SQL file not readable: ' . $sqlFile);
|
|
}
|
|
|
|
$db = Factory::getDbo();
|
|
$prefix = $db->getPrefix();
|
|
|
|
$handle = fopen($sqlFile, 'r');
|
|
|
|
if ($handle === false) {
|
|
throw new \RuntimeException('Cannot open SQL file: ' . $sqlFile);
|
|
}
|
|
|
|
$statementsExecuted = 0;
|
|
$currentStatement = '';
|
|
$inMultiLineComment = false;
|
|
|
|
try {
|
|
while (($line = fgets($handle)) !== false) {
|
|
$trimmed = trim($line);
|
|
|
|
// Skip empty lines
|
|
if ($trimmed === '') {
|
|
continue;
|
|
}
|
|
|
|
// Skip single-line comments
|
|
if (str_starts_with($trimmed, '--') || str_starts_with($trimmed, '#')) {
|
|
continue;
|
|
}
|
|
|
|
// Handle multi-line comments
|
|
if (str_starts_with($trimmed, '/*')) {
|
|
$inMultiLineComment = true;
|
|
}
|
|
|
|
if ($inMultiLineComment) {
|
|
if (str_contains($trimmed, '*/')) {
|
|
$inMultiLineComment = false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Accumulate the statement
|
|
$currentStatement .= $line;
|
|
|
|
// Check if statement is complete (ends with semicolon)
|
|
if (str_ends_with($trimmed, ';')) {
|
|
$statement = trim($currentStatement);
|
|
$currentStatement = '';
|
|
|
|
if (empty($statement)) {
|
|
continue;
|
|
}
|
|
|
|
// Replace abstract #__ prefix with the current site's prefix
|
|
$statement = str_replace('#__', $prefix, $statement);
|
|
|
|
try {
|
|
$db->setQuery($statement);
|
|
$db->execute();
|
|
$statementsExecuted++;
|
|
} catch (\Exception $e) {
|
|
// Log but don't abort — some statements may fail on
|
|
// different MySQL versions (e.g. charset differences)
|
|
// but the overall restore should continue. Errors are
|
|
// collected so the caller can surface a warning status.
|
|
error_log('MokoSuiteBackup SQL import warning: ' . $e->getMessage());
|
|
$this->errors[] = $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Execute any remaining statement without trailing semicolon
|
|
$remaining = trim($currentStatement);
|
|
|
|
if (!empty($remaining)) {
|
|
$remaining = str_replace('#__', $prefix, $remaining);
|
|
|
|
try {
|
|
$db->setQuery($remaining);
|
|
$db->execute();
|
|
$statementsExecuted++;
|
|
} catch (\Exception $e) {
|
|
error_log('MokoSuiteBackup SQL import warning (final): ' . $e->getMessage());
|
|
$this->errors[] = $e->getMessage();
|
|
}
|
|
}
|
|
} finally {
|
|
fclose($handle);
|
|
}
|
|
|
|
return $statementsExecuted;
|
|
}
|
|
|
|
/**
|
|
* Non-fatal errors from the last import(), if any. A non-empty result
|
|
* means the restore completed with problems and should be treated as a
|
|
* warning rather than a clean success.
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public function getErrors(): array
|
|
{
|
|
return $this->errors;
|
|
}
|
|
|
|
/**
|
|
* Whether the last import() had any non-fatal statement errors.
|
|
*/
|
|
public function hasErrors(): bool
|
|
{
|
|
return $this->errors !== [];
|
|
}
|
|
}
|