From c315434f075b67699e3cf40ebfe78c794c240de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=81Z=C3=81R=20Imre=20AI=20Agent?= Date: Thu, 16 Jul 2026 11:39:02 +0200 Subject: [PATCH] feat(deployed): add Akeeba site component (completeness) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit components/com_akeeba (frontend) — the admin side was added earlier; this completes the module. Signed-off-by: LÁZÁR Imre Assisted-by: claude-code@claude-opus-4-8 --- .../com_akeeba/Controller/Backup.php | 195 +++++++++++++ .../com_akeeba/Controller/Check.php | 62 ++++ .../components/com_akeeba/Controller/Json.php | 67 +++++ .../Controller/Mixin/ActivateProfile.php | 41 +++ .../Controller/Mixin/CustomRedirection.php | 36 +++ .../Controller/Mixin/FrontEndPermissions.php | 54 ++++ .../com_akeeba/Controller/index.html | 1 + .../com_akeeba/Dispatcher/Dispatcher.php | 117 ++++++++ .../components/com_akeeba/Model/Backup.php | 15 + .../components/com_akeeba/Model/Browser.php | 17 ++ .../com_akeeba/Model/DatabaseFilters.php | 17 ++ .../com_akeeba/Model/FileFilters.php | 17 ++ .../com_akeeba/Model/IncludeFolders.php | 17 ++ .../components/com_akeeba/Model/Json.php | 227 +++++++++++++++ .../com_akeeba/Model/Json/Encapsulation.php | 267 ++++++++++++++++++ .../Model/Json/Encapsulation/AesCbc128.php | 70 +++++ .../Model/Json/Encapsulation/AesCbc256.php | 70 +++++ .../Model/Json/Encapsulation/AesCtr128.php | 69 +++++ .../Model/Json/Encapsulation/AesCtr256.php | 69 +++++ .../Model/Json/Encapsulation/Base.php | 165 +++++++++++ .../Model/Json/Encapsulation/Raw.php | 92 ++++++ .../Model/Json/EncapsulationInterface.php | 84 ++++++ .../components/com_akeeba/Model/Json/Task.php | 124 ++++++++ .../Model/Json/Task/AbstractTask.php | 72 +++++ .../com_akeeba/Model/Json/Task/Browse.php | 65 +++++ .../com_akeeba/Model/Json/Task/Delete.php | 56 ++++ .../Model/Json/Task/DeleteFiles.php | 56 ++++ .../Model/Json/Task/DeleteProfile.php | 60 ++++ .../com_akeeba/Model/Json/Task/Download.php | 101 +++++++ .../Model/Json/Task/DownloadDirect.php | 161 +++++++++++ .../Model/Json/Task/ExportConfiguration.php | 72 +++++ .../Model/Json/Task/GetBackupInfo.php | 84 ++++++ .../Model/Json/Task/GetDBEntities.php | 67 +++++ .../Model/Json/Task/GetDBFilters.php | 67 +++++ .../com_akeeba/Model/Json/Task/GetDBRoots.php | 56 ++++ .../Model/Json/Task/GetFSEntities.php | 82 ++++++ .../Model/Json/Task/GetFSFilters.php | 68 +++++ .../com_akeeba/Model/Json/Task/GetFSRoots.php | 57 ++++ .../Model/Json/Task/GetGUIConfiguration.php | 54 ++++ .../Model/Json/Task/GetIncludedDBs.php | 57 ++++ .../Json/Task/GetIncludedDirectories.php | 57 ++++ .../Model/Json/Task/GetProfiles.php | 50 ++++ .../Model/Json/Task/GetRegexDBFilters.php | 68 +++++ .../Model/Json/Task/GetRegexFSFilters.php | 68 +++++ .../com_akeeba/Model/Json/Task/GetVersion.php | 46 +++ .../Model/Json/Task/ImportConfiguration.php | 60 ++++ .../Model/Json/Task/ListBackups.php | 50 ++++ .../com_akeeba/Model/Json/Task/Log.php | 45 +++ .../Model/Json/Task/RemoveIncludedDB.php | 68 +++++ .../Json/Task/RemoveIncludedDirectory.php | 68 +++++ .../Model/Json/Task/SaveConfiguration.php | 72 +++++ .../Model/Json/Task/SaveProfile.php | 90 ++++++ .../Model/Json/Task/SetDBFilter.php | 95 +++++++ .../Model/Json/Task/SetFSFilter.php | 112 ++++++++ .../Model/Json/Task/SetIncludedDB.php | 85 ++++++ .../Model/Json/Task/SetIncludedDirectory.php | 103 +++++++ .../Model/Json/Task/SetRegexDBFilter.php | 95 +++++++ .../Model/Json/Task/SetRegexFSFilter.php | 95 +++++++ .../Model/Json/Task/StartBackup.php | 89 ++++++ .../com_akeeba/Model/Json/Task/StepBackup.php | 85 ++++++ .../Model/Json/Task/TestDBConnection.php | 57 ++++ .../Model/Json/Task/UpdateGetInformation.php | 50 ++++ .../com_akeeba/Model/Json/TaskInterface.php | 44 +++ .../com_akeeba/Model/MultipleDatabases.php | 17 ++ .../components/com_akeeba/Model/Profiles.php | 17 ++ .../com_akeeba/Model/RegExDatabaseFilters.php | 17 ++ .../com_akeeba/Model/RegExFileFilters.php | 17 ++ .../com_akeeba/Model/Statistics.php | 17 ++ .../components/com_akeeba/Model/Updates.php | 17 ++ .../akeeba/components/com_akeeba/akeeba.php | 18 ++ .../akeeba/components/com_akeeba/index.html | 1 + 71 files changed, 4901 insertions(+) create mode 100644 deployed/akeeba/components/com_akeeba/Controller/Backup.php create mode 100644 deployed/akeeba/components/com_akeeba/Controller/Check.php create mode 100644 deployed/akeeba/components/com_akeeba/Controller/Json.php create mode 100644 deployed/akeeba/components/com_akeeba/Controller/Mixin/ActivateProfile.php create mode 100644 deployed/akeeba/components/com_akeeba/Controller/Mixin/CustomRedirection.php create mode 100644 deployed/akeeba/components/com_akeeba/Controller/Mixin/FrontEndPermissions.php create mode 100644 deployed/akeeba/components/com_akeeba/Controller/index.html create mode 100644 deployed/akeeba/components/com_akeeba/Dispatcher/Dispatcher.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Backup.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Browser.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/DatabaseFilters.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/FileFilters.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/IncludeFolders.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCbc128.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCbc256.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCtr128.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCtr256.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/Base.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/Raw.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/EncapsulationInterface.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/AbstractTask.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/Browse.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/Delete.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/DeleteFiles.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/DeleteProfile.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/Download.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/DownloadDirect.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/ExportConfiguration.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetBackupInfo.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBEntities.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBFilters.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBRoots.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSEntities.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSFilters.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSRoots.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetGUIConfiguration.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetIncludedDBs.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetIncludedDirectories.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetProfiles.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetRegexDBFilters.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetRegexFSFilters.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/GetVersion.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/ImportConfiguration.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/ListBackups.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/Log.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/RemoveIncludedDB.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/RemoveIncludedDirectory.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/SaveConfiguration.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/SaveProfile.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/SetDBFilter.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/SetFSFilter.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/SetIncludedDB.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/SetIncludedDirectory.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/SetRegexDBFilter.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/SetRegexFSFilter.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/StartBackup.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/StepBackup.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/TestDBConnection.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/Task/UpdateGetInformation.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Json/TaskInterface.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/MultipleDatabases.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Profiles.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/RegExDatabaseFilters.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/RegExFileFilters.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Statistics.php create mode 100644 deployed/akeeba/components/com_akeeba/Model/Updates.php create mode 100644 deployed/akeeba/components/com_akeeba/akeeba.php create mode 100644 deployed/akeeba/components/com_akeeba/index.html diff --git a/deployed/akeeba/components/com_akeeba/Controller/Backup.php b/deployed/akeeba/components/com_akeeba/Controller/Backup.php new file mode 100644 index 00000000..58b9d8b1 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Controller/Backup.php @@ -0,0 +1,195 @@ +setPredefinedTaskList(['main', 'step']); + } + + /** + * Start a front-end legacy backup + * + * @return void + */ + public function main() + { + $this->checkPermissions(); + $this->setProfile(); + + // Get the backup ID + $backupId = $this->input->get('backupid', null, 'cmd'); + + if (empty($backupId)) + { + $backupId = null; + } + + /** @var \Akeeba\Backup\Site\Model\Backup $model */ + $model = $this->container->factory->model('Backup')->tmpInstance(); + + JLoader::import('joomla.utilities.date'); + $dateNow = new Date(); + + $model->setState('tag', AKEEBA_BACKUP_ORIGIN); + $model->setState('backupid', $backupId); + $model->setState('description', JText::_('COM_AKEEBA_BACKUP_DEFAULT_DESCRIPTION') . ' ' . $dateNow->format(JText::_('DATE_FORMAT_LC2'), true)); + $model->setState('comment', ''); + + $array = $model->startBackup(); + + $backupId = $model->getState('backupid', null, 'cmd'); + + $this->processEngineReturnArray($array, $backupId); + } + + /** + * Step through a front-end legacy backup + * + * @return void + */ + public function step() + { + // Setup + $this->checkPermissions(); + $this->setProfile(); + + // Get the backup ID + $backupId = $this->input->get('backupid', null, 'cmd'); + + if (empty($backupId)) + { + $backupId = null; + } + + /** @var \Akeeba\Backup\Site\Model\Backup $model */ + $model = $this->container->factory->model('Backup')->tmpInstance(); + + $model->setState('tag', AKEEBA_BACKUP_ORIGIN); + $model->setState('backupid', $backupId); + + $array = $model->stepBackup(); + + $backupId = $model->getState('backupid', null, 'cmd'); + + $this->processEngineReturnArray($array, $backupId); + } + + /** + * Used by the tasks to process Akeeba Engine's return array. Depending on the result and the component options we + * may throw text output or send an HTTP redirection header. + * + * @param array $array The return array to process + * @param string $backupId The backup ID (used to step the backup process) + */ + private function processEngineReturnArray($array, $backupId) + { + if ($array['Error'] != '') + { + @ob_end_clean(); + echo '500 ERROR -- ' . $array['Error']; + flush(); + + $this->container->platform->closeApplication(); + } + + if ($array['HasRun'] == 1) + { + // All done + Factory::nuke(); + Factory::getFactoryStorage()->reset(); + @ob_end_clean(); + header('Content-type: text/plain'); + header('Connection: close'); + echo '200 OK'; + flush(); + + $this->container->platform->closeApplication(); + } + + $noredirect = $this->input->get('noredirect', 0, 'int'); + + if ($noredirect != 0) + { + @ob_end_clean(); + header('Content-type: text/plain'); + header('Connection: close'); + echo "301 More work required -- BACKUPID ###$backupId###"; + flush(); + + $this->container->platform->closeApplication(); + } + + $curUri = JUri::getInstance(); + $ssl = $curUri->isSSL() ? 1 : 0; + $tempURL = JRoute::_('index.php?option=com_akeeba', false, $ssl); + $uri = new JUri($tempURL); + + $uri->setVar('view', 'Backup'); + $uri->setVar('task', 'step'); + $uri->setVar('key', $this->input->get('key', '', 'none', 2)); + $uri->setVar('profile', $this->input->get('profile', 1, 'int')); + + if (!empty($backupId)) + { + $uri->setVar('backupid', $backupId); + } + + // Maybe we have a multilingual site? + $language = $this->container->platform->getLanguage(); + $languageTag = $language->getTag(); + + $uri->setVar('lang', $languageTag); + + $redirectionUrl = $uri->toString(); + + $this->customRedirect($redirectionUrl); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Controller/Check.php b/deployed/akeeba/components/com_akeeba/Controller/Check.php new file mode 100644 index 00000000..d4f0f776 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Controller/Check.php @@ -0,0 +1,62 @@ +setPredefinedTaskList(['main']); + } + + /** + * Checks for failed backups and sends out any notification emails + */ + public function main() + { + // Check permissions + $this->checkPermissions(); + + /** @var Statistics $model */ + $model = $this->container->factory->model('Statistics')->tmpInstance(); + $result = $model->notifyFailed(); + + $message = $result['result'] ? '200 ' : '500 '; + $message .= implode(', ', $result['message']); + + @ob_end_clean(); + header('Content-type: text/plain'); + header('Connection: close'); + echo $message; + flush(); + + $this->container->platform->closeApplication(); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Controller/Json.php b/deployed/akeeba/components/com_akeeba/Controller/Json.php new file mode 100644 index 00000000..26d1db2f --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Controller/Json.php @@ -0,0 +1,67 @@ +setPredefinedTaskList(['json']); + } + + /** + * Handles API calls + */ + public function json() + { + // Use the model to parse the JSON message + if (function_exists('ob_start')) + { + @ob_start(); + } + + $sourceJSON = $this->input->get('json', null, 'raw', 2); + + /** @var \Akeeba\Backup\Site\Model\Json $model */ + $model = $this->getModel(); + $json = $model->execute($sourceJSON); + + if (function_exists('ob_end_clean')) + { + @ob_end_clean(); + } + + // Just dump the JSON and tear down the application, without plugins executing + header('Content-type: text/plain'); + header('Connection: close'); + echo $json; + + $this->container->platform->closeApplication(); + } + +} diff --git a/deployed/akeeba/components/com_akeeba/Controller/Mixin/ActivateProfile.php b/deployed/akeeba/components/com_akeeba/Controller/Mixin/ActivateProfile.php new file mode 100644 index 00000000..5d23d957 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Controller/Mixin/ActivateProfile.php @@ -0,0 +1,41 @@ +input->get('profile', 1, 'int'); + $profile = max(1, $profile); + + $this->container->platform->setSessionVar('profile', $profile, 'akeeba'); + + /** + * 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); + } + +} diff --git a/deployed/akeeba/components/com_akeeba/Controller/Mixin/CustomRedirection.php b/deployed/akeeba/components/com_akeeba/Controller/Mixin/CustomRedirection.php new file mode 100644 index 00000000..aa24b17f --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Controller/Mixin/CustomRedirection.php @@ -0,0 +1,36 @@ +container->platform->closeApplication(); + } + +} diff --git a/deployed/akeeba/components/com_akeeba/Controller/Mixin/FrontEndPermissions.php b/deployed/akeeba/components/com_akeeba/Controller/Mixin/FrontEndPermissions.php new file mode 100644 index 00000000..bf625e52 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Controller/Mixin/FrontEndPermissions.php @@ -0,0 +1,54 @@ +get_platform_configuration_option('frontend_enable', 0) != 0; + + // Is the Secret Key strong enough? + $validKey = Platform::getInstance()->get_platform_configuration_option('frontend_secret_word', ''); + $validKeyTrim = trim($validKey); + + if (!Complexify::isStrongEnough($validKey, false)) + { + $febEnabled = false; + } + + // Is the key good? + $key = $this->input->get('key', '', 'none', 2); + + if (!$febEnabled || ($key != $validKey) || (empty($validKeyTrim))) + { + @ob_end_clean(); + echo '403 ' . JText::_('COM_AKEEBA_COMMON_ERR_NOT_ENABLED'); + flush(); + + $this->container->platform->closeApplication(); + } + } + +} diff --git a/deployed/akeeba/components/com_akeeba/Controller/index.html b/deployed/akeeba/components/com_akeeba/Controller/index.html new file mode 100644 index 00000000..0b551329 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Controller/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/deployed/akeeba/components/com_akeeba/Dispatcher/Dispatcher.php b/deployed/akeeba/components/com_akeeba/Dispatcher/Dispatcher.php new file mode 100644 index 00000000..7fdf832a --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Dispatcher/Dispatcher.php @@ -0,0 +1,117 @@ +defaultView = 'Backup'; + + $this->viewNameAliases = [ + 'backup' => 'Backup', + 'backups' => 'Backup', + 'check' => 'Check', + 'checks' => 'Check', + 'json' => 'Json', + 'jsons' => 'Json', + ]; + } + + + /** + * Executes before dispatching the request to the appropriate controller + */ + public function onBeforeDispatch() + { + $this->onBeforeDispatchViewAliases(); + + // Load the FOF language + $lang = $this->container->platform->getLanguage(); + $lang->load('lib_fof30', JPATH_SITE, 'en-GB', true, true); + $lang->load('lib_fof30', JPATH_SITE, null, true, false); + + // Necessary defines for Akeeba Engine + if ( !defined('AKEEBAENGINE')) + { + define('AKEEBAENGINE', 1); + define('AKEEBAROOT', $this->container->backEndPath . '/BackupEngine'); + define('ALICEROOT', $this->container->backEndPath . '/AliceEngine'); + } + + // Make sure we have a profile set throughout the component's lifetime + $profile_id = $this->container->platform->getSessionVar('profile', null, 'akeeba'); + + if (is_null($profile_id)) + { + $this->container->platform->setSessionVar('profile', 1, 'akeeba'); + } + + // Load Akeeba Engine + $basePath = $this->container->backEndPath; + require_once $basePath . '/BackupEngine/Factory.php'; + + // Load the Akeeba Engine configuration + Platform::addPlatform('joomla3x', JPATH_COMPONENT_ADMINISTRATOR . '/BackupPlatform/Joomla3x'); + $akeebaEngineConfig = Factory::getConfiguration(); + Platform::getInstance()->load_configuration(); + unset($akeebaEngineConfig); + + // Prevents the "SQLSTATE[HY000]: General error: 2014" due to resource sharing with Akeeba Engine + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // !!!!! WARNING: ALWAYS GO THROUGH JFactory; DO NOT GO THROUGH $this->container->db !!!!! + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + $jDbo = JFactory::getDbo(); + + if ($jDbo->name == 'pdomysql') + { + @JFactory::getDbo()->disconnect(); + } + + // Load the utils helper library + Platform::getInstance()->load_version_defines(); + + // Make sure the front-end backup Secret Word is stored encrypted + $params = $this->container->params; + SecretWord::enforceEncryption($params, 'frontend_secret_word'); + + // Make sure we have a version loaded + @include_once($this->container->backEndPath . '/version.php'); + + if (!defined('AKEEBA_VERSION')) + { + define('AKEEBA_VERSION', 'dev'); + define('AKEEBA_DATE', date('Y-m-d')); + } + + // Create a media file versioning tag + $this->container->mediaVersion = md5(AKEEBA_VERSION . AKEEBA_DATE); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Backup.php b/deployed/akeeba/components/com_akeeba/Model/Backup.php new file mode 100644 index 00000000..d0b359f8 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Backup.php @@ -0,0 +1,15 @@ +encapsulation = new Encapsulation($this->serverKey()); + } + + /** + * Parses the JSON data sent by the client and executes the appropriate JSON API task + * + * @param string $json The raw JSON data received from the remote client + * + * @return string The JSON-encoded, fully encapsulated response + */ + public function execute($json) + { + // Check if we're activated + $enabled = Platform::getInstance()->get_platform_configuration_option('frontend_enable', 0); + + // Is the Secret Key strong enough? + $validKey = $this->serverKey(); + + if (!Complexify::isStrongEnough($validKey, false)) + { + $enabled = false; + } + + $rawEncapsulation = $this->encapsulation->getEncapsulationByCode('ENCAPSULATION_RAW'); + + if (!$enabled) + { + return $this->getResponse('Access denied', 503); + } + + // Try to JSON-decode the request's input first + $request = @json_decode($json, true); + + if (is_null($request)) + { + return $this->getResponse('JSON decoding error', 500); + } + + // Transform legacy requests + if (!is_array($request)) + { + $request = array( + 'encapsulation' => $rawEncapsulation, + 'body' => $request + ); + } + + // Transform partial requests + if (!isset($request['encapsulation'])) + { + $request['encapsulation'] = $rawEncapsulation; + } + + // Make sure we have a request body + if (!isset($request['body'])) + { + $request['body'] = ''; + } + + try + { + $request['body'] = $this->encapsulation->decode($request['encapsulation'], $request['body']); + } + catch (\Exception $e) + { + return $this->getResponse($e->getMessage(), $e->getCode()); + } + + // Replicate the encapsulation preferences of the client for our own output + $this->encapsulationType = $request['encapsulation']; + + // Store the client-specified key, or use the server key if none specified and the request + // came encrypted. + $this->password = isset($request['body']['key']) ? $request['body']['key'] : $this->serverKey(); + + // Run the method + $params = array(); + + if (isset($request['body']['data'])) + { + $params = (array)$request['body']['data']; + } + + try + { + $taskHandler = new Task($this->container); + $data = $taskHandler->execute($request['body']['method'], $params); + } + catch (\RuntimeException $e) + { + return $this->getResponse($e->getMessage(), $e->getCode()); + } + + return $this->getResponse($data); + } + + /** + * Packages the response to a JSON-encoded object, optionally encrypting the data part with a caller-supplied + * password. + * + * @param mixed $data The response to encapsulate + * @param int $status The status code to return. 200 = Success, anything else is treated as an error. + * + * @return string The JSON-encoded response + */ + private function getResponse($data, $status = 200) + { + // Initialize the response + $response = array( + 'encapsulation' => $this->encapsulationType, + 'body' => array( + 'status' => $status, + 'data' => null + ) + ); + + if ($status != 200) + { + $response['encapsulation'] = $this->encapsulation->getEncapsulationByCode('ENCAPSULATION_RAW'); + } + + try + { + $response['body']['data'] = $this->encapsulation->encode($response['encapsulation'], $data, $this->password); + } + catch (\Exception $e) + { + $response['encapsulation'] = $this->encapsulation->getEncapsulationByCode('ENCAPSULATION_RAW'); + $response['body'] = array( + 'status' => $e->getCode(), + 'data' => $e->getMessage(), + ); + } + + return '###' . json_encode($response) . '###'; + } + + /** + * Get the server key, i.e. the Secret Word for the front-end backups and JSON API + * + * @return mixed + */ + private function serverKey() + { + static $key = null; + + if (is_null($key)) + { + $key = Platform::getInstance()->get_platform_configuration_option('frontend_secret_word', ''); + } + + return $key; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation.php b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation.php new file mode 100644 index 00000000..edf5ca3c --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation.php @@ -0,0 +1,267 @@ +serverKey = $serverKey; + + // Populate the list of encapsulation handlers + $this->initialiseHandlers(); + } + + /** + * Returns the encapsulation ID given its code. For example given $code == 'ENCAPSULATION_AESCTR256' it will return + * the ID integer 3. + * + * @param string $code The encapsulation code, e.g. ENCAPSULATION_AESCTR256 + * + * @return int The numeric ID, e.g. 3 + */ + public function getEncapsulationByCode($code) + { + $info = $this->getEncapsulationInfoByCode($code); + + return $info['id']; + } + + /** + * Returns the encapsulation information array given its code. For example given $code == 'ENCAPSULATION_AESCTR256' + * it will return the information for the data in AES-256 stream (CTR) mode encrypted JSON type. + * + * @param string $code The encapsulation code, e.g. ENCAPSULATION_AESCTR256 + * + * @return array The information of the encapsulation handler + */ + public function getEncapsulationInfoByCode($code) + { + // Normalise the code + $code = strtoupper($code); + + // If we have no idea what the encapsulation should be revert to raw (plain text) + if (!isset($this->encapsulations[$code])) + { + return $this->encapsulations['ENCAPSULATION_RAW']; + } + + return $this->encapsulations[$code]; + } + + /** + * Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON- + * decoding the result. If any error occurs along the way the appropriate exception is thrown. + * + * The data being decoded corresponds to the Request Body described in the API documentation + * + * @param int $encapsulation The encapsulation type + * @param string $data Encoded data + * + * @return array The decoded data. + * + * @throw \RuntimeException When the server capabilities don't match the requested encapsulation + * @throw \InvalidArgumentException When $data cannot be decoded successfully + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html + */ + public function decode($encapsulation, $data) + { + $body = null; + + // Find the suitable handler and encode the data + foreach ($this->handlers as $handler) + { + if ($handler->isSupported($encapsulation)) + { + $body = $handler->decode($this->serverKey, $data); + + break; + } + } + + // If the data cannot be encoded throw an exception + if (!isset($handler) || is_null($body)) + { + throw new \RuntimeException('The requested encapsulation type is not supported', 503); + } + + $authorised = true; + $body = rtrim($body, chr(0)); + + // Make sure it looks like a valid JSON string and is at least 12 characters (minimum valid message length) + if ((strlen($body) < 12) || (substr($body, 0, 1) != '{') || (substr($body, -1) != '}')) + { + $authorised = false; + } + + // Try to JSON decode the body + if ($authorised) + { + $body = json_decode($body, true); + + if (is_null($body)) + { + $authorised = false; + } + elseif (!is_array($body)) + { + $authorised = false; + } + } + + // Make sure there is a requested method + if ($authorised) + { + if (!isset($body['method']) || empty($body['method'])) + { + $authorised = false; + } + } + + if ($authorised) + { + $authorised = $handler->isAuthorised($this->serverKey, $body); + } + + if (!$authorised) + { + throw new \InvalidArgumentException('Authentication failed', 401); + } + + return (array)$body; + } + + /** + * Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted + * encapsulations will then encrypt the data and base64-encode it before returning it. + * + * The data being encoded correspond to the body > data structure described in the API documentation + * + * @param int $encapsulation The encapsulation type + * @param mixed $data The data to encode, typically a string, array or object + * @param string $key Key to use for encoding. If not provided we revert to $this->serverKey + * + * @return string The encapsulated data + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html + * + * @throw \RuntimeException When the server capabilities don't match the requested encapsulation + * @throw \InvalidArgumentException When $data cannot be converted to JSON + */ + public function encode($encapsulation, $data, $key = null) + { + // Try to JSON-encode the data + $data = json_encode($data); + + // If the data cannot be JSON-encoded throw an exception + if ($data === false) + { + throw new \InvalidArgumentException('Empty data cannot be encapsulated', 500); + } + + // Make sure we have a valid key + if (empty($key)) + { + $key = $this->serverKey; + } + + // Find the suitable handler and encode the data + foreach ($this->handlers as $handler) + { + if ($handler->isSupported($encapsulation)) + { + return $handler->encode($key, $data); + } + } + + // If the data cannot be encoded throw an exception + $format = print_r($encapsulation, true); + throw new \RuntimeException("Data cannot be encapsulated in the requested format ($format)", 500); + } + + /** + * Initialises the encapsulation handlers + * + * @return void + */ + protected function initialiseHandlers() + { + // Reset the arrays + $this->handlers = array(); + $this->encapsulations = array(); + + // Look all files in the Encapsulation handlers' directory + $dh = new \DirectoryIterator(__DIR__ . '/Encapsulation'); + + /** @var \DirectoryIterator $entry */ + foreach ($dh as $entry) + { + $fileName = $entry->getFilename(); + + // Ignore non-PHP files + if (substr($fileName, -4) != '.php') + { + continue; + } + + // Ignore the Base class + if ($fileName == 'Base.php') + { + continue; + } + + // Get the class name + $className = '\\Akeeba\\Backup\\Site\\Model\\Json\\Encapsulation\\' . substr($fileName, 0, -4); + + // Check if the class really exists + if (!class_exists($className, true)) + { + continue; + } + + /** @var EncapsulationInterface $o */ + $o = new $className; + $info = $o->getInformation(); + $this->encapsulations[$info['code']] = $info; + $this->handlers[] = $o; + } + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCbc128.php b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCbc128.php new file mode 100644 index 00000000..93dd0900 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCbc128.php @@ -0,0 +1,70 @@ +getEncryption()->AESDecryptCBC($data, $serverKey, 128); + } + + /** + * Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted + * encapsulations will then encrypt the data and base64-encode it before returning it. + * + * The data being encoded correspond to the body > data structure described in the API documentation + * + * @param string $serverKey The server key we need to encode data + * @param mixed $data The data to encode, typically a string, array or object + * + * @return string The encapsulated data + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html + * + * @throws \RuntimeException When the server capabilities don't match the requested encapsulation + * @throws \InvalidArgumentException When $data cannot be converted to JSON + */ + public function encode($serverKey, $data) + { + return base64_encode($this->getEncryption()->AESEncryptCBC($data, $serverKey, 128)); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCbc256.php b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCbc256.php new file mode 100644 index 00000000..4a5afe52 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCbc256.php @@ -0,0 +1,70 @@ +getEncryption()->AESDecryptCBC($data, $serverKey, 256); + } + + /** + * Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted + * encapsulations will then encrypt the data and base64-encode it before returning it. + * + * The data being encoded correspond to the body > data structure described in the API documentation + * + * @param string $serverKey The server key we need to encode data + * @param mixed $data The data to encode, typically a string, array or object + * + * @return string The encapsulated data + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html + * + * @throws \RuntimeException When the server capabilities don't match the requested encapsulation + * @throws \InvalidArgumentException When $data cannot be converted to JSON + */ + public function encode($serverKey, $data) + { + return base64_encode($this->getEncryption()->AESEncryptCBC($data, $serverKey, 256)); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCtr128.php b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCtr128.php new file mode 100644 index 00000000..d08a9a3e --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCtr128.php @@ -0,0 +1,69 @@ +getEncryption()->AESDecryptCtr($data, $serverKey, 128); + } + + /** + * Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted + * encapsulations will then encrypt the data and base64-encode it before returning it. + * + * The data being encoded correspond to the body > data structure described in the API documentation + * + * @param string $serverKey The server key we need to encode data + * @param mixed $data The data to encode, typically a string, array or object + * + * @return string The encapsulated data + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html + * + * @throws \RuntimeException When the server capabilities don't match the requested encapsulation + * @throws \InvalidArgumentException When $data cannot be converted to JSON + */ + public function encode($serverKey, $data) + { + return $this->getEncryption()->AESEncryptCtr($data, $serverKey, 128); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCtr256.php b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCtr256.php new file mode 100644 index 00000000..9fc94ee3 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/AesCtr256.php @@ -0,0 +1,69 @@ +getEncryption()->AESDecryptCtr($data, $serverKey, 256); + } + + /** + * Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted + * encapsulations will then encrypt the data and base64-encode it before returning it. + * + * The data being encoded correspond to the body > data structure described in the API documentation + * + * @param string $serverKey The server key we need to encode data + * @param mixed $data The data to encode, typically a string, array or object + * + * @return string The encapsulated data + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html + * + * @throws \RuntimeException When the server capabilities don't match the requested encapsulation + * @throws \InvalidArgumentException When $data cannot be converted to JSON + */ + public function encode($serverKey, $data) + { + return $this->getEncryption()->AESEncryptCtr($data, $serverKey, 256); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/Base.php b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/Base.php new file mode 100644 index 00000000..781b4ff7 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/Base.php @@ -0,0 +1,165 @@ +id = $id; + $this->code = strtoupper($code); + $this->description = $description; + } + + /** + * Returns information about the encapsulation supported by this class. The return array has the following keys: + * id: The numeric ID of the encapsulation, e.g. 3 + * code: The short code of the encapsulation, e.g. ENCAPSULATION_AESCTR256 + * description: A human readable descriptions, e.g. "Data in AES-256 stream (CTR) mode encrypted JSON" + * + * @return array See above + */ + public function getInformation() + { + return array( + 'id' => $this->id, + 'code' => $this->code, + 'description' => $this->description, + ); + } + + /** + * Checks if the request body authorises the user to use the API. Each encapsulation can implement its own + * authorisation method. This method is only called after the request body has been successfully decoded, therefore + * encrypted encapsulations can simply return true. + * + * @param string $serverKey The server key we need to check the authorisation + * @param array $body The decoded body (as returned by the decode() method) + * + * @return bool True if authorised + */ + public function isAuthorised($serverKey, $body) + { + return true; + } + + /** + * Is the provided encapsulation type supported by this class? + * + * @param int $encapsulation Encapsulation type + * + * @return bool True if supported + */ + public function isSupported($encapsulation) + { + return $encapsulation == $this->id; + } + + /** + * Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON- + * decoding the result. If any error occurs along the way the appropriate exception is thrown. + * + * The data being decoded corresponds to the Request Body described in the API documentation + * + * @param string $serverKey The server key we need to decode data + * @param string $data Encoded data + * + * @return string The decoded data. + * + * @throws \RuntimeException When the server capabilities don't match the requested encapsulation + * @throws \InvalidArgumentException When $data cannot be decoded successfully + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html + */ + public function decode($serverKey, $data) + { + } + + /** + * Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted + * encapsulations will then encrypt the data and base64-encode it before returning it. + * + * The data being encoded correspond to the body > data structure described in the API documentation + * + * @param string $serverKey The server key we need to encode data + * @param mixed $data The data to encode, typically a string, array or object + * + * @return string The encapsulated data + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html + * + * @throws \RuntimeException When the server capabilities don't match the requested encapsulation + * @throws \InvalidArgumentException When $data cannot be converted to JSON + */ + public function encode($serverKey, $data) + { + } + + /** + * Returns an encryption object normalized for use in the JSON API: PBKDF2 uses a dynamic salt with SHA1 algorithm. + * This is necessary when we are running a backup against a profile which uses a static salt. In this case the + * static salt is not included in the ciphertext, making it impossible for the remote side to decipher our message, + * leading to backup failure. + * + * @return Encrypt + */ + protected function getEncryption() + { + if (is_null($this->encryption)) + { + $encryption = Factory::getEncryption(); + $this->encryption = clone $encryption; + $this->encryption->setPbkdf2UseStaticSalt(false); + $this->encryption->setPbkdf2Algorithm('sha1'); + } + + return $this->encryption; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/Raw.php b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/Raw.php new file mode 100644 index 00000000..5101b185 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Encapsulation/Raw.php @@ -0,0 +1,92 @@ + data structure described in the API documentation + * + * @param string $serverKey The server key we need to encode data + * @param mixed $data The data to encode, typically a string, array or object + * + * @return string The encapsulated data + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html + * + * @throws \RuntimeException When the server capabilities don't match the requested encapsulation + * @throws \InvalidArgumentException When $data cannot be converted to JSON + */ + public function encode($serverKey, $data) + { + return $data; + } + + /** + * Checks if the request body authorises the user to use the API. Each encapsulation can implement its own + * authorisation method. This method is only called after the request body has been successfully decoded, therefore + * encrypted encapsulations can simply return true. + * + * @param string $serverKey The server key we need to check the authorisation + * @param array $body The decoded body (as returned by the decode() method) + * + * @return bool True if authorised + */ + public function isAuthorised($serverKey, $body) + { + $authenticated = false; + + if (isset($body['challenge']) && (strpos($body['challenge'], ':') >= 2) && (strlen($body['challenge']) >= 3)) + { + list ($challengeData, $providedHash) = explode(':', $body['challenge']); + $computedHash = strtolower(md5($challengeData . $serverKey)); + $authenticated = ($computedHash == $providedHash); + } + + return $authenticated; + } + +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/EncapsulationInterface.php b/deployed/akeeba/components/com_akeeba/Model/Json/EncapsulationInterface.php new file mode 100644 index 00000000..f9842e32 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/EncapsulationInterface.php @@ -0,0 +1,84 @@ + data structure described in the API documentation + * + * @param string $serverKey The server key we need to encode data + * @param mixed $data The data to encode, typically a string, array or object + * + * @return string The encapsulated data + * + * @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html + * + * @throws \RuntimeException When the server capabilities don't match the requested encapsulation + * @throws \InvalidArgumentException When $data cannot be converted to JSON + */ + public function encode($serverKey, $data); + + /** + * Checks if the request body authorises the user to use the API. Each encapsulation can implement its own + * authorisation method. This method is only called after the request body has been successfully decoded, therefore + * encrypted encapsulations can simply return true. + * + * @param string $serverKey The server key we need to check the authorisation + * @param array $body The decoded body (as returned by the decode() method) + * + * @return bool True if authorised + */ + public function isAuthorised($serverKey, $body); +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task.php new file mode 100644 index 00000000..a3fb02ae --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task.php @@ -0,0 +1,124 @@ +container = $container; + + // Populate the list of task handlers + $this->initialiseHandlers(); + } + + /** + * Do I have a specific task handling method? + * + * @param string $method The method to check for + * + * @return bool + */ + public function hasMethod($method) + { + $method = strtolower($method); + + return isset($this->handlers[$method]); + } + + /** + * Execute a JSON API method + * + * @param string $method The method's name + * @param array $parameters The parameters to the method (optional) + * + * @return mixed + * + * @throws \RuntimeException When the method requested is not known to us + */ + public function execute($method, $parameters = array()) + { + if (!$this->hasMethod($method)) + { + throw new \RuntimeException("Invalid method $method", 405); + } + + $method = strtolower($method); + + return $this->handlers[$method]->execute($parameters); + } + + /** + * Initialises the encapsulation handlers + * + * @return void + */ + protected function initialiseHandlers() + { + // Reset the array + $this->handlers = array(); + + // Look all files in the Task handlers' directory + $dh = new \DirectoryIterator(__DIR__ . '/Task'); + + /** @var \DirectoryIterator $entry */ + foreach ($dh as $entry) + { + $fileName = $entry->getFilename(); + + // Ignore non-PHP files + if (substr($fileName, -4) != '.php') + { + continue; + } + + // Ignore the Base class + if ($fileName == 'AbstractTask.php') + { + continue; + } + + // Get the class name + $className = '\\Akeeba\\Backup\\Site\\Model\\Json\\Task\\' . substr($fileName, 0, -4); + + // Check if the class really exists + if (!class_exists($className, true)) + { + continue; + } + + /** @var TaskInterface $o */ + $o = new $className($this->container); + $name = $o->getMethodName(); + $name = strtolower($name); + $this->handlers[$name] = $o; + } + } + +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/AbstractTask.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/AbstractTask.php new file mode 100644 index 00000000..6aa4df79 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/AbstractTask.php @@ -0,0 +1,72 @@ +container = $container; + + $path = explode('\\', get_class($this)); + $shortName = array_pop($path); + $this->methodName = lcfirst($shortName); + } + + /** + * Return the JSON API task's name ("method" name). Remote clients will use it to call us. + * + * @return string + */ + public function getMethodName() + { + return $this->methodName; + } + + /** + * Execute the JSON API task + * + * @param array $parameters The parameters to this task + * + * @return mixed + * + * @throws \RuntimeException In case of an error + */ + public function execute(array $parameters = array()) + { + throw new \LogicException(__CLASS__ . ' has not implemented its execute() method yet.'); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/Browse.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/Browse.php new file mode 100644 index 00000000..78de7cd9 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/Browse.php @@ -0,0 +1,65 @@ + '', + 'processfolder' => 0 + ); + + $defConfig = array_merge($defConfig, $parameters); + + $folder = $filter->clean($defConfig['folder'], 'string'); + $processFolder = $filter->clean($defConfig['processfolder'], 'bool'); + + /** @var Browser $model */ + $model = $this->container->factory->model('Browser')->tmpInstance(); + $model->setState('folder', $folder); + $model->setState('processfolder', $processFolder); + $model->makeListing(); + + $ret = array( + 'folder' => $model->getState('folder'), + 'folder_raw' => $model->getState('folder_raw'), + 'parent' => $model->getState('parent'), + 'exists' => $model->getState('exists'), + 'inRoot' => $model->getState('inRoot'), + 'openbasedirRestricted' => $model->getState('openbasedirRestricted'), + 'writable' => $model->getState('writable'), + 'subfolders' => $model->getState('subfolders'), + 'breadcrumbs' => $model->getState('breadcrumbs'), + ); + + return $ret; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/Delete.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/Delete.php new file mode 100644 index 00000000..db97b477 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/Delete.php @@ -0,0 +1,56 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $backup_id = (int)$defConfig['backup_id']; + + /** @var Statistics $model */ + $model = $this->container->factory->model('Statistics')->tmpInstance(); + $model->setState('id', $backup_id); + + try + { + $model->delete(); + } + catch (\Exception $e) + { + throw new \RuntimeException($e->getMessage(), 500); + } + + return true; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/DeleteFiles.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/DeleteFiles.php new file mode 100644 index 00000000..fd2eea77 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/DeleteFiles.php @@ -0,0 +1,56 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $backup_id = (int)$defConfig['backup_id']; + + /** @var Statistics $model */ + $model = $this->container->factory->model('Statistics')->tmpInstance(); + $model->setState('id', $backup_id); + + try + { + $model->deleteFile(); + } + catch (\Exception $e) + { + throw new \RuntimeException($e->getMessage(), 500); + } + + return true; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/DeleteProfile.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/DeleteProfile.php new file mode 100644 index 00000000..3480b1ef --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/DeleteProfile.php @@ -0,0 +1,60 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = (int)$defConfig['profile']; + + // You need to specify the profile + if (empty($profile)) + { + throw new \RuntimeException('Invalid profile ID', 404); + } + + if ($profile == 1) + { + throw new \RuntimeException('You cannot delete the default backup profile', 404); + } + + // Get a profile model + /** @var Profiles $profileModel */ + $profileModel = $this->container->factory->model('Profiles')->tmpInstance(); + $profileModel->findOrFail($profile); + $profileModel->delete(); + + return true; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/Download.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/Download.php new file mode 100644 index 00000000..8d055206 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/Download.php @@ -0,0 +1,101 @@ + 0, + 'part_id' => 1, + 'segment' => 1, + 'chunk_size' => 1 + ); + + $defConfig = array_merge($defConfig, $parameters); + + $backup_id = (int)$defConfig['backup_id']; + $part_id = (int)$defConfig['part_id']; + $segment = (int)$defConfig['segment']; + $chunk_size = (int)$defConfig['chunk_size']; + + $backup_stats = Platform::getInstance()->get_statistics($backup_id); + + if (empty($backup_stats)) + { + // Backup record doesn't exist + throw new \RuntimeException('Invalid backup record identifier', 404); + } + + $files = Factory::getStatistics()->get_all_filenames($backup_stats); + + if ((count($files) < $part_id) || ($part_id <= 0)) + { + // Invalid part + throw new \RuntimeException('Invalid backup part', 404); + } + + $file = $files[ $part_id - 1 ]; + + $filesize = @filesize($file); + $seekPos = $chunk_size * 1048576 * ($segment - 1); + + if ($seekPos > $filesize) + { + // Trying to seek past end of file + throw new \RuntimeException('Invalid segment', 404); + } + + $fp = fopen($file, 'rb'); + + if ($fp === false) + { + // Could not read file + throw new \RuntimeException('Error reading backup archive', 500); + } + + rewind($fp); + if (fseek($fp, $seekPos, SEEK_SET) === -1) + { + // Could not seek to position + throw new \RuntimeException('Error reading specified segment', 500); + } + + $buffer = fread($fp, 1048576); + + if ($buffer === false) + { + throw new \RuntimeException('Error reading specified segment', 500); + } + + fclose($fp); + + return base64_encode($buffer); + + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/DownloadDirect.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/DownloadDirect.php new file mode 100644 index 00000000..fbe1c9e1 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/DownloadDirect.php @@ -0,0 +1,161 @@ + 0, + 'part_id' => 1, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $backup_id = (int)$defConfig['backup_id']; + $part_id = (int)$defConfig['part_id']; + + $backup_stats = Platform::getInstance()->get_statistics($backup_id); + + if (empty($backup_stats)) + { + // Backup record doesn't exist + @ob_end_clean(); + header('HTTP/1.1 500 Invalid backup record identifier'); + flush(); + + $this->container->platform->closeApplication(); + } + + $files = Factory::getStatistics()->get_all_filenames($backup_stats); + + if ((count($files) < $part_id) || ($part_id <= 0)) + { + // Invalid part + @ob_end_clean(); + header('HTTP/1.1 500 Invalid backup part'); + flush(); + + $this->container->platform->closeApplication(); + } + + $filename = $files[ $part_id - 1 ]; + @clearstatcache(); + + // For a certain unmentionable browser + if (function_exists('ini_get') && function_exists('ini_set')) + { + if (ini_get('zlib.output_compression')) + { + ini_set('zlib.output_compression', 'Off'); + } + } + + // Remove php's time limit + if (function_exists('ini_get') && function_exists('set_time_limit')) + { + if (!ini_get('safe_mode')) + { + @set_time_limit(0); + } + } + + $basename = @basename($filename); + $fileSize = @filesize($filename); + $extension = strtolower(str_replace(".", "", strrchr($filename, "."))); + + while (@ob_end_clean()) + { + ; + } + @clearstatcache(); + // Send MIME headers + header('MIME-Version: 1.0'); + header('Content-Disposition: attachment; filename="' . $basename . '"'); + header('Content-Transfer-Encoding: binary'); + header('Accept-Ranges: bytes'); + + switch ($extension) + { + case 'zip': + // ZIP MIME type + header('Content-Type: application/zip'); + break; + + default: + // Generic binary data MIME type + header('Content-Type: application/octet-stream'); + break; + } + + // Notify of file size, if this info is available + if ($fileSize > 0) + { + header('Content-Length: ' . @filesize($filename)); + } + + // Disable caching + header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); + header("Expires: 0"); + header('Pragma: no-cache'); + flush(); + + if ($fileSize > 0) + { + // If the filesize is reported, use 1M chunks for echoing the data to the browser + $blockSize = 1048576; //1M chunks + $handle = @fopen($filename, "r"); + + // Now we need to loop through the file and echo out chunks of file data + if ($handle !== false) + { + while (!@feof($handle)) + { + echo @fread($handle, $blockSize); + @ob_flush(); + flush(); + } + } + + if ($handle !== false) + { + @fclose($handle); + } + } + else + { + // If the filesize is not reported, hope that readfile works + @readfile($filename); + } + + flush(); + + $this->container->platform->closeApplication(); + + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/ExportConfiguration.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/ExportConfiguration.php new file mode 100644 index 00000000..1b48f4d4 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/ExportConfiguration.php @@ -0,0 +1,72 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile_id = (int)$defConfig['profile']; + + if ($profile_id <= 0) + { + $profile_id = 1; + } + + /** @var Profiles $profile */ + $profile = $this->container->factory->model('Profiles')->tmpInstance(); + + $data = $profile->findOrFail($profile_id)->getData(); + + if (substr($data['configuration'], 0, 12) == '###AES128###') + { + // Load the server key file if necessary + if (!defined('AKEEBA_SERVERKEY')) + { + $filename = JPATH_COMPONENT_ADMINISTRATOR . '/BackupEngine/serverkey.php'; + + include_once $filename; + } + + $key = Factory::getSecureSettings()->getKey(); + + $data['configuration'] = Factory::getSecureSettings()->decryptSettings($data['configuration'], $key); + } + + return array( + 'description' => $data['description'], + 'configuration' => $data['configuration'], + 'filters' => $data['filters'], + ); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetBackupInfo.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetBackupInfo.php new file mode 100644 index 00000000..9833cbb9 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetBackupInfo.php @@ -0,0 +1,84 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $backup_id = (int)$defConfig['backup_id']; + + // Get the basic statistics + $record = Platform::getInstance()->get_statistics($backup_id); + + // Get a list of filenames + $backup_stats = Platform::getInstance()->get_statistics($backup_id); + + // Backup record doesn't exist + if (empty($backup_stats)) + { + throw new \RuntimeException('Invalid backup record identifier', 404); + } + + $filenames = Factory::getStatistics()->get_all_filenames($record); + + if (empty($filenames)) + { + // Archives are not stored on the server or no files produced + $record['filenames'] = array(); + } + else + { + $filedata = array(); + $i = 0; + + // Get file sizes per part + foreach ($filenames as $file) + { + $i++; + $size = @filesize($file); + $size = is_numeric($size) ? $size : 0; + $filedata[] = array( + 'part' => $i, + 'name' => basename($file), + 'size' => $size + ); + } + + // Add the file info to $record['filenames'] + $record['filenames'] = $filedata; + } + + return $record; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBEntities.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBEntities.php new file mode 100644 index 00000000..b2f5e109 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBEntities.php @@ -0,0 +1,67 @@ + 0, + 'root' => '[SITEDB]', + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown database root', 500); + } + + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var DatabaseFilters $model */ + $model = $this->container->factory->model('DatabaseFilters')->tmpInstance(); + + return $model->make_listing($root); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBFilters.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBFilters.php new file mode 100644 index 00000000..b516fce8 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBFilters.php @@ -0,0 +1,67 @@ + 0, + 'root' => '[SITEDB]', + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown database root', 500); + } + + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var DatabaseFilters $model */ + $model = $this->container->factory->model('DatabaseFilters')->tmpInstance(); + + return $model->get_filters($root); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBRoots.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBRoots.php new file mode 100644 index 00000000..ddcb2f1b --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetDBRoots.php @@ -0,0 +1,56 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = (int)$defConfig['profile']; + + if ($profile <= 0) + { + $profile = 1; + } + + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var DatabaseFilters $model */ + $model = $this->container->factory->model('DatabaseFilters')->tmpInstance(); + + return $model->get_roots(); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSEntities.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSEntities.php new file mode 100644 index 00000000..a05cb382 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSEntities.php @@ -0,0 +1,82 @@ + 0, + 'root' => '[SITEROOT]', + 'subdirectory' => '', + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + $subdirectory = $filter->clean($defConfig['subdirectory'], 'path'); + $crumbs = array(); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown filesystem root', 500); + } + + // Get the subdirectory and explode it to its parts + if (!empty($subdirectory)) + { + $subdirectory = trim($subdirectory, '/'); + } + + if (!empty($subdirectory)) + { + $crumbs = explode('/', $subdirectory); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var FileFilters $model */ + $model = $this->container->factory->model('FileFilters')->tmpInstance(); + + return $model->make_listing($root, $crumbs); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSFilters.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSFilters.php new file mode 100644 index 00000000..99f71371 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSFilters.php @@ -0,0 +1,68 @@ + 0, + 'root' => '[SITEROOT]', + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown filesystem root', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var FileFilters $model */ + $model = $this->container->factory->model('FileFilters')->tmpInstance(); + + return $model->get_filters($root); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSRoots.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSRoots.php new file mode 100644 index 00000000..709cfb61 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetFSRoots.php @@ -0,0 +1,57 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = (int)$defConfig['profile']; + + if ($profile <= 0) + { + $profile = 1; + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var FileFilters $model */ + $model = $this->container->factory->model('FileFilters')->tmpInstance(); + + return $model->get_roots(); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetGUIConfiguration.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetGUIConfiguration.php new file mode 100644 index 00000000..a092314f --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetGUIConfiguration.php @@ -0,0 +1,54 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = (int)$defConfig['profile']; + + if ($profile <= 0) + { + $profile = 1; + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + return Factory::getEngineParamsProvider()->getJsonGuiDefinition(); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetIncludedDBs.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetIncludedDBs.php new file mode 100644 index 00000000..d015f0c8 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetIncludedDBs.php @@ -0,0 +1,57 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = (int)$defConfig['profile']; + + if ($profile <= 0) + { + $profile = 1; + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var MultipleDatabases $model */ + $model = $this->container->factory->model('MultipleDatabases')->tmpInstance(); + + return $model->get_databases(); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetIncludedDirectories.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetIncludedDirectories.php new file mode 100644 index 00000000..31bc9e65 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetIncludedDirectories.php @@ -0,0 +1,57 @@ + 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = (int)$defConfig['profile']; + + if ($profile <= 0) + { + $profile = 1; + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var IncludeFolders $model */ + $model = $this->container->factory->model('IncludeFolders')->tmpInstance(); + + return $model->get_directories(); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetProfiles.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetProfiles.php new file mode 100644 index 00000000..f6fd8e75 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetProfiles.php @@ -0,0 +1,50 @@ +container->factory->model('Profiles')->tmpInstance(); + + $profiles = $model->get(true); + $ret = array(); + + if (count($profiles)) + { + foreach ($profiles as $profile) + { + $temp = new \stdClass(); + $temp->id = $profile->id; + $temp->name = $profile->description; + $ret[] = $temp; + } + } + + return $ret; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetRegexDBFilters.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetRegexDBFilters.php new file mode 100644 index 00000000..23010c1f --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetRegexDBFilters.php @@ -0,0 +1,68 @@ + 0, + 'root' => '[SITEDB]', + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown database root', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var RegExDatabaseFilters $model */ + $model = $this->container->factory->model('RegExDatabaseFilters')->tmpInstance(); + + return $model->get_regex_filters($root); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetRegexFSFilters.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetRegexFSFilters.php new file mode 100644 index 00000000..22b5c36c --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetRegexFSFilters.php @@ -0,0 +1,68 @@ + 0, + 'root' => '[SITEROOT]', + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown database root', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var RegExFileFilters $model */ + $model = $this->container->factory->model('RegExFileFilters')->tmpInstance(); + + return $model->get_regex_filters($root); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetVersion.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetVersion.php new file mode 100644 index 00000000..79ce35fe --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/GetVersion.php @@ -0,0 +1,46 @@ +container->factory->model('Updates')->tmpInstance(); + + $updateInformation = $model->getUpdates(); + + $edition = AKEEBA_PRO ? 'pro' : 'core'; + + return (object)array( + 'api' => AKEEBA_JSON_API_VERSION, + 'component' => AKEEBA_VERSION, + 'date' => AKEEBA_DATE, + 'edition' => $edition, + 'updateinfo' => $updateInformation, + ); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/ImportConfiguration.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/ImportConfiguration.php new file mode 100644 index 00000000..b93495d3 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/ImportConfiguration.php @@ -0,0 +1,60 @@ + 0, + 'data' => null, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile_id = (int)$defConfig['profile']; + $data = $defConfig['data']; + + if ($profile_id <= 0) + { + $profile_id = 0; + } + + /** @var Profiles $profile */ + $profile = $this->container->factory->model('Profiles')->tmpInstance(); + + if ($profile_id) + { + $profile->find($profile_id); + } + + $profile->import($data); + + return true; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/ListBackups.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/ListBackups.php new file mode 100644 index 00000000..4bcf3c1a --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/ListBackups.php @@ -0,0 +1,50 @@ + 0, + 'limit' => 50 + ); + + $defConfig = array_merge($defConfig, $parameters); + + $from = (int)$defConfig['from']; + $limit = (int)$defConfig['limit']; + + /** @var Statistics $model */ + $model = $this->container->factory->model('Statistics')->tmpInstance(); + $model->setState('limitstart', $from); + $model->setState('limit', $limit); + + return $model->getStatisticsListWithMeta(false); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/Log.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/Log.php new file mode 100644 index 00000000..35fe0aa5 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/Log.php @@ -0,0 +1,45 @@ + 'remote' + ); + + $defConfig = array_merge($defConfig, $parameters); + + $tag = (int)$defConfig['tag']; + + $filename = Factory::getLog()->getLogFilename($tag); + + return file_get_contents($filename); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/RemoveIncludedDB.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/RemoveIncludedDB.php new file mode 100644 index 00000000..e58123b8 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/RemoveIncludedDB.php @@ -0,0 +1,68 @@ + 0, + 'name' => '', + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $name = $filter->clean($defConfig['name'], 'string'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a uuid + if (empty($name)) + { + throw new \RuntimeException('The database name is required', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var MultipleDatabases $model */ + $model = $this->container->factory->model('MultipleDatabases')->tmpInstance(); + + return $model->remove($name); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/RemoveIncludedDirectory.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/RemoveIncludedDirectory.php new file mode 100644 index 00000000..1ec08ba1 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/RemoveIncludedDirectory.php @@ -0,0 +1,68 @@ + 0, + 'uuid' => '', + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $uuid = $filter->clean($defConfig['uuid'], 'string'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a uuid + if (empty($uuid)) + { + throw new \RuntimeException('UUID is required', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var IncludeFolders $model */ + $model = $this->container->factory->model('IncludeFolders')->tmpInstance(); + + return $model->remove($uuid); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/SaveConfiguration.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SaveConfiguration.php new file mode 100644 index 00000000..3c9e4080 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SaveConfiguration.php @@ -0,0 +1,72 @@ + -1, + 'engineconfig' => array() + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = (int)$defConfig['profile']; + $data = $defConfig['engineconfig']; + + if (empty($profile)) + { + throw new \RuntimeException('Invalid profile ID', 404); + } + + // Forbid stupidly selecting the site's root as the output or temporary directory + if (array_key_exists('akeeba.basic.output_directory', $data)) + { + $folder = $data['akeeba.basic.output_directory']; + $folder = Factory::getFilesystemTools()->translateStockDirs($folder, true, true); + + $check = Factory::getFilesystemTools()->translateStockDirs('[SITEROOT]', true, true); + + if ($check == $folder) + { + $data['akeeba.basic.output_directory'] = '[DEFAULT_OUTPUT]'; + } + } + + // Merge it + $config = Factory::getConfiguration(); + $protectedKeys = $config->getProtectedKeys(); + $config->resetProtectedKeys(); + $config->mergeArray($data, false, false); + $config->setProtectedKeys($protectedKeys); + + // Save configuration + return Platform::getInstance()->save_configuration($profile); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/SaveProfile.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SaveProfile.php new file mode 100644 index 00000000..d98ae66b --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SaveProfile.php @@ -0,0 +1,90 @@ + 0, + 'description' => null, + 'quickicon' => null, + 'source' => 0, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = (int)$defConfig['profile']; + $description = $defConfig['description']; + $quickicon = $defConfig['quickicon']; + $source = (int)$defConfig['source']; + + if ($profile <= 0) + { + $profile = null; + } + + // At least one of these parameters is required + if (empty($profile) && empty($source) && empty($description)) + { + throw new \RuntimeException('Invalid profile ID', 404); + } + + // Get a profile model + /** @var Profiles $profileModel */ + $profileModel = $this->container->factory->model('Profiles')->tmpInstance(); + + // Load the profile + $sourceId = empty($profile) ? $source : $profile; + + if (!empty($sourceId)) + { + $profileModel->findOrFail($sourceId); + } + else + { + $profileModel->reset(true); + } + + $profileModel->setFieldValue('id', $profile); + + if ($description) + { + $profileModel->setFieldValue('description', $description); + } + + if (!is_null($quickicon)) + { + $profileModel->setFieldValue('quickicon', (int)$quickicon); + } + + $profileModel->save(); + + return true; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetDBFilter.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetDBFilter.php new file mode 100644 index 00000000..6fdf08ac --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetDBFilter.php @@ -0,0 +1,95 @@ + 0, + 'root' => '[SITEDB]', + 'table' => '', + 'type' => 'tables', + 'status' => 1 + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + $table = $filter->clean($defConfig['table'], 'string'); + $type = $filter->clean($defConfig['type'], 'cmd'); + $status = $filter->clean($defConfig['status'], 'bool'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown database root', 500); + } + + // We need a table name + if (empty($table)) + { + throw new \RuntimeException('Table name is mandatory', 500); + } + + // We need a table name + if (empty($type)) + { + throw new \RuntimeException('Filter type is mandatory', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var DatabaseFilters $model */ + $model = $this->container->factory->model('DatabaseFilters')->tmpInstance(); + + if ($status) + { + $ret = $model->setFilter($root, $table, $type); + } + else + { + $ret = $model->remove($root, $table, $type); + } + + return $ret; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetFSFilter.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetFSFilter.php new file mode 100644 index 00000000..6146012e --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetFSFilter.php @@ -0,0 +1,112 @@ + 0, + 'root' => '[SITEROOT]', + 'path' => '', + 'type' => '', + 'status' => 1 + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + $path = $filter->clean($defConfig['path'], 'path'); + $type = $filter->clean($defConfig['type'], 'cmd'); + $status = $filter->clean($defConfig['status'], 'bool'); + + $crumbs = array(); + $node = ''; + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown filesystem root', 500); + } + + // We need a path + if (empty($path)) + { + throw new \RuntimeException('Unknown path', 500); + } + + // Get the subdirectory and explode it to its parts + $path = trim($path, '/'); + + if (!empty($path)) + { + $crumbs = explode('/', $root); + $node = array_pop($crumbs); + } + + if (empty($node)) + { + throw new \RuntimeException('Unknown path', 500); + } + + // We need a table name + if (empty($type)) + { + throw new \RuntimeException('Filter type is mandatory', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var FileFilters $model */ + $model = $this->container->factory->model('FileFilters')->tmpInstance(); + + if ($status) + { + $ret = $model->setFilter($root, $crumbs, $node, $type); + } + else + { + $ret = $model->remove($root, $crumbs, $node, $type); + } + + return $ret; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetIncludedDB.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetIncludedDB.php new file mode 100644 index 00000000..e23875e0 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetIncludedDB.php @@ -0,0 +1,85 @@ + 0, + 'name' => '', + 'connection' => array(), + 'test' => true, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $name = $filter->clean($defConfig['name'], 'string'); + $connection = $filter->clean($defConfig['connection'], 'array'); + $test = $filter->clean($defConfig['test'], 'bool'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + if ( + empty($connection) || !isset($connection['host']) || !isset($connection['driver']) + || !isset($connection['database']) || !isset($connection['user']) + || !isset($connection['password']) + ) + { + throw new \RuntimeException('Connection information missing or incomplete', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var MultipleDatabases $model */ + $model = $this->container->factory->model('MultipleDatabases')->tmpInstance(); + + if ($test) + { + $result = $model->test($connection); + + if (!$result['status']) + { + throw new \RuntimeException('Connection test failed: ' . $result['message'], 500); + } + } + + return $model->setFilter($name, $connection); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetIncludedDirectory.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetIncludedDirectory.php new file mode 100644 index 00000000..0f9eb1b6 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetIncludedDirectory.php @@ -0,0 +1,103 @@ + 0, + 'uuid' => '', + 'path' => '', + 'virtualFolder' => '', + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $path = $filter->clean($defConfig['path'], 'path'); + $uuid = $filter->clean($defConfig['uuid'], 'string'); + $virtualFolder = $filter->clean($defConfig['virtualFolder'], 'string'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a path + if (empty($path)) + { + throw new \RuntimeException('Path is required', 500); + } + + // We need a uuid + if (empty($uuid)) + { + $uuid = $this->uuid_v4(); + } + + // We need a vf + if (empty($virtualFolder)) + { + $virtualFolder = basename($path); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var IncludeFolders $model */ + $model = $this->container->factory->model('IncludeFolders')->tmpInstance(); + + $data = array($path, $virtualFolder); + + return $model->setFilter($uuid, $data); + } + + /** + * Generate a UUID v4 + * + * @return string + */ + private function uuid_v4() + { + $randval = new RandomValue(); + $data = $randval->generate(16); + + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100 + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 + + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetRegexDBFilter.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetRegexDBFilter.php new file mode 100644 index 00000000..e26db277 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetRegexDBFilter.php @@ -0,0 +1,95 @@ + 0, + 'root' => '[SITEDB]', + 'regex' => '', + 'type' => 'tables', + 'status' => 1 + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + $regex = $filter->clean($defConfig['regex'], 'string'); + $type = $filter->clean($defConfig['type'], 'cmd'); + $status = $filter->clean($defConfig['status'], 'bool'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown database root', 500); + } + + // We need a regex name + if (empty($regex)) + { + throw new \RuntimeException('Regex is mandatory', 500); + } + + // We need a regex name + if (empty($type)) + { + throw new \RuntimeException('Filter type is mandatory', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var RegExDatabaseFilters $model */ + $model = $this->container->factory->model('RegExDatabaseFilters')->tmpInstance(); + + if ($status) + { + $ret = $model->setFilter($type, $root, $regex); + } + else + { + $ret = $model->remove($type, $root, $regex); + } + + return $ret; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetRegexFSFilter.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetRegexFSFilter.php new file mode 100644 index 00000000..f7d6db22 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/SetRegexFSFilter.php @@ -0,0 +1,95 @@ + 0, + 'root' => '[SITEDB]', + 'regex' => '', + 'type' => 'directories', + 'status' => 1 + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $root = $filter->clean($defConfig['root'], 'string'); + $regex = $filter->clean($defConfig['regex'], 'string'); + $type = $filter->clean($defConfig['type'], 'cmd'); + $status = $filter->clean($defConfig['status'], 'bool'); + + // We need a valid profile ID + if ($profile <= 0) + { + $profile = 1; + } + + // We need a root + if (empty($root)) + { + throw new \RuntimeException('Unknown database root', 500); + } + + // We need a regex name + if (empty($regex)) + { + throw new \RuntimeException('Regex is mandatory', 500); + } + + // We need a regex name + if (empty($type)) + { + throw new \RuntimeException('Filter type is mandatory', 500); + } + + // Set the active profile + $this->container->platform->setSessionVar('profile', $profile); + + // Load the configuration + Platform::getInstance()->load_configuration($profile); + + /** @var RegExFileFilters $model */ + $model = $this->container->factory->model('RegExFileFilters')->tmpInstance(); + + if ($status) + { + $ret = $model->setFilter($type, $root, $regex); + } + else + { + $ret = $model->remove($type, $root, $regex); + } + + return $ret; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/StartBackup.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/StartBackup.php new file mode 100644 index 00000000..a109daa1 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/StartBackup.php @@ -0,0 +1,89 @@ + 1, + 'description' => '', + 'comment' => '', + 'backupid' => null, + 'overrides' => array(), + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = (int) $defConfig['profile']; + $profile = max(1, $profile); // Make sure $profile is a positive integer >= 1 + $description = $filter->clean($defConfig['description'], 'string'); + $comment = $filter->clean($defConfig['comment'], 'string'); + $backupid = $filter->clean($defConfig['backupid'], 'cmd'); + $backupid = empty($backupid) ? null : $backupid; // Otherwise the Engine doesn't set a backup ID + $overrides = $filter->clean($defConfig['overrides'], 'array'); + + $this->container->platform->setSessionVar('profile', $profile); + define('AKEEBA_PROFILE', $profile); + + /** + * 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); + + /** @var \Akeeba\Backup\Site\Model\Backup $model */ + $model = $this->container->factory->model('Backup')->tmpInstance(); + $model->setState('tag', AKEEBA_BACKUP_ORIGIN); + $model->setState('backupid', $backupid); + $model->setState('description', $description); + $model->setState('comment', $comment); + + $array = $model->startBackup($overrides); + + if ($array['Error'] != '') + { + throw new \RuntimeException('A backup error has occurred: ' . $array['Error'], 500); + } + + // BackupID contains the numeric backup record ID. backupid contains the backup id (usually in the form id123) + $statistics = Factory::getStatistics(); + $array['BackupID'] = $statistics->getId(); + + // Remote clients expect a boolean, not an integer. + $array['HasRun'] = ($array['HasRun'] === 0); + + return $array; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/StepBackup.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/StepBackup.php new file mode 100644 index 00000000..815605df --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/StepBackup.php @@ -0,0 +1,85 @@ + null, + 'tag' => AKEEBA_BACKUP_ORIGIN, + 'backupid' => null, + ); + + $defConfig = array_merge($defConfig, $parameters); + + $profile = $filter->clean($defConfig['profile'], 'int'); + $tag = $filter->clean($defConfig['tag'], 'cmd'); + $backupid = $filter->clean($defConfig['backupid'], 'cmd'); + + if (is_null($backupid) && defined('AKEEBA_BACKUP_ID')) + { + $tag = AKEEBA_BACKUP_ID; + } + + if (empty($backupid)) + { + throw new \RuntimeException("JSON API :: stepBackup -- You have not provided the required backupid parameter. This parameter is MANDATORY since May 2016. Please update your client software to include this parameter."); + } + + // Try to set the profile from the setup parameters + if (!empty($profile)) + { + $profile = max(1, $profile); // Make sure $profile is a positive integer >= 1 + $this->container->platform->setSessionVar('profile', $profile); + define('AKEEBA_PROFILE', $profile); + } + + /** @var \Akeeba\Backup\Site\Model\Backup $model */ + $model = $this->container->factory->model('Backup')->tmpInstance(); + $model->setState('tag', $tag); + $model->setState('backupid', $backupid); + $array = $model->stepBackup(false); + + if ($array['Error'] != '') + { + throw new \RuntimeException('A backup error has occurred: ' . $array['Error'], 500); + } + + // BackupID contains the numeric backup record ID. backupid contains the backup id (usually in the form id123) + $statistics = Factory::getStatistics(); + $array['BackupID'] = $statistics->getId(); + + // Remote clients expect a boolean, not an integer. + $array['HasRun'] = ($array['HasRun'] === 0); + + return $array; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/TestDBConnection.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/TestDBConnection.php new file mode 100644 index 00000000..7f73a9cc --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/TestDBConnection.php @@ -0,0 +1,57 @@ + array(), + ); + + $defConfig = array_merge($defConfig, $parameters); + + $connection = $filter->clean($defConfig['connection'], 'array'); + + if ( + empty($connection) || !isset($connection['host']) || !isset($connection['driver']) + || !isset($connection['database']) || !isset($connection['user']) + || !isset($connection['password']) + ) + { + throw new \RuntimeException('Connection information missing or incomplete', 500); + } + + /** @var MultipleDatabases $model */ + $model = $this->container->factory->model('MultipleDatabases')->tmpInstance(); + + return $model->test($connection); + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/Task/UpdateGetInformation.php b/deployed/akeeba/components/com_akeeba/Model/Json/Task/UpdateGetInformation.php new file mode 100644 index 00000000..438811b4 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/Task/UpdateGetInformation.php @@ -0,0 +1,50 @@ + 0 + ); + + $defConfig = array_merge($defConfig, $parameters); + + $force = $filter->clean($defConfig['force'], 'bool'); + + /** @var Updates $model */ + $model = $this->container->factory->model('Updates')->tmpInstance(); + + $updateInformation = $model->getUpdates($force); + + return (object)$updateInformation; + } +} diff --git a/deployed/akeeba/components/com_akeeba/Model/Json/TaskInterface.php b/deployed/akeeba/components/com_akeeba/Model/Json/TaskInterface.php new file mode 100644 index 00000000..92266079 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/Model/Json/TaskInterface.php @@ -0,0 +1,44 @@ +dispatcher->dispatch(); diff --git a/deployed/akeeba/components/com_akeeba/index.html b/deployed/akeeba/components/com_akeeba/index.html new file mode 100644 index 00000000..0b551329 --- /dev/null +++ b/deployed/akeeba/components/com_akeeba/index.html @@ -0,0 +1 @@ + \ No newline at end of file