<?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace Akeeba\Engine\Util; defined('AKEEBAENGINE') || die(); use Akeeba\Engine\Factory; use Akeeba\Engine\Platform; use DirectoryIterator; use LogicException; /** * Unified engine parameters helper class. Deals with scripting, GUI configuration elements and information on engine * parts (filters, dump engines, scan engines, archivers, installers). */ class EngineParameters { /** * Holds the parsed scripting.json contents * * @var array */ public $scripting = null; /** * The currently active scripting type * * @var string */ protected $activeType = null; /** * Holds the known paths holding JSON definitions of engines, installers and configuration gui elements * * @var array */ protected $enginePartPaths = []; /** * Cache of the engines known to this object * * @var array */ protected $engine_list = []; /** * Cache of the GUI configuration elements known to this object * * @var array */ protected $gui_list = []; /** * Cache of the installers known to this object * * @var array */ protected $installer_list = []; /** * Append a path to the end of the paths list for a specific section * * @param string $path Absolute filesystem path to add * @param string $section The section to add it to (gui, engine, installer, filters) * * @return void */ public function addPath(string $path, string $section = 'gui') { $path = Factory::getFilesystemTools()->TranslateWinPath($path); // If the array is empty, populate with the defaults if (!array_key_exists($section, $this->enginePartPaths)) { $this->getEnginePartPaths($section); } // If the path doesn't already exist, add it if (!in_array($path, $this->enginePartPaths[$section])) { $this->enginePartPaths[$section][] = $path; } } /** * Returns an array with domain keys and domain class names for the current * backup type. The idea is that shifting this array walks through the backup * process. When the array is empty, the backup is done. * * Each element of the array is an array with two keys: domain and class. * * @return array */ public function getDomainChain(): array { $configuration = Factory::getConfiguration(); $script = $configuration->get('akeeba.basic.backup_type', 'full'); $scripting = $this->loadScripting(); $domains = $scripting['domains']; $keys = $scripting['scripts'][$script]['chain']; $result = []; foreach ($keys as $domain_key) { $result[] = [ 'domain' => $domains[$domain_key]['domain'], 'class' => $domains[$domain_key]['class'], ]; } return $result; } /** * Get the paths for a specific section * * @param string $section The section to get the path list for (engine, installer, gui, filter) * * @return array */ public function getEnginePartPaths(string $section = 'gui') { // Create the key if it's not already present if (!array_key_exists($section, $this->enginePartPaths)) { $this->enginePartPaths[$section] = []; } if (!empty($this->enginePartPaths[$section])) { return $this->enginePartPaths[$section]; } // Add the defaults if the list is empty switch ($section) { case 'engine': $this->enginePartPaths[$section] = [ Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot()), ]; break; case 'installer': $this->enginePartPaths[$section] = [ Factory::getFilesystemTools()->TranslateWinPath(Platform::getInstance()->get_installer_images_path()), ]; break; case 'gui': // Add core GUI definitions $this->enginePartPaths[$section] = [ Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Core'), ]; // Add platform GUI definition files $platform_paths = Platform::getInstance()->getPlatformDirectories(); foreach ($platform_paths as $p) { $this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config'); $pro = defined('AKEEBA_PRO') && AKEEBA_PRO; $pro = defined('AKEEBABACKUP_PRO') ? (AKEEBABACKUP_PRO ? true : false) : $pro; if ($pro) { $this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config/Pro'); } } break; case 'filter': $this->enginePartPaths[$section] = [ Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Platform/Filter/Stack'), Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Filter/Stack'), ]; $platform_paths = Platform::getInstance()->getPlatformDirectories(); foreach ($platform_paths as $p) { $this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Filter/Stack'); } break; default: throw new LogicException(sprintf('Can not get paths for engine section ā%sā. No section by this name is known to Akeeba Engine.', $section)); } return $this->enginePartPaths[$section]; } /** * Returns a hash list of Akeeba engines and their data. Each entry has the engine name as key and contains two * arrays, under the 'information' and 'parameters' keys. * * @param string $engine_type The engine type to return information for * * @return array */ public function getEnginesList(string $engine_type): array { $engine_type = ucfirst($engine_type); // Try to serve cached data first if (isset($this->engine_list[$engine_type])) { return $this->engine_list[$engine_type]; } // Find absolute path to normal and plugins directories $temp = $this->getEnginePartPaths('engine'); $path_list = []; foreach ($temp as $path) { $path_list[] = $path . '/' . $engine_type; } // Initialize the array where we store our data $this->engine_list[$engine_type] = []; // Loop for the paths where engines can be found foreach ($path_list as $path) { if (!@is_dir($path)) { continue; } if (!@is_readable($path)) { continue; } $di = new DirectoryIterator($path); /** @var DirectoryIterator $file */ foreach ($di as $file) { if (!$file->isFile()) { continue; } if ($file->getExtension() !== 'json') { continue; } $bare_name = ucfirst($file->getBasename('.json')); // Some hosts copy .json and .php files, renaming them (ie foobar.1.php) // We need to exclude them, otherwise we'll get a fatal error for declaring the same class twice if (preg_match('/[^A-Za-z0-9]/', $bare_name)) { continue; } $information = []; $parameters = []; $this->parseEngineJSON($file->getRealPath(), $information, $parameters); $this->engine_list[$engine_type][lcfirst($bare_name)] = [ 'information' => $information, 'parameters' => $parameters, ]; } } return $this->engine_list[$engine_type]; } /** * Parses the GUI JSON files and returns an array of groups and their data * * @return array */ public function getGUIGroups(): array { // Try to serve cached data first if (!empty($this->gui_list) && is_array($this->gui_list)) { if (count($this->gui_list) > 0) { return $this->gui_list; } } // Find absolute path to normal and plugins directories $path_list = $this->getEnginePartPaths('gui'); // Initialize the array where we store our data $this->gui_list = []; // Loop for the paths where engines can be found foreach ($path_list as $path) { if (!@is_dir($path)) { continue; } if (!@is_readable($path)) { continue; } $allJSONFiles = []; $di = new DirectoryIterator($path); /** @var DirectoryIterator $file */ foreach ($di as $file) { if (!$file->isFile()) { continue; } // PHP 5.3.5 and earlier do not support getExtension if ($file->getExtension() !== 'json') { continue; } $allJSONFiles[] = $file->getRealPath(); } if (empty($allJSONFiles)) { continue; } // Sort GUI files alphabetically asort($allJSONFiles); // Include each GUI def file foreach ($allJSONFiles as $filename) { $information = []; $parameters = []; $this->parseInterfaceJSON($filename, $information, $parameters); // This effectively skips non-GUI JSONs (e.g. the scripting JSON) if (!empty($information['description'])) { if (!isset($information['merge'])) { $information['merge'] = 0; } $group_name = substr(basename($filename), 0, -5); $def = [ 'information' => $information, 'parameters' => $parameters, ]; if (!$information['merge'] || !isset($this->gui_list[$group_name])) { $this->gui_list[$group_name] = $def; } else { $this->gui_list[$group_name]['information'] = array_merge($this->gui_list[$group_name]['information'], $def['information']); $this->gui_list[$group_name]['parameters'] = array_merge($this->gui_list[$group_name]['parameters'], $def['parameters']); } } } } ksort($this->gui_list); // Push stack filter settings to the 03.filters section $path_list = $this->getEnginePartPaths('filter'); // Loop for the paths where optional filters can be found foreach ($path_list as $path) { if (!@is_dir($path)) { continue; } if (!@is_readable($path)) { continue; } // Store JSON names in temp array because we'll sort based on filename (GUI order IS IMPORTANT!!) $allJSONFiles = []; $di = new DirectoryIterator($path); /** @var DirectoryIterator $file */ foreach ($di as $file) { if (!$file->isFile()) { continue; } // PHP 5.3.5 and earlier do not support getExtension if ($file->getExtension() !== 'json') { continue; } $allJSONFiles[] = $file->getRealPath(); } if (empty($allJSONFiles)) { continue; } // Sort filter files alphabetically asort($allJSONFiles); // Include each filter def file foreach ($allJSONFiles as $filename) { $information = []; $parameters = []; $this->parseInterfaceJSON($filename, $information, $parameters); if (!array_key_exists('03.filters', $this->gui_list)) { $this->gui_list['03.filters'] = ['parameters' => []]; } if (!array_key_exists('parameters', $this->gui_list['03.filters'])) { $this->gui_list['03.filters']['parameters'] = []; } if (!is_array($parameters)) { $parameters = []; } $this->gui_list['03.filters']['parameters'] = array_merge($this->gui_list['03.filters']['parameters'], $parameters); } } /** * Parse showon attributes * * The GUI list array format is like this: * ``` * [ * '01.sectionName' => [ * 'information' => [...], * 'parameters' => [ * 'some.parameter.name' => [ * 'title' => 'something', * ... * 'showon' => 'some.other.parameter.name:1' * ], * ... * ] * ], * ... * ] * ``` * * We need to convert the shown of the parameters to JSON data which can be used by the `showon` JavaScript * code. The assumption only made here is that all parameter keys are turned into `INPUT` elements with an * attribute `name="var[some.parameter.name]"`. This is how the GUI code in all backup applications our company * makes works. * * For backup applications which do not have showon support (they lack the JavaScript code) this does not matter * and neither does it break anything. All parameters are shown all the time like we have been doing for years. */ $this->gui_list = array_map( function (array $section): array { foreach ($section['parameters'] as $paramName => &$paramDef) { if (isset($paramDef['showon'])) { // Parse showon $paramDef['showon'] = $this->parseShowOnConditions($paramDef['showon']); } } return $section; }, $this->gui_list ); return $this->gui_list; } /** * Parses the installer JSON files and returns an array of installers and their data * * @param boolean $forDisplay If true only returns the information relevant for displaying the GUI * * @return array */ public function getInstallerList(bool $forDisplay = false): array { // Try to serve cached data first if (!empty($this->installer_list) && is_array($this->installer_list)) { if (count($this->installer_list) > 0) { return $this->installer_list; } } // Find absolute path to normal and plugins directories $path_list = [ Platform::getInstance()->get_installer_images_path(), ]; // Initialize the array where we store our data $this->installer_list = []; // Loop for the paths where engines can be found foreach ($path_list as $path) { if (!@is_dir($path)) { continue; } if (!@is_readable($path)) { continue; } $di = new DirectoryIterator($path); /** @var DirectoryIterator $file */ foreach ($di as $file) { if (!$file->isFile()) { continue; } // PHP 5.3.5 and earlier do not support getExtension if ($file->getExtension() !== 'json') { continue; } $rawData = file_get_contents($file->getRealPath()); $data = empty($rawData) ? [] : json_decode($rawData, true); if ($forDisplay) { $innerData = reset($data); if (array_key_exists('listinoptions', $innerData)) { if ($innerData['listinoptions'] == 0) { continue; } } } foreach ($data as $key => $values) { $this->installer_list[$key] = []; foreach ($values as $key2 => $value) { $this->installer_list[$key][$key2] = $value; } } } } return $this->installer_list; } /** * Returns the JSON representation of the GUI definition and the associated values * * @return string */ public function getJsonGuiDefinition(): string { // Initialize the array which will be converted to JSON representation $json_array = [ 'engines' => [], 'installers' => [], 'gui' => [], ]; // Get a reference to the configuration $configuration = Factory::getConfiguration(); // Get data for all engines $engine_types = [ 'archiver', 'dump', 'scan', 'writer', 'postproc', ]; foreach ($engine_types as $type) { $engines = $this->getEnginesList($type); $tempArray = []; $engineTitles = []; foreach ($engines as $engine_name => $engine_data) { // Translate information foreach ($engine_data['information'] as $key => $value) { switch ($key) { case 'title': case 'description': $value = Platform::getInstance()->translate($value); break; } $tempArray[$engine_name]['information'][$key] = $value; if ($key == 'title') { $engineTitles[$engine_name] = $value; } } // Process parameters $parameters = []; foreach ($engine_data['parameters'] as $param_key => $param) { $param['default'] = $configuration->get($param_key, $param['default'], false); foreach ($param as $option_key => $option_value) { // Translate title, description, enumkeys switch ($option_key) { case 'title': case 'description': case 'labelempty': case 'labelnotempty': $param[$option_key] = Platform::getInstance()->translate($option_value); break; case 'enumkeys': $enumkeys = explode('|', $option_value); $new_keys = []; foreach ($enumkeys as $old_key) { $new_keys[] = Platform::getInstance()->translate($old_key); } $param[$option_key] = implode('|', $new_keys); break; case 'showon': $param[$option_key] = $this->parseShowOnConditions($param[$option_key]); default: } } $parameters[$param_key] = $param; } // Add processed parameters $tempArray[$engine_name]['parameters'] = $parameters; } asort($engineTitles); foreach ($engineTitles as $engineName => $title) { $json_array['engines'][$type][$engineName] = $tempArray[$engineName]; } } // Get data for GUI elements $json_array['gui'] = []; $groupdefs = $this->getGUIGroups(); foreach ($groupdefs as $groupKey => $definition) { $group_name = ''; if (isset($definition['information']) && isset($definition['information']['description'])) { $group_name = Platform::getInstance()->translate($definition['information']['description']); } // Skip no-name groups if (empty($group_name)) { continue; } $parameters = []; foreach ($definition['parameters'] as $param_key => $param) { $param['default'] = $configuration->get($param_key, $param['default'], false); foreach ($param as $option_key => $option_value) { // Translate title, description, enumkeys switch ($option_key) { case 'title': case 'description': $param[$option_key] = Platform::getInstance()->translate($option_value); break; case 'enumkeys': $enumkeys = explode('|', $option_value); $new_keys = []; foreach ($enumkeys as $old_key) { $new_keys[] = Platform::getInstance()->translate($old_key); } $param[$option_key] = implode('|', $new_keys); break; default: } } $parameters[$param_key] = $param; } $json_array['gui'][$group_name] = $parameters; } // Get data for the installers $json_array['installers'] = $this->getInstallerList(true); uasort($json_array['installers'], function ($a, $b) { if ($a['name'] == $b['name']) { return 0; } return ($a['name'] < $b['name']) ? -1 : 1; }); $json = json_encode($json_array); return $json; } /** * Returns a volatile scripting parameter for the active backup type * * @param string $key The relative key, e.g. core.createarchive * @param mixed $default Default value * * @return mixed The scripting parameter's value */ public function getScriptingParameter(string $key, $default = null) { $configuration = Factory::getConfiguration(); if (is_null($this->activeType)) { $this->activeType = $configuration->get('akeeba.basic.backup_type', 'full'); } return $configuration->get('volatile.scripting.' . $this->activeType . '.' . $key, $default); } /** * Imports the volatile scripting parameters to the registry * * @return void */ public function importScriptingToRegistry(): void { $scripting = $this->loadScripting(); $configuration = Factory::getConfiguration(); $configuration->mergeArray($scripting['data'], false); } /** * Loads the scripting.json and returns an array with the domains, the scripts and the raw data * * @return array The parsed scripting.json. Array keys: domains, scripts, data */ public function loadScripting(?string $jsonPath = ''): ?array { if (!empty($this->scripting)) { return $this->scripting; } $this->scripting = []; $jsonPath = $jsonPath ?: Factory::getAkeebaRoot() . '/Core/scripting.json'; if (!@file_exists($jsonPath)) { return $this->scripting; } $rawData = file_get_contents($jsonPath); $rawScriptingData = empty($rawData) ? [] : json_decode($rawData, true); $domain_keys = explode('|', $rawScriptingData['volatile.akeebaengine.domains']); $domains = []; foreach ($domain_keys as $key) { $record = [ 'domain' => $rawScriptingData['volatile.domain.' . $key . '.domain'], 'class' => $rawScriptingData['volatile.domain.' . $key . '.class'], 'text' => $rawScriptingData['volatile.domain.' . $key . '.text'], ]; $domains[$key] = $record; } $script_keys = explode('|', $rawScriptingData['volatile.akeebaengine.scripts']); $scripts = []; foreach ($script_keys as $key) { $record = [ 'chain' => explode('|', $rawScriptingData['volatile.scripting.' . $key . '.chain']), 'text' => $rawScriptingData['volatile.scripting.' . $key . '.text'], ]; $scripts[$key] = $record; } $this->scripting = [ 'domains' => $domains, 'scripts' => $scripts, 'data' => $rawScriptingData, ]; return $this->scripting; } /** * Parses an engine JSON file returning two arrays, one with the general information * of that engine and one with its configuration variables' definitions * * @param string $jsonPath Absolute path to engine JSON file * @param array $information [out] The engine information hash array * @param array $parameters [out] The parameters hash array * * @return bool True if the file was loaded */ public function parseEngineJSON(string $jsonPath, array &$information, array &$parameters): bool { if (!file_exists($jsonPath)) { return false; } $information = [ 'title' => '', 'description' => '', ]; $parameters = []; $rawData = file_get_contents($jsonPath); $jsonData = empty($rawData) ? [] : json_decode($rawData, true); foreach ($jsonData ?? [] as $section => $data) { if (is_array($data)) { if ($section == '_information') { // Parse information foreach ($data as $key => $value) { $information[$key] = $value; } } elseif (substr($section, 0, 1) != '_') { // Parse parameters $newparam = [ 'title' => '', 'description' => '', 'type' => 'string', 'default' => '', ]; foreach ($data as $key => $value) { $newparam[$key] = $value; } $parameters[$section] = $newparam; } } } return true; } /** * Parses a graphical interface JSON file returning two arrays, one with the general * information of that configuration section and one with its configuration variables' * definitions. * * @param string $jsonPath Absolute path to engine JSON file * @param array $information [out] The GUI information hash array * @param array $parameters [out] The parameters hash array * * @return bool True if the file was loaded */ public function parseInterfaceJSON(string $jsonPath, array &$information, array &$parameters): bool { if (!file_exists($jsonPath)) { return false; } $information = [ 'description' => '', ]; $parameters = []; $rawData = file_get_contents($jsonPath); $jsonData = empty($rawData) ? [] : json_decode($rawData, true); foreach ($jsonData as $section => $data) { if (is_array($data)) { if ($section == '_group') { // Parse information foreach ($data as $key => $value) { $information[$key] = $value; } continue; } if (substr($section, 0, 1) != '_') { // Parse parameters $newparam = [ 'title' => '', 'description' => '', 'type' => 'string', 'default' => '', 'protected' => 0, ]; foreach ($data as $key => $value) { $newparam[$key] = $value; } $parameters[$section] = $newparam; } } } return true; } /** * Add a path to the beginning of the paths list for a specific section * * @param string $path Absolute filesystem path to add * @param string $section The section to add it to (gui, engine, installer, filters) * * @return void */ public function prependPath(string $path, string $section = 'gui'): void { $path = Factory::getFilesystemTools()->TranslateWinPath($path); // If the array is empty, populate with the defaults if (!array_key_exists($section, $this->enginePartPaths)) { $this->getEnginePartPaths($section); } // If the path doesn't already exist, add it if (!in_array($path, $this->enginePartPaths[$section])) { array_unshift($this->enginePartPaths[$section], $path); } } /** * Parse the `showon` conditions text into an instructions array for the ShowOn JavaScript * * @param string|null $showOn The `showon` conditions text * @param string|null $arrayName The array all of our parameters are members of, default 'var'. * * @return array The ShowOn JavaScript instructions * * @since 9.3.1 */ private function parseShowOnConditions(?string $showOn, ?string $arrayName = 'var'): array { if (empty($showOn)) { return []; } $showOnData = []; $showOnParts = preg_split('#(\[AND\]|\[OR\])#', $showOn, -1, PREG_SPLIT_DELIM_CAPTURE); $op = ''; foreach ($showOnParts as $showOnPart) { if (in_array($showOnPart, ['[AND]', '[OR]'])) { $op = trim($showOnPart, '[]'); continue; } $compareEqual = strpos($showOnPart, '!:') === false; $showOnPartBlocks = explode(($compareEqual ? ':' : '!:'), $showOnPart, 2); $field = $arrayName ? sprintf("%s[%s]", $arrayName, $showOnPartBlocks[0]) : $showOnPartBlocks[0]; $showOnData[] = [ 'field' => $field, 'values' => explode(',', $showOnPartBlocks[1]), 'sign' => $compareEqual === true ? '=' : '!=', 'op' => $op, ]; $op = ''; } return $showOnData; } }