diff --git a/Sources/Actions/Admin/ACP.php b/Sources/Actions/Admin/ACP.php index 3ff82b74546..90b8a4e1f83 100644 --- a/Sources/Actions/Admin/ACP.php +++ b/Sources/Actions/Admin/ACP.php @@ -30,6 +30,7 @@ use SMF\Menu; use SMF\Parser; use SMF\Routable; +use SMF\Sapi; use SMF\SecurityToken; use SMF\Theme; use SMF\Url; @@ -780,7 +781,7 @@ public function execute(): void // Now - finally - call the right place! if (isset($menu->include_data['file'])) { - require_once Config::canonicalPath(Config::$sourcedir . '/' . $menu->include_data['file']); + require_once Sapi::canonicalPath(Config::$sourcedir . '/' . $menu->include_data['file']); } // Get the right callable. @@ -1888,7 +1889,7 @@ protected function init() ]); if (file_exists($include)) { - require_once Config::canonicalPath($include); + require_once Sapi::canonicalPath($include); } } } diff --git a/Sources/Actions/Admin/Find.php b/Sources/Actions/Admin/Find.php index 3efcfad7004..c07c146fc05 100644 --- a/Sources/Actions/Admin/Find.php +++ b/Sources/Actions/Admin/Find.php @@ -194,7 +194,7 @@ public function internal(): void IntegrationHook::call('integrate_admin_search', [&$this->language_files, &$this->include_files, &$this->settings_search]); foreach ($this->include_files as $file) { - require_once Config::canonicalPath(Config::$sourcedir . '/' . $file . '.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/' . $file . '.php'); } /* This is the huge array that defines everything... it's a huge array of items formatted as follows: diff --git a/Sources/Actions/Admin/Languages.php b/Sources/Actions/Admin/Languages.php index cf1c32d1b92..6096ba2251b 100644 --- a/Sources/Actions/Admin/Languages.php +++ b/Sources/Actions/Admin/Languages.php @@ -28,6 +28,7 @@ use SMF\Menu; use SMF\PackageManager\PackageUtils; use SMF\PackageManager\XmlArray; +use SMF\Sapi; use SMF\SecurityToken; use SMF\Theme; use SMF\User; @@ -905,7 +906,7 @@ function ($val1, $val2) { // Quickly load General language entries. $old_txt = Lang::$txt; - require Config::canonicalPath($general_filename); + require Sapi::canonicalPath($general_filename); Utils::$context['lang_file_not_writable_message'] = is_writable($general_filename) ? '' : Lang::getTxt('lang_file_not_writable', ['file' => $general_filename], file: 'ManageSettings'); diff --git a/Sources/Actions/Admin/Logs.php b/Sources/Actions/Admin/Logs.php index 3ef5d905b43..a377ba356b9 100644 --- a/Sources/Actions/Admin/Logs.php +++ b/Sources/Actions/Admin/Logs.php @@ -23,6 +23,7 @@ use SMF\IntegrationHook; use SMF\Lang; use SMF\Menu; +use SMF\Sapi; use SMF\Theme; use SMF\User; use SMF\Utils; @@ -156,7 +157,7 @@ public function execute(): void ]; if (!empty(self::$subactions[$this->subaction][0])) { - require_once Config::canonicalPath(Config::$sourcedir . '/' . self::$subactions[$this->subaction][0]); + require_once Sapi::canonicalPath(Config::$sourcedir . '/' . self::$subactions[$this->subaction][0]); } $call = \is_string(self::$subactions[$this->subaction][1]) && method_exists($this, self::$subactions[$this->subaction][1]) ? [$this, self::$subactions[$this->subaction][1]] : Utils::getCallable(self::$subactions[$this->subaction][1]); diff --git a/Sources/Actions/Admin/Server.php b/Sources/Actions/Admin/Server.php index f2f360299d5..63e3626a517 100644 --- a/Sources/Actions/Admin/Server.php +++ b/Sources/Actions/Admin/Server.php @@ -554,7 +554,7 @@ public function export(): void ACP::saveDBSettings($config_vars); // Create the new directory, but revert to the previous one if anything goes wrong. - require_once Config::canonicalPath(Config::$sourcedir . '/Actions/Profile/Export.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Actions/Profile/Export.php'); create_export_dir($prev_export_dir); // Ensure we don't lose track of any existing export files. diff --git a/Sources/Actions/Admin/Subscriptions.php b/Sources/Actions/Admin/Subscriptions.php index 1ca67d63b46..96790cceec0 100644 --- a/Sources/Actions/Admin/Subscriptions.php +++ b/Sources/Actions/Admin/Subscriptions.php @@ -26,6 +26,7 @@ use SMF\Lang; use SMF\Menu; use SMF\Parser; +use SMF\Sapi; use SMF\SecurityToken; use SMF\TaskRunner; use SMF\Theme; @@ -2099,7 +2100,7 @@ public static function loadPaymentGateways(): array fclose($fp); if (str_contains($header, '// SMF Payment Gateway: ' . strtolower($matches[1]))) { - require_once Config::canonicalPath(Config::$sourcedir . '/' . $file); + require_once Sapi::canonicalPath(Config::$sourcedir . '/' . $file); $gateways[] = [ 'filename' => $file, diff --git a/Sources/Actions/Moderation/Main.php b/Sources/Actions/Moderation/Main.php index 4edfc8200b0..2256ef4f0d1 100644 --- a/Sources/Actions/Moderation/Main.php +++ b/Sources/Actions/Moderation/Main.php @@ -25,6 +25,7 @@ use SMF\Lang; use SMF\Menu; use SMF\Routable; +use SMF\Sapi; use SMF\Theme; use SMF\User; use SMF\Utils; @@ -240,7 +241,7 @@ public function execute(): void $this->createMenu(); if (isset(Menu::$loaded['moderate']->include_data['file'])) { - require_once Config::canonicalPath(Config::$sourcedir . '/' . Menu::$loaded['moderate']->include_data['file']); + require_once Sapi::canonicalPath(Config::$sourcedir . '/' . Menu::$loaded['moderate']->include_data['file']); } $call = \is_string(Menu::$loaded['moderate']->include_data['function']) && method_exists($this, Menu::$loaded['moderate']->include_data['function']) ? [$this, Menu::$loaded['moderate']->include_data['function']] : Utils::getCallable(Menu::$loaded['moderate']->include_data['function']); diff --git a/Sources/Actions/Profile/Main.php b/Sources/Actions/Profile/Main.php index f96d74e9953..919b9791c0b 100644 --- a/Sources/Actions/Profile/Main.php +++ b/Sources/Actions/Profile/Main.php @@ -647,7 +647,7 @@ public function execute(): void // File to include? if (!empty($menu->include_data['file'])) { - require_once Config::canonicalPath(Config::$sourcedir . '/' . $menu->include_data['file']); + require_once Sapi::canonicalPath(Config::$sourcedir . '/' . $menu->include_data['file']); } // Build the link tree. diff --git a/Sources/Actions/Register.php b/Sources/Actions/Register.php index f4cbd160c5c..7be44cdb58a 100644 --- a/Sources/Actions/Register.php +++ b/Sources/Actions/Register.php @@ -26,6 +26,7 @@ use SMF\Parser; use SMF\Profile; use SMF\Routable; +use SMF\Sapi; use SMF\SecurityToken; use SMF\Theme; use SMF\User; @@ -281,7 +282,7 @@ public function show(): void // Or any standard ones? if (!empty(Config::$modSettings['registration_fields'])) { - require_once Config::canonicalPath(Config::$sourcedir . '/Profile-Modify.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Profile-Modify.php'); // Setup some important context. Theme::loadTemplate('Profile'); diff --git a/Sources/Actions/VerificationCode.php b/Sources/Actions/VerificationCode.php index 31d17a3494a..fdb3e848398 100644 --- a/Sources/Actions/VerificationCode.php +++ b/Sources/Actions/VerificationCode.php @@ -21,6 +21,7 @@ use SMF\Cache\CacheApi; use SMF\Config; use SMF\Routable; +use SMF\Sapi; use SMF\Theme; use SMF\User; use SMF\Utils; @@ -683,7 +684,7 @@ protected function showLetterImage(string $letter): bool // Include it! header('content-type: image/png'); - include Config::canonicalPath(Theme::$current->settings['default_theme_dir'] . '/fonts/' . $random_font . '/' . strtoupper($letter) . '.png'); + include Sapi::canonicalPath(Theme::$current->settings['default_theme_dir'] . '/fonts/' . $random_font . '/' . strtoupper($letter) . '.png'); // Nothing more to come. die(); diff --git a/Sources/Actions/XmlHttp.php b/Sources/Actions/XmlHttp.php index f178aa4fe05..b3d7b4077b8 100644 --- a/Sources/Actions/XmlHttp.php +++ b/Sources/Actions/XmlHttp.php @@ -32,6 +32,7 @@ use SMF\Parser; use SMF\Profile; use SMF\Routable; +use SMF\Sapi; use SMF\Theme; use SMF\User; use SMF\Utils; @@ -230,7 +231,7 @@ public function newsletterpreview(): void */ public function sig_preview(): void { - require_once Config::canonicalPath(Config::$sourcedir . '/Profile-Modify.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Profile-Modify.php'); $user = isset($_POST['user']) ? (int) $_POST['user'] : 0; $is_owner = $user == User::$me->id; diff --git a/Sources/Cache/CacheApi.php b/Sources/Cache/CacheApi.php index 26b1cb1ffd8..e1cf66ec40c 100644 --- a/Sources/Cache/CacheApi.php +++ b/Sources/Cache/CacheApi.php @@ -19,6 +19,7 @@ use SMF\Config; use SMF\Debug\DebugUtils; use SMF\IntegrationHook; +use SMF\Sapi; use SMF\Utils; abstract class CacheApi @@ -514,7 +515,7 @@ final public static function quickGet(string $key, string $file, string|array $f ) ) { if (!empty($file) && is_file(Config::$sourcedir . '/' . $file)) { - require_once Config::canonicalPath(Config::$sourcedir . '/' . $file); + require_once Sapi::canonicalPath(Config::$sourcedir . '/' . $file); } $cache_block = \call_user_func_array($function, $params); diff --git a/Sources/Config.php b/Sources/Config.php index a636f9a3a10..c7f0b00eb4d 100644 --- a/Sources/Config.php +++ b/Sources/Config.php @@ -1191,7 +1191,7 @@ public static function reloadModSettings(): void $include = strtr(trim($include), ['$boarddir' => self::$boarddir, '$sourcedir' => self::$sourcedir]); if (file_exists($include)) { - require_once self::canonicalPath($include); + require_once Sapi::canonicalPath($include); } } } @@ -2851,129 +2851,6 @@ public static function checkCron(): void } } - /** - * Normalizes directory separators and resolves '.' and '..' in a file path. - * - * The $path does not need to point to an existing file. - * - * If $path does point to an existing file, or if an ancestor directory of - * $path exists, then \realpath() will be used to resolve that part of the - * path, unless the $real parameter is set to false. - * - * @param string $path The file path. - * @param string|bool $base_dir Base directory for relative paths. - * - If a string, relative paths are prepended with the string and a - * directory separator. Note that directory separators in this string - * will be normalized just like in $path. - * - If true, relative paths are prepended with the current working - * directory and a directory separator. - * - If false, relative paths are processed as given. - * Default: false. - * @param bool $real Whether to get the real path for existing files. This - * can be set to false if the caller wants to canonicalize a hypothetical - * path without any possibility of the real file structure interfering - * with the result. - * Default: true. - * @return string The canonical file path. - */ - public static function canonicalPath(string $path, string|bool $base_dir = false, bool $real = true): string - { - // If $path points to a real file, this is all we need to do. - if (!empty($real) && ($realpath = @realpath($path)) !== false) { - return $realpath; - } - - $base_dir = \is_string($base_dir) ? rtrim(str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $base_dir), DIRECTORY_SEPARATOR) : (!empty($base_dir) ? getcwd() : false); - - $path = trim(str_replace(['\\', '/'], DIRECTORY_SEPARATOR, (string) $path)); - - // We need to know the path of the root directory. - if (DIRECTORY_SEPARATOR === '/') { - $root = ''; - $is_absolute = str_starts_with($path, DIRECTORY_SEPARATOR); - } else { - // Windows network shares and devices. - if (str_starts_with($path, DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR)) { - if (\in_array(substr($path, 2, 2), ['?' . DIRECTORY_SEPARATOR, '.' . DIRECTORY_SEPARATOR])) { - $root = substr($path, 0, strpos($path, DIRECTORY_SEPARATOR, 3)); - } else { - $root = ''; - - for ($i = 0; $i < 3; $i++) { - $root = substr($path, 0, strpos($path, DIRECTORY_SEPARATOR, \strlen($root) + 1)); - } - } - } - // Windows absolute DOS-style path. - elseif (strpos($path, ':') !== false && strpos($path, DIRECTORY_SEPARATOR) === strpos($path, ':') + 1) { - $root = substr($path, 0, strpos($path, DIRECTORY_SEPARATOR)); - } - // Windows relative path. - else { - $root = substr(getcwd(), 0, strcspn(getcwd(), DIRECTORY_SEPARATOR)); - - // If relative to current drive's root, make it absolute. - if (strpos($path, DIRECTORY_SEPARATOR) === 0) { - $path = $root . $path; - } - } - - $is_absolute = str_starts_with($path, $root . DIRECTORY_SEPARATOR); - } - - // Build canonical path. - $canonical_path = ''; - - if ($is_absolute) { - $path = substr($path, \strlen($root . DIRECTORY_SEPARATOR)); - $path_parts = [$root]; - } elseif (\is_string($base_dir)) { - $path_parts = explode(DIRECTORY_SEPARATOR, $base_dir); - } else { - $path_parts = []; - } - - foreach (explode(DIRECTORY_SEPARATOR, $path) as $key => $part) { - if (empty($part) || $part === '.') { - continue; - } - - if ($part === '..') { - if ($is_absolute && $path_parts === [$root]) { - continue; - } - - if (empty($path_parts) || $path_parts[0] === '..') { - $path_parts[] = $part; - } else { - array_pop($path_parts); - } - } else { - $path_parts[] = $part; - } - - $canonical_path = implode(DIRECTORY_SEPARATOR, $path_parts); - - if (empty($real) || \in_array($canonical_path, ['', '.', '..'])) { - continue; - } - - // Check for intermediate symlinks. - $realpath = @realpath($canonical_path); - - if ($realpath !== false && $realpath !== $canonical_path) { - $path_parts = explode(DIRECTORY_SEPARATOR, $realpath); - } - } - - // Ambiguity is bad. - if ($canonical_path === '') { - $canonical_path = $is_absolute ? $root . DIRECTORY_SEPARATOR : '.'; - } - - return $canonical_path; - } - /************************* * Internal static methods *************************/ diff --git a/Sources/Forum.php b/Sources/Forum.php index 24906ff0e92..63f7124a514 100644 --- a/Sources/Forum.php +++ b/Sources/Forum.php @@ -807,7 +807,7 @@ protected static function findAction(?string $action): string|callable|false // Otherwise, it was set - so let's go to that action. if (!empty(self::$actions[$action][0])) { - require_once Config::canonicalPath(Config::$sourcedir . '/' . self::$actions[$action][0]); + require_once Sapi::canonicalPath(Config::$sourcedir . '/' . self::$actions[$action][0]); } // Do the right thing. diff --git a/Sources/Graphics/Gif/File.php b/Sources/Graphics/Gif/File.php index 720123aa71f..f0173ba768a 100644 --- a/Sources/Graphics/Gif/File.php +++ b/Sources/Graphics/Gif/File.php @@ -24,6 +24,7 @@ namespace SMF\Graphics\Gif; use SMF\Config; +use SMF\Sapi; class File { @@ -184,5 +185,5 @@ public function get_png_data(string $background_color): string|bool // 64-bit only functions? if (!\function_exists('smf_crc32')) { - require_once Config::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); } diff --git a/Sources/ItemList.php b/Sources/ItemList.php index ab0939d3715..d32cb32a3ed 100644 --- a/Sources/ItemList.php +++ b/Sources/ItemList.php @@ -332,7 +332,7 @@ protected function setStartAndItemsPerPage(): void $this->total_num_items = $this->options['get_count']['value']; } else { if (isset($this->options['get_count']['file'])) { - require_once Config::canonicalPath($this->options['get_count']['file']); + require_once Sapi::canonicalPath($this->options['get_count']['file']); } $call = Utils::getCallable($this->options['get_count']['function']); @@ -483,7 +483,7 @@ protected function getItems(): void } else { // Get the file with the function for the item list. if (isset($this->options['get_items']['file'])) { - require_once Config::canonicalPath($this->options['get_items']['file']); + require_once Sapi::canonicalPath($this->options['get_items']['file']); } $call = Utils::getCallable($this->options['get_items']['function']); diff --git a/Sources/Lang.php b/Sources/Lang.php index 79753563486..553639d5b15 100644 --- a/Sources/Lang.php +++ b/Sources/Lang.php @@ -295,7 +295,7 @@ public static function load(string $filename, string $lang = '', bool $fatal = t if (file_exists($file[0] . '/' . $file[2] . '/' . $file[1] . '.php')) { // Include it! // {DIR} / {locale} / {file} .php - require Config::canonicalPath($file[0] . '/' . $file[2] . '/' . $file[1] . '.php'); + require Sapi::canonicalPath($file[0] . '/' . $file[2] . '/' . $file[1] . '.php'); // Note that we found it. $found = true; @@ -305,7 +305,7 @@ public static function load(string $filename, string $lang = '', bool $fatal = t DebugUtils::addDebugSource( lang_key: 'language_files', key: implode('|', $file), - value: Config::canonicalPath((Config::$languagesdir == $file[0] ? basename($file[0]) : ltrim(str_replace(array_map('dirname', self::$dirs), '', $file[0]), '/')) . '/' . $file[2] . '/' . $file[1] . '.php'), + value: Sapi::canonicalPath((Config::$languagesdir == $file[0] ? basename($file[0]) : ltrim(str_replace(array_map('dirname', self::$dirs), '', $file[0]), '/')) . '/' . $file[2] . '/' . $file[1] . '.php'), ); } @@ -582,7 +582,7 @@ public static function get(bool $use_cache = true): array 'name' => $langName ?? $entry, 'selected' => false, 'filename' => $entry, - 'location' => Config::canonicalPath($language_dir . '/' . $entry . '/General.php'), + 'location' => Sapi::canonicalPath($language_dir . '/' . $entry . '/General.php'), ]; } $dir->close(); @@ -1053,7 +1053,7 @@ public static function loadOld(array $attempts): bool $oldLanguage = $locale_to_lang[$file[2]] ?? false; if ($oldLanguage !== false && file_exists($file[0] . '/' . $file[1] . '.' . $oldLanguage . '.php')) { - require Config::canonicalPath($file[0] . '/' . $file[1] . '.' . $oldLanguage . '.php'); + require Sapi::canonicalPath($file[0] . '/' . $file[1] . '.' . $oldLanguage . '.php'); // Note that we found it. $found = true; diff --git a/Sources/Localization/AsciiTransliterator.php b/Sources/Localization/AsciiTransliterator.php index 5da5a41d294..25fef085961 100644 --- a/Sources/Localization/AsciiTransliterator.php +++ b/Sources/Localization/AsciiTransliterator.php @@ -15,9 +15,9 @@ namespace SMF\Localization; -use SMF\Config; use SMF\IntegrationHook; use SMF\Lang; +use SMF\Sapi; use SMF\Unicode\Utf8String; use SMF\Utils; @@ -291,7 +291,7 @@ public static function manual(string $string): string $ord = mb_ord($char); if (file_exists(__DIR__ . '/data/AsciiTransliteration_' . \sprintf('%04d', $ord >> 8) . '.php')) { - include_once Config::canonicalPath(__DIR__ . '/data/AsciiTransliteration_' . \sprintf('%04d', $ord >> 8) . '.php'); + include_once Sapi::canonicalPath(__DIR__ . '/data/AsciiTransliteration_' . \sprintf('%04d', $ord >> 8) . '.php'); $new_chars[$char_num] = $ascii_transliteration[$ord >> 8][$ord & 255] ?? $char; } diff --git a/Sources/Localization/MessageFormatter.php b/Sources/Localization/MessageFormatter.php index cec533f11cf..b11c3e8775e 100644 --- a/Sources/Localization/MessageFormatter.php +++ b/Sources/Localization/MessageFormatter.php @@ -17,6 +17,7 @@ use SMF\Config; use SMF\Lang; +use SMF\Sapi; use SMF\Time; use SMF\User; use SMF\Utils; @@ -182,7 +183,7 @@ public static function formatMessage(string $message, array $args = []): string } // Try to guess the currency based on country. else { - require_once Config::canonicalPath(Config::$sourcedir . '/Unicode/Currencies.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Unicode/Currencies.php'); $country_currencies = \function_exists('country_currencies') ? country_currencies() : []; @@ -505,7 +506,7 @@ protected static function getPluralizationCategory(int|float|string $num, string // Ensure we have our pluralization rules. if (empty(self::$plural_rules)) { - require_once Config::canonicalPath(Config::$sourcedir . '/Unicode/Plurals.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Unicode/Plurals.php'); self::$plural_rules = \SMF\Unicode\plurals(); } @@ -836,7 +837,7 @@ protected static function applyNumberSkeleton(int|float|string $number, string $ break; case 'currency': - require_once Config::canonicalPath(Config::$sourcedir . '/Unicode/Currencies.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Unicode/Currencies.php'); $currencies = currencies(); diff --git a/Sources/Mail.php b/Sources/Mail.php index 7ad60a6d2eb..e766f8be462 100644 --- a/Sources/Mail.php +++ b/Sources/Mail.php @@ -751,7 +751,7 @@ public static function sendSmtp(array $mail_to_array, string $subject, string $m } if (!\function_exists('idn_to_ascii')) { - require_once Config::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); } $helo = idn_to_ascii($helo, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); diff --git a/Sources/MailAgent/APIs/SMTP.php b/Sources/MailAgent/APIs/SMTP.php index ba0fe5ea986..f692bea13e6 100644 --- a/Sources/MailAgent/APIs/SMTP.php +++ b/Sources/MailAgent/APIs/SMTP.php @@ -338,7 +338,7 @@ private function getHostname(): string } if (!\function_exists('idn_to_ascii')) { - require_once Config::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); } $helo = idn_to_ascii($helo, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); diff --git a/Sources/Maintenance/Tools/ToolsBase.php b/Sources/Maintenance/Tools/ToolsBase.php index 654b2827e8f..0a30da29339 100644 --- a/Sources/Maintenance/Tools/ToolsBase.php +++ b/Sources/Maintenance/Tools/ToolsBase.php @@ -252,7 +252,7 @@ public function loadMaintenanceDatabase(string $db_type): Db { $db_class = '\\SMF\\Db\\APIs\\' . Db::getClass(Config::$db_type); - require_once Config::canonicalPath(Config::$sourcedir . '/Db/APIs/' . Db::getClass(Config::$db_type) . '.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Db/APIs/' . Db::getClass(Config::$db_type) . '.php'); return new $db_class(); } diff --git a/Sources/PackageManager/PackageManager.php b/Sources/PackageManager/PackageManager.php index 4bf93c4ff6b..cc443db1514 100644 --- a/Sources/PackageManager/PackageManager.php +++ b/Sources/PackageManager/PackageManager.php @@ -1135,7 +1135,7 @@ public function install(): void extract($backcompat_globals, EXTR_REFS | EXTR_SKIP); } - require Config::canonicalPath(Config::$packagesdir . '/temp/' . Utils::$context['base_path'] . $action['filename']); + require Sapi::canonicalPath(Config::$packagesdir . '/temp/' . Utils::$context['base_path'] . $action['filename']); } } elseif ($action['type'] == 'credits') { // Time to build the billboard @@ -1181,7 +1181,7 @@ public function install(): void extract($backcompat_globals, EXTR_REFS | EXTR_SKIP); } - require Config::canonicalPath(Config::$packagesdir . '/temp/' . Utils::$context['base_path'] . $action['filename']); + require Sapi::canonicalPath(Config::$packagesdir . '/temp/' . Utils::$context['base_path'] . $action['filename']); } } // Handle a redirect... diff --git a/Sources/PackageManager/PackageUtils.php b/Sources/PackageManager/PackageUtils.php index 2e1e8f3ed82..5a3be4c092b 100644 --- a/Sources/PackageManager/PackageUtils.php +++ b/Sources/PackageManager/PackageUtils.php @@ -3857,7 +3857,7 @@ private static function fixLangFilePathForRemoval(array &$return, array &$this_a */ private static function smf_crc32(string $number): string { - require_once Config::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); return smf_crc32($number); } diff --git a/Sources/Punycode.php b/Sources/Punycode.php index 32f6f20d19a..7ef3ba193d1 100755 --- a/Sources/Punycode.php +++ b/Sources/Punycode.php @@ -540,7 +540,7 @@ protected function codePointToChar(int $code): string */ protected function preprocess(string $domain, array &$errors = []): string { - require_once Config::canonicalPath(Config::$sourcedir . '/Unicode/Idna.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Unicode/Idna.php'); $regexes = idna_regex(); $maps = idna_maps(); @@ -617,7 +617,7 @@ protected function validateLabel(string $label, bool $toPunycode = true): int return self::IDNA_ERROR_LEADING_COMBINING_MARK; } - require_once Config::canonicalPath(Config::$sourcedir . '/Unicode/Idna.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Unicode/Idna.php'); $regexes = Unicode\idna_regex(); diff --git a/Sources/Sapi.php b/Sources/Sapi.php index 43dcb1a3e5e..bdb84c2233d 100644 --- a/Sources/Sapi.php +++ b/Sources/Sapi.php @@ -536,4 +536,127 @@ public static function getCpuCount(bool $update = false): int return self::$cpu_count; } + + /** + * Normalizes directory separators and resolves '.' and '..' in a file path. + * + * The $path does not need to point to an existing file. + * + * If $path does point to an existing file, or if an ancestor directory of + * $path exists, then \realpath() will be used to resolve that part of the + * path, unless the $real parameter is set to false. + * + * @param string $path The file path. + * @param string|bool $base_dir Base directory for relative paths. + * - If a string, relative paths are prepended with the string and a + * directory separator. Note that directory separators in this string + * will be normalized just like in $path. + * - If true, relative paths are prepended with the current working + * directory and a directory separator. + * - If false, relative paths are processed as given. + * Default: false. + * @param bool $real Whether to get the real path for existing files. This + * can be set to false if the caller wants to canonicalize a hypothetical + * path without any possibility of the real file structure interfering + * with the result. + * Default: true. + * @return string The canonical file path. + */ + public static function canonicalPath(string $path, string|bool $base_dir = false, bool $real = true): string + { + // If $path points to a real file, this is all we need to do. + if (!empty($real) && ($realpath = @realpath($path)) !== false) { + return $realpath; + } + + $base_dir = \is_string($base_dir) ? rtrim(str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $base_dir), DIRECTORY_SEPARATOR) : (!empty($base_dir) ? getcwd() : false); + + $path = trim(str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path)); + + // We need to know the path of the root directory. + if (DIRECTORY_SEPARATOR === '/') { + $root = ''; + $is_absolute = str_starts_with($path, DIRECTORY_SEPARATOR); + } else { + // Windows network shares and devices. + if (str_starts_with($path, DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR)) { + if (\in_array(substr($path, 2, 2), ['?' . DIRECTORY_SEPARATOR, '.' . DIRECTORY_SEPARATOR])) { + $root = substr($path, 0, strpos($path, DIRECTORY_SEPARATOR, 3)); + } else { + $root = ''; + + for ($i = 0; $i < 3; $i++) { + $root = substr($path, 0, strpos($path, DIRECTORY_SEPARATOR, \strlen($root) + 1)); + } + } + } + // Windows absolute DOS-style path. + elseif (strpos($path, ':') !== false && strpos($path, DIRECTORY_SEPARATOR) === strpos($path, ':') + 1) { + $root = substr($path, 0, strpos($path, DIRECTORY_SEPARATOR)); + } + // Windows relative path. + else { + $root = substr(getcwd(), 0, strcspn(getcwd(), DIRECTORY_SEPARATOR)); + + // If relative to current drive's root, make it absolute. + if (strpos($path, DIRECTORY_SEPARATOR) === 0) { + $path = $root . $path; + } + } + + $is_absolute = str_starts_with($path, $root . DIRECTORY_SEPARATOR); + } + + // Build canonical path. + $canonical_path = ''; + + if ($is_absolute) { + $path = substr($path, \strlen($root . DIRECTORY_SEPARATOR)); + $path_parts = [$root]; + } elseif (\is_string($base_dir)) { + $path_parts = explode(DIRECTORY_SEPARATOR, $base_dir); + } else { + $path_parts = []; + } + + foreach (explode(DIRECTORY_SEPARATOR, $path) as $key => $part) { + if (empty($part) || $part === '.') { + continue; + } + + if ($part === '..') { + if ($is_absolute && $path_parts === [$root]) { + continue; + } + + if (empty($path_parts) || $path_parts[0] === '..') { + $path_parts[] = $part; + } else { + array_pop($path_parts); + } + } else { + $path_parts[] = $part; + } + + $canonical_path = implode(DIRECTORY_SEPARATOR, $path_parts); + + if (empty($real) || \in_array($canonical_path, ['', '.', '..'])) { + continue; + } + + // Check for intermediate symlinks. + $realpath = @realpath($canonical_path); + + if ($realpath !== false && $realpath !== $canonical_path) { + $path_parts = explode(DIRECTORY_SEPARATOR, $realpath); + } + } + + // Ambiguity is bad. + if ($canonical_path === '') { + $canonical_path = $is_absolute ? $root . DIRECTORY_SEPARATOR : '.'; + } + + return $canonical_path; + } } diff --git a/Sources/Search/APIs/Parsed.php b/Sources/Search/APIs/Parsed.php index 84e141e36f4..667a5ce89ca 100644 --- a/Sources/Search/APIs/Parsed.php +++ b/Sources/Search/APIs/Parsed.php @@ -1043,7 +1043,7 @@ protected function prepareString(string $string): string $string = mb_decode_numericentity($string, [0x010000, 0x10FFFF, 0, 0xFFFFFF], 'UTF-8'); // Separate emoji from regular words. - require_once Config::canonicalPath(Config::$sourcedir . '/Unicode/RegularExpressions.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Unicode/RegularExpressions.php'); $prop_classes = utf8_regex_properties(); $string = preg_replace_callback( diff --git a/Sources/Search/SearchApi.php b/Sources/Search/SearchApi.php index 257f4069b14..c94e40335ce 100644 --- a/Sources/Search/SearchApi.php +++ b/Sources/Search/SearchApi.php @@ -24,6 +24,7 @@ use SMF\Lang; use SMF\PackageManager\PackageUtils; use SMF\Parser; +use SMF\Sapi; use SMF\User; use SMF\Utils; @@ -902,7 +903,7 @@ final public static function detect(): array continue; } - require_once Config::canonicalPath($file_info->getPathname()); + require_once Sapi::canonicalPath($file_info->getPathname()); if (!class_exists($class_name, false)) { continue; diff --git a/Sources/TaskRunner.php b/Sources/TaskRunner.php index c460d770a48..ea071ac6d0f 100644 --- a/Sources/TaskRunner.php +++ b/Sources/TaskRunner.php @@ -556,7 +556,7 @@ protected function performTask(array $task_details): bool $include = strtr(trim($task_details['task_file']), ['$boarddir' => Config::$boarddir, '$sourcedir' => Config::$sourcedir]); if (file_exists($include)) { - require_once Config::canonicalPath($include); + require_once Sapi::canonicalPath($include); } } diff --git a/Sources/Tasks/ExportProfileData.php b/Sources/Tasks/ExportProfileData.php index e45a6299be7..7f838dc8320 100644 --- a/Sources/Tasks/ExportProfileData.php +++ b/Sources/Tasks/ExportProfileData.php @@ -1647,7 +1647,7 @@ protected function buildStylesheet(): void $this->_details['format'] = 'XML_XSLT'; } - require_once Config::canonicalPath(Config::$sourcedir . '/Actions/Profile/Export.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Actions/Profile/Export.php'); $export_formats = Export::getFormats(); /* Notes: diff --git a/Sources/Theme.php b/Sources/Theme.php index d6bd44a0874..861fa74388f 100644 --- a/Sources/Theme.php +++ b/Sources/Theme.php @@ -236,7 +236,7 @@ public function initialize(): void $include = strtr(trim($include), ['$boarddir' => Config::$boarddir, '$sourcedir' => Config::$sourcedir, '$themedir' => $this->settings['theme_dir']]); if (file_exists($include)) { - require_once Config::canonicalPath($include); + require_once Sapi::canonicalPath($include); } } } @@ -2347,9 +2347,9 @@ protected static function templateInclude(string $filename, bool $once = false): $file_found = file_exists($filename); if ($once && $file_found) { - require_once Config::canonicalPath($filename); + require_once Sapi::canonicalPath($filename); } elseif ($file_found) { - require Config::canonicalPath($filename); + require Sapi::canonicalPath($filename); } if ($file_found !== true) { diff --git a/Sources/Topic.php b/Sources/Topic.php index 9311fbd3a2b..cae63040523 100644 --- a/Sources/Topic.php +++ b/Sources/Topic.php @@ -1556,7 +1556,7 @@ public static function remove(array|int $topics, bool $decreasePostCount = true, self::move($recycleTopics, (int) Config::$modSettings['recycle_board']); // Close reports that are being recycled. - require_once Config::canonicalPath(Config::$sourcedir . '/Actions/Moderation/Main.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Actions/Moderation/Main.php'); Db::$db->query( 'UPDATE {db_prefix}log_reported diff --git a/Sources/Url.php b/Sources/Url.php index b763d84dcd9..995d829dabb 100644 --- a/Sources/Url.php +++ b/Sources/Url.php @@ -223,7 +223,7 @@ public function toAscii(): self if (!empty($this->host)) { if (!\function_exists('idn_to_ascii')) { - require_once Config::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); } // Convert the host using the Punycode algorithm @@ -275,7 +275,7 @@ public function toUtf8(): self if (!empty($this->host)) { if (!\function_exists('idn_to_utf8')) { - require_once Config::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); } // Decode the domain from Punycode. @@ -713,7 +713,7 @@ function ($line) { // Convert Punycode to Unicode if (!\function_exists('idn_to_utf8')) { - require_once Config::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); + require_once Sapi::canonicalPath(Config::$sourcedir . '/Subs-Compat.php'); } foreach ($tlds as &$tld) { diff --git a/Sources/Utils.php b/Sources/Utils.php index 78a378b1d42..32544997ef3 100644 --- a/Sources/Utils.php +++ b/Sources/Utils.php @@ -2522,14 +2522,14 @@ final protected static function loadFile(string $string): string|false // Load the file if it can be loaded. if (is_file($path)) { - require_once Config::canonicalPath($path); + require_once Sapi::canonicalPath($path); } // No? Try a fallback to Config::$sourcedir. else { $path = Config::$sourcedir . '/' . $file; if (is_file($path)) { - require_once Config::canonicalPath($path); + require_once Sapi::canonicalPath($path); } // Sorry, can't do much for you at this point. elseif (empty(Utils::$context['uninstalling'])) {