| Current Path : /var/www/html/libraries/noboss/src/Util/ |
| Current File : /var/www/html/libraries/noboss/src/Util/NbInstallScriptUtil.php |
<?php
/**
* @package No Boss Extensions
* @subpackage No Boss Library
* @author No Boss Technology <contact@nobosstechnology.com>
* @copyright Copyright (C) 2026 No Boss Technology. All rights reserved.
* @license GNU Lesser General Public License version 3 or later; see <https://www.gnu.org/licenses/lgpl-3.0.en.html>
*/
namespace Noboss\Library\Util;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Log\Log;
use Noboss\Library\Util\NbCurlUtil;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
class NbInstallScriptUtil{
/**
* Recebe o token do usuário e manda a url do site atual para o servidor da noboss
*
* @param String $token token da licença do usuário
* @return void
*/
public static function saveAuthorizedUrl($token){
$url = self::getUrlNbExtensions(). '/index.php?option=com_nbextensoes&task=externallicenses.changeAuthorizedUrl&format=raw';
// Dados a enviar na requisicao
$dataPost = array(
'token' => $token,
'newUrl' => (str_replace(array('https://www.', 'http://www.', 'https://', 'http://'), '', Uri::root())),
'overwrite' => false,
'jversion' => JVERSION
);
NbCurlUtil::request('POST', $url, $dataPost, null, 10, null, null);
}
/**
* Verifica se extensao pode ser atualizada a partir de um token
*
* @param String $token Token da extensao
* @return Mixed Boolean 1 se update estiver em dia, 0 se nao estiver em dia ou 'INVALID_TOKEN' se nao localizado
*/
public static function updateLicenseIsValid($token){
$url = self::getUrlNbExtensions(). 'index.php?option=com_nbextensoes&task=externallicenses.updateLicenseIsValid&format=raw';
$dataPost = array(
'token' => $token
);
// Faz uma requisição curl usando a library
$curlRequest = NbCurlUtil::request('POST', $url, $dataPost, null, 20);
if ($curlRequest->success) {
return $curlRequest->data;
}
return false;
}
/**
* Remove registro da url do site na base da No Boss
*
* @param String $token Token da extensao
* @return Boolean True ou false
*/
public static function removeUrlSite($token){
$url = self::getUrlNbExtensions(). 'index.php?option=com_nbextensoes&task=externallicenses.removeUrlSite&format=raw';
$dataPost = array(
'token' => $token,
'url' => base64_encode(str_replace(array('https://www.', 'http://www.', 'https://', 'http://'), '', Uri::root()))
);
$curlRequest = NbCurlUtil::request('POST', $url, $dataPost, null, 20);
if ($curlRequest->success) {
return $curlRequest->data;
}
return false;
}
/**
* Recebe o nome de extensão e o valor da coluna extra_query e atualiza
*
* @param String $extensionName Name da extensão
* @param String $extraQuery valor para a coluna extra_query
*/
public static function updateExtraQuery($parent, $extraQuery){
// Pega o nome e os servidores de update da extensão
$extName = $parent->getName();
$servers = (array) $parent->getManifest()->updateservers;
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true);
// Para cada servidor de update
foreach ($servers as $server) {
// Busca o id od servidor de update
$query = $db->getQuery(true);
$query
->select('update_site_id')
->from('#__update_sites')
->where("location = '{$server}' AND enabled = '1'");
$db->setQuery($query);
$id = $db->loadResult();
// Caso tenha encontrado, apenas faz update do extra_query
if(!empty($id)){
$query = $db->getQuery(true);
$query->update("#__update_sites")
->set("extra_query = '{$extraQuery}'")
->where("update_site_id = '{$id}'");
$db->setQuery($query);
$db->execute();
// Caso não exista no banco, insere um novo registro
} else {
$query = $db->getQuery(true);
$query->insert("#__update_sites")
->columns('name, type, location, enabled, extra_query')
->values("'{$extName}', 'extension', '{$server}', 1, '{$extraQuery}'");
$db->setQuery($query);
$db->execute();
}
}
}
/**
* Retorna o numero de extensões instaladas no site, não incluindo os extra.
*
* @param String Library name of XML
*
* @return Array Um array com as extensões dependentes da library
*/
public static function getDependencies($extraExtensions){
// Faz uma busca no banco, na tabela extensions
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true);
// Array para as buscas
$likeArray = array('com_noboss', 'plg_noboss', 'mod_noboss', 'tpl_noboss');
//Monta a query para buscar na tabella extension
$query->select("COUNT(1)")
->from("#__extensions")
->where("element NOT IN ('".implode("', '", $extraExtensions)."')");
$val = '(';
foreach($likeArray as $key => $like){
if($key != 0){
$val .= ' OR ';
}
$val .= "element LIKE '{$like}%'";
}
$val .= ')';
$query->where($val);
$db->setQuery($query);
$result = $db->loadResult();
return $result;
}
public static function getLibraryUpdateId($parent){
// Pega o alias da library
//$libElement = $parent->getElement();
// Faz uma busca no banco, na tabela extensions
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true);
//Monta a query para buscar na tabella extension
$query->select("b.update_id")
->from("#__extensions as a")
->join('INNER',"#__updates as b ON a.extension_id = b.extension_id")
->where("a.element = 'noboss'");
$db->setQuery($query);
$result = $db->loadResult();
return (int) $result;
}
/**
* Busca por todos os extras e desinstala um por um
*
* @return void
*/
public static function uninstallExtras($extraExtensions){
$extras = self::getExtrasList($extraExtensions);
// Para cada extra, executa o processo de desinstalção
foreach ($extras as $extra) {
$tmpInstaller = new Installer;
// Tenta fazer a desinstalação
try{
$result = $tmpInstaller->uninstall($extra->type, $extra->extension_id);
if(!$result){
throw new \RuntimeException();
}
} catch (\Exception $e){
// Verifica se existe a constante, caso não exista, monta um texto fixo
if($msg = Text::sprintf('SCRIPT_EXTRAS_UNINSTALL_ERROR', $extra->element) == 'SCRIPT_EXTRAS_UNINSTALL_ERROR'){
$msg = "<p>Houve um erro durante a desinstalação do pacote: {$extra->element}</p>";
}
Log::add($msg, Log::WARNING, 'jerror');
}
}
}
/**
* Busca no banco por todos os extras
*
* @return Array Retorna um array com informações dos extras
*/
public static function getExtrasList($extraExtensions){
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true);
//Monta a query para buscar na tabela extension
$query->select('extension_id, element, type')
->from($db->quoteName('#__extensions'))
->where("element IN('".implode("', '", $extraExtensions)."')");
$db->setQuery($query);
$result = $db->loadObjectList();
return $result;
}
/**
* Busca por todas as extensões que estão no mesmo pacote que o parametro
*
* @param string $element alias do pacote que estamos removendo (Ex: 'pkg_nobossembed')
* @return array Array com os id das extensçoes que estão no mesmo pacote
*/
public static function getPackageExtensions($elementPack) {
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true);
//Monta a query para buscar na tabela extension
$query->select('a.extension_id, a.type')
->from('#__extensions AS a')
->where("a.package_id = (SELECT b.package_id FROM #__extensions AS b WHERE b.element='{$elementPack}') AND a.package_id != 0 AND a.extension_id != '{$extensionId}'");
$db->setQuery($query);
$result = $db->loadObjectList();
return $result;
}
/**
* Desistala cada extensão passada no array
*
* @return void
*/
public static function uninstallPackageExtensions($extensions){
$tmpInstaller = new Installer;
foreach ($extensions as $extension) {
// Caso não seja o pacote em si
if($extension->type != 'package'){
// Tenta fazer a desinstalação
$tmpInstaller->uninstall($extension->type, $extension->extension_id);
} else {
// Caso seja o pacote, deleta do banco
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true);
//Monta a query para buscar na tabela extension
$query
->delete('#__extensions')
->where("extension_id = {$extension->extension_id}");
$db->setQuery($query);
$db->execute();
}
}
}
/**
* Função que retorna a url base da plataforma No Boss Extensions para a realizacao de requisicoes
*
* @return String Url base da plataforma
**/
public static function getUrlNbExtensions(){
// Objeto com dados do config
$config = Factory::getApplication()->getConfig();
// Obtem a tag do idioma que esta sendo navegado
$currentLanguage = Factory::getApplication()->getLanguage()->getTag();
/* TODO: modificado para pegar sef do idioma direto no idioma corrente cortando estring (ex: extraimos 'pt' de 'pt-BR')
- Anteriormente buscavamos o sef dos idiomas de conteúdo instalados, mas isso poderia dar problema pq eles podem estar desabilitados sem que o acesso pelo idioma esteja desabilitado
*/
// $languages = LanguageHelper::getLanguages('lang_code');
// $langSef = $languages[$currentLanguage];
// $langSef = $langSef->sef;
$langSef = substr($currentLanguage, 0, 2);
// TODO: qnd colocarmos o idioma ingles como default, eh necessario invester a ordem para setar 'pt' somente se usuario estiver em pt
// Idioma que esta sendo navegado nao eh portugues brasil: forca para colocar idioma ingles na navegacao
if($langSef != 'pt'){
$langSef = '/en';
}
// Deixa sem tag de idioma para pegar portugues
else{
$langSef = '/';
}
// Obtem a url definida no config (caso exista)
$urlNbExtensions = $config->get('url_nb_extensions');
// Url refinida no config: retorna ela mesmo
if (isset($urlNbExtensions) && !empty($urlNbExtensions)){
return $urlNbExtensions.$langSef;
}
// Retorna url do ambiente de producao
return 'https://www.nobossextensions.com'.$langSef;
}
/**
* Funcao que verifica se pasta 'noboss' esta criada dentro de 'layouts' e realiza acoes qnd necessario
*
* Utilizado na area admin dos nossos componentes
*/
public static function checkLibraryLayoutFolder(){
// Diretorio 'noboss' nao existe na pasta 'layouts' (provavelmente nao criou na instalacao da library)
if(!is_dir(JPATH_SITE.'/layouts/noboss')) {
$urlLibrary = 'https://www.nobossextensions.com/installation/nobosslibrary';
$urlDocPermission = 'https://docs.nobosstechnology.com/extensoes-joomla/ajustando-permissoes-de-diretorios-e-arquivos-no-joomla';
// Seta mensagem a ser exibida orientando sobre o problema
$message = "<br>
We have identified that the 'No Boss Library' that is required for this extension to work is not installed correctly on the site.<br><br>
The most likely problem is the lack of permissions on the site directory 'layouts/noboss'. <br>
When the library is installed or updated, it tries to make changes to this directory. <br><br>
To resolve the issue, follow the two steps below:<br>
1. Correct the permissions of the 'layouts/noboss' directory. If you have questions about how to do it, visit our <a href='{$urlDocPermission}' target='_blank'>documentation on setting Joomla file and directory permissions</a>.' <br>
2. Install the 'No Boss Library' library using url <a href='{$urlLibrary}' target='_blank'>{$urlLibrary}</a>.<br><br>
If the problem persists, contact the support team. <br><br>";
// Exibe mensagem como warning (fundo amarelo)
Factory::getApplication()->enqueueMessage($message, 'Warning');
return false;
}
return true;
}
/**
* Registra mensagem de diagnóstico no Joomla e no log do PHP.
*
* @param string $type Tipo da mensagem no Joomla (error, warning, notice, message).
* @param string $stepName Nome da funcao / etapa de update.
* @param string $message Texto principal da mensagem de diagnóstico.
* @param string $extensionName Nome da extensão (ex: 'mod_nobosscalendar').
* @param \Throwable|null $exception Exceção opcional para enriquecer o contexto.
* @param string|null $dumpFileName Caminho completo do arquivo de dump, se existir.
* @param bool $displayOnUi Define se deve exibir a mensagem também na interface do Joomla.
*
* @return void
*/
public static function registerDiagnostic($type, $stepName, $message, $extensionName = '', $exception = null, $dumpFileName = null, $displayOnUi = true){
$extensionName = strtolower((string) $extensionName);
$message = "[{$stepName}] {$message}";
// Mensagem completa com todos os detalhes técnicos para o log
$timestamp = date('Y-m-d H:i:s');
$fullMessage = "[{$timestamp}] [{$extensionName}] [{$type}] {$message}";
// Mensagem simplificada para exibição na UI do Joomla
$uiMessage = $message;
// Adiciona detalhes da exceção às mensagens
if ($exception instanceof \Throwable) {
$exceptionDetails = ' | Exception: ' . get_class($exception)
. ' | Message: ' . $exception->getMessage()
. ' | File: ' . $exception->getFile() . ':' . $exception->getLine();
// Detalhes completos vão para o log
$fullMessage .= $exceptionDetails;
// Na UI, apenas indica que há mais detalhes no log (com quebra de linha)
if ($type === 'error') {
$uiMessage .= '<br><br><strong>Error:</strong> ' . $exception->getMessage();
}
}
// Adiciona informação sobre o dump quando aplicável
if (($type === 'error') && !empty($dumpFileName)) {
// Extract just the filename from the full path
$dumpFileBasename = basename($dumpFileName);
// Build restore URL
$siteUrl = rtrim(Uri::root(), '/');
$restoreUrl = $siteUrl . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackupAjax&extensionName=' . urlencode($extensionName) . '&dumpFileName=' . urlencode($dumpFileBasename) . '&format=raw';
$dumpInfo = ' If necessary, restore the affected database tables using the dump file: ' . $dumpFileName;
$fullMessage .= $dumpInfo;
$uiMessage .= '<br><br><strong>Backup:</strong> ' . $dumpInfo;
$uiMessage .= '<br><strong>Restore Backup:</strong> Click here to <a href="' . $restoreUrl . '" target="_blank" style="color:#4ec9b0;font-weight:bold;">restore from this backup file</a>';
}
// Define caminho do arquivo de log específico da extensão
$logFile = JPATH_ADMINISTRATOR . "/logs/{$extensionName}_update.php";
// Adiciona referência ao log para erros e warnings (apenas na UI)
if (($type === 'error' || $type === 'warning') && $displayOnUi) {
$uiMessage .= "<br><br><strong>Detailed Info:</strong> For detailed technical information, check the log file: <code>{$logFile}</code>";
// URL para re-executar o update manualmente (somente quando nome da extensão existir)
if (!empty($extensionName)) {
$siteUrl = rtrim(Uri::root(), '/');
$manualUpdateUrl = $siteUrl . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=runUpdateManually&extensionName=' . urlencode($extensionName) . '&format=raw';
$uiMessage .= "<br><br><strong>Manual Update:</strong> Database update scripts can be re-executed directly at: <a href='{$manualUpdateUrl}' target='_blank'>{$manualUpdateUrl}</a>";
}
// Link para suporte técnico
$supportUrl = 'https://www.nobossextensions.com/customer-area/request-technical-support';
$uiMessage .= "<br><br><strong>Technical Support:</strong> If the problem persists, please contact our technical support team providing a screenshot of this error and the details from the log file: <a href='{$supportUrl}' target='_blank'>{$supportUrl}</a>";
}
// Exibir mensagem na tela do Joomla
if ($displayOnUi) {
try {
Factory::getApplication()->enqueueMessage($uiMessage, $type);
} catch (\Throwable $ignored) {
// Evita interromper o fluxo caso não seja possível exibir mensagem na UI.
}
}
// Cria o arquivo de log com header de segurança do Joomla se não existir
if (!file_exists($logFile)) {
$header = "#<?php die('Forbidden.'); ?>\n";
$header .= "#Date: " . date('Y-m-d H:i:s') . "\n";
$header .= "#Software: {$extensionName} Update Log\n\n";
@file_put_contents($logFile, $header);
@chmod($logFile, 0644);
}
// Registra mensagem detalhada no arquivo de log específico
error_log($fullMessage . PHP_EOL, 3, $logFile);
}
/**
* Executa uma etapa do update e registra erro, quando ocorrer.
*
* @param string $stepName Nome da etapa crítica executada.
* @param callable $callback Callback que executa a etapa.
* @param string $extensionName Nome da extensão (ex: 'mod_nobosscalendar').
* @param string|null $dumpFileName Caminho completo do arquivo de dump, se existir.
*
* @return void
*/
public static function runUpdateStep($stepName, $callback, $extensionName = '', $dumpFileName = null){
try {
// Executa a funcao recebida como parametro e captura o retorno
$result = $callback();
// Se a função retornou uma mensagem (string), processa ela
if (is_string($result) && !empty(trim($result))) {
// Verifica se o retorno indica erro (texto contém "error" ou "erro")
if (stripos($result, 'error') !== false || stripos($result, 'erro') !== false) {
// Registra como erro no log de diagnóstico
self::registerDiagnostic('error', $stepName, $result, $extensionName, null, $dumpFileName);
} else {
// Registra como notice (sucesso ou informação)
self::registerDiagnostic('notice', $stepName, $result, $extensionName, null, $dumpFileName, false);
}
}
} catch (\Throwable $e) {
self::registerDiagnostic('error', $stepName, 'Failure while executing update step.', $extensionName, $e, $dumpFileName);
}
}
/**
* Grava SQL em disco e aplica retenção de dumps antigos.
*
* @return string Mensagem de sucesso ou erro
*/
private static function persistDumpString(string $dumpString, string $outPath, string $fileName, ?int $daysToKeepDumps): string
{
if (!is_dir($outPath)) {
mkdir($outPath, 0775, true);
}
chmod($outPath, 0775);
if (!is_writable($outPath)) {
return "Error: The directory '{$outPath}' does not have write permission.";
}
if (empty(trim($dumpString))) {
return "Error: The dump generated is empty. No records found in tables.";
}
if (!(file_put_contents($outPath . $fileName, $dumpString))) {
return "Error: Could not generate the dump file using PHP function 'file_put_contents'.";
}
chmod($outPath . $fileName, 0775);
if ($daysToKeepDumps !== null && is_int($daysToKeepDumps) && $daysToKeepDumps > 0) {
$currentTime = time();
$maxAge = $daysToKeepDumps * 24 * 60 * 60;
$dumpFiles = glob($outPath . '*.sql');
if (!empty($dumpFiles)) {
foreach ($dumpFiles as $dumpFile) {
if ($dumpFile === $outPath . $fileName) {
continue;
}
$fileAge = $currentTime - filemtime($dumpFile);
if ($fileAge > $maxAge) {
@unlink($dumpFile);
}
}
}
}
return "Dump generated successfully at {$outPath}{$fileName}";
}
/**
* Informative SQL footer: the site module list is cached as files (com_modules group).
* There is no portable SQL to purge it; operators should follow these steps after importing the dump.
*/
private static function appendModuleListCacheHintSql(string $dumpString): string
{
return $dumpString . "\n\n"
. "-- -----------------------------------------------------------------------------\n"
. "-- Post-import — site module list cache (group `com_modules`, files under cache/):\n"
. "-- no reliable SQL statement clears this cache. After running this script:\n"
. "-- • Administrator → System → Clear Cache (include compiled cache if shown); or\n"
. "-- • On the server: delete the contents of the `cache/com_modules/` folder at the Joomla root.\n"
. "-- Module rows inserted earlier use placeholder asset lft/rgt (0). If asset-tree or ACL warnings appear:\n"
. "-- Administrator → System → Maintenance → Rebuild Web Assets (or equivalent in your Joomla version).\n";
}
/**
* Lê title/rules do asset do módulo na origem (geração do dump).
*
* @return array{0: string, 1: string} [title, rules json]
*/
private static function getModuleAssetTitleRulesForDump(
DatabaseInterface $db,
string $prefix,
int $moduleId,
string $fallbackTitle
): array {
$assetsTable = $prefix . 'assets';
$name = 'com_modules.module.' . $moduleId;
$q = $db->getQuery(true)
->select($db->quoteName(['title', 'rules']))
->from($db->quoteName($assetsTable))
->where($db->quoteName('name') . ' = ' . $db->quote($name));
$db->setQuery($q);
$assetRow = $db->loadAssoc();
if ($assetRow === null) {
return [$fallbackTitle !== '' ? $fallbackTitle : 'Module', '{}'];
}
$title = (string) ($assetRow['title'] ?? '');
if ($title === '') {
$title = $fallbackTitle !== '' ? $fallbackTitle : 'Module';
}
$rules = (string) ($assetRow['rules'] ?? '');
if ($rules === '') {
$rules = '{}';
}
return [$title, $rules];
}
/**
* SQL: INSERT em #__assets (filho de com_modules) + UPDATE em #__modules.asset_id.
* lft/rgt = 0 placeholder (rebuild de assets no admin recomendado no destino).
*
* @param bool $moduleIdIsUserVariable true: nome do asset e UPDATE usam @nb_mod_{$dumpKeyId}; false: id literal
*/
private static function buildSqlInsertModuleAssetAndLinkModule(
DatabaseInterface $db,
string $prefix,
int $dumpKeyId,
bool $moduleIdIsUserVariable,
string $title,
string $rules
): string {
$assetsTable = $prefix . 'assets';
$modulesTable = $prefix . 'modules';
$parentResolve = 'SELECT ' . $db->quoteName('id') . ' INTO @nb_parent_com_modules FROM ' . $db->quoteName($assetsTable)
. ' WHERE ' . $db->quoteName('name') . ' = ' . $db->quote('com_modules')
. ' ORDER BY ' . $db->quoteName('id') . ' ASC LIMIT 1';
$nameExpr = $moduleIdIsUserVariable
? ('CONCAT(' . $db->quote('com_modules.module.') . ', @nb_mod_' . $dumpKeyId . ')')
: $db->quote('com_modules.module.' . $dumpKeyId);
$modRef = $moduleIdIsUserVariable ? ('@nb_mod_' . $dumpKeyId) : (string) (int) $dumpKeyId;
$sql = $parentResolve . ";\n" . 'INSERT INTO ' . $db->quoteName($assetsTable) . ' ('
. implode(', ', array_map([$db, 'quoteName'], ['parent_id', 'lft', 'rgt', 'level', 'name', 'title', 'rules']))
. ') VALUES (@nb_parent_com_modules, 0, 0, 2, '
. $nameExpr . ', '
. self::quoteSqlDumpScalar($db, $title) . ', '
. self::quoteSqlDumpScalar($db, $rules !== '' ? $rules : '{}')
. ");\n";
$sql .= 'SELECT LAST_INSERT_ID() INTO @nb_ast_' . $dumpKeyId . ";\n";
$sql .= 'UPDATE ' . $db->quoteName($modulesTable)
. ' SET ' . $db->quoteName('asset_id') . ' = @nb_ast_' . $dumpKeyId
. ' WHERE ' . $db->quoteName('id') . ' = ' . $modRef . ";\n";
return $sql;
}
/**
* Detecta valores de data/hora legados rejeitados em sql_mode estrito (NO_ZERO_DATE / NO_ZERO_IN_DATE).
*/
private static function shouldExportSqlScalarAsNull($value): bool
{
if (!is_string($value)) {
return false;
}
$v = trim($value);
if ($v === '0000-00-00' || $v === '0000-00-00 00:00:00') {
return true;
}
return preg_match('/^0000-00-00(\s+\d{1,2}:\d{1,2}:\d{1,2}(\.\d+)?)?$/', $v) === 1;
}
/**
* Escapa valor para INSERT no dump SQL (NULL, zero-dates → NULL, demais quoted).
*
* @param DatabaseInterface $db
*/
private static function quoteSqlDumpScalar($db, $value): string
{
if ($value === null || self::shouldExportSqlScalarAsNull($value)) {
return 'NULL';
}
return $db->quote($value);
}
/**
* Alguma coluna FK para #__modules aponta para id > 0 que não está no conjunto exportado.
*
* @param array<string, mixed> $row
* @param array<int, string> $moduleFkColumns
* @param array<int, true> $dumpedModuleIds
*/
private static function rowReferencesUnknownDumpedModuleId(array $row, array $moduleFkColumns, array $dumpedModuleIds): bool
{
foreach ($moduleFkColumns as $col) {
if (!array_key_exists($col, $row)) {
continue;
}
$v = $row[$col];
if ($v === null || $v === '') {
continue;
}
$i = (int) $v;
if ($i > 0 && !isset($dumpedModuleIds[$i])) {
return true;
}
}
return false;
}
/**
* DELETE multi-tabela (MySQL/MariaDB): linhas em #__modules (alias nbm) e, opcionalmente,
* #__modules_menu e #__assets ligadas a esses módulos.
* Em #__assets, o registro do módulo usa `name` = CONCAT('com_modules.module.', nbm.id).
*
* @param string $moduleWhereExpr Condição já qualificada com o alias `nbm` (ex.: `nbm`.`module` = 'mod_x').
*/
private static function buildModulesRelatedMultiDeleteSql(
DatabaseInterface $db,
string $prefix,
string $moduleWhereExpr,
bool $joinModulesMenu,
bool $joinAssets
): string {
$nbm = 'nbm';
$nbmm = 'nbmm';
$nba = 'nba';
$tMod = $db->quoteName($prefix . 'modules');
$deleteTargets = [$db->quoteName($nbm)];
if ($joinModulesMenu) {
$deleteTargets[] = $db->quoteName($nbmm);
}
if ($joinAssets) {
$deleteTargets[] = $db->quoteName($nba);
}
$sql = 'DELETE ' . implode(', ', $deleteTargets)
. ' FROM ' . $tMod . ' AS ' . $db->quoteName($nbm);
if ($joinModulesMenu) {
$tMm = $db->quoteName($prefix . 'modules_menu');
$sql .= ' LEFT JOIN ' . $tMm . ' AS ' . $db->quoteName($nbmm)
. ' ON ' . $db->quoteName($nbmm) . '.' . $db->quoteName('moduleid')
. ' = ' . $db->quoteName($nbm) . '.' . $db->quoteName('id');
}
if ($joinAssets) {
$tAssets = $db->quoteName($prefix . 'assets');
$sql .= ' LEFT JOIN ' . $tAssets . ' AS ' . $db->quoteName($nba)
. ' ON ' . $db->quoteName($nba) . '.' . $db->quoteName('name')
. ' = CONCAT(' . $db->quote('com_modules.module.') . ', '
. $db->quoteName($nbm) . '.' . $db->quoteName('id') . ')';
}
return $sql . ' WHERE ' . $moduleWhereExpr;
}
/**
* DELETE único por module='mod_xxx': opcionalmente inclui #__modules_menu e #__assets (mesmo comando).
* $where deve ser no formato module='mod_xxx'.
*
* @return string|null SQL ou null se $where não for só module='…' ou se nenhum join extra for solicitado
*/
private static function buildModulesAndMenuMultiDeleteSql(
DatabaseInterface $db,
string $prefix,
string $where,
bool $joinModulesMenu,
bool $joinAssets
): ?string {
if (!$joinModulesMenu && !$joinAssets) {
return null;
}
if (!preg_match("/^\s*module\s*=\s*'([^']+)'\s*$/i", trim($where), $m)) {
return null;
}
$element = $m[1];
$nbm = 'nbm';
$qualWhere = $db->quoteName($nbm) . '.' . $db->quoteName('module') . ' = ' . $db->quote($element);
return self::buildModulesRelatedMultiDeleteSql($db, $prefix, $qualWhere, $joinModulesMenu, $joinAssets);
}
/**
* DELETE único por id + element: opcionalmente inclui #__modules_menu e #__assets (dump de instância única).
*/
private static function buildModulesAndMenuMultiDeleteSqlByModuleId(
DatabaseInterface $db,
string $prefix,
int $moduleId,
string $moduleName,
bool $joinModulesMenu,
bool $joinAssets
): string {
$nbm = 'nbm';
$qualWhere = $db->quoteName($nbm) . '.' . $db->quoteName('id') . ' = ' . $moduleId
. ' AND ' . $db->quoteName($nbm) . '.' . $db->quoteName('module') . ' = ' . $db->quote($moduleName);
return self::buildModulesRelatedMultiDeleteSql($db, $prefix, $qualWhere, $joinModulesMenu, $joinAssets);
}
/**
* Funcao generica para gerar dump de dados de uma extensao usando PHP puro
*
* Esta funcao eh generica e pode ser usada por qualquer extensao No Boss.
* Gera um arquivo SQL com INSERTs dos dados das tabelas especificadas.
*
* @param array<int, array<string, mixed>> $tables Array de tabelas e condicoes. Entradas opcionais:
* - module_fk_columns: lista de colunas cujo valor referencia #__modules.id (usado com omit_modules_insert_id).
* - modules_menu_all_pages (entrada name=modules): DELETE multi-tabela #__modules + opcionalmente #__modules_menu e #__assets num comando; após cada INSERT de módulo (omit id) gera INSERT em modules_menu (moduleid=@nb_mod_*, menuid=0), INSERT em #__assets ligado ao novo id e UPDATE modules.asset_id.
* @param string $outPath Caminho para salvar o dump
* @param string $fileName Nome do arquivo de dump
* @param int|null $daysToKeepDumps Numero de dias para manter dumps antigos. Se fornecido, remove dumps mais antigos que esse prazo (opcional)
* @param array<string, mixed> $options omit_modules_insert_id: true para não incluir coluna id em INSERTs da tabela modules e usar @nb_mod_{id} nas colunas declaradas em module_fk_columns.
* include_deletes_and_drop: false para omitir todos os DELETE e DROP TABLE (estilo append); padrão true.
*
* @return string Mensagem de sucesso ou erro
*/
public static function dumpDataGeneric($tables, $outPath, $fileName, $daysToKeepDumps = null, array $options = []){
try {
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$config = Factory::getApplication()->getConfig();
$prefix = $config->get('dbprefix');
$omitModulesInsertId = !empty($options['omit_modules_insert_id']);
$includeDeletesAndDrop = array_key_exists('include_deletes_and_drop', $options)
? (bool) $options['include_deletes_and_drop']
: true;
$dumpedModuleIds = [];
$dumpString = '';
if (!$includeDeletesAndDrop) {
$dumpString .= "-- Option: DELETE and DROP TABLE omitted (append-only).\n";
}
if ($omitModulesInsertId) {
$dumpString .= "-- Full dump: #__modules INSERT omits `id` (AUTO_INCREMENT).\n";
$dumpString .= "-- After each module row: SELECT LAST_INSERT_ID() INTO @nb_mod_{oldId} (MySQL/MariaDB, one connection, sequential execution).\n";
$dumpString .= "-- Related rows: module FK columns use @nb_mod_*; rows referencing module ids not included in this dump are skipped.\n";
$dumpString .= "-- #__modules + #__modules_menu + #__assets: one DELETE removes menu rows, module asset (name com_modules.module.{id}), and modules;\n";
$dumpString .= "-- then each module gets modules_menu (menuid=0), new #__assets row (parent com_modules), and UPDATE modules.asset_id.\n";
} else {
$dumpString .= "-- Full dump: #__modules INSERT includes explicit `id` (preserve IDs); FK columns keep literal module id values.\n";
$dumpString .= "-- modules_menu + assets: one row/menuid=0 per module; asset recreated + modules.asset_id updated when `assets` exists.\n";
}
$dumpString .= "-- Zero dates (0000-00-00 / 0000-00-00 00:00:00) are exported as NULL for strict sql_mode on target (e.g. NO_ZERO_DATE).\n";
foreach ($tables as $seedInfo) {
if ((string) ($seedInfo['name'] ?? '') !== 'modules') {
continue;
}
$seedTable = $prefix . 'modules';
$db->setQuery("SHOW TABLES LIKE " . $db->quote($seedTable));
if (!$db->loadResult()) {
break;
}
$seedWhere = $seedInfo['where'] ?? null;
$q = $db->getQuery(true)->select('*')->from($db->quoteName($seedTable));
if ($seedWhere) {
$q->where($seedWhere);
}
$db->setQuery($q);
$seedRows = $db->loadAssocList() ?: [];
foreach ($seedRows as $seedRow) {
$mid = (int) ($seedRow['id'] ?? 0);
if ($mid > 0) {
$dumpedModuleIds[$mid] = true;
}
}
break;
}
$hasModulesMenuTable = null;
$hasAssetsTable = null;
foreach ($tables as $tableInfo) {
$logicalName = (string) ($tableInfo['name'] ?? '');
$tableName = $prefix . $logicalName;
$where = $tableInfo['where'] ?? null;
$moduleFkColumns = $tableInfo['module_fk_columns'] ?? [];
if (!is_array($moduleFkColumns)) {
$moduleFkColumns = [];
}
// Verifica se tabela existe
$query = "SHOW TABLES LIKE " . $db->quote($tableName);
$db->setQuery($query);
if (!$db->loadResult()) {
continue; // Tabela não existe, pula
}
if ($logicalName === 'modules') {
if ($hasModulesMenuTable === null) {
$db->setQuery('SHOW TABLES LIKE ' . $db->quote($prefix . 'modules_menu'));
$hasModulesMenuTable = (bool) $db->loadResult();
}
if ($hasAssetsTable === null) {
$db->setQuery('SHOW TABLES LIKE ' . $db->quote($prefix . 'assets'));
$hasAssetsTable = (bool) $db->loadResult();
}
}
// Busca todos os registros da tabela
$query = $db->getQuery(true);
$query->select('*')->from($db->quoteName($tableName));
if ($where) {
$query->where($where);
}
$db->setQuery($query);
$rows = $db->loadAssocList();
if (empty($rows)) {
continue; // Tabela vazia, pula
}
// DROP TABLE e CREATE TABLE (tabelas exclusivas da extensao)
if ($includeDeletesAndDrop && !empty($tableInfo['drop_create'])) {
$dumpString .= "DROP TABLE IF EXISTS " . $db->quoteName($tableName) . ";\n";
// Obtem estrutura da tabela
$query = "SHOW CREATE TABLE " . $db->quoteName($tableName);
$db->setQuery($query);
$createTable = $db->loadAssoc();
if (!empty($createTable['Create Table'])) {
$dumpString .= $createTable['Create Table'] . ";\n";
}
}
// DELETE (tabelas compartilhadas com outras extensoes)
if ($includeDeletesAndDrop && !empty($tableInfo['delete']) && $where) {
$modulesMenuAllPages = !empty($tableInfo['modules_menu_all_pages']) && $logicalName === 'modules';
if ($modulesMenuAllPages && ($hasModulesMenuTable || $hasAssetsTable)) {
$multiDel = self::buildModulesAndMenuMultiDeleteSql(
$db,
$prefix,
$where,
(bool) $hasModulesMenuTable,
(bool) $hasAssetsTable
);
$dumpString .= ($multiDel !== null ? $multiDel : 'DELETE FROM ' . $db->quoteName($tableName) . " WHERE {$where}") . ";\n";
} else {
$dumpString .= 'DELETE FROM ' . $db->quoteName($tableName) . " WHERE {$where};\n";
}
}
// Gera INSERT para cada registro
foreach ($rows as $row) {
if (
$logicalName !== 'modules'
&& $moduleFkColumns !== []
&& self::rowReferencesUnknownDumpedModuleId($row, $moduleFkColumns, $dumpedModuleIds)
) {
continue;
}
$modulePkForFooter = ($logicalName === 'modules' && isset($row['id']))
? (int) $row['id']
: 0;
$oldModuleIdForSet = 0;
if ($omitModulesInsertId && $logicalName === 'modules' && array_key_exists('id', $row)) {
$oldModuleIdForSet = (int) $row['id'];
if ($oldModuleIdForSet > 0) {
unset($row['id']);
} else {
$oldModuleIdForSet = 0;
}
}
if (
$logicalName === 'modules'
&& !empty($tableInfo['modules_menu_all_pages'])
&& $hasAssetsTable
&& $modulePkForFooter > 0
) {
unset($row['asset_id']);
}
$columns = array_keys($row);
$escapedValues = [];
foreach ($columns as $columnName) {
$value = $row[$columnName];
if (
$omitModulesInsertId
&& $logicalName !== 'modules'
&& $moduleFkColumns !== []
&& in_array($columnName, $moduleFkColumns, true)
&& $value !== null && $value !== ''
&& (int) $value > 0
) {
$escapedValues[] = '@nb_mod_' . (int) $value;
} else {
$escapedValues[] = self::quoteSqlDumpScalar($db, $value);
}
}
$dumpString .= 'INSERT INTO ' . $db->quoteName($tableName)
. ' (' . implode(', ', array_map([$db, 'quoteName'], $columns)) . ') '
. 'VALUES (' . implode(', ', $escapedValues) . ");\n";
if ($logicalName === 'modules' && !empty($tableInfo['modules_menu_all_pages'])) {
if ($omitModulesInsertId && $oldModuleIdForSet > 0) {
$dumpString .= 'SELECT LAST_INSERT_ID() INTO @nb_mod_' . $oldModuleIdForSet . ";\n";
if ($hasModulesMenuTable) {
$mmTable = $prefix . 'modules_menu';
$dumpString .= 'INSERT INTO ' . $db->quoteName($mmTable)
. ' (' . $db->quoteName('moduleid') . ', ' . $db->quoteName('menuid') . ') '
. 'VALUES (@nb_mod_' . $oldModuleIdForSet . ', 0);' . "\n";
}
if ($hasAssetsTable) {
[$astTitle, $astRules] = self::getModuleAssetTitleRulesForDump(
$db,
$prefix,
$oldModuleIdForSet,
(string) ($row['title'] ?? '')
);
$dumpString .= self::buildSqlInsertModuleAssetAndLinkModule(
$db,
$prefix,
$oldModuleIdForSet,
true,
$astTitle,
$astRules
);
}
} elseif (!$omitModulesInsertId && $modulePkForFooter > 0) {
if ($hasModulesMenuTable) {
$mmTable = $prefix . 'modules_menu';
$dumpString .= 'INSERT INTO ' . $db->quoteName($mmTable)
. ' (' . $db->quoteName('moduleid') . ', ' . $db->quoteName('menuid') . ') '
. 'VALUES (' . $modulePkForFooter . ', 0);' . "\n";
}
if ($hasAssetsTable) {
[$astTitle, $astRules] = self::getModuleAssetTitleRulesForDump(
$db,
$prefix,
$modulePkForFooter,
(string) ($row['title'] ?? '')
);
$dumpString .= self::buildSqlInsertModuleAssetAndLinkModule(
$db,
$prefix,
$modulePkForFooter,
false,
$astTitle,
$astRules
);
}
}
}
}
}
foreach ($tables as $specEntry) {
if ((string) ($specEntry['name'] ?? '') === 'modules') {
$dumpString = self::appendModuleListCacheHintSql($dumpString);
break;
}
}
return self::persistDumpString($dumpString, $outPath, $fileName, $daysToKeepDumps);
} catch(\Exception $e){
return "Error generating dump: " . $e->getMessage();
}
}
/**
* Verifica se o usuário está autenticado como administrador do Joomla.
*
* Esta função é uma validação de segurança genérica que pode ser usada por qualquer
* função AJAX que requeira permissões administrativas.
*
* REQUISITOS DE SEGURANÇA:
* - Usuário deve estar autenticado (não pode ser guest)
* - Usuário deve ter permissão 'core.admin' OU pertencer ao grupo 8 (Super Users)
* - Quando executado via frontend, a autenticação deve existir no frontend OU o site
* deve estar configurado para compartilhar sessão entre frontend e administrator
*
* Se a autenticação falhar, exibe uma mensagem de erro HTML e encerra a execução.
*
* @return void (encerra execução com exit se não autenticado)
*/
public static function checkAdminAuthentication() {
header('Content-Type: text/html; charset=utf-8');
$app = Factory::getApplication();
$user = $app->getIdentity();
// Verifica se o usuário está autenticado
if ($user->guest) {
$returnUrl = base64_encode(Uri::getInstance()->toString());
$frontendLoginUrl = Uri::root() . 'index.php?option=com_users&view=login&return=' . urlencode($returnUrl);
echo '<html><head><meta charset="utf-8"><title>Access Denied</title></head><body>';
echo '<style>
body{font-family:"Segoe UI",Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:20px;text-align:center;}
.error-box{background:#2d2d2d;border-radius:8px;padding:40px;margin:50px auto;max-width:500px;border:2px solid #f48771;}
h2{color:#f48771;margin-top:0;}
p{color:#d4d4d4;line-height:1.6;}
.icon{font-size:64px;margin-bottom:20px;}
.btn-login{display:inline-block;background:#4ec9b0;color:#1e1e1e;text-decoration:none;font-weight:bold;padding:12px 20px;border-radius:4px;margin-top:14px;}
.btn-login:hover{background:#6eddc8;}
.note{margin-top:20px;background:#252525;border-left:4px solid #dcdcaa;border-radius:4px;padding:14px;text-align:left;}
</style>';
echo '<div class="error-box">';
echo '<div class="icon">🔒</div>';
echo '<h2>Access Denied</h2>';
echo '<p><strong>Authentication Required</strong></p>';
echo '<p>You must be logged in as an administrator to access this function.</p>';
echo '<a class="btn-login" href="' . htmlspecialchars($frontendLoginUrl) . '">Login no Front-end do Joomla</a>';
echo '<div class="note">';
echo '<strong>Important:</strong><br>';
echo 'This AJAX endpoint runs via frontend (index.php). You must be authenticated in frontend, or your site must be configured to share session between administrator and frontend.';
echo '</div>';
echo '</div>';
echo '</body></html>';
exit;
}
// Verifica se o usuário tem permissões administrativas (Super User ou grupos de admin)
$isAdmin = $user->authorise('core.admin') || in_array(8, $user->groups); // 8 = Super Users
if (!$isAdmin) {
echo '<html><head><meta charset="utf-8"><title>Access Denied</title></head><body>';
echo '<style>
body{font-family:"Segoe UI",Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:20px;text-align:center;}
.error-box{background:#2d2d2d;border-radius:8px;padding:40px;margin:50px auto;max-width:500px;border:2px solid #f48771;}
h2{color:#f48771;margin-top:0;}
p{color:#d4d4d4;line-height:1.6;}
.icon{font-size:64px;margin-bottom:20px;}
</style>';
echo '<div class="error-box">';
echo '<div class="icon">🔒</div>';
echo '<h2>Insufficient Permissions</h2>';
echo '<p><strong>Administrator Access Required</strong></p>';
echo '<p>You do not have sufficient permissions to access this function.</p>';
echo '<p>Only Super Users or Administrators can access this resource.</p>';
echo '</div>';
echo '</body></html>';
exit;
}
}
/**
* Executa manualmente o método de update da extensão via AJAX.
*
* Esta função é genérica e usa o parâmetro extensionName para localizar
* a extensão no banco e carregar seu script de instalação.
*
* Parâmetros da URL:
* - extensionName (obrigatório): Alias da extensão (ex: mod_nobosscalendar)
*
* URL para executar:
* WEBSITE/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=runUpdateManually&extensionName=mod_nobosscalendar&format=raw
*
* @return void (exibe resultado diretamente na tela)
*/
public static function runUpdateManually() {
self::checkAdminAuthentication();
header('Content-Type: text/html; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$input = Factory::getApplication()->input;
$extensionName = strtolower($input->getString('extensionName', ''));
if (empty($extensionName)) {
echo "<html><head><meta charset='utf-8'><title>Manual Update Execution</title></head><body>";
echo "<h2 style='color:#f48771;'>Error: Missing Required Parameter</h2>";
echo "<p>The parameter <code>extensionName</code> is required.</p>";
echo "<p>Example: <code>...&extensionName=mod_nobosscalendar</code></p>";
echo "</body></html>";
exit;
}
// Quando for um componente (com_*), resolve para o módulo de mesmo nome (mod_*)
if (strpos($extensionName, 'com_') === 0) {
$moduleName = 'mod_' . substr($extensionName, 4);
} elseif (strpos($extensionName, 'mod_') === 0) {
$moduleName = $extensionName;
} else {
echo "<html><head><meta charset='utf-8'><title>Manual Update Execution</title></head><body>";
echo "<h2 style='color:#f48771;'>Unsupported Extension Type</h2>";
echo "<p>This generic manual update currently supports module (<code>mod_*</code>) and component (<code>com_*</code>) extensions.</p>";
echo "<p>Received: <code>" . htmlspecialchars($extensionName) . "</code></p>";
echo "</body></html>";
exit;
}
$classBase = preg_replace('/^mod_/', '', $moduleName);
$classBase = str_replace(' ', '', ucwords(str_replace('_', ' ', $classBase)));
$installerClass = "\\mod_{$classBase}InstallerScript";
$scriptPath = JPATH_ROOT . '/modules/' . $moduleName . '/script.extension.php';
$confirmed = $input->getString('confirmed', '') === '1';
$commonStyles = "<style>
body{font-family:monospace;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
.success{color:#4ec9b0;}.error{color:#f48771;}.warning{color:#dcdcaa;}.info{color:#569cd6;}
pre{background:#2d2d2d;padding:15px;border-radius:5px;overflow-x:auto;}
hr{border-color:#444;}
.confirm-box{background:#2d2d2d;border:2px solid #dcdcaa;border-radius:8px;padding:30px;margin:30px auto;max-width:600px;}
.confirm-box h2{color:#dcdcaa;margin-top:0;}
.btn{display:inline-block;font-weight:bold;padding:10px 24px;border-radius:4px;text-decoration:none;cursor:pointer;font-family:monospace;font-size:14px;border:none;}
.btn-confirm{background:#f48771;color:#1e1e1e;margin-right:12px;}
.btn-confirm:hover{background:#ff6a50;}
.btn-cancel{background:#444;color:#d4d4d4;}
.btn-cancel:hover{background:#555;}
</style>";
if (!$confirmed) {
$currentUrl = Uri::getInstance()->toString();
$confirmUrl = htmlspecialchars(rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=runUpdateManually&extensionName=' . urlencode($extensionName) . '&format=raw&confirmed=1');
echo "<html><head><meta charset='utf-8'><title>Manual Update Execution</title>{$commonStyles}</head><body>";
echo "<div class='confirm-box'>";
echo "<h2>⚠ Confirm Manual Update</h2>";
echo "<p>You are about to manually execute the <strong>update()</strong> script for:</p>";
echo "<p class='info'><strong>Extension:</strong> " . htmlspecialchars($extensionName) . "</p>";
if ($moduleName !== $extensionName) {
echo "<p class='warning'><strong>Resolved module:</strong> <code>" . htmlspecialchars($moduleName) . "</code></p>";
}
echo "<p class='warning'>This will re-run all database migration steps of the installation script. This action may modify database tables and data.</p>";
echo "<p>Are you sure you want to proceed?</p>";
echo "<hr>";
echo "<a class='btn btn-confirm' href='{$confirmUrl}'>Yes, execute update</a>";
echo "<a class='btn btn-cancel' href='javascript:history.back()'>Cancel</a>";
echo "</div>";
echo "</body></html>";
exit;
}
echo "<html><head><meta charset='utf-8'><title>Manual Update Execution</title>{$commonStyles}</head><body>";
echo "<h2>Manual database update execution</h2>";
echo "<p class='info'>Extension: " . htmlspecialchars($extensionName) . "</p>";
echo "<hr>";
try {
ob_start();
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from('#__extensions')
->where("element = " . $db->quote($moduleName))
->where("type = 'module'");
$db->setQuery($query);
$extension = $db->loadObject();
if (!$extension) {
throw new \Exception("Extension {$moduleName} not found in database");
}
$manifestCache = json_decode($extension->manifest_cache);
$currentVersion = !empty($manifestCache->version) ? $manifestCache->version : 'unknown';
echo "<p class='info'>Extension found: " . htmlspecialchars($extension->name) . "</p>";
echo "<p class='info'>Current version: " . htmlspecialchars($currentVersion) . "</p>";
echo "<hr>";
$mockParent = new class($moduleName, $currentVersion) {
public $manifest;
public function __construct($moduleName, $version) {
$this->manifest = new \stdClass();
$this->manifest->name = $moduleName;
$this->manifest->version = $version;
}
public function get($key) {
if ($key === 'manifest') {
return $this->manifest;
}
return null;
}
public function getManifest() {
return $this->manifest;
}
};
if (!file_exists($scriptPath)) {
throw new \Exception("Installation script not found: {$scriptPath}");
}
require_once $scriptPath;
if (!class_exists($installerClass)) {
throw new \Exception("Installer class not found: {$installerClass}");
}
echo "<p class='warning'>Starting update() method execution...</p>";
echo "<hr>";
echo "<div style='background:#2d2d2d;padding:15px;border-radius:5px;'>";
$installer = new $installerClass();
$startTime = microtime(true);
$result = $installer->update($mockParent);
$endTime = microtime(true);
$executionTime = round($endTime - $startTime, 2);
echo "</div>";
echo "<hr>";
$app = Factory::getApplication();
$messages = $app->getMessageQueue();
$hasErrors = false;
if (!empty($messages)) {
foreach ($messages as $msg) {
if (strtolower($msg['type'] ?? '') === 'error') {
$hasErrors = true;
break;
}
}
}
if ($result === false || $hasErrors) {
echo "<p class='error'>The update() method completed with ERRORS</p>";
} else {
echo "<p class='success'>Update executed successfully!</p>";
}
echo "<p class='info'>Execution time: {$executionTime} seconds</p>";
if (!empty($messages)) {
echo "<hr><h3>Joomla System Messages:</h3>";
foreach ($messages as $msg) {
$class = strtolower($msg['type'] ?? 'info');
echo "<p class='{$class}'>[" . htmlspecialchars($msg['type']) . "] " . $msg['message'] . "</p>";
}
}
$output = ob_get_clean();
echo $output;
} catch (\Exception $e) {
$output = ob_get_clean();
echo $output;
echo "<hr>";
echo "<h3 class='error'>EXCEPTION CAUGHT:</h3>";
echo "<pre class='error'>" . htmlspecialchars($e->getMessage()) . "</pre>";
echo "<p class='error'><strong>File:</strong> " . htmlspecialchars($e->getFile()) . "</p>";
echo "<p class='error'><strong>Line:</strong> " . $e->getLine() . "</p>";
echo "<hr>";
echo "<h4>Stack Trace:</h4>";
echo "<pre class='error'>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
} catch (\Throwable $e) {
$output = ob_get_clean();
echo $output;
echo "<hr>";
echo "<h3 class='error'>FATAL ERROR:</h3>";
echo "<pre class='error'>" . htmlspecialchars($e->getMessage()) . "</pre>";
echo "<p class='error'><strong>File:</strong> " . htmlspecialchars($e->getFile()) . "</p>";
echo "<p class='error'><strong>Line:</strong> " . $e->getLine() . "</p>";
}
echo "<hr>";
echo "<p><em>Execution finished at " . date('Y-m-d H:i:s') . "</em></p>";
echo "</body></html>";
exit;
}
/**
* Valida se o id informado é uma instância do módulo esperado em #__modules.
*
* @return \stdClass|null Linha (id, title, module, …) ou null se inválido
*/
private static function getValidatedModuleInstance(int $moduleId, string $moduleName): ?\stdClass
{
if ($moduleId <= 0 || $moduleName === '') {
return null;
}
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true);
$query->select($db->quoteName(['id', 'title', 'module', 'position', 'published']))
->from($db->quoteName('#__modules'))
->where($db->quoteName('id') . ' = ' . $moduleId)
->where($db->quoteName('module') . ' = ' . $db->quote($moduleName));
$db->setQuery($query);
$row = $db->loadObject();
return $row ?: null;
}
/**
* Carrega getDumpTablesSpec() do installer (para UI e filtro do dump).
*
* @return array<int, array<string, mixed>>|null
*/
private static function loadInstallerDumpTablesSpec(string $scriptPath, string $installerClass): ?array
{
if (!is_file($scriptPath)) {
return null;
}
if (!class_exists($installerClass)) {
require_once $scriptPath;
}
if (!class_exists($installerClass) || !method_exists($installerClass, 'getDumpTablesSpec')) {
return null;
}
$spec = (array) $installerClass::getDumpTablesSpec();
return array_values(array_filter($spec, 'is_array'));
}
/**
* Lista lógica única de tabelas no spec (ordem preservada).
*
* @param array<int, array<string, mixed>> $spec
*
* @return array<int, string>
*/
private static function getUniqueDumpTableNamesFromSpec(array $spec): array
{
$out = [];
$seen = [];
foreach ($spec as $entry) {
$name = (string) ($entry['name'] ?? '');
if ($name === '' || isset($seen[$name])) {
continue;
}
$seen[$name] = true;
$out[] = $name;
}
return $out;
}
/**
* Emite checkboxes dump_inc[] (só na secção UI «Full extension dump»; todas marcadas por defeito).
*
* @param array<int, array<string, mixed>> $spec
*/
private static function emitFullDumpTableSelectionFieldset(array $spec): void
{
$names = self::getUniqueDumpTableNamesFromSpec($spec);
if ($names === []) {
echo '<p class="warning">No tables found in <code>getDumpTablesSpec()</code> for selection.</p>';
return;
}
echo '<fieldset class="dump-table-pick" style="border:1px solid #444;border-radius:6px;padding:12px;margin:14px 0;">';
echo '<legend style="color:#dcdcaa;">Tables to include</legend>';
echo '<p class="info" style="margin-top:0;font-size:13px;">Uncheck a table to omit it from the file (no DELETE, DROP, CREATE, or INSERT for that table).</p>';
foreach ($names as $logical) {
$id = 'dump_inc_' . preg_replace('/[^a-zA-Z0-9_]/', '_', $logical);
echo '<label style="display:block;margin:6px 0;cursor:pointer;" for="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . '">';
echo '<input type="checkbox" name="dump_inc[]" id="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8')
. '" value="' . htmlspecialchars($logical, ENT_QUOTES, 'UTF-8') . '" checked> ';
echo '<code>#__' . htmlspecialchars($logical, ENT_QUOTES, 'UTF-8') . '</code>';
echo '</label>';
}
echo '</fieldset>';
}
/**
* Formata uma entrada do array retornado por getDumpTablesSpec() no script de instalação.
*
* @param array<string, mixed> $tableInfo
*/
private static function formatDumpTableSpecEntryHtml(array $tableInfo): string
{
$logical = (string) ($tableInfo['name'] ?? '');
$where = $tableInfo['where'] ?? null;
$hasDelete = !empty($tableInfo['delete']);
$hasDropCreate = !empty($tableInfo['drop_create']);
$tableLabel = '<code>#__' . htmlspecialchars($logical, ENT_QUOTES, 'UTF-8') . '</code>';
if ($hasDropCreate) {
$line = "{$tableLabel} — full table (DROP/CREATE + INSERT)";
} elseif ($hasDelete && $where !== null && $where !== '') {
$w = htmlspecialchars((string) $where, ENT_QUOTES, 'UTF-8');
$line = "{$tableLabel} — filtered rows: <code>{$w}</code> (DELETE matching + INSERT)";
} elseif ($where !== null && $where !== '') {
$w = htmlspecialchars((string) $where, ENT_QUOTES, 'UTF-8');
$line = "{$tableLabel} — rows where <code>{$w}</code>";
} else {
$line = "{$tableLabel} — all rows (INSERT)";
}
if ($logical === 'modules' && ($hasDelete || ($where !== null && $where !== ''))) {
$line .= ' — <code>id</code> omitted from INSERT; <code>SELECT LAST_INSERT_ID() INTO @nb_mod_{oldId}</code> after each row';
}
if ($logical === 'modules' && !empty($tableInfo['modules_menu_all_pages'])) {
$line .= ' — one DELETE removes matching <code>#__modules</code>, related <code>#__modules_menu</code> (when present), and <code>#__assets</code> rows with <code>name = com_modules.module.{id}</code> (when present); after each dumped module: <code>modules_menu</code> (<code>menuid=0</code>), new <code>#__assets</code> row + <code>UPDATE</code> <code>asset_id</code> (when <code>assets</code> exists; run Rebuild Web Assets on target if needed)';
}
$moduleFkCols = $tableInfo['module_fk_columns'] ?? [];
if (is_array($moduleFkCols) && $moduleFkCols !== []) {
$listed = '<code>' . htmlspecialchars(implode(', ', $moduleFkCols), ENT_QUOTES, 'UTF-8') . '</code>';
$line .= " — FK columns to #__modules.id rewritten as <code>@nb_mod_*</code> in dump: {$listed}";
}
return $line;
}
/**
* Linhas HTML para o escopo do dump completo (getDumpTablesSpec() no script de instalação).
*
* @return array<int, string>
*/
private static function resolveDumpTableDescriptions(string $scriptPath, string $installerClass): array
{
if (is_file($scriptPath)) {
if (!class_exists($installerClass)) {
require_once $scriptPath;
}
if (class_exists($installerClass) && method_exists($installerClass, 'getDumpTablesSpec')) {
$spec = (array) $installerClass::getDumpTablesSpec();
$lines = [];
foreach ($spec as $entry) {
if (!is_array($entry)) {
continue;
}
$lines[] = self::formatDumpTableSpecEntryHtml($entry);
}
if ($lines !== []) {
return $lines;
}
}
}
return [
'Tables defined in this extension\'s <code>dumpBeforeUpdate()</code> (see <code>script.extension.php</code>). Add <code>getDumpTablesSpec()</code> or expect <code>#__modules_menu</code>, <code>#__modules</code>, and extension-specific tables.',
];
}
/**
* Dump de uma instância: #__modules (com id, sem asset_id no INSERT se houver #__assets) + #__modules_menu (menuid=0) + #__assets + UPDATE asset_id.
*
* @param array<string, mixed> $options include_deletes_and_drop (default true): quando false, omite o bloco DELETE inicial.
*
* @return string Mesmo formato de retorno que dumpDataGeneric
*/
private static function dumpSingleModuleInstanceSql(
int $moduleId,
string $moduleName,
string $outPath,
string $fileName,
?int $daysToKeepDumps,
array $options = []
): string {
try {
$includeDeletesAndDrop = array_key_exists('include_deletes_and_drop', $options)
? (bool) $options['include_deletes_and_drop']
: true;
$db = Factory::getContainer()->get(DatabaseInterface::class);
$config = Factory::getApplication()->getConfig();
$prefix = $config->get('dbprefix');
$tableName = $prefix . 'modules';
$query = "SHOW TABLES LIKE " . $db->quote($tableName);
$db->setQuery($query);
if (!$db->loadResult()) {
return "Error: Table {$tableName} does not exist.";
}
$where = $db->quoteName('id') . ' = ' . $moduleId
. ' AND ' . $db->quoteName('module') . ' = ' . $db->quote($moduleName);
$select = $db->getQuery(true);
$select->select('*')
->from($db->quoteName($tableName))
->where($where);
$db->setQuery($select);
$rows = $db->loadAssocList();
if (empty($rows)) {
return "Error: The dump generated is empty. No records found in tables.";
}
$dumpString = "-- #__modules + one #__modules_menu row (menuid=0) + #__assets for this instance (full module columns, includes PK).\n";
$dumpString .= "-- Zero dates (0000-00-00) exported as NULL for strict modes on target.\n";
$mmTable = $prefix . 'modules_menu';
$db->setQuery('SHOW TABLES LIKE ' . $db->quote($mmTable));
$hasModulesMenu = (bool) $db->loadResult();
$db->setQuery('SHOW TABLES LIKE ' . $db->quote($prefix . 'assets'));
$hasAssets = (bool) $db->loadResult();
if ($includeDeletesAndDrop) {
if ($hasModulesMenu || $hasAssets) {
$dumpString .= self::buildModulesAndMenuMultiDeleteSqlByModuleId(
$db,
$prefix,
$moduleId,
$moduleName,
$hasModulesMenu,
$hasAssets
) . ";\n";
} else {
$dumpString .= 'DELETE FROM ' . $db->quoteName($tableName) . ' WHERE ' . $where . ";\n";
}
$dumpString .= "\n";
} else {
$dumpString .= "-- Omitted DELETE statements (append-only).\n\n";
}
$moduleTitleFallback = (string) ($rows[0]['title'] ?? '');
foreach ($rows as $row) {
if ($hasAssets) {
unset($row['asset_id']);
}
$columns = array_keys($row);
$escapedValues = [];
foreach ($columns as $columnName) {
$escapedValues[] = self::quoteSqlDumpScalar($db, $row[$columnName]);
}
$dumpString .= 'INSERT INTO ' . $db->quoteName($tableName)
. ' (' . implode(', ', array_map([$db, 'quoteName'], $columns)) . ') '
. 'VALUES (' . implode(', ', $escapedValues) . ");\n";
}
if ($hasModulesMenu) {
$dumpString .= 'INSERT INTO ' . $db->quoteName($mmTable)
. ' (' . $db->quoteName('moduleid') . ', ' . $db->quoteName('menuid') . ') '
. 'VALUES (' . (int) $moduleId . ', 0);' . "\n";
}
if ($hasAssets) {
[$astTitle, $astRules] = self::getModuleAssetTitleRulesForDump(
$db,
$prefix,
$moduleId,
$moduleTitleFallback
);
$dumpString .= self::buildSqlInsertModuleAssetAndLinkModule(
$db,
$prefix,
$moduleId,
false,
$astTitle,
$astRules
);
}
$dumpString = self::appendModuleListCacheHintSql($dumpString);
return self::persistDumpString($dumpString, $outPath, $fileName, $daysToKeepDumps);
} catch (\Exception $e) {
return 'Error generating dump: ' . $e->getMessage();
}
}
/**
* Executa dump das tabelas da extensão via AJAX chamando o método dumpBeforeUpdate() do script de instalação.
*
* Esta função é genérica e usa o parâmetro extensionName para localizar
* a extensão e carregar seu script de instalação.
*
* REQUISITOS DE SEGURANÇA:
* - Requer autenticação como administrador do Joomla
* - Usuário deve ter permissão 'core.admin'
*
* Parâmetros da URL:
* - extensionName (obrigatório): Alias da extensão (ex: mod_nobosscalendar)
* - id_module (opcional): ID em #__modules; quando informado, a tela inicial permite escolher entre dump
* completo (dumpBeforeUpdate) ou escopo reduzido: <code>#__modules</code> + uma linha <code>#__modules_menu</code> (menuid=0) para essa instância
* - dump_scope (obrigatório após confirmação se id_module foi usado): full | module_only
* - confirmed: 1 to run after choosing options / scope
* - dump_opts_sent: 1 when options were submitted from the confirmation form (checkbox state is interpreted only then).
* - dump_include_deletes: 1 (default when form used) to emit DELETE / DROP TABLE; omit or 0 for append-only (when dump_opts_sent=1).
* - dump_inc[]: logical table names to include in full dump (from UI checkboxes); required when dump_opts_sent=1 — at least one. Omitted names are skipped entirely.
* - dump_preserve_module_ids: 1 to keep literal #__modules ids and FKs in the full dump; 0 for AUTO_INCREMENT + @nb_mod_* remapping (when dump_opts_sent=1; default 0).
*
* URL para executar:
* WEBSITE/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=dumpExtension&extensionName=mod_nobosscalendar&format=raw
* Com instância específica:
* ...&id_module=123&format=raw
*
* @return void (exibe resultado diretamente na tela)
*/
public static function dumpExtension() {
self::checkAdminAuthentication();
header('Content-Type: text/html; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$input = Factory::getApplication()->input;
$extensionName = strtolower($input->getString('extensionName', ''));
$idModule = $input->getInt('id_module', 0);
$dumpScope = $input->getCmd('dump_scope', '');
$confirmed = $input->getString('confirmed', '') === '1';
if (empty($extensionName)) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: Missing Required Parameter</h2>';
echo '<p>The parameter <code>extensionName</code> is required.</p>';
echo '<p>Example: <code>...&extensionName=mod_nobosscalendar</code></p>';
echo '</body></html>';
exit;
}
$isModule = strpos($extensionName, 'mod_') === 0;
// Quando for um componente (com_*), resolve para o módulo de mesmo nome (mod_*)
if (!$isModule) {
if (strpos($extensionName, 'com_') === 0) {
$moduleName = 'mod_' . substr($extensionName, 4);
} else {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">Unsupported Extension Type</h2>';
echo '<p>This function currently supports module (<code>mod_*</code>) and component (<code>com_*</code>) extensions.</p>';
echo '<p>Received: <code>' . htmlspecialchars($extensionName) . '</code></p>';
echo '</body></html>';
exit;
}
} else {
$moduleName = $extensionName;
}
$classBase = preg_replace('/^mod_/', '', $moduleName);
$classBase = str_replace(' ', '', ucwords(str_replace('_', ' ', $classBase)));
$installerClass = "\\mod_{$classBase}InstallerScript";
$scriptPath = JPATH_ROOT . '/modules/' . $moduleName . '/script.extension.php';
$moduleRow = null;
if ($idModule > 0) {
$moduleRow = self::getValidatedModuleInstance($idModule, $moduleName);
if ($moduleRow === null) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Invalid module instance</h2>';
echo '<p>No row in <code>#__modules</code> with <code>id=' . (int) $idModule . '</code> and <code>module=' . htmlspecialchars($moduleName) . '</code>.</p>';
echo '</body></html>';
exit;
}
}
$commonStyles = "<style>
body{font-family:monospace;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
.success{color:#4ec9b0;}.error{color:#f48771;}.warning{color:#dcdcaa;}.info{color:#569cd6;}
hr{border-color:#444;}
.confirm-box{background:#2d2d2d;border:2px solid #4ec9b0;border-radius:8px;padding:30px;margin:30px auto;max-width:720px;}
.confirm-box h2{color:#4ec9b0;margin-top:0;}
.btn{display:inline-block;font-weight:bold;padding:10px 24px;border-radius:4px;text-decoration:none;cursor:pointer;font-family:monospace;font-size:14px;border:none;margin:6px 6px 6px 0;}
.btn-confirm{background:#4ec9b0;color:#1e1e1e;}
.btn-confirm:hover{background:#6eddc8;}
.btn-full{background:#569cd6;color:#1e1e1e;}
.btn-full:hover{background:#78b4e8;}
.btn-module{background:#ce9178;color:#1e1e1e;}
.btn-module:hover{background:#e0a080;}
.btn-cancel{background:#444;color:#d4d4d4;}
.btn-cancel:hover{background:#555;}
code{background:#1e1e1e;padding:2px 6px;border-radius:3px;color:#ce9178;}
ul.dump-tables{text-align:left;margin:12px 0;padding-left:22px;}
ul.dump-tables li{margin:4px 0;}
.scope-block{border:1px solid #444;border-radius:8px;padding:16px;margin:16px 0;background:#252525;}
.scope-block h3{margin-top:0;color:#dcdcaa;font-size:15px;}
.dump-options{margin:16px 0;padding:12px;background:#1e1e1e;border-radius:6px;border:1px solid #444;}
.dump-options label{display:block;margin:8px 0;cursor:pointer;}
.dump-options input{margin-right:8px;vertical-align:middle;}
.dump-table-pick{margin:14px 0;}
.dump-table-pick legend{padding:0 8px;}
</style>";
$dumpOptsSent = $input->getInt('dump_opts_sent', 0) === 1;
$includeDeletesAndDrop = $dumpOptsSent
? ($input->getInt('dump_include_deletes', 0) === 1)
: true;
$preserveModuleIds = $dumpOptsSent
? ($input->getInt('dump_preserve_module_ids', 0) === 1)
: false;
if (!$confirmed) {
$root = rtrim(Uri::root(), '/');
$actionUrl = htmlspecialchars($root . '/index.php');
if ($idModule <= 0) {
$dumpSpecForUi = self::loadInstallerDumpTablesSpec($scriptPath, $installerClass);
$hasTablePick = $dumpSpecForUi !== null
&& self::getUniqueDumpTableNamesFromSpec($dumpSpecForUi) !== [];
$tableDescriptions = $hasTablePick
? []
: self::resolveDumpTableDescriptions($scriptPath, $installerClass);
echo "<html><head><meta charset='utf-8'><title>Confirm Dump</title>{$commonStyles}</head><body>";
echo "<div class='confirm-box'>";
echo "<h2>📦 Confirm Database Dump</h2>";
echo "<p>You are about to generate a database dump for:</p>";
echo "<p class='info'><strong>Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code></p>";
if ($moduleName !== $extensionName) {
echo "<p class='warning'><strong>Resolved module:</strong> <code>" . htmlspecialchars($moduleName) . "</code></p>";
}
echo '<form method="get" action="' . $actionUrl . '" class="dump-options-form">';
echo '<input type="hidden" name="option" value="com_nobossajax">';
echo '<input type="hidden" name="library" value="noboss.src.Util.NbInstallScriptUtil">';
echo '<input type="hidden" name="method" value="dumpExtension">';
echo '<input type="hidden" name="extensionName" value="' . htmlspecialchars($extensionName, ENT_COMPAT, 'UTF-8') . '">';
echo '<input type="hidden" name="format" value="raw">';
echo '<input type="hidden" name="confirmed" value="1">';
echo '<input type="hidden" name="dump_opts_sent" value="1">';
echo '<div class="dump-options">';
echo '<label><input type="checkbox" name="dump_include_deletes" value="1" checked> Include <strong>DELETE</strong> and <strong>DROP TABLE</strong> statements (uncheck for append-only SQL)</label>';
echo '<label><input type="checkbox" name="dump_preserve_module_ids" value="1" checked> <strong>Preserve module IDs</strong> — keep literal <code>#__modules.id</code> and FK values (otherwise new AUTO_INCREMENT ids and <code>@nb_mod_*</code> variables)</label>';
echo '</div>';
echo "<div class='scope-block'>";
echo "<h3>Full extension dump</h3>";
echo '<p>Includes all data defined by the installer for this extension.</p>';
if ($hasTablePick) {
self::emitFullDumpTableSelectionFieldset($dumpSpecForUi);
} else {
echo "<ul class='dump-tables'>";
foreach ($tableDescriptions as $line) {
echo '<li>' . $line . '</li>';
}
echo '</ul>';
}
echo "<p class='warning'>Output is one SQL backup file.</p>";
echo '<p>Are you sure you want to proceed?</p>';
echo '<hr>';
echo '<button type="submit" class="btn btn-confirm">✓ Yes, generate full dump</button>';
echo '</div>';
echo '</form>';
echo "<a class='btn btn-cancel' href='javascript:history.back()'>✗ Cancel</a>";
echo "</div>";
echo "</body></html>";
exit;
}
// id_module presente: escolher escopo antes de confirmar
$dumpSpecForUi = self::loadInstallerDumpTablesSpec($scriptPath, $installerClass);
$hasTablePick = $dumpSpecForUi !== null
&& self::getUniqueDumpTableNamesFromSpec($dumpSpecForUi) !== [];
$tableDescriptions = $hasTablePick
? []
: self::resolveDumpTableDescriptions($scriptPath, $installerClass);
echo "<html><head><meta charset='utf-8'><title>Choose dump scope</title>{$commonStyles}</head><body>";
echo "<div class='confirm-box'>";
echo "<h2>📦 Choose backup scope</h2>";
echo "<p class='info'><strong>Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code></p>";
if ($moduleName !== $extensionName) {
echo "<p class='warning'><strong>Resolved module:</strong> <code>" . htmlspecialchars($moduleName) . "</code></p>";
}
echo "<p class='info'><strong>Module instance id:</strong> <code>" . (int) $idModule . "</code> — <strong>Title:</strong> " . htmlspecialchars((string) $moduleRow->title) . "</p>";
echo '<form method="get" action="' . $actionUrl . '" class="dump-options-form">';
echo '<input type="hidden" name="option" value="com_nobossajax">';
echo '<input type="hidden" name="library" value="noboss.src.Util.NbInstallScriptUtil">';
echo '<input type="hidden" name="method" value="dumpExtension">';
echo '<input type="hidden" name="extensionName" value="' . htmlspecialchars($extensionName, ENT_COMPAT, 'UTF-8') . '">';
echo '<input type="hidden" name="id_module" value="' . (int) $idModule . '">';
echo '<input type="hidden" name="format" value="raw">';
echo '<input type="hidden" name="confirmed" value="1">';
echo '<input type="hidden" name="dump_opts_sent" value="1">';
echo '<div class="dump-options">';
echo '<label><input type="checkbox" name="dump_include_deletes" value="1" checked> Include <strong>DELETE</strong> and <strong>DROP TABLE</strong> on full dump (module-only respects this for the opening DELETE block only)</label>';
echo '<label><input type="checkbox" name="dump_preserve_module_ids" value="1"> <strong>Preserve module IDs</strong> (full dump only — literal module PKs and FKs)</label>';
echo '</div>';
echo "<div class='scope-block'>";
echo "<h3>Full extension dump</h3>";
echo '<p>Includes all data defined by the installer for this extension.</p>';
if ($hasTablePick) {
self::emitFullDumpTableSelectionFieldset($dumpSpecForUi);
} else {
echo "<ul class='dump-tables'>";
foreach ($tableDescriptions as $line) {
echo '<li>' . $line . '</li>';
}
echo '</ul>';
}
echo '<button type="submit" name="dump_scope" value="full" class="btn btn-full">✓ Generate full dump</button>';
echo "</div>";
echo "<div class='scope-block'>";
echo "<h3>Module row only</h3>";
echo "<p class='warning'><code>#__modules</code>, <code>#__modules_menu</code> (when present), and <code>#__assets</code> (when present; <code>name = com_modules.module.{id}</code>) for <code>id=" . (int) $idModule . '</code> / <code>module=' . htmlspecialchars($moduleName) . "</code>. SQL runs one <strong>DELETE</strong> (matching rows in those tables) when the option above is enabled, then <strong>INSERT</strong> module (includes PK) and a single <code>modules_menu</code> row with <code>menuid=0</code> when that table exists. Zero legacy dates as <code>NULL</code>. No extension data tables.</p>";
echo '<button type="submit" name="dump_scope" value="module_only" class="btn btn-module">✓ Generate module-row-only dump</button>';
echo "</div>";
echo '<hr>';
echo "<a class='btn btn-cancel' href='javascript:history.back()'>✗ Cancel</a>";
echo '</form>';
echo "</div></body></html>";
exit;
}
// Confirmado: com id_module exige dump_scope
if ($idModule > 0) {
if ($dumpScope !== 'full' && $dumpScope !== 'module_only') {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Missing dump scope</h2>';
echo '<p>When using <code>id_module</code>, open the dump page without <code>confirmed=1</code> first and choose <strong>full</strong> or <strong>module_only</strong>.</p>';
echo '</body></html>';
exit;
}
} else {
$dumpScope = 'full';
}
$fullDumpTablesIncluded = [];
if ($dumpScope === 'module_only') {
$outPath = JPATH_ROOT . '/tmp/' . $moduleName . '/dumps/';
$fileName = date('d-m-Y_H-i') . '_module' . (int) $idModule . '.sql';
$result = self::dumpSingleModuleInstanceSql((int) $idModule, $moduleName, $outPath, $fileName, 30, [
'include_deletes_and_drop' => $includeDeletesAndDrop,
]);
} else {
if (!file_exists($scriptPath)) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: Installation Script Not Found</h2>';
echo '<p>File does not exist: <code>' . htmlspecialchars($scriptPath) . '</code></p>';
echo '</body></html>';
exit;
}
if (!class_exists($installerClass)) {
require_once $scriptPath;
}
if (!class_exists($installerClass)) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: Installer Class Not Found</h2>';
echo '<p>Class <code>' . htmlspecialchars($installerClass) . '</code> not found in <code>' . htmlspecialchars($scriptPath) . '</code></p>';
echo '</body></html>';
exit;
}
if (!method_exists($installerClass, 'getDumpTablesSpec')) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: getDumpTablesSpec Missing</h2>';
echo '<p>Installer class must define <code>getDumpTablesSpec()</code>.</p>';
echo '</body></html>';
exit;
}
/** @var array<int, array<string, mixed>> $fullSpec */
$fullSpec = array_values(array_filter((array) $installerClass::getDumpTablesSpec(), 'is_array'));
$validNameSet = [];
foreach ($fullSpec as $e) {
$n = (string) ($e['name'] ?? '');
if ($n !== '') {
$validNameSet[$n] = true;
}
}
$tables = $fullSpec;
if ($dumpOptsSent) {
$reqRaw = $input->get('dump_inc', [], 'array');
if (!is_array($reqRaw)) {
$reqRaw = ($reqRaw !== null && $reqRaw !== '') ? [$reqRaw] : [];
}
$reqMap = [];
foreach ($reqRaw as $v) {
$s = is_string($v) ? trim($v) : trim((string) $v);
if ($s !== '' && isset($validNameSet[$s])) {
$reqMap[$s] = true;
}
}
$reqList = array_keys($reqMap);
sort($reqList);
if ($reqList === []) {
echo '<html><head><meta charset="utf-8"><title>Dump Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Select at least one table</h2>';
echo '<p>Choose one or more tables under <strong>Tables to include in full dump</strong> and try again.</p>';
echo '<p><a href="javascript:history.back()">Go back</a></p>';
echo '</body></html>';
exit;
}
$pick = array_flip($reqList);
$tables = array_values(array_filter($fullSpec, static function ($entry) use ($pick) {
if (!is_array($entry)) {
return false;
}
$n = (string) ($entry['name'] ?? '');
return $n !== '' && isset($pick[$n]);
}));
if ($tables === []) {
echo '<html><head><meta charset="utf-8"><title>Dump Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Invalid table selection</h2>';
echo '<p>No matching <code>getDumpTablesSpec()</code> entries for the chosen tables.</p>';
echo '<p><a href="javascript:history.back()">Go back</a></p>';
echo '</body></html>';
exit;
}
}
$fullDumpTablesIncluded = self::getUniqueDumpTableNamesFromSpec($tables);
$outPath = JPATH_ROOT . '/tmp/' . $moduleName . '/dumps/';
$fileSuffix = '';
$allSpecTableNames = self::getUniqueDumpTableNamesFromSpec($fullSpec);
if ($allSpecTableNames !== [] && $fullDumpTablesIncluded !== []) {
$cmpA = $allSpecTableNames;
$cmpB = $fullDumpTablesIncluded;
sort($cmpA, SORT_STRING);
sort($cmpB, SORT_STRING);
if ($cmpA === $cmpB) {
$fileSuffix = '_complete';
}
}
$fileName = date('d-m-Y_H-i') . $fileSuffix . '.sql';
$fullPath = $outPath . $fileName;
$rc = new \ReflectionClass($installerClass);
if ($rc->hasProperty('lastDumpFileName')) {
$rp = $rc->getProperty('lastDumpFileName');
$rp->setAccessible(true);
if ($rp->isStatic()) {
$rp->setValue(null, $fullPath);
}
}
$result = self::dumpDataGeneric($tables, $outPath, $fileName, 30, [
'omit_modules_insert_id' => !$preserveModuleIds,
'include_deletes_and_drop' => $includeDeletesAndDrop,
]);
}
$isSuccess = (strpos($result, 'Dump generated successfully at ') === 0);
$resultStyles = "<style>
body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
h2{margin-top:0;}
.box{border-radius:8px;padding:30px;margin:40px auto;max-width:640px;}
.box-success{background:#2d2d2d;border:2px solid #4ec9b0;}
.box-error{background:#2d2d2d;border:2px solid #f48771;}
.box h2.success{color:#4ec9b0;}
.box h2.error{color:#f48771;}
.file-info{background:#252525;border-radius:4px;padding:16px;margin:20px 0;font-size:14px;}
.file-info p{margin:6px 0;}
.file-info strong{color:#dcdcaa;}
.actions{display:flex;gap:12px;margin-top:24px;flex-wrap:wrap;}
.btn{display:inline-block;font-weight:bold;padding:10px 22px;border-radius:4px;text-decoration:none;cursor:pointer;font-size:14px;border:none;font-family:inherit;}
.btn-download{background:#569cd6;color:#1e1e1e;}
.btn-download:hover{background:#78b4e8;}
.btn-list{background:#444;color:#d4d4d4;}
.btn-list:hover{background:#555;}
code{background:#1e1e1e;padding:2px 6px;border-radius:3px;color:#ce9178;font-size:13px;word-break:break-all;}
</style>";
$scopeLabel = $dumpScope === 'module_only'
? 'Module instance (#__modules + one #__modules_menu row, menuid=0)'
: 'Full extension (installer dump)';
if ($isSuccess) {
$dumpFilePath = trim(str_replace('Dump generated successfully at ', '', $result));
$dumpFileBasename = basename($dumpFilePath);
$fileSize = file_exists($dumpFilePath) ? number_format(filesize($dumpFilePath) / 1024, 2) . ' KB' : 'N/A';
$fileDate = file_exists($dumpFilePath) ? date('Y-m-d H:i:s', filemtime($dumpFilePath)) : 'N/A';
$downloadUrl = htmlspecialchars(
rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup'
. '&extensionName=' . urlencode($extensionName)
. '&downloadFileName=' . urlencode($dumpFileBasename)
. '&format=raw'
);
$listUrl = htmlspecialchars(
rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup'
. '&extensionName=' . urlencode($extensionName)
. '&format=raw'
);
echo "<html><head><meta charset='utf-8'><title>Dump Generated</title>{$resultStyles}</head><body>";
echo "<div class='box box-success'>";
echo "<h2 class='success'>✓ Dump Generated Successfully</h2>";
echo "<div class='file-info'>";
echo "<p><strong>📦 Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code></p>";
if ($moduleName !== $extensionName) {
echo "<p><strong>🔗 Resolved module:</strong> <code>" . htmlspecialchars($moduleName) . "</code></p>";
}
echo "<p><strong>Scope:</strong> " . htmlspecialchars($scopeLabel) . "</p>";
if ($dumpScope === 'full' && $fullDumpTablesIncluded !== []) {
echo '<p><strong>Tables in this dump:</strong> <code>' . htmlspecialchars(implode(', ', $fullDumpTablesIncluded)) . '</code></p>';
}
if ($dumpOptsSent) {
echo '<p><strong>DELETE / DROP TABLE:</strong> ' . ($includeDeletesAndDrop ? 'included' : 'omitted (append-only)') . '</p>';
$preserveNote = $dumpScope === 'module_only'
? ' (option applies to full dump; this export always keeps the module PK)'
: '';
echo '<p><strong>Preserve module IDs:</strong> ' . ($preserveModuleIds ? 'yes' : 'no') . htmlspecialchars($preserveNote) . '</p>';
}
if ($idModule > 0) {
echo "<p><strong>id_module:</strong> <code>" . (int) $idModule . "</code></p>";
}
echo "<p><strong>📄 File:</strong> <code>" . htmlspecialchars($dumpFileBasename) . "</code></p>";
echo "<p><strong>📅 Date:</strong> " . htmlspecialchars($fileDate) . "</p>";
echo "<p><strong>💾 Size:</strong> " . htmlspecialchars($fileSize) . "</p>";
echo "<p><strong>📁 Path:</strong> <code>" . htmlspecialchars($dumpFilePath) . "</code></p>";
echo "</div>";
echo "<div class='actions'>";
echo "<a class='btn btn-download' href='{$downloadUrl}'>⬇ Download dump</a>";
echo "<a class='btn btn-list' href='{$listUrl}'>📦 Ver lista de dumps</a>";
echo "</div>";
echo "</div>";
echo "</body></html>";
} else {
echo "<html><head><meta charset='utf-8'><title>Dump Error</title>{$resultStyles}</head><body>";
echo "<div class='box box-error'>";
echo "<h2 class='error'>✗ Dump Failed</h2>";
echo "<p><strong>📦 Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code></p>";
echo "<p><strong>Scope:</strong> " . htmlspecialchars($scopeLabel) . "</p>";
echo "<p>" . htmlspecialchars($result) . "</p>";
echo "</div>";
echo "</body></html>";
}
exit;
}
/**
* Remove um diretório e todo o seu conteúdo recursivamente.
*
* @param string $dir Caminho absoluto para o diretório a ser removido.
*
* @return bool Retorna true se o diretório foi removido com sucesso, false caso contrário.
*/
public static function removeDirectory($dir) {
if (!is_dir($dir)) {
return false;
}
$items = scandir($dir);
foreach ($items as $item) {
if ($item == '.' || $item == '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($path)) {
self::removeDirectory($path);
} else {
unlink($path);
}
}
return rmdir($dir);
}
/**
* Função para restaurar backup do banco de dados a partir de arquivo SQL dump via AJAX
*
* Esta função permite restaurar tabelas do banco de dados a partir de um arquivo de dump gerado previamente.
* Pode ser chamada diretamente com um arquivo de dump específico ou exibirá lista para seleção.
*
* REQUISITOS DE SEGURANÇA:
* - Requer autenticação como administrador do Joomla
* - Usuário deve ter permissão 'core.admin'
*
* Parâmetros da URL:
* - extensionName (obrigatório): Alias da extensão (ex: 'mod_nobosscalendar')
* - dumpFileName (opcional): Nome específico do arquivo de dump para restaurar. Se não informado, exibe lista de seleção
* - confirmed (opcional): Definido como '1' para executar restore imediatamente (após usuário confirmar via modal)
*
* URL para executar: WEBSITE/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup&extensionName=mod_nobosscalendar&format=raw
*
* @return void (exibe resultado diretamente na tela)
*/
public static function restoreBackup() {
// Verifica autenticação administrativa
self::checkAdminAuthentication();
header('Content-Type: text/html; charset=utf-8');
header('Access-Control-Allow-Origin: *');
// Obtém parâmetros da URL
$input = Factory::getApplication()->input;
$extensionName = strtolower($input->getString('extensionName', ''));
$dumpFileName = $input->getString('dumpFileName', '');
$deleteFileName = $input->getString('deleteFileName', '');
$downloadFileName = $input->getString('downloadFileName', '');
$confirmed = $input->getInt('confirmed', 0);
// Valida parâmetro obrigatório
if (empty($extensionName)) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: Missing Required Parameter</h2>';
echo '<p>The parameter <code>extensionName</code> is required.</p>';
echo '<p>Example: <code>...&extensionName=mod_nobosscalendar</code></p>';
echo '</body></html>';
exit;
}
// Quando for um componente (com_*), resolve para o módulo de mesmo nome (mod_*)
if (strpos($extensionName, 'com_') === 0) {
$moduleName = 'mod_' . substr($extensionName, 4);
} elseif (strpos($extensionName, 'mod_') === 0) {
$moduleName = $extensionName;
} else {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">Unsupported Extension Type</h2>';
echo '<p>This function currently supports module (<code>mod_*</code>) and component (<code>com_*</code>) extensions.</p>';
echo '<p>Received: <code>' . htmlspecialchars($extensionName) . '</code></p>';
echo '</body></html>';
exit;
}
// Monta caminho do diretório de dumps: /caminho/completo/do/site/tmp/mod_nobosscalendar/dumps/
$dumpDir = JPATH_ROOT . '/tmp/' . $moduleName . '/dumps/';
// Verifica se diretório de dumps existe
if (!is_dir($dumpDir)) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: Dump Directory Not Found</h2>';
echo '<p>Directory does not exist: <code>' . htmlspecialchars($dumpDir) . '</code></p>';
echo '<p>No backup files available for this extension.</p>';
echo '</body></html>';
exit;
}
// Download de dump solicitado
if (!empty($downloadFileName)) {
$fileToDownload = $dumpDir . basename($downloadFileName);
if (!file_exists($fileToDownload)) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: Dump File Not Found</h2>';
echo '<p>File does not exist: <code>' . htmlspecialchars($fileToDownload) . '</code></p>';
echo '</body></html>';
exit;
}
$safeBasename = basename($fileToDownload);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safeBasename . '"');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($fileToDownload));
ob_clean();
flush();
readfile($fileToDownload);
exit;
}
// Exclusão de dump solicitada
if (!empty($deleteFileName)) {
$fileToDelete = $dumpDir . basename($deleteFileName);
if (!file_exists($fileToDelete)) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: Dump File Not Found</h2>';
echo '<p>File does not exist: <code>' . htmlspecialchars($fileToDelete) . '</code></p>';
echo '</body></html>';
exit;
}
if (unlink($fileToDelete)) {
$listUrl = htmlspecialchars(rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup&extensionName=' . urlencode($extensionName) . '&format=raw');
echo '<html><head><meta charset="utf-8"><title>Dump Deleted</title>';
echo '<style>body{font-family:monospace;background:#1e1e1e;color:#d4d4d4;padding:20px;}.success{color:#4ec9b0;}.info{color:#569cd6;}hr{border-color:#444;}a{color:#4ec9b0;}</style>';
echo '</head><body>';
echo '<h2 class="success">✓ Dump Deleted Successfully</h2><hr>';
echo '<p class="info">File <strong>' . htmlspecialchars(basename($deleteFileName)) . '</strong> has been deleted.</p>';
echo '<p><a href="' . $listUrl . '">← Back to dump list</a></p>';
echo '</body></html>';
} else {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: Could Not Delete File</h2>';
echo '<p>Failed to delete: <code>' . htmlspecialchars($fileToDelete) . '</code></p>';
echo '</body></html>';
}
exit;
}
// Determina qual arquivo de dump usar
$targetDumpFile = '';
if (!empty($dumpFileName)) {
// Usa arquivo específico fornecido como parâmetro
$targetDumpFile = $dumpDir . basename($dumpFileName);
if (!file_exists($targetDumpFile)) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: Dump File Not Found</h2>';
echo '<p>File does not exist: <code>' . htmlspecialchars($targetDumpFile) . '</code></p>';
echo '</body></html>';
exit;
}
} else {
// Encontra arquivos .sql no diretório
$sqlFiles = glob($dumpDir . '*.sql');
if (empty($sqlFiles)) {
echo '<html><head><meta charset="utf-8"><title>Error</title></head><body>';
echo '<h2 style="color:#f48771;">✗ Error: No Dump Files Found</h2>';
echo '<p>No SQL dump files found in: <code>' . htmlspecialchars($dumpDir) . '</code></p>';
echo '</body></html>';
exit;
}
// Ordena por data de modificação (mais recente primeiro)
usort($sqlFiles, function($a, $b) {
return filemtime($b) - filemtime($a);
});
// Exibe lista de arquivos para o usuário escolher
echo "<html><head><meta charset='utf-8'><title>Select Backup File</title>";
echo "<style>
body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
h2{color:#569cd6;border-bottom:2px solid #444;padding-bottom:15px;}
.info{background:#2d2d3d;border-left:4px solid #569cd6;padding:15px;margin:20px 0;border-radius:4px;}
.file-list{background:#2d2d2d;border-radius:8px;padding:20px;margin:20px 0;}
.file-item{background:#252525;border-radius:4px;padding:15px;margin:10px 0;border:1px solid #444;}
.file-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;}
.file-name{font-weight:bold;font-size:15px;color:#d4d4d4;}
.file-meta{color:#808080;font-size:14px;margin-top:10px;}
.file-meta strong{color:#dcdcaa;}
.badge{background:#4ec9b0;color:#1e1e1e;padding:2px 8px;border-radius:3px;font-size:12px;font-weight:bold;margin-left:8px;}
.file-actions{display:flex;gap:8px;flex-shrink:0;}
.btn{display:inline-block;font-weight:bold;padding:7px 16px;border-radius:4px;text-decoration:none;cursor:pointer;font-size:13px;border:none;font-family:inherit;}
.btn-restore{background:#4ec9b0;color:#1e1e1e;}
.btn-restore:hover{background:#6eddc8;}
.btn-download{background:#569cd6;color:#1e1e1e;}
.btn-download:hover{background:#78b4e8;}
.btn-delete{background:#f48771;color:#1e1e1e;}
.btn-delete:hover{background:#ff6a50;}
code{background:#1e1e1e;padding:2px 6px;border-radius:3px;color:#ce9178;}
</style></head><body>";
echo "<h2>📦 Select Backup File</h2>";
echo "<div class='info'>";
echo "<strong>📦 Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code><br>";
echo "<strong>📁 Directory:</strong> <code>" . htmlspecialchars($dumpDir) . "</code><br>";
echo "<strong>📄 Available files:</strong> " . count($sqlFiles);
echo "</div>";
echo "<div class='file-list'>";
$currentUrl = Uri::getInstance();
$baseUrl = $currentUrl->toString(['scheme', 'host', 'port', 'path', 'query']);
foreach ($sqlFiles as $index => $filePath) {
$fileName = basename($filePath);
$fileSize = filesize($filePath);
$fileDate = date('Y-m-d H:i:s', filemtime($filePath));
$fileSizeKB = number_format($fileSize / 1024, 2);
$restoreUrl = htmlspecialchars($baseUrl . '&dumpFileName=' . urlencode($fileName));
$downloadUrl = htmlspecialchars($baseUrl . '&downloadFileName=' . urlencode($fileName));
$deleteUrl = htmlspecialchars($baseUrl . '&deleteFileName=' . urlencode($fileName));
$badge = ($index === 0) ? '<span class="badge">MOST RECENT</span>' : '';
echo "<div class='file-item'>";
echo "<div class='file-header'>";
echo "<span class='file-name'>📄 " . htmlspecialchars($fileName) . $badge . "</span>";
echo "<div class='file-actions'>";
echo "<a class='btn btn-restore' href='{$restoreUrl}'>▶ Restaurar dump</a>";
echo "<a class='btn btn-download' href='{$downloadUrl}'>⬇ Baixar dump</a>";
echo "<a class='btn btn-delete' href='{$deleteUrl}' onclick=\"return confirm('Excluir o dump "" . htmlspecialchars($fileName, ENT_QUOTES) . ""? Esta ação não pode ser desfeita.')\">✗ Excluir dump</a>";
echo "</div>";
echo "</div>";
echo "<div class='file-meta'>";
echo "<strong>📅 Date:</strong> " . htmlspecialchars($fileDate) . " | ";
echo "<strong>💾 Size:</strong> " . $fileSizeKB . " KB";
echo "</div>";
echo "</div>";
}
echo "</div>";
echo "</body></html>";
exit;
}
$dumpFileBasename = basename($targetDumpFile);
$dumpFileSize = filesize($targetDumpFile);
$dumpFileDate = date('Y-m-d H:i:s', filemtime($targetDumpFile));
// Se não confirmado, exibe modal de confirmação
if (!$confirmed) {
echo "<html><head><meta charset='utf-8'><title>Confirm Backup Restore</title>";
echo "<style>
body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#1e1e1e;color:#d4d4d4;padding:0;margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;}
.modal{background:#2d2d2d;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5);max-width:600px;width:90%;padding:30px;}
h2{margin-top:0;color:#569cd6;border-bottom:2px solid #444;padding-bottom:15px;}
.warning{background:#3d3d2d;border-left:4px solid #dcdcaa;padding:15px;margin:20px 0;border-radius:4px;}
.info{background:#2d2d3d;border-left:4px solid #569cd6;padding:15px;margin:20px 0;border-radius:4px;}
.file-info{background:#252525;padding:15px;border-radius:4px;margin:15px 0;}
.file-info strong{color:#4ec9b0;}
.buttons{display:flex;gap:15px;justify-content:center;margin-top:25px;}
button{padding:12px 30px;border:none;border-radius:4px;font-size:16px;cursor:pointer;font-weight:bold;transition:all 0.3s;}
.btn-confirm{background:#4ec9b0;color:#1e1e1e;}
.btn-confirm:hover{background:#6eddc8;}
.btn-download{background:#569cd6;color:#1e1e1e;}
.btn-download:hover{background:#78b4e8;}
.btn-cancel{background:#555;color:#d4d4d4;}
.btn-cancel:hover{background:#666;}
code{background:#1e1e1e;padding:2px 6px;border-radius:3px;color:#ce9178;}
</style></head><body>";
echo "<div class='modal'>";
echo "<h2>⚠️ Confirm Database Restore</h2>";
echo "<div class='warning'>";
echo "<strong>⚠️ WARNING:</strong> This action will restore database tables from a backup file. ";
echo "This operation will overwrite current data in the affected tables.";
echo "</div>";
echo "<div class='info'>";
echo "<strong>📦 Extension:</strong> <code>" . htmlspecialchars($extensionName) . "</code>";
echo "</div>";
echo "<div class='file-info'>";
echo "<strong>📄 Dump File:</strong> " . htmlspecialchars($dumpFileBasename) . "<br>";
echo "<strong>📅 Created:</strong> " . htmlspecialchars($dumpFileDate) . "<br>";
echo "<strong>💾 Size:</strong> " . number_format($dumpFileSize / 1024, 2) . " KB<br>";
echo "<strong>📁 Path:</strong> <code>" . htmlspecialchars($targetDumpFile) . "</code>";
echo "</div>";
echo "<div class='info'>";
echo "<strong>ℹ️ Note:</strong> It is recommended to create a new backup before restoring an old one.";
echo "</div>";
// Monta URL de confirmação e de download
$currentUrl = Uri::getInstance();
$confirmUrl = $currentUrl->toString() . '&confirmed=1';
$baseListUrl = rtrim(Uri::root(), '/') . '/index.php?option=com_nobossajax&library=noboss.src.Util.NbInstallScriptUtil&method=restoreBackup&extensionName=' . urlencode($extensionName) . '&format=raw';
$downloadUrl = htmlspecialchars($baseListUrl . '&downloadFileName=' . urlencode($dumpFileBasename));
echo "<div class='buttons'>";
echo "<button class='btn-confirm' onclick=\"window.location.href='" . htmlspecialchars($confirmUrl) . "'\">✓ Confirm Restore</button>";
echo "<a class='btn-download' style='padding:12px 30px;font-size:16px;font-weight:bold;border-radius:4px;text-decoration:none;' href='{$downloadUrl}'>⬇ Download dump</a>";
echo "<button class='btn-cancel' onclick='window.history.back();'>✗ Cancel</button>";
echo "</div>";
echo "</div>";
echo "</body></html>";
exit;
}
// Usuário confirmou - prossegue com restore
echo "<html><head><meta charset='utf-8'><title>Restoring Backup</title>";
echo "<style>
body{font-family:monospace;background:#1e1e1e;color:#d4d4d4;padding:20px;line-height:1.6;}
.success{color:#4ec9b0;}.error{color:#f48771;}.warning{color:#dcdcaa;}.info{color:#569cd6;}
pre{background:#2d2d2d;padding:15px;border-radius:5px;overflow-x:auto;max-height:400px;}
hr{border-color:#444;margin:20px 0;}
.progress{background:#2d2d2d;padding:10px;border-radius:5px;margin:10px 0;}
</style></head><body>";
echo "<h2>🔄 Restoring Database Backup</h2>";
echo "<hr>";
echo "<p class='info'>📦 Extension: " . htmlspecialchars($extensionName) . "</p>";
echo "<p class='info'>📄 Dump File: " . htmlspecialchars($dumpFileBasename) . "</p>";
echo "<hr>";
try {
// Lê conteúdo do arquivo SQL
echo "<p class='info'>📖 Reading SQL file...</p>";
$sqlContent = file_get_contents($targetDumpFile);
if ($sqlContent === false) {
throw new \Exception('Failed to read dump file');
}
echo "<p class='success'>✓ File read successfully (" . number_format(strlen($sqlContent) / 1024, 2) . " KB)</p>";
// Obtém conexão com o banco de dados
echo "<p class='info'>🔌 Connecting to database...</p>";
$db = Factory::getContainer()->get(DatabaseInterface::class);
echo "<p class='success'>✓ Database connection established</p>";
// Remove comment lines first (line by line)
echo "<p class='info'>⚙️ Removing comment lines...</p>";
$lines = explode("\n", $sqlContent);
$cleanedLines = array_filter($lines, function($line) {
$trimmed = trim($line);
// Skip empty lines and comment lines
return !empty($trimmed) &&
!preg_match('/^(--|#)/', $trimmed) &&
!preg_match('/^\/\*/', $trimmed);
});
$cleanedContent = implode("\n", $cleanedLines);
// Divide SQL em statements individuais (separados por ponto e vírgula)
echo "<p class='info'>⚙️ Parsing SQL statements...</p>";
$statements = array_filter(
array_map('trim', preg_split('/;[\r\n]+/', $cleanedContent)),
function($stmt) {
return !empty($stmt);
}
);
$totalStatements = count($statements);
echo "<p class='success'>✓ Found {$totalStatements} SQL statements to execute</p>";
echo "<hr>";
// Executa cada statement
echo "<p class='info'>🚀 Executing SQL statements...</p>";
echo "<div class='progress'>";
$executedCount = 0;
$errors = [];
foreach ($statements as $index => $statement) {
try {
$db->setQuery($statement);
$db->execute();
$executedCount++;
// Registra progresso a cada 10 statements
if (($executedCount % 10 == 0) || ($executedCount == $totalStatements)) {
$percentage = round(($executedCount / $totalStatements) * 100, 1);
echo "<p>Progress: {$executedCount}/{$totalStatements} statements ({$percentage}%)</p>";
flush();
ob_flush();
}
} catch (\Exception $e) {
$errors[] = [
'statement_num' => $index + 1,
'error' => $e->getMessage(),
'statement' => substr($statement, 0, 100) . '...'
];
}
}
echo "</div>";
echo "<hr>";
// Exibe resultados
if (empty($errors)) {
echo "<h3 class='success'>✓ RESTORE COMPLETED SUCCESSFULLY!</h3>";
echo "<p class='success'>All {$executedCount} SQL statements executed successfully.</p>";
} else {
echo "<h3 class='warning'>⚠️ RESTORE COMPLETED WITH ERRORS</h3>";
echo "<p class='success'>Successfully executed: {$executedCount}/{$totalStatements} statements</p>";
echo "<p class='error'>Failed: " . count($errors) . " statements</p>";
echo "<hr>";
echo "<h4 class='error'>Errors:</h4>";
echo "<pre class='error'>";
foreach ($errors as $error) {
echo "Statement #{$error['statement_num']}:\n";
echo "Error: {$error['error']}\n";
echo "SQL: {$error['statement']}\n\n";
}
echo "</pre>";
}
} catch (\Exception $e) {
echo "<hr>";
echo "<h3 class='error'>✗ RESTORE FAILED</h3>";
echo "<pre class='error'>" . htmlspecialchars($e->getMessage()) . "</pre>";
echo "<p class='error'><strong>File:</strong> " . htmlspecialchars($e->getFile()) . "</p>";
echo "<p class='error'><strong>Line:</strong> " . $e->getLine() . "</p>";
}
echo "<hr>";
echo "<p><em>Execution finished at " . date('Y-m-d H:i:s') . "</em></p>";
echo "</body></html>";
exit;
}
}