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