shell bypass 403
<?php /** * @package akeebabackup * @copyright Copyright (c)2006-2025 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace Akeeba\Component\AkeebaBackup\Administrator\Model; defined('_JEXEC') || die; use Akeeba\Component\AkeebaBackup\Administrator\Mixin\ModelExclusionFilterTrait; use Akeeba\Engine\Factory; use Akeeba\Engine\Platform; use InvalidArgumentException; use Joomla\CMS\MVC\Model\BaseModel; #[\AllowDynamicProperties] class FilefiltersModel extends BaseModel { use ModelExclusionFilterTrait; public function __construct($config = []) { parent::__construct($config); $this->knownFilterTypes = ['directories', 'files', 'skipdirs', 'skipfiles']; } /** * Returns an array with the listing and filter status of a directory * * @param string $root Root directory * @param array $crumbs Breadcrumbs in array format, defining the parent directory * @param string $child The child directory we want to scan * * @return array */ public function makeListing(string $root, array $crumbs = [], string $child = ''): array { // Construct the full node $node = $this->glueCrumbs($crumbs, $child); // Create the new crumbs if (!is_array($crumbs)) { $crumbs = []; } if (!empty($child)) { $crumbs[] = $child; } // Get listing with the filter info $listing = $this->getListing($root, $node); // Assemble the array $listing['root'] = $root; $listing['crumbs'] = $crumbs; return $listing; } /** * Toggle a filter * * @param string $root Root directory * @param array $crumbs Components of the current directory relative to the root * @param string $item The child item of the current directory we want to toggle the filter for * @param string $filter The name of the filter to apply (directories, skipfiles, skipdirs, files) * * @return array */ public function toggle(string $root, array $crumbs, string $item, string $filter): array { $node = $this->glueCrumbs($crumbs, $item); return $this->applyExclusionFilter($filter, $root, $node, 'toggle'); } /** * Set a filter * * @param string $root Root directory * @param array $crumbs Components of the current directory relative to the root * @param string $item The child item of the current directory we want to set the filter for * @param string $filter The name of the filter to apply (directories, skipfiles, skipdirs, files) * * @return array */ public function setFilter(string $root, array $crumbs, string $item, string $filter): array { $node = $this->glueCrumbs($crumbs, $item); return $this->applyExclusionFilter($filter, $root, $node, 'set'); } /** * Remove a filter * * @param string $root Root directory * @param array $crumbs Components of the current directory relative to the root * @param string $item The child item of the current directory we want to remove the filter for * @param string $filter The name of the filter to apply (directories, skipfiles, skipdirs, files) * * @return array */ public function remove(string $root, array $crumbs, string $item, string $filter): array { $node = $this->glueCrumbs($crumbs, $item); return $this->applyExclusionFilter($filter, $root, $node, 'remove'); } /** * Swap a filter * * @param string $root Root directory * @param array $crumbs Components of the current directory relative to the root * @param string $old_item The child item of the current directory we want to remove a filter for * @param string $new_item The child item of the current directory we want to add a filter for * @param string $filter The name of the filter to apply (directories, skipfiles, skipdirs, files) * * @return array */ public function swap(string $root, array $crumbs, string $old_item, string $new_item, string $filter): array { $new_node = $this->glueCrumbs($crumbs, $new_item); $old_node = $this->glueCrumbs($crumbs, $old_item); return $this->applyExclusionFilter($filter, $root, $new_node, 'swap', $old_node); } /** * Retrieves the filters as an array. Used for the tabular filter editor. * * @param string $root The root node to search filters on * * @return array A collection of hash arrays containing node and type for each filtered element */ public function getFilters(string $root): array { return $this->getTabularFilters($root); } /** * Resets the filters * * @param string $root Root directory * * @return array */ public function resetFilters(string $root): array { $this->resetAllFilters($root); return $this->makeListing($root); } /** * Handles a request coming in through AJAX. Basically, this is a simple proxy to the model methods. * * @return array */ public function doAjax(): array { $action = $this->getState('action'); $verb = array_key_exists('verb', get_object_vars($action)) ? $action->verb : null; if (!array_key_exists('crumbs', get_object_vars($action))) { $action->crumbs = []; } $ret_array = []; switch ($verb) { // Return a listing for the normal view case 'list': $ret_array = $this->makeListing($action->root, $action->crumbs, $action->node); break; // Toggle a filter's state case 'toggle': $ret_array = $this->toggle($action->root, $action->crumbs, $action->node, $action->filter); break; // Set a filter (used by the editor) case 'set': $ret_array = $this->setFilter($action->root, $action->crumbs, $action->node, $action->filter); break; // Swap a filter (used by the editor) case 'swap': $ret_array = $this->swap($action->root, $action->crumbs, $action->old_node, $action->new_node, $action->filter); break; case 'tab': $ret_array = [ 'list' => $this->getFilters($action->root) ]; break; // Reset filters case 'reset': $ret_array = $this->resetFilters($action->root); break; } return $ret_array; } /** * Returns a listing of contained directories and files, as well as their exclusion status * * @param string $root The root directory * @param string $node The subdirectory to scan * * @return array */ private function getListing(string $root, string $node): array { // Initialize the absolute directory root $directory = substr($root, 0); // Replace stock directory tags, like [SITEROOT] $stock_dirs = Platform::getInstance()->get_stock_directories(); if (!empty($stock_dirs)) { foreach ($stock_dirs as $key => $replacement) { $directory = str_replace($key, $replacement, $directory); } } $directory = Factory::getFilesystemTools()->TranslateWinPath($directory); // Clean and add the node $node = Factory::getFilesystemTools()->TranslateWinPath($node); // Just a directory separator is treated as no directory at all if (($node == '/')) { $node = ''; } // Trim leading and trailing slashes $node = trim($node, '/'); // Add node to directory if (!empty($node)) { $directory .= '/' . $node; } // Add any required trailing slash to the node to be used below if (!empty($node)) { $node .= '/'; } // Get a filters instance $filters = Factory::getFilters(); // Get a listing of folders and process it $folders = Factory::getFileLister()->getFolders($directory); $folders_out = []; if (!empty($folders)) { asort($folders); foreach ($folders as $folder) { $folder = Factory::getFilesystemTools()->TranslateWinPath($folder); // Filter out files whose names result to an empty JSON representation $json_folder = json_encode($folder); $folder = json_decode($json_folder); if (empty($folder)) { continue; } $test = $node . $folder; $status = []; // Check dir/all filter (exclude) $result = $filters->isFilteredExtended($test, $root, 'dir', 'all', $byFilter); $status['directories'] = (!$result) ? 0 : (($byFilter == 'directories') ? 1 : 2); // Check dir/content filter (skip_files) $result = $filters->isFilteredExtended($test, $root, 'dir', 'content', $byFilter); $status['skipfiles'] = (!$result) ? 0 : (($byFilter == 'skipfiles') ? 1 : 2); // Check dir/children filter (skip_dirs) $result = $filters->isFilteredExtended($test, $root, 'dir', 'children', $byFilter); $status['skipdirs'] = (!$result) ? 0 : (($byFilter == 'skipdirs') ? 1 : 2); $status['link'] = @is_link($directory . '/' . $folder); // Add to output array $folders_out[$folder] = $status; } } unset($folders); $folders = $folders_out; // Get a listing of files and process it $files = Factory::getFileLister()->getFiles($directory); $files_out = []; if (!empty($files)) { asort($files); foreach ($files as $file) { // Filter out files whose names result to an empty JSON representation $json_file = json_encode($file); $file = json_decode($json_file); if (empty($file)) { continue; } $test = $node . $file; $status = []; // Check file/all filter (exclude) $result = $filters->isFilteredExtended($test, $root, 'file', 'all', $byFilter); $status['files'] = (!$result) ? 0 : (($byFilter == 'files') ? 1 : 2); $status['size'] = $this->formatSize(@filesize($directory . '/' . $file), 1); $status['link'] = @is_link($directory . '/' . $file); // Add to output array $files_out[$file] = $status; } } unset($files); $files = $files_out; // Return a compiled array $retArray = [ 'folders' => $folders, 'files' => $files, ]; return $retArray; /* Return array format * [array] : * 'folders' [array] : * (folder_name) => [array]: * 'directories' => 0|1|2 * 'skipfiles' => 0|1|2 * 'skipdirs' => 0|1|2 * 'files' [array] : * (file_name) => [array]: * 'files' => 0|1|2 * * Legend: * 0 -> Not excluded * 1 -> Excluded by the direct filter * 2 -> Excluded by another filter (regex, api, an unknown plugin filter...) */ } /** * Glues the current directory crumbs and the child directory into a node string * * @param array $crumbs Breadcrumbs in array or JSON encoded array format * @param string $child The child folder (relative to the root defined by crumbs) * * @return string The absolute node (path) of the $child */ private function glueCrumbs(array $crumbs, string $child): string { // Construct the full node $node = ''; array_walk($crumbs, function ($value, $index) { if (in_array(trim($value), ['.', '..'])) { throw new InvalidArgumentException("Unacceptable folder crumbs"); } }); if ((stristr($child, '/..') !== false) || (stristr($child, '\..') !== false)) { throw new InvalidArgumentException("Unacceptable child folder"); } if (!empty($crumbs)) { $node = implode('/', $crumbs); } if (!empty($node)) { $node .= '/'; } if (!empty($child)) { $node .= $child; } return $node; } /** * Format the size of the file (given in bytes) to something human readable, e.g. 123 MB * * @param int $bytes The file size in bytes * @param int $decimals How many decimals you want (default: 0) * * @return string The human-readable, formatted size */ private function formatSize(int $bytes, int $decimals = 0): string { $bytes = empty($bytes) ? 0 : (int) $bytes; $format = empty($decimals) ? '%0u' : '%0.' . $decimals . 'f'; $uom = [ 'TB' => 1048576 * 1048576, 'GB' => 1024 * 1048576, 'MB' => 1048576, 'KB' => 1024, 'B' => 1, ]; // Whole bytes cannot have decimal positions if (!empty($decimals)) { unset($uom['B']); } foreach ($uom as $unit => $byteSize) { if (floatval($bytes) >= $byteSize) { return sprintf($format, $bytes / $byteSize) . ' ' . $unit; } } // If the number is either too big or too small, return sprintf('%0u B', $bytes); } }