Current File : /home/pacjaorg/www/nsa/plugins/task/akeebabackup/src/Extension/AkeebaBackup.php |
<?php
/**
* @package akeebabackup
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Joomla\Plugin\Task\AkeebaBackup\Extension;
defined('_JEXEC') or die;
use Akeeba\Component\AkeebaBackup\Administrator\CliCommands\MixIt\InitialiseEngine;
use Akeeba\Component\AkeebaBackup\Administrator\Model\BackupModel;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Exception;
use InvalidArgumentException;
use Joomla\Application\ApplicationInterface;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Filesystem\Path;
use Joomla\CMS\Form\Form;
use Joomla\CMS\MVC\Factory\MVCFactoryServiceInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent;
use Joomla\Component\Scheduler\Administrator\Task\Status;
use Joomla\Component\Scheduler\Administrator\Task\Task;
use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\ParameterType;
use Joomla\Event\EventInterface;
use Joomla\Event\SubscriberInterface;
use Joomla\Registry\Registry;
use RuntimeException;
use function array_key_exists;
use function strlen;
/**
* The Akeeba Backup task plugin.
*
* @since 9.2.0
*/
class AkeebaBackup extends CMSPlugin implements SubscriberInterface
{
use TaskPluginTrait;
use InitialiseEngine;
private const TASKS_MAP = [
'akeebabackup.backup' => [
'langConstPrefix' => 'PLG_TASK_AKEEBABACKUP_TASK_BACKUP',
'method' => 'takeBackup',
'form' => 'backupTaskForm',
],
'akeebabackup.clibackup' => [
'langConstPrefix' => 'PLG_TASK_AKEEBABACKUP_TASK_CLIBACKUP',
'method' => 'takeCLIBackup',
'form' => 'cliBackupTaskForm',
],
];
/**
* The application under which we are running.
*
* @var ApplicationInterface
* @since 9.2.0
*/
protected $app;
/**
* Affects constructor behavior. If true, language files will be loaded automatically.
*
* @var boolean
* @since 9.2.0
*/
protected $autoloadLanguage = true;
/**
* The application's database driver object
*
* @var DatabaseDriver
* @since 9.2.0
*/
protected $db;
/**
* A registry object to keep track of parameters between subsequent calls of the resumable task.
*
* @var Registry
* @since 9.2.0
*/
private $taskInfoRegistry;
/**
* Returns an array of events this subscriber will listen to.
*
* This is mostly boilerplate code as per every built-in Task plugin in Joomla.
*
* @return array
* @since 9.2.0
*/
public static function getSubscribedEvents(): array
{
// This task is disabled if the Akeeba Backup component is not installed or has been unpublished
if (!ComponentHelper::isEnabled('com_akeebabackup'))
{
return [];
}
return [
'onTaskOptionsList' => 'advertiseRoutines',
'onExecuteTask' => 'standardRoutineHandler',
'onContentPrepareForm' => 'enhanceTaskItemForm',
];
}
/**
* Enhance the task form with routine-specific fields from an XML file declared through the TASKS_MAP constant.
* If a plugin only supports the task form and does not need additional logic, this method can be mapped to the
* `onContentPrepareForm` event through {@see SubscriberInterface::getSubscribedEvents()} and will take care
* of injecting the fields without additional logic in the plugin class.
*
* @param EventInterface|Form $context The onContentPrepareForm event or the Form object.
* @param mixed $data The form data, required when $context is a {@see Form} instance.
*
* @return boolean True if the form was successfully enhanced or the context was not relevant.
*
* @throws Exception
* @since 9.2.0
*/
public function enhanceTaskItemForm($context, $data = null): bool
{
if ($context instanceof EventInterface)
{
/** @var Form $form */
$form = $context->getArgument('0');
$data = $context->getArgument('1');
}
elseif ($context instanceof Form)
{
$form = $context;
}
else
{
throw new InvalidArgumentException(
sprintf(
'Argument 0 of %1$s must be an instance of %2$s or %3$s',
__METHOD__,
EventInterface::class,
Form::class
)
);
}
if ($form->getName() !== 'com_scheduler.task')
{
return true;
}
$routineId = $this->getRoutineId($form, $data);
$isSupported = array_key_exists($routineId, self::TASKS_MAP);
$enhancementFormName = self::TASKS_MAP[$routineId]['form'] ?? '';
// Return if routine is not supported by the plugin or the routine does not have a form linked in TASKS_MAP.
if (!$isSupported || strlen($enhancementFormName) === 0)
{
return true;
}
// We expect the form XML in "{PLUGIN_PATH}/forms/{FORM_NAME}.xml"
$path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name;
$enhancementFormFile = $path . '/forms/' . $enhancementFormName . '.xml';
try
{
$enhancementFormFile = Path::check($enhancementFormFile);
}
catch (Exception $e)
{
return false;
}
if (is_file($enhancementFormFile))
{
return $form->loadFile($enhancementFormFile);
}
return false;
}
/**
* Get an #__akeebabackup_storage key for the task being executed by the event.
*
* This is used to store the temporary information which survives consecutive calls to the resumable task.
*
* @param ExecuteTaskEvent $event
*
* @return string
* @since 9.2.0
*/
private function getTaskKey(ExecuteTaskEvent $event): string
{
return 'task.' . $event->getRoutineId() . '.' . $event->getTaskId();
}
/**
* Load the temporary information for the resumable task being executed by the event.
*
* @param ExecuteTaskEvent $event
*
* @return void
* @since 9.2.0
*/
private function loadTaskRegistry(ExecuteTaskEvent $event): void
{
$key = $this->getTaskKey($event);
try
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('data'))
->from($db->quoteName('#__akeebabackup_storage'))
->where($db->quoteName('tag') . ' = :key')
->bind(':key', $key, ParameterType::STRING);
$json = $db->setQuery($query)->loadResult() ?: null;
}
catch (Exception $e)
{
$json = null;
}
$this->taskInfoRegistry = new Registry($json);
$this->removeTaskRegistry($event);
}
/**
* Remove the temporary information for the resumable task being executed by the event.
*
* @param ExecuteTaskEvent $event
*
* @return void
* @since 9.2.0
*/
private function removeTaskRegistry(ExecuteTaskEvent $event): void
{
$key = $this->getTaskKey($event);
try
{
$db = $this->db;
$query = $db->getQuery(true)
->delete($db->quoteName('#__akeebabackup_storage'))
->where($db->quoteName('tag') . ' = :key')
->bind(':key', $key, ParameterType::STRING);
$db->setQuery($query)->execute();
}
catch (Exception $e)
{
// No worries.
}
}
/**
* Save the temporary information for the resumable task being executed by the event.
*
* @param ExecuteTaskEvent $event
*
* @return void
* @since 9.2.0
*/
private function saveTaskRegistry(ExecuteTaskEvent $event): void
{
$this->removeTaskRegistry($event);
$key = $this->getTaskKey($event);
try
{
$o = (object) [
'tag' => $key,
'data' => $this->taskInfoRegistry->toString(),
];
$this->db->insertObject('#__akeebabackup_storage', $o, 'tag');
}
catch (Exception $e)
{
// No worries.
}
}
/**
* Run a backup using a resumable task
*
* @param ExecuteTaskEvent $event
*
* @return int
* @throws Exception
* @since 9.2.0
*/
private function takeBackup(ExecuteTaskEvent $event): int
{
// Get some basic information about the task at hand.
/** @var Task $task */
$task = $event->getArgument('subject');
$profile = (int) $event->getArgument('params')->profileid ?? 1;
// Make sure Akeeba Backup is installed and enabled.
$component = ComponentHelper::isEnabled('com_akeebabackup')
? JoomlaFactory::getApplication()->bootComponent('com_akeebabackup')
: null;
if (!($component instanceof MVCFactoryServiceInterface))
{
throw new RuntimeException('The Akeeba Backup component is not installed or has been disabled.');
}
if (!defined('AKEEBA_BACKUP_ORIGIN'))
{
define('AKEEBA_BACKUP_ORIGIN', 'joomla');
}
// Make sure $profile is a positive integer >= 1
$profile = max(1, $profile);
// Set the active profile
define('AKEEBA_PROFILE', $profile);
$this->initialiseComponent($this->app);
/**
* DO NOT REMOVE!
*
* The Model will only try to load the configuration after nuking the factory. This causes Profile 1 to be
* loaded first. Then it figures out it needs to load a different profile and it does – but the protected keys
* are NOT replaced, meaning that certain configuration parameters are not replaced. Most notably, the chain.
* This causes backups to behave weirdly. So, DON'T REMOVE THIS UNLESS WE REFACTOR THE MODEL.
*/
Platform::getInstance()->load_configuration($profile);
// Set up the backup model
$factory = $component->getMVCFactory();
/** @var BackupModel $model */
$model = $factory->createModel('Backup', 'Administrator', ['ignore_request' => true]);
$model->setState('profile', $profile);
// Am I resuming a backup or starting a new one?
if ($task->get('last_exit_code', Status::OK) == Status::WILL_RESUME)
{
$this->logTask(sprintf('Resuming task %d', $task->get('id')));
$this->loadTaskRegistry($event);
$backupTag = $this->taskInfoRegistry->get('tag', null);
$backupId = $this->taskInfoRegistry->get('backup_id', null);
if (empty($backupTag) || empty($backupId))
{
throw new RuntimeException(sprintf('Cannot resume backup task #%d; the temporary backup information is missing.', $event->getTaskId()));
}
$model->setState('tag', $backupTag);
$model->setState('backupid', $backupId);
$backupResult = $model->stepBackup(true);
}
else
{
$this->taskInfoRegistry = new Registry();
$this->logTask(sprintf('Starting new task %d', $task->get('id')));
$model->setState('tag', 'joomla');
$model->setState('comment', 'Backup taken automatically using the Joomla Scheduled Tasks feature.');
$timeStart = microtime(true);
$backupResult = $model->startBackup();
$timeEnd = microtime(true);
$willResume = ($backupResult['HasRun'] ?: 0) != 1;
$error = $backupResult['Error'] ?? '';
if (($timeEnd - $timeStart < 0.5) && empty($error) && $willResume)
{
$backupResult = $model->stepBackup(true);
}
}
// Did the backup end in an error?
if (isset($backupResult['Error']) && !empty($backupResult['Error']))
{
$this->removeTaskRegistry($event);
throw new RuntimeException($backupResult['Error']);
}
// Should I resume the backup?
$willResume = ($backupResult['HasRun'] ?: 0) != 1;
if ($willResume)
{
$this->logTask(sprintf('Backup at %0.2f%%; will resume next time this scheduled task is told to run.', $backupResult['Progress'] ?: 0.0));
$this->taskInfoRegistry->set('tag', 'joomla');
$this->taskInfoRegistry->set('backup_id', $model->getState('backupid', null));
$this->saveTaskRegistry($event);
}
else
{
$this->logTask('Backup completed successfully.');
$this->removeTaskRegistry($event);
}
// Should I log any warnings?
if (isset($backupResult['Warnings']) && !empty($backupResult['Warnings']))
{
$this->logTask('Backup step generated warnings');
foreach ($backupResult['Warnings'] as $warning)
{
$this->logTask($warning, 'warning');
}
}
return $willResume ? Status::WILL_RESUME : Status::OK;
}
/**
* Run a backup with an all–at–once, CLI–only task.
*
* This is similar to using Joomla's CLI application to run a backup, albeit with far less flexibility.
*
* @param ExecuteTaskEvent $event
*
* @return int
* @throws Exception
* @since 9.2.0
*/
private function takeCLIBackup(ExecuteTaskEvent $event): int
{
// Get some basic information about the task at hand.
/** @var Task $task */
$task = $event->getArgument('subject');
$profile = (int) $event->getArgument('params')->profileid ?? 1;
if (!defined('AKEEBA_BACKUP_ORIGIN'))
{
define('AKEEBA_BACKUP_ORIGIN', 'joomla');
}
// Make sure Akeeba Backup is installed and enabled.
$component = ComponentHelper::isEnabled('com_akeebabackup')
? JoomlaFactory::getApplication()->bootComponent('com_akeebabackup')
: null;
if (!($component instanceof MVCFactoryServiceInterface))
{
throw new RuntimeException('The Akeeba Backup component is not installed or has been disabled.');
}
// Make sure $profile is a positive integer >= 1
$profile = max(1, $profile);
// Set the active profile
define('AKEEBA_PROFILE', $profile);
$this->initialiseComponent($this->app);
/**
* DO NOT REMOVE!
*
* The Model will only try to load the configuration after nuking the factory. This causes Profile 1 to be
* loaded first. Then it figures out it needs to load a different profile and it does – but the protected keys
* are NOT replaced, meaning that certain configuration parameters are not replaced. Most notably, the chain.
* This causes backups to behave weirdly. So, DON'T REMOVE THIS UNLESS WE REFACTOR THE MODEL.
*/
Platform::getInstance()->load_configuration($profile);
// Set up the backup model
$factory = $component->getMVCFactory();
/** @var BackupModel $model */
$model = $factory->createModel('Backup', 'Administrator', ['ignore_request' => true]);
$model->setState('profile', $profile);
$this->logTask(sprintf('Starting new task %d', $task->get('id')));
$model->setState('tag', 'joomla');
$model->setState('description', $model->getDefaultDescription() . ' (Joomla Scheduled Tasks, CLI-only task)');
$model->setState('comment', 'Backup taken automatically using the Joomla Scheduled Tasks feature.');
// Dummy backupResult so that the loop iterates once
$backupResult = [
'HasRun' => 0,
'Error' => '',
'cli_firstrun' => 1,
];
$hasWarnings = false;
while (($backupResult['HasRun'] != 1) && (empty($backupResult['Error'])))
{
if (isset($backupResult['cli_firstrun']) && $backupResult['cli_firstrun'])
{
$this->logTask(sprintf('Starting a new backup with profile %u.', $profile));
$backupResult = $model->startBackup([
'akeeba.tuning.min_exec_time' => 0,
'akeeba.tuning.max_exec_time' => 15,
'akeeba.tuning.run_time_bias' => 100,
'akeeba.advanced.autoresume' => 0,
'akeeba.tuning.nobreak.beforelargefile' => 1,
'akeeba.tuning.nobreak.afterlargefile' => 1,
'akeeba.tuning.nobreak.proactive' => 1,
'akeeba.tuning.nobreak.finalization' => 1,
'akeeba.tuning.settimelimit' => 0,
'akeeba.tuning.setmemlimit' => 1,
'akeeba.tuning.nobreak.domains' => 0,
]);
}
else
{
$this->logTask('Continuing the backup');
$backupResult = $model->stepBackup();
}
// Print the new progress bar and info
$this->logTask(sprintf('Last tick: %s', date('Y-m-d H:i:s \G\M\TO (T)')), 'debug');
$this->logTask(sprintf('Domain: %s', $backupResult['Domain'] ?? ''), 'debug');
$this->logTask(sprintf('Step: %s', $backupResult['Step'] ?? ''), 'debug');
$this->logTask(sprintf('Substep: %s', $backupResult['Substep'] ?? ''), 'debug');
$this->logTask(sprintf('Progress: %s', $backupResult['Progress'] ?? 0.0), 'debug');
// Output any warnings
if (!empty($backupResult['Warnings']))
{
$hasWarnings = true;
foreach ($backupResult['Warnings'] as $warning)
{
$this->logTask($warning, 'warning');
}
}
// Recycle the database connection to minimise problems with database timeouts
$db = Factory::getDatabase();
$db->close();
$db->open();
// Reset the backup timer
Factory::getTimer()->resetTime();
}
// Did the backup end in an error?
if (isset($backupResult['Error']) && !empty($backupResult['Error']))
{
throw new RuntimeException($backupResult['Error']);
}
if ($hasWarnings)
{
$this->logTask('Backup finished successfully, with warnings.');
}
else
{
$this->logTask('Backup finished successfully');
}
return Status::OK;
}
}