a13f7ca6a6
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update all references in Makefile, manifest.xml, .gitignore, and CI workflows (ci-joomla, pr-check, repo-health) to use source/ as the primary directory with src/ as a fallback for compatibility. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
222 lines
5.3 KiB
PHP
222 lines
5.3 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoJoomBackup
|
|
* @subpackage com_mokobackup
|
|
* @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
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoBackup\Administrator\Engine;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
|
|
class DatabaseDumper
|
|
{
|
|
/** @var array Tables to exclude entirely (both structure and data) */
|
|
private array $excludeBoth = [];
|
|
|
|
/** @var array Tables to exclude data only (structure is kept) */
|
|
private array $excludeDataOnly = [];
|
|
|
|
/** @var array Tables to exclude structure only (data is kept — unusual) */
|
|
private array $excludeStructureOnly = [];
|
|
|
|
private int $tablesCount = 0;
|
|
|
|
/**
|
|
* @param array $excludeTables Table names to exclude (with #__ prefix).
|
|
* Supports suffixes: :data-only, :structure-only.
|
|
* No suffix = exclude both (backward compatible).
|
|
*/
|
|
public function __construct(array $excludeTables = [])
|
|
{
|
|
foreach ($excludeTables as $entry) {
|
|
if (str_ends_with($entry, ':data-only')) {
|
|
$this->excludeDataOnly[] = substr($entry, 0, -10);
|
|
} elseif (str_ends_with($entry, ':structure-only')) {
|
|
$this->excludeStructureOnly[] = substr($entry, 0, -15);
|
|
} else {
|
|
$this->excludeBoth[] = $entry;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dump all database tables to SQL.
|
|
*
|
|
* @return string The SQL dump
|
|
*/
|
|
public function dump(): string
|
|
{
|
|
$db = Factory::getDbo();
|
|
$prefix = $db->getPrefix();
|
|
$output = [];
|
|
|
|
$output[] = '-- MokoJoomBackup Database Dump';
|
|
$output[] = '-- Generated: ' . date('Y-m-d H:i:s');
|
|
$output[] = '-- Server: ' . $db->getServerType();
|
|
$output[] = '-- Database: ' . $db->getName();
|
|
$output[] = '-- Prefix: ' . $prefix;
|
|
$output[] = '';
|
|
$output[] = 'SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";';
|
|
$output[] = 'SET time_zone = "+00:00";';
|
|
$output[] = '';
|
|
|
|
// Get all tables with the site prefix
|
|
$tables = $db->getTableList();
|
|
$siteTables = [];
|
|
|
|
foreach ($tables as $table) {
|
|
if (str_starts_with($table, $prefix)) {
|
|
$siteTables[] = $table;
|
|
}
|
|
}
|
|
|
|
foreach ($siteTables as $table) {
|
|
// Check if excluded
|
|
$abstractName = '#__' . substr($table, strlen($prefix));
|
|
|
|
if ($this->isExcludedBoth($abstractName, $table)) {
|
|
continue;
|
|
}
|
|
|
|
$skipData = $this->isExcludedDataOnly($abstractName, $table);
|
|
$skipStructure = $this->isExcludedStructureOnly($abstractName, $table);
|
|
|
|
$this->tablesCount++;
|
|
|
|
$output[] = '-- --------------------------------------------------------';
|
|
$output[] = '-- Table: ' . $table;
|
|
|
|
if ($skipData) {
|
|
$output[] = '-- (data excluded)';
|
|
}
|
|
|
|
if ($skipStructure) {
|
|
$output[] = '-- (structure excluded)';
|
|
}
|
|
|
|
$output[] = '-- --------------------------------------------------------';
|
|
$output[] = '';
|
|
|
|
// Get CREATE TABLE statement (unless structure is excluded)
|
|
if (!$skipStructure) {
|
|
$db->setQuery('SHOW CREATE TABLE ' . $db->quoteName($table));
|
|
$createRow = $db->loadRow();
|
|
|
|
if (!$createRow || empty($createRow[1])) {
|
|
continue;
|
|
}
|
|
|
|
$output[] = 'DROP TABLE IF EXISTS ' . $db->quoteName($table) . ';';
|
|
$output[] = $createRow[1] . ';';
|
|
$output[] = '';
|
|
}
|
|
|
|
// Dump data (unless data is excluded)
|
|
if ($skipData) {
|
|
$output[] = '';
|
|
continue;
|
|
}
|
|
|
|
$db->setQuery('SELECT COUNT(*) FROM ' . $db->quoteName($table));
|
|
$rowCount = (int) $db->loadResult();
|
|
|
|
if ($rowCount === 0) {
|
|
$output[] = '-- (empty table)';
|
|
$output[] = '';
|
|
continue;
|
|
}
|
|
|
|
$chunkSize = 500;
|
|
|
|
for ($offset = 0; $offset < $rowCount; $offset += $chunkSize) {
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select('*')
|
|
->from($db->quoteName($table)),
|
|
$offset,
|
|
$chunkSize
|
|
);
|
|
$rows = $db->loadAssocList();
|
|
|
|
if (empty($rows)) {
|
|
break;
|
|
}
|
|
|
|
foreach ($rows as $row) {
|
|
$values = [];
|
|
|
|
foreach ($row as $value) {
|
|
if ($value === null) {
|
|
$values[] = 'NULL';
|
|
} else {
|
|
$values[] = $db->quote($value);
|
|
}
|
|
}
|
|
|
|
$columns = array_map([$db, 'quoteName'], array_keys($row));
|
|
$output[] = 'INSERT INTO ' . $db->quoteName($table)
|
|
. ' (' . implode(', ', $columns) . ')'
|
|
. ' VALUES (' . implode(', ', $values) . ');';
|
|
}
|
|
}
|
|
|
|
$output[] = '';
|
|
}
|
|
|
|
return implode("\n", $output);
|
|
}
|
|
|
|
/**
|
|
* Check if a table is fully excluded (both data and structure).
|
|
*/
|
|
private function isExcludedBoth(string $abstractName, string $realName): bool
|
|
{
|
|
foreach ($this->excludeBoth as $pattern) {
|
|
if ($pattern === $abstractName || $pattern === $realName) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a table's data is excluded (structure only).
|
|
*/
|
|
private function isExcludedDataOnly(string $abstractName, string $realName): bool
|
|
{
|
|
foreach ($this->excludeDataOnly as $pattern) {
|
|
if ($pattern === $abstractName || $pattern === $realName) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a table's structure is excluded (data only).
|
|
*/
|
|
private function isExcludedStructureOnly(string $abstractName, string $realName): bool
|
|
{
|
|
foreach ($this->excludeStructureOnly as $pattern) {
|
|
if ($pattern === $abstractName || $pattern === $realName) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function getTablesCount(): int
|
|
{
|
|
return $this->tablesCount;
|
|
}
|
|
}
|