diff --git a/Sources/Actions/Login2.php b/Sources/Actions/Login2.php index 112c636b4a3..2b291b1b9ad 100644 --- a/Sources/Actions/Login2.php +++ b/Sources/Actions/Login2.php @@ -31,7 +31,6 @@ use SMF\SecurityToken; use SMF\Theme; use SMF\User; -use SMF\UserDataset; use SMF\Utils; /** @@ -312,17 +311,36 @@ public function main(): void $this->member = reset($loaded); // Bad password! Thought you could fool the database?! - if (!Security::hashVerifyPassword(Utils::htmlspecialcharsDecode($_POST['passwrd']), $this->member->passwd)) { - // If the forum was recently upgraded, password might be encrypted - // using a different algorithm. If so, fix it. Otherwise, bail out. - if (!$this->checkPasswordFallbacks()) { - return; + if ( + !Security::checkPassword( + Utils::htmlspecialcharsDecode($_POST['passwrd']), + $this->member, + Security::PASSWORD_FALLBACK_ALL, + ) + ) { + // They've messed up again - keep a count to see if they need a hand. + $_SESSION['failed_login'] ??= 0; + $_SESSION['failed_login']++; + + // Hmm... don't remember it, do you? Here, try the password reminder ;). + if ($_SESSION['failed_login'] >= Config::$modSettings['failed_login_threshold']) { + Utils::redirectexit('action=reminder'); + } + // We'll give you another chance... + else { + // Log an error so we know that it didn't go well in the error log. + ErrorHandler::log(Lang::getTxt('incorrect_password', file: 'Login') . ' - ' . $this->member->username . '', 'user'); + + Utils::$context['login_errors'] = [Lang::getTxt('invalid_credentials', file: 'General')]; } + + return; } + // Correct password, but it took multiple tries... - elseif (!empty($this->member->passwd_flood)) { + if (!empty($this->member->passwd_flood)) { // Let's be sure they weren't a little hacker. - self::validatePasswordFlood($this->member->id, $this->member->username, $this->member->passwd_flood, true); + Security::validatePasswordFlood($this->member->id, $this->member->username, $this->member->passwd_flood, true); // If we got here then we can reset the flood counter. $this->member->passwd_flood = ''; @@ -388,74 +406,6 @@ public static function checkAjax(): void } } - /** - * This protects against brute force attacks on a member's password. - * Importantly, even if the password was right we DON'T TELL THEM! - * - * @param int $id_member The ID of the member - * @param string $member_name The name of the member. - * @param bool|string $password_flood_value False if we don't have a flood value, otherwise a string with a timestamp and number of tries separated by a | - * @param bool $was_correct Whether or not the password was correct - * @param bool $tfa Whether we're validating for two-factor authentication - */ - public static function validatePasswordFlood(int $id_member, string $member_name, bool|string $password_flood_value = false, bool $was_correct = false, bool $tfa = false): void - { - // As this is only brute protection, we allow 5 attempts every 10 seconds. - - // Destroy any session or cookie data about this member, as they validated wrong. - // Only if they're not validating for 2FA - if (!$tfa) { - Cookie::setLoginCookie(-3600, 0); - - if (isset($_SESSION['login_' . Config::$cookiename])) { - unset($_SESSION['login_' . Config::$cookiename]); - } - } - - // We need a member! - if (!$id_member) { - // Redirect back! - Utils::redirectexit(); - - // Probably not needed, but still make sure... - ErrorHandler::fatalLang('no_access', false); - } - - // Right, have we got a flood value? - if ($password_flood_value !== false) { - @list($time_stamp, $number_tries) = explode('|', $password_flood_value); - } - - // Timestamp or number of tries invalid? - if (empty($number_tries) || empty($time_stamp)) { - $number_tries = 0; - $time_stamp = time(); - } - - // They've failed logging in already - if (!empty($number_tries)) { - // Give them less chances if they failed before - $number_tries = $time_stamp < time() - 20 ? 2 : $number_tries; - - // They are trying too fast, make them wait longer - if ($time_stamp < time() - 10) { - $time_stamp = time(); - } - } - - $number_tries++; - - // Broken the law? - if ($number_tries > 5) { - ErrorHandler::fatalLang('login_threshold_brute_fail', 'login', [$member_name]); - } - - // Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it! - $member = current(User::load($id_member, dataset: UserDataset::None)); - $member->passwd_flood = $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries; - $member->save(); - } - /****************** * Internal methods ******************/ @@ -507,216 +457,6 @@ protected function validateInput(): bool return true; } - /** - * Checks $_POST['passwrd'] against other possible encrypted strings. - * - * If a match is found, the old encrypted string is replaced with an updated - * version that uses modern encryption. - * - * This allows seamlessly updating the encryption after the forum has been - * upgraded or converted. - * - * @return bool Whether the supplied password was correct. - */ - protected function checkPasswordFallbacks(): bool - { - // Let's be cautious, no hacking please. thanx. - self::validatePasswordFlood($this->member->id, $this->member->username, $this->member->passwd_flood); - - // Maybe we were too hasty... let's try some other authentication methods. - $other_passwords = []; - - // SMF 2.1 prepended the username before the password. - if (Security::hashVerifyPassword(Utils::strtolower($this->member->username) . Utils::htmlspecialcharsDecode($_POST['passwrd']), $this->member->passwd)) { - $other_passwords[] = $this->member->passwd; - } - - // SMF 1.1 and 2.0 password styles. - if (\strlen($this->member->passwd) == 40) { - // Maybe they are using a hash from before the password fix. - // This is also valid for SMF 1.1 to 2.0 style of hashing, changed to bcrypt in SMF 2.1 - $other_passwords[] = sha1(strtolower($this->member->username) . Utils::htmlspecialcharsDecode($_POST['passwrd'])); - - // Perhaps we converted to UTF-8 and have a valid password being hashed differently. - if (!empty(Config::$modSettings['previousCharacterSet']) && Config::$modSettings['previousCharacterSet'] != 'utf8') { - // Try iconv first, for no particular reason. - if (\function_exists('iconv')) { - $other_passwords['iconv'] = sha1(strtolower(iconv('UTF-8', Config::$modSettings['previousCharacterSet'], $this->member->username)) . Utils::htmlspecialcharsDecode(iconv('UTF-8', Config::$modSettings['previousCharacterSet'], $_POST['passwrd']))); - } - - // Say it aint so, iconv failed! - if (empty($other_passwords['iconv']) && \function_exists('mb_convert_encoding')) { - $other_passwords[] = sha1(strtolower(mb_convert_encoding($this->member->username, 'UTF-8', Config::$modSettings['previousCharacterSet'])) . Utils::htmlspecialcharsDecode(mb_convert_encoding($_POST['passwrd'], 'UTF-8', Config::$modSettings['previousCharacterSet']))); - } - } - } - - // None of the below cases will be used most of the time (because the salt is normally set.) - if (!empty(Config::$modSettings['enable_password_conversion']) && $this->member->password_salt == '') { - // YaBB SE, Discus, MD5 (used a lot), SHA-1 (used some), SMF 1.0.x, IkonBoard, and none at all. - switch (\strlen($this->member->passwd)) { - case 13: - $other_passwords[] = crypt($_POST['passwrd'], substr($_POST['passwrd'], 0, 2)); - $other_passwords[] = crypt($_POST['passwrd'], substr($this->member->passwd, 0, 2)); - $other_passwords[] = crypt($_POST['passwrd'], $this->member->passwd); - - // This one is a strange one... MyPHP, crypt() on the MD5 hash. - $other_passwords[] = crypt(md5($_POST['passwrd']), md5($_POST['passwrd'])); - break; - - case 32: - $other_passwords[] = md5($_POST['passwrd']); - $other_passwords[] = hash_hmac('md5', $_POST['passwrd'], strtolower($this->member->username)); - $other_passwords[] = md5($_POST['passwrd'] . strtolower($this->member->username)); - $other_passwords[] = md5(md5($_POST['passwrd'])); - - // APBoard 2 Login Method. - $other_passwords[] = md5(crypt($_POST['passwrd'], 'CRYPT_MD5')); - break; - - case 34: - // phpBB3. - $other_passwords[] = $this->phpBB3_password_check($_POST['passwrd'], $this->member->passwd); - break; - - case 40: - $other_passwords[] = sha1($_POST['passwrd']); - break; - - case 64: - // Snitz style - SHA-256. - $other_passwords[] = hash('sha256', $_POST['passwrd']); - break; - } - - $other_passwords[] = $_POST['passwrd']; - } - // If the salt is set let's try some other options - elseif (!empty(Config::$modSettings['enable_password_conversion']) && $this->member->password_salt != '') { - switch (\strlen($this->member->passwd)) { - case 32: - // MyBB - $other_passwords[] = md5(md5($this->member->password_salt) . md5($_POST['passwrd'])); - - // vBulletin 3 style hashing? Let's welcome them with open arms \o/. - $other_passwords[] = md5(md5($_POST['passwrd']) . stripslashes($this->member->password_salt)); - - // Hmm.. p'raps it's Invision 2 style? - $other_passwords[] = md5(md5($this->member->password_salt) . md5($_POST['passwrd'])); - - // Some common md5 ones. - $other_passwords[] = md5($this->member->password_salt . $_POST['passwrd']); - $other_passwords[] = md5($_POST['passwrd'] . $this->member->password_salt); - break; - - case 40: - // BurningBoard3 style of hashing. - $other_passwords[] = sha1($this->member->password_salt . sha1($this->member->password_salt . sha1($_POST['passwrd']))); - // PunBB - $other_passwords[] = sha1($this->member->password_salt . sha1($_POST['passwrd'])); - break; - - case 64: - // PHP-Fusion - $other_passwords[] = hash_hmac('sha256', $_POST['passwrd'], $this->member->password_salt); - break; - } - } - - // Allows mods to easily extend the $other_passwords array - IntegrationHook::call('integrate_other_passwords', [&$other_passwords]); - - // Whichever encryption it was using, let's make it use SMF's now ;). - if (\in_array($this->member->passwd, $other_passwords)) { - $this->member->passwd = Security::hashPassword(Utils::htmlspecialcharsDecode($_POST['passwrd'])); - $this->member->password_salt = bin2hex(random_bytes(16)); - $this->member->passwd_flood = ''; - - $this->member->save(); - } - // Okay, they for sure didn't enter the password! - else { - // They've messed up again - keep a count to see if they need a hand. - $_SESSION['failed_login'] = isset($_SESSION['failed_login']) ? ($_SESSION['failed_login'] + 1) : 1; - - // Hmm... don't remember it, do you? Here, try the password reminder ;). - if ($_SESSION['failed_login'] >= Config::$modSettings['failed_login_threshold']) { - Utils::redirectexit('action=reminder'); - } - // We'll give you another chance... - else { - // Log an error so we know that it didn't go well in the error log. - ErrorHandler::log(Lang::getTxt('incorrect_password', file: 'Login') . ' - ' . $this->member->username . '', 'user'); - - Utils::$context['login_errors'] = [Lang::getTxt('invalid_credentials', file: 'General')]; - - return false; - } - } - - return true; - } - - /** - * Custom encryption for phpBB3 based passwords. - * - * @return ?string The hashed version of $_POST['passwrd'] - */ - protected function phpBB3_password_check(string $passwd, string $passwd_hash): ?string - { - // Too long or too short? - if (\strlen($passwd_hash) != 34) { - return null; - } - - // Range of characters allowed. - $range = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - - // Tests - $strpos = strpos($range, $passwd_hash[3]); - $count = 1 << $strpos; - $salt = substr($passwd_hash, 4, 8); - - $hash = md5($salt . $passwd, true); - - for (; $count != 0; --$count) { - $hash = md5($hash . $passwd, true); - } - - $output = substr($passwd_hash, 0, 12); - $i = 0; - - while ($i < 16) { - $value = \ord($hash[$i++]); - $output .= $range[$value & 0x3f]; - - if ($i < 16) { - $value |= \ord($hash[$i]) << 8; - } - - $output .= $range[($value >> 6) & 0x3f]; - - if ($i++ >= 16) { - break; - } - - if ($i < 16) { - $value |= \ord($hash[$i]) << 16; - } - - $output .= $range[($value >> 12) & 0x3f]; - - if ($i++ >= 16) { - break; - } - - $output .= $range[($value >> 18) & 0x3f]; - } - - // Return now. - return $output; - } - /** * Check activation status of the current user. * diff --git a/Sources/Actions/LoginTFA.php b/Sources/Actions/LoginTFA.php index 52a2b170b25..7d426f51165 100644 --- a/Sources/Actions/LoginTFA.php +++ b/Sources/Actions/LoginTFA.php @@ -79,7 +79,7 @@ public function execute(): void Utils::redirectexit(); } else { - parent::validatePasswordFlood($member->id, $member->username, $member->passwd_flood, false, true); + Security::validatePasswordFlood($member->id, $member->username, $member->passwd_flood, false, true); Utils::$context['tfa_error'] = true; Utils::$context['tfa_value'] = $_POST['tfa_code']; @@ -108,7 +108,7 @@ public function execute(): void Utils::redirectexit('action=profile;area=tfasetup;backup'); } else { - parent::validatePasswordFlood($member->id, $member->username, $member->passwd_flood, false, true); + Security::validatePasswordFlood($member->id, $member->username, $member->passwd_flood, false, true); Utils::$context['tfa_backup_error'] = true; Utils::$context['tfa_value'] = $_POST['tfa_code']; diff --git a/Sources/Actions/Profile/ShowPermissions.php b/Sources/Actions/Profile/ShowPermissions.php index dcaa31118a9..0aa8490b194 100644 --- a/Sources/Actions/Profile/ShowPermissions.php +++ b/Sources/Actions/Profile/ShowPermissions.php @@ -21,6 +21,7 @@ use SMF\Board; use SMF\Db\DatabaseApi as Db; use SMF\Lang; +use SMF\Permissions\Permission; use SMF\Permissions\PermissionProfile; use SMF\Profile; use SMF\Theme; @@ -131,7 +132,7 @@ public function execute(): void while ($row = Db::$db->fetch_assoc($result)) { // We don't know about this permission, it doesn't exist :P. - if (!Lang::txtExists('permissionname_' . $row['permission'], file: 'ManagePermissions')) { + if (!Permission::exists($row['permission'])) { continue; } @@ -200,7 +201,7 @@ public function execute(): void while ($row = Db::$db->fetch_assoc($request)) { // We don't know about this permission, it doesn't exist :P. - if (!Lang::txtExists('permissionname_' . $row['permission'], file: 'ManagePermissions')) { + if (!Permission::exists($row['permission'])) { continue; } diff --git a/Sources/Actions/Reminder.php b/Sources/Actions/Reminder.php index fac56d66c62..367f29e228b 100644 --- a/Sources/Actions/Reminder.php +++ b/Sources/Actions/Reminder.php @@ -257,13 +257,13 @@ public function setPassword2(): void // Quit if this code is not right. if (empty($_POST['code']) || $real_code !== $_POST['code'] || $issue_time + 3600 < time()) { // Stop brute force attacks like this. - Login2::validatePasswordFlood($this->member->id, $this->member->username, $this->member->passwd_flood, false); + Security::validatePasswordFlood($this->member->id, $this->member->username, $this->member->passwd_flood, false); ErrorHandler::fatal(Lang::getTxt('invalid_activation_code', file: 'Login'), false); } // Just in case, flood control. - Login2::validatePasswordFlood($this->member->id, $this->member->username, $this->member->passwd_flood, true); + Security::validatePasswordFlood($this->member->id, $this->member->username, $this->member->passwd_flood, true); // User validated. Update the database! $this->member->validation_code = ''; diff --git a/Sources/Cache/CacheApi.php b/Sources/Cache/CacheApi.php index d2792f96ff1..0f6a9c0508b 100644 --- a/Sources/Cache/CacheApi.php +++ b/Sources/Cache/CacheApi.php @@ -45,7 +45,7 @@ abstract class CacheApi * * This is an copy of the $cache_enable setting in Settings.php. */ - public static int $enable; + public static int $enable = 0; /** * @var string @@ -345,9 +345,7 @@ public function getImplementationClassKeyName(): string */ final public static function load(string $overrideCache = '', bool $fallbackSMF = true): object|bool { - if (!isset(self::$enable)) { - self::$enable = min(max((int) Config::$cache_enable, 0), 3); - } + self::$enable = min(max((int) (Config::$cache_enable ?? 0), self::$enable, 0), 3); if (!isset(self::$accelerator)) { self::$accelerator = Config::$cache_accelerator; diff --git a/Sources/Group.php b/Sources/Group.php index cb49b1a2409..619ab7997fe 100644 --- a/Sources/Group.php +++ b/Sources/Group.php @@ -1509,6 +1509,10 @@ public function copyPermissionsFrom(int $other_group, bool $inherit = false): vo ); while ($row = Db::$db->fetch_assoc($request)) { + if (!Permission::exists($row['permission'])) { + continue; + } + if (empty($illegal_permissions) || !\in_array($row['permission'], $illegal_permissions)) { $inserts[] = [$this->id, $row['permission'], $row['add_deny']]; } @@ -1539,6 +1543,10 @@ public function copyPermissionsFrom(int $other_group, bool $inherit = false): vo ); while ($row = Db::$db->fetch_assoc($request)) { + if (!Permission::exists($row['permission'])) { + continue; + } + $inserts[] = [$this->id, $row['id_profile'], $row['permission'], $row['add_deny']]; } diff --git a/Sources/Maintenance/Maintenance.php b/Sources/Maintenance/Maintenance.php index 8c47056f45d..06adcd75163 100644 --- a/Sources/Maintenance/Maintenance.php +++ b/Sources/Maintenance/Maintenance.php @@ -52,13 +52,6 @@ class Maintenance * Public static properties **************************/ - /** - * @var array - * - * General variables we pass between the logic and template. - */ - public static array $context = []; - /** * @var array * @@ -232,7 +225,7 @@ public function __construct() self::$theme_dir = self::getBaseDir() . '/Themes/default'; // This might be overwritten by the tool, but we need a default value. - self::$context['started'] = (int) TIME_START; + Utils::$context['started'] = (int) TIME_START; self::$script_start = (int) TIME_START; // No integration hooks allowed during maintenance. @@ -284,10 +277,10 @@ public function execute(int $type): void self::$tool->setStep($step); // The current weight of this step in terms of overall progress. - self::$context['step_weight'] = $step->getProgress(); + Utils::$context['step_weight'] = $step->getProgress(); // Make sure we reset the skip button. - self::$context['skip'] = false; + Utils::$context['skip'] = false; // What should we call for this step? if (($callable = Utils::getCallable($step->getFunction(), true)) === false) { @@ -305,7 +298,7 @@ public function execute(int $type): void self::setCurrentStart(0); // No warnings pass on. - self::$context['warning'] = ''; + Utils::$context['warning'] = ''; self::$overall_percent += (int) $step->getProgress(); } @@ -679,9 +672,9 @@ public static function loginAdmin( Db::$db->free_result($request); if (!empty($row)) { - list($id_member, $name, $password, $id_group, $addGroups, $user_language) = $row; + list($id_member, $name, $passwd, $id_group, $additional_groups, $user_language) = $row; - $groups = explode(',', $addGroups); + $groups = explode(',', $additional_groups); $groups[] = (int) $id_group; foreach ($groups as $k => $v) { @@ -690,16 +683,16 @@ public static function loginAdmin( if ( // SMF 3.0+ - Security::hashVerifyPassword($_REQUEST['passwrd'], $password) + Security::hashVerifyPassword($password, $passwd) // SMF 2.1 prepended the username to the password. - || Security::hashVerifyPassword(Utils::strtolower($name) . $_REQUEST['passwrd'], $password) + || Security::hashVerifyPassword(Utils::strtolower($name) . $password, $passwd) // SMF 2.0 used sha1 - || ($use_old_hashing && $password === sha1(strtolower($name) . $_REQUEST['passwrd'])) + || ($use_old_hashing && hash_equals($passwd, sha1(strtolower($name) . $password))) ) { $id = (int) $id_member; } - // We have a valid login. + // We have a valid login, but are they really an admin? if ($id > 0 && !\in_array(1, $groups)) { $request = Db::$db->query( 'SELECT permission @@ -738,7 +731,7 @@ public static function loginWithDatabasePassword( #[\SensitiveParameter] string $password, ): bool { - return Config::$db_passwd === $password; + return hash_equals(Config::$db_passwd, $password); } /** @@ -748,7 +741,7 @@ public static function loginWithDatabasePassword( */ public static function getTimeElapsed(): string { - $duration = (new \DateTime('@' . self::$context['started']))->diff(new \DateTime()); + $duration = (new \DateTime('@' . Utils::$context['started']))->diff(new \DateTime()); if ((int) $duration->format('%a') > 0) { return \strval((int) $duration->format('%h') + ((int) $duration->format('%a') * 24)) . $duration->format(':%I:%S'); @@ -771,7 +764,7 @@ public static function getTimeElapsed(): string public static function isOutOfTime(): bool { if (Sapi::isCLI()) { - if (time() - self::$context['started'] > 1 && !self::$tool->isDebug()) { + if (time() - Utils::$context['started'] > 1 && !self::$tool->isDebug()) { echo '.'; } @@ -872,7 +865,7 @@ public static function exit(bool $fallthrough = false): void // Call the template. if (self::$sub_template !== '') { - self::$context['form_url'] = self::getSelf() . '?step=' . self::getCurrentStep(); + Utils::$context['form_url'] = self::getSelf() . '?step=' . self::getCurrentStep(); \call_user_func([self::$template, self::$sub_template]); } diff --git a/Sources/Maintenance/Migration/MigrationBase.php b/Sources/Maintenance/Migration/MigrationBase.php index 9612bb56ac7..15320233e85 100644 --- a/Sources/Maintenance/Migration/MigrationBase.php +++ b/Sources/Maintenance/Migration/MigrationBase.php @@ -20,6 +20,7 @@ use SMF\Maintenance\Maintenance; use SMF\Maintenance\SubStepInterface; use SMF\Sapi; +use SMF\Utils; /** * Migration container for a maintenance task. @@ -140,7 +141,7 @@ protected function query(string $db_string, array $db_values = [], ?object $conn throw new \ErrorException($db_error_message, 0, E_USER_ERROR, $file, $line); } - Maintenance::$context['try_again'] = true; + Utils::$context['try_again'] = true; Maintenance::$fatal_error = ' ' . Lang::getTxt('upgrade_unsuccessful', file: 'Maintenance') . '
diff --git a/Sources/Maintenance/Migration/v2_1/AttachmentDirectory.php b/Sources/Maintenance/Migration/v2_1/AttachmentDirectory.php index 7662637ebb9..7f4300db80e 100644 --- a/Sources/Maintenance/Migration/v2_1/AttachmentDirectory.php +++ b/Sources/Maintenance/Migration/v2_1/AttachmentDirectory.php @@ -16,7 +16,9 @@ namespace SMF\Maintenance\Migration\v2_1; use SMF\Config; +use SMF\Maintenance\Maintenance; use SMF\Maintenance\Migration\MigrationBase; +use SMF\Utils; class AttachmentDirectory extends MigrationBase { @@ -33,36 +35,81 @@ class AttachmentDirectory extends MigrationBase * Public methods ****************/ - /** - * - */ - public function isCandidate(): bool - { - return empty(Config::$modSettings['json_done']); - } - /** * */ public function execute(): bool { + // Is it a simple file path? if ( !\is_array(Config::$modSettings['attachmentUploadDir']) && is_dir(Config::$modSettings['attachmentUploadDir']) ) { - Config::$modSettings['attachmentUploadDir'] = serialize([1 => Config::$modSettings['attachmentUploadDir']]); - - Config::updateModSettings([ - 'attachmentUploadDir' => Config::$modSettings['attachmentUploadDir'], - 'currentAttachmentUploadDir' => 1, - ]); - } elseif (\is_array(Config::$modSettings['attachmentUploadDir'])) { - Config::updateModSettings([ - 'attachmentUploadDir' => serialize(Config::$modSettings['attachmentUploadDir']), - ]); - // Assume currentAttachmentUploadDir is already set + return $this->update([1 => Config::$modSettings['attachmentUploadDir']]); } + // Is it an array of file paths? + if (\is_array(Config::$modSettings['attachmentUploadDir'])) { + return $this->update(Config::$modSettings['attachmentUploadDir']); + } + + // Is it a serialized string? + if ( + \is_array( + @Utils::safeUnserialize( + Config::$modSettings['attachmentUploadDir'], + ), + ) + ) { + return $this->update( + Utils::safeUnserialize( + Config::$modSettings['attachmentUploadDir'], + ), + ); + } + + // Is it a JSON string? + if ( + \is_array( + @Utils::jsonDecode( + Config::$modSettings['attachmentUploadDir'], + associative: true, + should_log: false, + ), + ) + ) { + return $this->update( + Utils::jsonDecode( + Config::$modSettings['attachmentUploadDir'], + associative: true, + should_log: false, + ), + ); + } + + // If all else failed, fall back to the default. + return $this->update([1 => Config::$boarddir . DIRECTORY_SEPARATOR . 'attachments']); + } + + /****************** + * Internal methods + ******************/ + + /** + * Updates + * + * @param mixed $value + * @return bool + */ + private function update(array $attach_dirs): bool + { + $current_attach_dir = Config::$modSettings['currentAttachmentUploadDir'] ?? array_key_first($attach_dirs); + + Maintenance::$tool->updateModSettings([ + 'attachmentUploadDir' => Utils::jsonEncode($attach_dirs), + 'currentAttachmentUploadDir' => $current_attach_dir, + ]); + return true; } } diff --git a/Sources/Maintenance/Migration/v2_1/LegacyAttachments.php b/Sources/Maintenance/Migration/v2_1/LegacyAttachments.php index 301a3e7da40..69e1a4a4340 100644 --- a/Sources/Maintenance/Migration/v2_1/LegacyAttachments.php +++ b/Sources/Maintenance/Migration/v2_1/LegacyAttachments.php @@ -68,12 +68,6 @@ public function execute(): bool $custom_av_dir = $this->checkCustomAvatarDirectory(); Maintenance::$total_items = $this->getTotalAttachments(); - // We may be using multiple attachment directories. - if (!empty(Config::$modSettings['currentAttachmentUploadDir']) && !\is_array(Config::$modSettings['attachmentUploadDir']) && empty(Config::$modSettings['json_done'])) { - Config::$modSettings['attachmentUploadDir'] = @unserialize(Config::$modSettings['attachmentUploadDir']); - } - - $is_done = false; while (!$is_done) { @@ -84,7 +78,8 @@ public function execute(): bool FROM {db_prefix}attachments WHERE attachment_type != 1 ORDER BY id_attach - LIMIT {int:start}, 100', + LIMIT 100 + OFFSET {int:start}', [ 'start' => $start, ], @@ -97,9 +92,9 @@ public function execute(): bool while ($row = Db::$db->fetch_assoc($request)) { // The current folder. - $currentFolder = !empty(Config::$modSettings['currentAttachmentUploadDir']) ? Config::$modSettings['attachmentUploadDir'][$row['id_folder']] : Config::$modSettings['attachmentUploadDir']; + $current_folder = Sapi::canonicalPath(Config::$modSettings['attachmentUploadDir'][$row['id_folder']]); - $fileHash = ''; + $file_hash = ''; // Old School? if (empty($row['file_hash'])) { @@ -108,52 +103,84 @@ public function execute(): bool if (empty(Config::$db_character_set) || Config::$db_character_set != 'utf8') { $row['filename'] = strtr( $row['filename'], - "\x8a\x8e\x9a\x9e\x9f\xc0\xc1\xc2\xc3\xc4\xc5\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xe0\xe1\xe2\xe3\xe4\xe5\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xff", - 'SZszYAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy', + [ + "\x8a" => 'S', "\x8c" => 'OE', "\x8e" => 'Z', + "\x9a" => 's', "\x9c" => 'oe', "\x9e" => 'z', + "\x9f" => 'Y', "\xb5" => 'u', "\xc0" => 'A', + "\xc1" => 'A', "\xc2" => 'A', "\xc3" => 'A', + "\xc4" => 'A', "\xc5" => 'A', "\xc6" => 'AE', + "\xc7" => 'C', "\xc8" => 'E', "\xc9" => 'E', + "\xca" => 'E', "\xcb" => 'E', "\xcc" => 'I', + "\xcd" => 'I', "\xce" => 'I', "\xcf" => 'I', + "\xd0" => 'DH', "\xd1" => 'N', "\xd2" => 'O', + "\xd3" => 'O', "\xd4" => 'O', "\xd5" => 'O', + "\xd6" => 'O', "\xd8" => 'O', "\xd9" => 'U', + "\xda" => 'U', "\xdb" => 'U', "\xdc" => 'U', + "\xdd" => 'Y', "\xde" => 'TH', "\xdf" => 'ss', + "\xe0" => 'a', "\xe1" => 'a', "\xe2" => 'a', + "\xe3" => 'a', "\xe4" => 'a', "\xe5" => 'a', + "\xe6" => 'ae', "\xe7" => 'c', "\xe8" => 'e', + "\xe9" => 'e', "\xea" => 'e', "\xeb" => 'e', + "\xec" => 'i', "\xed" => 'i', "\xee" => 'i', + "\xef" => 'i', "\xf0" => 'dh', "\xf1" => 'n', + "\xf2" => 'o', "\xf3" => 'o', "\xf4" => 'o', + "\xf5" => 'o', "\xf6" => 'o', "\xf8" => 'o', + "\xf9" => 'u', "\xfa" => 'u', "\xfb" => 'u', + "\xfc" => 'u', "\xfd" => 'y', "\xfe" => 'th', + "\xff" => 'y', + ], ); - $row['filename'] = strtr($row['filename'], ["\xde" => 'TH', "\xfe" => - 'th', "\xd0" => 'DH', "\xf0" => 'dh', "\xdf" => 'ss', "\x8c" => 'OE', - "\x9c" => 'oe', "\xc6" => 'AE', "\xe6" => 'ae', "\xb5" => 'u']); } + // Sorry, no spaces, dots, or anything else but letters allowed. - $row['filename'] = preg_replace(['/\s/', '/[^\w_\.\-]/'], ['_', ''], $row['filename']); + $row['filename'] = preg_replace( + [ + '/\s/', + '/[^\w\.\-]/', + ], + [ + '_', + '', + ], + $row['filename'], + ); // Create a nice hash. - $fileHash = hash_hmac('sha1', $row['filename'] . time(), Config::$image_proxy_secret); + $file_hash = hash_hmac('sha1', $row['filename'] . time(), Config::$image_proxy_secret); // Iterate through the possible attachment names until we find the one that exists - $oldFile = $currentFolder . '/' . $row['id_attach'] . '_' . strtr($row['filename'], '.', '_') . md5($row['filename']); + $old_file = Sapi::canonicalPath($current_folder . '/' . $row['id_attach'] . '_' . strtr($row['filename'], '.', '_') . md5($row['filename'])); - if (!file_exists($oldFile)) { - $oldFile = $currentFolder . '/' . $row['filename']; + if (!file_exists($old_file)) { + $old_file = Sapi::canonicalPath($current_folder . '/' . $row['filename']); - if (!file_exists($oldFile)) { - $oldFile = false; + if (!file_exists($old_file)) { + $old_file = false; } } // Build the new file. - $newFile = $currentFolder . '/' . $row['id_attach'] . '_' . $fileHash . '.dat'; + $new_file = Sapi::canonicalPath($current_folder . '/' . $row['id_attach'] . '_' . $file_hash . '.dat'); } // Just rename the file. else { - $oldFile = $currentFolder . '/' . $row['id_attach'] . '_' . $row['file_hash']; - $newFile = $currentFolder . '/' . $row['id_attach'] . '_' . $row['file_hash'] . '.dat'; + $old_file = Sapi::canonicalPath($current_folder . '/' . $row['id_attach'] . '_' . $row['file_hash']); + $new_file = Sapi::canonicalPath($current_folder . '/' . $row['id_attach'] . '_' . $row['file_hash'] . '.dat'); // Make sure it exists... - if (!file_exists($oldFile)) { - $oldFile = false; + if (!file_exists($old_file)) { + $old_file = false; } } - if (!$oldFile) { + if (!$old_file) { // Existing attachment could not be found. Just skip it... continue; } // Check if the av is an attachment if ($row['id_member'] != 0) { - if (rename($oldFile, $custom_av_dir . '/' . $row['filename'])) { + if (rename($old_file, $custom_av_dir . '/' . $row['filename'])) { $this->query( 'UPDATE {db_prefix}attachments SET file_hash = {empty}, attachment_type = 1 @@ -167,39 +194,45 @@ public function execute(): bool } // Just a regular attachment. else { - rename($oldFile, $newFile); + rename($old_file, $new_file); } // Only update this if it was successful and the file was using the old system. - if (empty($row['file_hash']) && !empty($fileHash) && file_exists($newFile) && !file_exists($oldFile)) { + if ( + empty($row['file_hash']) + && !empty($file_hash) + && file_exists($new_file) + && !file_exists($old_file) + ) { $this->query( 'UPDATE {db_prefix}attachments SET file_hash = {string:file_hash} WHERE id_attach = {int:atach_id}', [ - 'file_hash' => $fileHash, + 'file_hash' => $file_hash, 'attach_id' => $row['id_attach'], ], ); } // While we're here, do we need to update the mime_type? - if (empty($row['mime_type']) && file_exists($newFile)) { - $size = @getimagesize($newFile); + if (empty($row['mime_type']) && file_exists($new_file)) { + $mime_type = Utils::getMimeType($new_file, is_path: true); - if (!empty($size['mime'])) { + if (!empty($mime_type)) { $this->query( 'UPDATE {db_prefix}attachments SET mime_type = {string:mime_type} WHERE id_attach = {int:id_attach}', [ 'id_attach' => $row['id_attach'], - 'mime_type' => substr($size['mime'], 0, 20), + 'mime_type' => $mime_type, ], ); } } } + Db::$db->free_result($request); $start += 100; @@ -224,19 +257,7 @@ protected function checkCustomAvatarDirectory(): string $custom_av_dir = !empty(Config::$modSettings['custom_avatar_dir']) ? Config::$modSettings['custom_avatar_dir'] : Config::$boarddir . '/custom_avatar'; // This little fellow has to cooperate... - if (!is_writable($custom_av_dir)) { - // Try 755 and 775 first since 777 doesn't always work and could be a risk... - $chmod_values = [0755, 0775, 0777]; - - foreach ($chmod_values as $val) { - // If it's writable, break out of the loop - if (is_writable($custom_av_dir)) { - break; - } - - @chmod($custom_av_dir, $val); - } - } + Utils::makeWritable($custom_av_dir); // If we already are using a custom dir, delete the predefined one. if (realpath($custom_av_dir) != realpath(Config::$boarddir . '/custom_avatar')) { diff --git a/Sources/Maintenance/Migration/v3_0/DropTimeOffset.php b/Sources/Maintenance/Migration/v3_0/DropTimeOffset.php index 9d8b1ae0ae9..f9ed8c8cb6c 100644 --- a/Sources/Maintenance/Migration/v3_0/DropTimeOffset.php +++ b/Sources/Maintenance/Migration/v3_0/DropTimeOffset.php @@ -67,7 +67,7 @@ public function execute(): bool ); while ($row = Db::$db->fetch_assoc($request)) { - if (isset($offsets[$row['offset']])) { + if (isset($offsets[$row['time_offset']])) { continue; } @@ -83,7 +83,7 @@ public function execute(): bool } } - $offsets[$row['offset']] = \is_string($tzid) ? $tzid : $forum_tzid; + $offsets[$row['time_offset']] = \is_string($tzid) ? $tzid : $forum_tzid; } Db::$db->free_result($request); @@ -94,6 +94,8 @@ public function execute(): bool $params = []; foreach ($offsets as $offset => $tzid) { + $offset = (string) $offset; + $set .= ' WHEN time_offset = {float:' . md5($offset) . '} THEN {string:' . md5($tzid) . '}'; $params[md5($offset)] = $offset; diff --git a/Sources/Maintenance/Tools/Install.php b/Sources/Maintenance/Tools/Install.php index 4e543bc4957..0a7b36fe6fc 100644 --- a/Sources/Maintenance/Tools/Install.php +++ b/Sources/Maintenance/Tools/Install.php @@ -136,7 +136,7 @@ public function __construct() $this->getProgress(); // Template needs to know about this. - Maintenance::$context['started'] = $this->time_started; + Utils::$context['started'] = $this->time_started; } /** @@ -256,11 +256,11 @@ public function welcome(): bool $this->logProgress(Lang::getTxt('log_starting_step', ['num' => $this->getStep()->getId(), 'step' => $this->getStep()->getName()])); if (Maintenance::isInstalled()) { - Maintenance::$context['warning'] = Lang::getTxt('error_already_installed', file: 'Maintenance'); - $this->logProgress(Maintenance::$context['warning']); + Utils::$context['warning'] = Lang::getTxt('error_already_installed', file: 'Maintenance'); + $this->logProgress(Utils::$context['warning']); } - Maintenance::$context['supported_databases'] = $this->supportedDatabases(); + Utils::$context['supported_databases'] = $this->supportedDatabases(); // Needs to at least meet our miniumn version. if ((version_compare(Maintenance::PHP_MIN_VERSION, PHP_VERSION, '>'))) { @@ -279,7 +279,7 @@ public function welcome(): bool } // Make sure we have a supported database - if (empty(Maintenance::$context['supported_databases'])) { + if (empty(Utils::$context['supported_databases'])) { Maintenance::$fatal_error = Lang::getTxt('error_db_missing', file: 'Maintenance'); $this->logProgress(Maintenance::$fatal_error); @@ -331,7 +331,7 @@ public function welcome(): bool } if (empty(Maintenance::$errors)) { - Maintenance::$context['continue'] = true; + Utils::$context['continue'] = true; } // Are we doing debug? @@ -386,8 +386,8 @@ public function checkFilesWritable(): bool */ public function databaseSettings(): bool { - Maintenance::$context['continue'] = true; - Maintenance::$context['databases'] = []; + Utils::$context['continue'] = true; + Utils::$context['databases'] = []; $foundOne = false; foreach ($this->supportedDatabases() as $db_type => $db) { @@ -396,11 +396,11 @@ public function databaseSettings(): bool continue; } - Maintenance::$context['databases'][$db_type] = $db; + Utils::$context['databases'][$db_type] = $db; // If we have not found a one, set some defaults. if (!$foundOne) { - Maintenance::$context['db'] = [ + Utils::$context['db'] = [ 'server' => $db->getDefaultHost() === '' ? 'localhost' : $db->getDefaultHost(), 'user' => $db->getDefaultUser(), 'name' => $db->getDefaultName(), @@ -415,13 +415,13 @@ public function databaseSettings(): bool } if (isset($_POST['db_user'])) { - Maintenance::$context['db']['user'] = $_POST['db_user']; - Maintenance::$context['db']['name'] = $_POST['db_name']; - Maintenance::$context['db']['server'] = $_POST['db_server']; - Maintenance::$context['db']['prefix'] = $_POST['db_prefix']; + Utils::$context['db']['user'] = $_POST['db_user']; + Utils::$context['db']['name'] = $_POST['db_name']; + Utils::$context['db']['server'] = $_POST['db_server']; + Utils::$context['db']['prefix'] = $_POST['db_prefix']; if (!empty($_POST['db_port'])) { - Maintenance::$context['db']['port'] = (int) $_POST['db_port']; + Utils::$context['db']['port'] = (int) $_POST['db_port']; } } @@ -438,27 +438,23 @@ public function databaseSettings(): bool $db_type = preg_replace('~[^A-Za-z0-9]~', '', $_POST['db_type']); $db_prefix = $_POST['db_prefix']; - if (!isset(Maintenance::$context['databases'][$db_type])) { - // upgrade_unknown_error, which used to be reported here, does not - // exist -- so this produced an empty fatal error and left no clue - // what had gone wrong. Naming the type and the alternatives matters - // most on the command line, where the type is typed out by hand - // rather than picked from a list of exactly these keys. + if (!isset(Utils::$context['databases'][$db_type])) { Maintenance::$fatal_error = Lang::getTxt( 'error_db_type_unknown', [ 'db_type' => $db_type, - 'supported' => Lang::sentenceList(array_keys(Maintenance::$context['databases'])), + 'supported' => Lang::sentenceList(array_keys(Utils::$context['databases'])), ], file: 'Maintenance', ); + $this->logProgress(Maintenance::$fatal_error); return false; } // Validate the prefix. - $db = Maintenance::$context['databases'][$db_type]; + $db = Utils::$context['databases'][$db_type]; try { $db->validatePrefix($db_prefix); @@ -471,7 +467,7 @@ public function databaseSettings(): bool } // Database names can not have periods, just complicates things. - if (strpos(Maintenance::$context['db']['name'], '.') !== false) { + if (strpos(Utils::$context['db']['name'], '.') !== false) { Maintenance::$fatal_error = Lang::getTxt('db_settings_database_invalid', file: 'Maintenance'); $this->logProgress(Maintenance::$fatal_error); @@ -517,7 +513,7 @@ public function databaseSettings(): bool // Attempt a connection. Db::load([ 'non_fatal' => true, - 'dont_select_db' => !Maintenance::$context['databases'][$db_type]->alwaysHasDb(), + 'dont_select_db' => !Utils::$context['databases'][$db_type]->alwaysHasDb(), ]); // Still no connection? Big fat error message :P. @@ -557,7 +553,7 @@ public function databaseSettings(): bool } // Let's try that database on for size... assuming we haven't already lost the opportunity. - if (Db::$db->name != '' && !Maintenance::$context['databases'][$db_type]->alwaysHasDb()) { + if (Db::$db->name != '' && !Utils::$context['databases'][$db_type]->alwaysHasDb()) { Db::$db->query( 'CREATE DATABASE IF NOT EXISTS {identifier:name}', [ @@ -621,20 +617,12 @@ public function forumSettings(): bool Db::load(); // Now, to put what we've learned together... and add a path. - // getSelf() is $_SERVER['PHP_SELF'], which in a request is a rooted path - // but on the command line is whatever was typed -- usually a bare - // 'install.php' with no directory in it at all. strrpos() then returns - // false, and substr() with a false length is fatal on PHP 8, so the - // installer died here on every CLI run. - $self = Maintenance::getSelf(); - $last_slash = strrpos($self, '/'); - - Maintenance::$context['detected_url'] = 'http' . (Sapi::httpsOn() ? 's' : '') . '://' . $this->defaultHost() . ($last_slash === false ? '' : substr($self, 0, $last_slash)); + Utils::$context['detected_url'] = 'http' . (Sapi::httpsOn() ? 's' : '') . '://' . $this->defaultHost() . (!str_contains(Maintenance::getSelf(), '/') ? '' : substr(Maintenance::getSelf(), 0, strrpos(Maintenance::getSelf(), '/'))); // Check if the database sessions will even work. - Maintenance::$context['test_dbsession'] = (\ini_get('session.auto_start') != 1); + Utils::$context['test_dbsession'] = (\ini_get('session.auto_start') != 1); - Maintenance::$context['continue'] = true; + Utils::$context['continue'] = true; // Do we have a failure of database configuration? try { @@ -647,22 +635,22 @@ public function forumSettings(): bool } // Setup the SSL checkbox... - Maintenance::$context['ssl_chkbx_protected'] = false; - Maintenance::$context['ssl_chkbx_checked'] = false; + Utils::$context['ssl_chkbx_protected'] = false; + Utils::$context['ssl_chkbx_checked'] = false; // If redirect in effect, force SSL ON. - $url = new Url(Maintenance::$context['detected_url']); + $url = new Url(Utils::$context['detected_url']); if ($url->redirectsToHttps()) { - Maintenance::$context['ssl_chkbx_protected'] = true; - Maintenance::$context['ssl_chkbx_checked'] = true; + Utils::$context['ssl_chkbx_protected'] = true; + Utils::$context['ssl_chkbx_checked'] = true; $_POST['force_ssl'] = true; } // If no cert, make sure SSL stays OFF. if (!$url->hasSSL()) { - Maintenance::$context['ssl_chkbx_protected'] = true; - Maintenance::$context['ssl_chkbx_checked'] = false; + Utils::$context['ssl_chkbx_protected'] = true; + Utils::$context['ssl_chkbx_checked'] = false; } // Submitting? @@ -710,7 +698,7 @@ public function forumSettings(): bool */ public function databasePopulation(): bool { - Maintenance::$context['continue'] = true; + Utils::$context['continue'] = true; // Already done? if (isset($_POST['pop_done'])) { @@ -759,7 +747,7 @@ public function databasePopulation(): bool } } - Maintenance::$context['sql_results'] = [ + Utils::$context['sql_results'] = [ 'tables' => 0, 'inserts' => 0, 'table_dups' => 0, @@ -786,16 +774,16 @@ public function databasePopulation(): bool throw new \Exception(Db::$db->error()); } - Maintenance::$context['sql_results']['tables']++; + Utils::$context['sql_results']['tables']++; $this->logProgress(Lang::getTxt('log_done', file: 'Maintenance')); } catch (\Throwable $e) { - Maintenance::$context['failures'][] = trim($e->getMessage()); + Utils::$context['failures'][] = trim($e->getMessage()); $this->logProgress(Lang::getTxt('log_failed_with_error', ['error' => trim($e->getMessage())], file: 'Maintenance')); continue; } } else { - Maintenance::$context['sql_results']['table_dups']++; + Utils::$context['sql_results']['table_dups']++; $this->logProgress(Lang::getTxt('log_skipped', file: 'Maintenance')); } @@ -806,12 +794,12 @@ public function databasePopulation(): bool try { $num_inserts = $table->populate(); - Maintenance::$context['sql_results']['inserts'] += $num_inserts; - Maintenance::$context['sql_results']['insert_dups'] += (\count($table->initial_data) - $num_inserts); + Utils::$context['sql_results']['inserts'] += $num_inserts; + Utils::$context['sql_results']['insert_dups'] += (\count($table->initial_data) - $num_inserts); $this->logProgress(Lang::getTxt('log_done', file: 'Maintenance')); } catch (\Throwable $e) { - Maintenance::$context['failures'][] = $table->name . ':' . $e->getMessage(); + Utils::$context['failures'][] = $table->name . ':' . $e->getMessage(); $this->logProgress(Lang::getTxt('log_failed_with_error', ['error' => $e->getMessage()], file: 'Maintenance')); } @@ -822,13 +810,13 @@ public function databasePopulation(): bool } // Sort out the context for the SQL. - foreach (Maintenance::$context['sql_results'] as $key => $number) { + foreach (Utils::$context['sql_results'] as $key => $number) { if ($number === 0) { - unset(Maintenance::$context['sql_results'][$key]); + unset(Utils::$context['sql_results'][$key]); } else { - Maintenance::$context['sql_results'][$key] = Lang::getTxt('db_populate_' . $key, [$number], file: 'Maintenance'); + Utils::$context['sql_results'][$key] = Lang::getTxt('db_populate_' . $key, [$number], file: 'Maintenance'); - $this->logProgress(Maintenance::$context['sql_results'][$key]); + $this->logProgress(Utils::$context['sql_results'][$key]); } } @@ -850,11 +838,11 @@ public function databasePopulation(): bool foreach ($install_tables as $table) { try { if (!(Db::$db->optimize_table(Config::$db_prefix . $table->name) > -1)) { - Maintenance::$context['failures'][] = Db::$db->error(); + Utils::$context['failures'][] = Db::$db->error(); $this->logProgress(Db::$db->error()); } } catch (\Throwable $e) { - Maintenance::$context['failures'][] = $e->getMessage(); + Utils::$context['failures'][] = $e->getMessage(); $this->logProgress($e->getMessage()); } } @@ -869,7 +857,7 @@ public function databasePopulation(): bool // Was this a refresh? if (\count($existing_tables) > 0) { $this->page_title = Lang::getTxt('user_refresh_install', file: 'Maintenance'); - Maintenance::$context['was_refresh'] = true; + Utils::$context['was_refresh'] = true; } return false; @@ -882,7 +870,7 @@ public function databasePopulation(): bool */ public function adminAccount(): bool { - Maintenance::$context['continue'] = true; + Utils::$context['continue'] = true; // Skipping? if (!empty($_POST['skip'])) { @@ -902,11 +890,11 @@ public function adminAccount(): bool // Reload $modSettings. Config::reloadModSettings(); - Maintenance::$context['username'] = htmlspecialchars($_POST['username'] ?? ''); - Maintenance::$context['email'] = htmlspecialchars($_POST['email'] ?? ''); - Maintenance::$context['server_email'] = htmlspecialchars($_POST['server_email'] ?? ''); + Utils::$context['username'] = htmlspecialchars($_POST['username'] ?? ''); + Utils::$context['email'] = htmlspecialchars($_POST['email'] ?? ''); + Utils::$context['server_email'] = htmlspecialchars($_POST['server_email'] ?? ''); - Maintenance::$context['require_db_confirm'] = empty(Config::$db_type); + Utils::$context['require_db_confirm'] = empty(Config::$db_type); // Only allow skipping if we think they already have an account setup. $request = Db::$db->query( @@ -921,7 +909,7 @@ public function adminAccount(): bool ); if (Db::$db->num_rows($request) != 0) { - Maintenance::$context['skip'] = true; + Utils::$context['skip'] = true; return false; } @@ -938,7 +926,7 @@ public function adminAccount(): bool $_POST['password3'] ??= ''; // Wrong password? - if (Maintenance::$context['require_db_confirm'] && $_POST['password3'] != Config::$db_passwd) { + if (Utils::$context['require_db_confirm'] && $_POST['password3'] != Config::$db_passwd) { Maintenance::$fatal_error = Lang::getTxt('error_db_connect', file: 'Maintenance'); $this->logProgress(Maintenance::$fatal_error); @@ -1020,10 +1008,10 @@ public function adminAccount(): bool ); if (Db::$db->num_rows($result) != 0) { - Maintenance::$context += Db::$db->fetch_row($result); + Utils::$context += Db::$db->fetch_row($result); Db::$db->free_result($result); - Maintenance::$context['account_existed'] = Lang::getTxt('error_user_settings_taken', file: 'Maintenance'); + Utils::$context['account_existed'] = Lang::getTxt('error_user_settings_taken', file: 'Maintenance'); return false; } @@ -1045,14 +1033,14 @@ public function adminAccount(): bool } if ($_POST['username'] != '') { - Maintenance::$context['password_salt'] = bin2hex(random_bytes(16)); + Utils::$context['password_salt'] = bin2hex(random_bytes(16)); $ip = IP::getUserIP(); $_POST['password1'] = Security::hashPassword($_POST['password1']); try { - Maintenance::$context['id_member'] = Db::$db->insert( + Utils::$context['id_member'] = Db::$db->insert( '', Db::$db->prefix . 'members', [ @@ -1088,7 +1076,7 @@ public function adminAccount(): bool 1, 0, time(), - Maintenance::$context['password_salt'], + Utils::$context['password_salt'], '', '', '', @@ -1109,7 +1097,7 @@ public function adminAccount(): bool Db::INSERT_RETURN_MODE_SINGLE, ); - if ((int) Maintenance::$context['id_member'] > 0) { + if ((int) Utils::$context['id_member'] > 0) { return true; } @@ -1138,7 +1126,7 @@ public function finalize(): bool $this->logProgress(Lang::getTxt('log_starting_step', ['num' => $this->getStep()->getId(), 'step' => $this->getStep()->getName()])); } - Maintenance::$context['continue'] = false; + Utils::$context['continue'] = false; // Rebuild the settings file. $this->updateSettingsFile(['maintenance_tool_progress' => ''], false, true); @@ -1153,15 +1141,15 @@ public function finalize(): bool // Everything below needs a current user: Time and Logging both read // User::$me to work out which time zone to record dates in. - if (isset(Maintenance::$context['id_member'])) { - User::setMe((int) Maintenance::$context['id_member']); + if (isset(Utils::$context['id_member'])) { + User::setMe((int) Utils::$context['id_member']); } else { User::loadMe(); } // Bring a warning over. - if (!empty(Maintenance::$context['account_existed'])) { - Maintenance::$warnings = Maintenance::$context['account_existed']; + if (!empty(Utils::$context['account_existed'])) { + Maintenance::$warnings = Utils::$context['account_existed']; } // As track stats is by default enabled let's add some activity. @@ -1179,7 +1167,7 @@ public function finalize(): bool Time::strftime('%Y-%m-%d', time()), 1, 1, - !empty(Maintenance::$context['id_member']) ? 1 : 0, + !empty(Utils::$context['id_member']) ? 1 : 0, ], ], ['date'], @@ -1202,18 +1190,11 @@ public function finalize(): bool Db::$db->free_result($request); } - // Sign the new administrator in, so the browser that just ran the - // installer lands on an admin session rather than a login form. - // - // None of that means anything on the command line: there is no browser - // to hold the cookie, and no user agent to record against the session. - // Attempting it anyway sent headers after output had already started and - // left four warnings on every run, then wrote a session row keyed on an - // undefined HTTP_USER_AGENT. + // Sign the new administrator in. (Not applicable on the command line.) if (!Sapi::isCLI()) { // Automatically log them in ;) - if (isset(Maintenance::$context['id_member'], Maintenance::$context['password_salt'])) { - Cookie::setLoginCookie(3153600 * 60, Maintenance::$context['id_member'], Cookie::encrypt($_POST['password1'], Maintenance::$context['password_salt'])); + if (isset(Utils::$context['id_member'], Utils::$context['password_salt'])) { + Cookie::setLoginCookie(3153600 * 60, Utils::$context['id_member'], Cookie::encrypt($_POST['password1'], Utils::$context['password_salt'])); } $result = Db::$db->query( @@ -1293,8 +1274,8 @@ public function finalize(): bool ]); // Some final context for the template. - Maintenance::$context['dir_still_writable'] = is_writable(Config::$boarddir); - Maintenance::$context['can_delete_script'] = $this->canDeleteTool(); + Utils::$context['dir_still_writable'] = is_writable(Config::$boarddir); + Utils::$context['can_delete_script'] = $this->canDeleteTool(); // Update hash's cost to an appropriate setting $this->updateModSettings([ @@ -1304,7 +1285,7 @@ public function finalize(): bool $this->logProgress(Lang::getTxt('log_install_complete', file: 'Maintenance')); if (!Sapi::isCLI() && $this->isDebug()) { - Maintenance::$context['log_contents'] = file_get_contents($this->log_file); + Utils::$context['log_contents'] = file_get_contents($this->log_file); } $this->finalizeLog(); @@ -1513,7 +1494,7 @@ private function toggleSmStats(array &$settings): void && empty(Config::$modSettings['allow_sm_stats']) && empty(Config::$modSettings['enable_sm_stats']) ) { - Maintenance::$context['allow_sm_stats'] = true; + Utils::$context['allow_sm_stats'] = true; // Attempt to register the site etc. $fp = @fsockopen('www.simplemachines.org', 443, $errno, $errstr); @@ -1548,7 +1529,7 @@ private function toggleSmStats(array &$settings): void } } // Don't remove stat collection unless we unchecked the box for real, not from the loop. - elseif (empty($_POST['stats']) && empty(Maintenance::$context['allow_sm_stats'])) { + elseif (empty($_POST['stats']) && empty(Utils::$context['allow_sm_stats'])) { $settings['enable_sm_stats'] = null; } } diff --git a/Sources/Maintenance/Tools/ToolsBase.php b/Sources/Maintenance/Tools/ToolsBase.php index 61ae5bd4145..94568c1ba99 100644 --- a/Sources/Maintenance/Tools/ToolsBase.php +++ b/Sources/Maintenance/Tools/ToolsBase.php @@ -373,7 +373,7 @@ final public function makeFilesWritable(array &$files): bool } // What still needs to be done? - Maintenance::$context['chmod_files'] = $files; + Utils::$context['chmod_files'] = $files; // If it's windows it's a mess... if (!empty($files) && Sapi::isOS(Sapi::OS_WINDOWS)) { @@ -392,29 +392,29 @@ final public function makeFilesWritable(array &$files): bool if (!empty($files)) { // Load any session data we might have... if (!isset($_POST['ftp_username']) && isset($_SESSION['temp_ftp'])) { - Maintenance::$context['chmod']['server'] = $_SESSION['temp_ftp']['server']; - Maintenance::$context['chmod']['port'] = $_SESSION['temp_ftp']['port']; - Maintenance::$context['chmod']['username'] = $_SESSION['temp_ftp']['username']; - Maintenance::$context['chmod']['password'] = $_SESSION['temp_ftp']['password']; - Maintenance::$context['chmod']['path'] = $_SESSION['temp_ftp']['path']; + Utils::$context['chmod']['server'] = $_SESSION['temp_ftp']['server']; + Utils::$context['chmod']['port'] = $_SESSION['temp_ftp']['port']; + Utils::$context['chmod']['username'] = $_SESSION['temp_ftp']['username']; + Utils::$context['chmod']['password'] = $_SESSION['temp_ftp']['password']; + Utils::$context['chmod']['path'] = $_SESSION['temp_ftp']['path']; } // Or have we submitted? elseif (isset($_POST['ftp_username'])) { - Maintenance::$context['chmod']['server'] = $_POST['ftp_server']; - Maintenance::$context['chmod']['port'] = $_POST['ftp_port']; - Maintenance::$context['chmod']['username'] = $_POST['ftp_username']; - Maintenance::$context['chmod']['password'] = $_POST['ftp_password']; - Maintenance::$context['chmod']['path'] = $_POST['ftp_path']; + Utils::$context['chmod']['server'] = $_POST['ftp_server']; + Utils::$context['chmod']['port'] = $_POST['ftp_port']; + Utils::$context['chmod']['username'] = $_POST['ftp_username']; + Utils::$context['chmod']['password'] = $_POST['ftp_password']; + Utils::$context['chmod']['path'] = $_POST['ftp_path']; } - if (isset(Maintenance::$context['chmod']['username'])) { - $ftp = new FtpConnection(Maintenance::$context['chmod']['server'], Maintenance::$context['chmod']['port'], Maintenance::$context['chmod']['username'], Maintenance::$context['chmod']['password']); + if (isset(Utils::$context['chmod']['username'])) { + $ftp = new FtpConnection(Utils::$context['chmod']['server'], Utils::$context['chmod']['port'], Utils::$context['chmod']['username'], Utils::$context['chmod']['password']); if ($ftp->error === false) { // Try it without /home/abc just in case they messed up. - if (!$ftp->chdir(Maintenance::$context['chmod']['path'])) { - Maintenance::$context['chmod']['ftp_error'] = $ftp->last_message; - $ftp->chdir(preg_replace('~^/home[2]?/[^/]+?~', '', Maintenance::$context['chmod']['path'])); + if (!$ftp->chdir(Utils::$context['chmod']['path'])) { + Utils::$context['chmod']['ftp_error'] = $ftp->last_message; + $ftp->chdir(preg_replace('~^/home[2]?/[^/]+?~', '', Utils::$context['chmod']['path'])); } } } @@ -426,32 +426,32 @@ final public function makeFilesWritable(array &$files): bool // Save the error so we can mess with listing... elseif ( $ftp->error !== false - && !isset(Maintenance::$context['chmod']['ftp_error']) + && !isset(Utils::$context['chmod']['ftp_error']) ) { - Maintenance::$context['chmod']['ftp_error'] = $ftp->last_message === null ? '' : $ftp->last_message; + Utils::$context['chmod']['ftp_error'] = $ftp->last_message === null ? '' : $ftp->last_message; } list($username, $detect_path, $found_path) = $ftp->detect_path(\dirname(__FILE__)); - if ($found_path || !isset(Maintenance::$context['chmod']['path'])) { - Maintenance::$context['chmod']['path'] = $detect_path; + if ($found_path || !isset(Utils::$context['chmod']['path'])) { + Utils::$context['chmod']['path'] = $detect_path; } - if (!isset(Maintenance::$context['chmod']['username'])) { - Maintenance::$context['chmod']['username'] = $username; + if (!isset(Utils::$context['chmod']['username'])) { + Utils::$context['chmod']['username'] = $username; } // Don't forget the login token. - Maintenance::$context += SecurityToken::create('login'); + SecurityToken::create('login'); return false; } // We want to do a relative path for FTP. - if (!\in_array(Maintenance::$context['chmod']['path'], ['', '/'])) { - $ftp_root = strtr(Config::$boarddir, [Maintenance::$context['chmod']['path'] => '']); + if (!\in_array(Utils::$context['chmod']['path'], ['', '/'])) { + $ftp_root = strtr(Config::$boarddir, [Utils::$context['chmod']['path'] => '']); - if (substr($ftp_root, -1) == '/' && (Maintenance::$context['chmod']['path'] == '' || Maintenance::$context['chmod']['path'][0] === '/')) { + if (substr($ftp_root, -1) == '/' && (Utils::$context['chmod']['path'] == '' || Utils::$context['chmod']['path'][0] === '/')) { $ftp_root = substr($ftp_root, 0, -1); } } else { @@ -460,11 +460,11 @@ final public function makeFilesWritable(array &$files): bool // Save the info for next time! $_SESSION['temp_ftp'] = [ - 'server' => Maintenance::$context['chmod']['server'], - 'port' => Maintenance::$context['chmod']['port'], - 'username' => Maintenance::$context['chmod']['username'], - 'password' => Maintenance::$context['chmod']['password'], - 'path' => Maintenance::$context['chmod']['path'], + 'server' => Utils::$context['chmod']['server'], + 'port' => Utils::$context['chmod']['port'], + 'username' => Utils::$context['chmod']['username'], + 'password' => Utils::$context['chmod']['password'], + 'path' => Utils::$context['chmod']['path'], 'root' => $ftp_root, ]; @@ -513,7 +513,7 @@ final public function makeFilesWritable(array &$files): bool } // What remains? - Maintenance::$context['chmod']['files'] = $files; + Utils::$context['chmod']['files'] = $files; return (bool) (empty($files)); } @@ -567,7 +567,7 @@ public function checkAndHandleTimeout(array $json_response_data = []): void // If this is not json, we need to do a few things. if (!Maintenance::isJson()) { // We're going to pause after this! - Maintenance::$context['pause'] = true; + Utils::$context['pause'] = true; Maintenance::setQueryString(); } else { diff --git a/Sources/Maintenance/Tools/Upgrade.php b/Sources/Maintenance/Tools/Upgrade.php index bda39f8ca07..57efb8ffd8b 100644 --- a/Sources/Maintenance/Tools/Upgrade.php +++ b/Sources/Maintenance/Tools/Upgrade.php @@ -87,9 +87,9 @@ class Upgrade extends ToolsBase implements ToolsInterface Migration\v2_1\FixDates::class, Migration\v2_1\CreateMemberLogins::class, Migration\v2_1\CollapsedCategories::class, + Migration\v2_1\AttachmentDirectory::class, Migration\v2_1\LegacyAttachments::class, Migration\v2_1\AttachmentSizes::class, - Migration\v2_1\AttachmentDirectory::class, Migration\v2_1\CreateLogGroupRequests::class, Migration\v2_1\PackageManager::class, Migration\v2_1\ValidationServers::class, @@ -413,7 +413,7 @@ public function __construct() } // Is this a large (and old) forum? We may do special logic then. - Maintenance::$context['is_large_forum'] = $this->is_large_forum = ( + Utils::$context['is_large_forum'] = $this->is_large_forum = ( version_compare( str_replace(' ', '.', strtolower($this->start_smf_version)), '1.1.rc.1', @@ -663,7 +663,7 @@ public function welcomeLogin(): bool // Try to make all the files writable. If we cannot, we will display a chmod page to attempt this with additional permissions. if (!$this->makeFilesWritable($writable_files)) { - Maintenance::$context['chmod']['files'] = $writable_files; + Utils::$context['chmod']['files'] = $writable_files; return false; } @@ -798,7 +798,7 @@ public function welcomeLogin(): bool ) { if (!SecurityToken::validate('login', 'post', false)) { Maintenance::$errors[] = Lang::getTxt('token_verify_fail', file: 'Errors'); - Maintenance::$context += SecurityToken::create('login'); + SecurityToken::create('login'); return false; } @@ -808,10 +808,8 @@ public function welcomeLogin(): bool !empty($_POST['db_pass']) && Maintenance::loginWithDatabasePassword((string) $_POST['db_pass']) ) { - $this->user = [ - 'id' => 0, - 'name' => 'Database Admin', - ]; + $this->user['id'] = 0; + $this->user['name'] = 'Database Admin'; $_SESSION['is_logged'] = true; @@ -821,20 +819,18 @@ public function welcomeLogin(): bool $use_old_hashing = version_compare(str_replace(' ', '.', strtolower(Config::$modSettings['smfVersion'] ?? '0.0.dev.0')), '2.1.dev.0', '<'); if (($id = Maintenance::loginAdmin((string) $_POST['user'], (string) $_POST['passwrd'], $use_old_hashing)) > 0) { - $this->user = [ - 'id' => $id, - 'name' => (string) $_POST['user'], - ]; + $this->user['id'] = $id; + $this->user['name'] = (string) $_POST['user']; $_SESSION['is_logged'] = true; return true; } } elseif (empty(Maintenance::$errors)) { - Maintenance::$context['continue'] = true; + Utils::$context['continue'] = true; } - Maintenance::$context += SecurityToken::create('login'); + SecurityToken::create('login'); return false; } @@ -848,7 +844,7 @@ public function upgradeOptions(): bool { $member_columns = Db::$db->list_columns('{db_prefix}members'); - Maintenance::$context['karma_installed'] = [ + Utils::$context['karma_installed'] = [ 'good' => \in_array('karma_good', $member_columns), 'bad' => \in_array('karma_bad', $member_columns), ]; @@ -856,9 +852,9 @@ public function upgradeOptions(): bool unset($member_columns); // Figure out a couple of recommendations. - Maintenance::$context['backup_recommended'] = $this->backupRecommended(); + Utils::$context['backup_recommended'] = $this->backupRecommended(); - Maintenance::$context['migrate_settings_recommended'] = ( + Utils::$context['migrate_settings_recommended'] = ( empty(Config::$modSettings['smfVersion']) || version_compare( str_replace(' ', '.', strtolower(Config::$modSettings['smfVersion'])), @@ -867,18 +863,18 @@ public function upgradeOptions(): bool ) ); - Maintenance::$context['db_prefix'] = Config::$db_prefix; + Utils::$context['db_prefix'] = Config::$db_prefix; - Maintenance::$context['message_title'] = htmlspecialchars(Config::$mtitle); - Maintenance::$context['message_body'] = htmlspecialchars(Config::$mmessage); + Utils::$context['message_title'] = htmlspecialchars(Config::$mtitle); + Utils::$context['message_body'] = htmlspecialchars(Config::$mmessage); - Maintenance::$context['attachment_conversion'] = isset(Config::$modSettings['attachments_21_done']); + Utils::$context['attachment_conversion'] = isset(Config::$modSettings['attachments_21_done']); - Maintenance::$context['sm_stats_configured'] = !empty(Config::$modSettings['allow_sm_stats']) || !empty(Config::$modSettings['enable_sm_stats']); + Utils::$context['sm_stats_configured'] = !empty(Config::$modSettings['allow_sm_stats']) || !empty(Config::$modSettings['enable_sm_stats']); // If we've not submitted then we're done. if (!Sapi::isCLI() && empty($_POST['upcont'])) { - Maintenance::$context['continue'] = true; + Utils::$context['continue'] = true; return false; } @@ -1061,8 +1057,8 @@ public function backupDatabase(): bool Maintenance::$total_substeps = \count($table_names); // Template things. - Maintenance::$context['cur_table_name'] = $table_names[Maintenance::getCurrentSubStep()]; - Maintenance::$context['continue'] = true; + Utils::$context['cur_table_name'] = $table_names[Maintenance::getCurrentSubStep()]; + Utils::$context['continue'] = true; // We are set up for backing up. if (!Sapi::isCLI() && !Maintenance::isJson()) { @@ -1183,7 +1179,7 @@ public function finalize(): bool $this->logProgress(Lang::getTxt('log_starting_step', ['num' => $this->getStep()->getId(), 'step' => $this->getStep()->getName()])); } - Maintenance::$context['form_action'] = Config::$boardurl . '/index.php'; + Utils::$context['form_action'] = Config::$boardurl . '/index.php'; // Update the database with the new SMF version. $this->updateModSettings(['smfVersion' => SMF_VERSION]); @@ -1229,18 +1225,6 @@ public function finalize(): bool ['id_task'], ); - // Log what we've done. - if (!isset(User::$me)) { - User::loadMe(); - } - - if (empty(User::$me->id) && !empty($this->user['id'])) { - User::load($this->user['id'], dataset: UserDataset::Minimal); - User::setMe($this->user['id']); - } - - User::$me->ip = IP::getUserIP(); - // Log the action manually, so CLI still works. Db::$db->insert( '', @@ -1260,20 +1244,21 @@ public function finalize(): bool [ time(), 3, - User::$me->id, - User::$me->ip, + $this->user['id'], + IP::getUserIP(), 'upgrade', 0, 0, 0, - json_encode(['version' => SMF_FULL_VERSION, 'member' => User::$me->id]), + json_encode([ + 'version' => SMF_FULL_VERSION, + 'member_acted' => $this->user['name'], + ]), ], ], ['id_action'], ); - User::setMe(0); - // Finalize some settings in the settings file. $file_settings = [ 'maintenance' => $this->user['maint'] ?? 0, @@ -1298,13 +1283,13 @@ public function finalize(): bool if (!Sapi::isCLI()) { // Can we delete the file? - Maintenance::$context['can_delete_script'] = $this->canDeleteTool(); + Utils::$context['can_delete_script'] = $this->canDeleteTool(); // Show Upgrade time in debug mode when we completed the upgrade process totally if ($this->isDebug()) { $active = time() - (int) $this->time_started; - Maintenance::$context['upgrade_completed_time'] = Lang::getTxt( + Utils::$context['upgrade_completed_time'] = Lang::getTxt( $active >= 3600 ? 'upgrade_completed_time_hms' : ($active >= 60 ? 'upgrade_completed_time_ms' : 'upgrade_completed_time_s'), [ 'h' => (int) ($active / 3600), @@ -1314,7 +1299,7 @@ public function finalize(): bool file: 'Maintenance', ); - Maintenance::$context['log_contents'] = file_get_contents($this->log_file); + Utils::$context['log_contents'] = file_get_contents($this->log_file); } } @@ -1405,9 +1390,9 @@ private function prepareUpgrade(): void $this->getProgress(); // Template needs to know about this. - Maintenance::$context['started'] = &$this->time_started; - Maintenance::$context['updated'] = &$this->time_updated; - Maintenance::$context['user'] = &$this->user; + Utils::$context['started'] = &$this->time_started; + Utils::$context['updated'] = &$this->time_updated; + Utils::$context['user'] = &$this->user; } /** @@ -1560,7 +1545,7 @@ private function toggleSmStats(array &$settings): void && empty(Config::$modSettings['allow_sm_stats']) && empty(Config::$modSettings['enable_sm_stats']) ) { - Maintenance::$context['allow_sm_stats'] = true; + Utils::$context['allow_sm_stats'] = true; // Attempt to register the site etc. $fp = @fsockopen('www.simplemachines.org', 443, $errno, $errstr); @@ -1595,7 +1580,7 @@ private function toggleSmStats(array &$settings): void } } // Don't remove stat collection unless we unchecked the box for real, not from the loop. - elseif (empty($_POST['stats']) && empty(Maintenance::$context['allow_sm_stats'])) { + elseif (empty($_POST['stats']) && empty(Utils::$context['allow_sm_stats'])) { $settings['enable_sm_stats'] = null; } } @@ -1612,8 +1597,8 @@ private function performSubsteps(array $substeps): bool // We are preparing for templating. if (!Sapi::isCLI() && !Maintenance::isJson()) { - Maintenance::$context['continue'] = true; - Maintenance::$context['current_substep'] = $substeps[Maintenance::getCurrentSubStep()]->name ?? ''; + Utils::$context['continue'] = true; + Utils::$context['current_substep'] = $substeps[Maintenance::getCurrentSubStep()]->name ?? ''; return false; } diff --git a/Sources/Maintenance/Utf8ConverterStep.php b/Sources/Maintenance/Utf8ConverterStep.php index e4309782ccd..c22a63e673d 100644 --- a/Sources/Maintenance/Utf8ConverterStep.php +++ b/Sources/Maintenance/Utf8ConverterStep.php @@ -20,6 +20,7 @@ use SMF\Db\Schema\Table; use SMF\Lang; use SMF\Sapi; +use SMF\Utils; /** * Used for converting MySQL databases to the utf8mb4 character set. @@ -620,10 +621,10 @@ public function convertDatabase(): bool Maintenance::$total_substeps = \count($substeps); // Template things. - Maintenance::$context['table_count'] = Maintenance::$total_substeps; - Maintenance::$context['cur_table_num'] = Maintenance::getCurrentSubStep(); - Maintenance::$context['cur_table_name'] = str_replace(Config::$db_prefix, '', $substeps[Maintenance::getCurrentSubStep()]->test_args[0]); - Maintenance::$context['continue'] = true; + Utils::$context['table_count'] = Maintenance::$total_substeps; + Utils::$context['cur_table_num'] = Maintenance::getCurrentSubStep(); + Utils::$context['cur_table_name'] = str_replace(Config::$db_prefix, '', $substeps[Maintenance::getCurrentSubStep()]->test_args[0]); + Utils::$context['continue'] = true; // We are set up for conversion. if (!Sapi::isCLI() && !Maintenance::isJson()) { @@ -802,7 +803,7 @@ public function convertTable(string $table_name): bool && (Config::$modSettings['search_index'] ?? null) === 'fulltext' ) { Config::updateModSettings(['search_index' => '']); - Maintenance::$context['dropping_index'] = true; + Utils::$context['dropping_index'] = true; } } } diff --git a/Sources/Permissions/GroupPermissionSet.php b/Sources/Permissions/GroupPermissionSet.php index 17126719b7d..2465d3033f1 100644 --- a/Sources/Permissions/GroupPermissionSet.php +++ b/Sources/Permissions/GroupPermissionSet.php @@ -356,6 +356,10 @@ protected static function loadGlobalPermissionData(array $groups): void ); while ($row = Db::$db->fetch_assoc($request)) { + if (!Permission::exists($row['permission'])) { + continue; + } + self::$loaded[PermissionProfile::DEFAULT][(int) $row['id_group']]->permissions[$row['permission']] = (int) $row['add_deny']; } @@ -445,7 +449,10 @@ protected static function loadBoardPermissionData(array $profiles, array $groups ); while ($row = Db::$db->fetch_assoc($request)) { - if (!isset(self::$loaded[(int) $row['id_profile']][(int) $row['id_group']])) { + if ( + !isset(self::$loaded[(int) $row['id_profile']][(int) $row['id_group']]) + || !Permission::exists($row['permission']) + ) { continue; } diff --git a/Sources/Permissions/Permission.php b/Sources/Permissions/Permission.php index 0edf87b4835..1f8a4e884d3 100644 --- a/Sources/Permissions/Permission.php +++ b/Sources/Permissions/Permission.php @@ -357,6 +357,17 @@ class Permission implements \ArrayAccess 'group_level' => self::GROUP_LEVEL_MODERATOR, 'never_guests' => true, ], + // Deprecated, but retained in case the admin kept the karma data during + // an upgrade. If a modification wants to restore the karma feature, it + // should use the integrate_permissions_list hook to set the 'hidden' + // property of this permission to false. + 'karma_edit' => [ + 'view_group' => 'profile', + 'scope' => 'global', + 'hidden' => true, + 'never_guests' => true, + 'never_banned' => true, + ], 'likes_like' => [ 'view_group' => 'likes', 'scope' => 'global', @@ -1309,6 +1320,9 @@ public static function getAll(): array self::$permissions[$name]['name'] = $name; } + // Did any old mods add custom permissions to the tables? + self::includeOrphanPermissions(); + // Important: do the ones with prerequisites last. uasort( self::$permissions, @@ -1402,6 +1416,54 @@ public static function getNonGuestPermissions(): array * Internal static methods *************************/ + /** + * Checks the permissions tables for any unknown permissions added by old + * mods and ensures that they are included in the list of known permissions. + * + * MOD AUTHORS: Please update your code to use integrate_permissions_list + * to add any custom permissions. + * + * @deprecated 3.0 + */ + protected static function includeOrphanPermissions(): void + { + // Only do this when backward compatibility mode is enabled. + if (empty(Config::$backward_compatibility)) { + return; + } + + foreach (['global' => 'permissions', 'board' => 'board_permissions'] as $scope => $tbl) { + $request = Db::$db->query( + 'SELECT DISTINCT permission + FROM {db_prefix}{raw:tbl} + WHERE permission NOT IN ({array_string:known_permissions})', + [ + 'tbl' => $tbl, + 'known_permissions' => array_keys(self::$permissions), + ], + ); + + while ($row = Db::$db->fetch_assoc($request)) { + self::$permissions[$row['permission']] = [ + 'name' => $row['permission'], + 'scope' => $scope, + 'view_group' => 'general', + ]; + + if ( + str_ends_with($row['permission'], '_own') + || str_ends_with($row['permission'], '_any') + ) { + self::$permissions[$row['permission']]['generic_name'] = substr($row['permission'], 0, -4); + self::$permissions[$row['permission']]['own_any'] = substr($row['permission'], -3); + } + } + + Db::$db->free_result($request); + + } + } + /** * Calls the deprecated integrate_heavy_permissions_session hook. * diff --git a/Sources/Security.php b/Sources/Security.php index 2a99244e008..cd16d04cd3d 100644 --- a/Sources/Security.php +++ b/Sources/Security.php @@ -24,19 +24,138 @@ */ class Security { + /***************** + * Class constants + *****************/ + + /** + * @var int + * + * Used to tell Security::checkPassword() not to check password fallbacks. + */ + public const PASSWORD_FALLBACK_NONE = 0b0000; + + /** + * @var int + * + * Used to tell Security::checkPassword() to check password fallbacks for + * other forum software and/or via the integrate_other_passwords hook. + * + * Note: Config::$modSettings['enable_password_conversion'] must also be + * true in order to check fallbacks for other forum software. + */ + public const PASSWORD_FALLBACK_OTHER = 0b0001; + + /** + * @var int + * + * Used to tell Security::checkPassword() to check password fallbacks for + * YaBB SE and SMF 1.0. + */ + public const PASSWORD_FALLBACK_SMF10 = 0b0010; + + /** + * @var int + * + * Used to tell Security::checkPassword() to check password fallbacks for + * SMF 1.1 and SMF 2.0. + */ + public const PASSWORD_FALLBACK_SMF20 = 0b0100; + + /** + * @var int + * + * Used to tell Security::checkPassword() to check password fallbacks for + * SMF 2.1. + */ + public const PASSWORD_FALLBACK_SMF21 = 0b1000; + + /** + * @var int + * + * Used to tell Security::checkPassword() to check password fallbacks for + * all versions of SMF. + */ + public const PASSWORD_FALLBACK_SMF = ( + self::PASSWORD_FALLBACK_SMF10 + | self::PASSWORD_FALLBACK_SMF20 + | self::PASSWORD_FALLBACK_SMF21 + ); + + /** + * @var int + * + * Used to tell Security::checkPassword() to check all password fallbacks. + */ + public const PASSWORD_FALLBACK_ALL = ( + self::PASSWORD_FALLBACK_OTHER + | self::PASSWORD_FALLBACK_SMF + ); + /*********************** * Public static methods ***********************/ /** - * Hashes the user's password + * Checks whether the password is the correct one for the given member. * - * @param string $password The unhashed password - * @param int|null $cost The cost - * @return string The hashed password + * @param string $password The plain text password to check. + * @param User $member The member whose password we are checking. + * @param int $allowed_fallbacks Which fallbacks to check. + * Must be a bitmask of this class's PASSWORD_FALLBACK_* constants. + * @param bool $flood_check Whether to check for flooding attempts. + * Default: true + * @param bool $update Whether to update the member's stored password if + * the given password is correct but the stored password uses the wrong + * encryption algorithm. This should almost always be set to true. + * Default: true + * @return bool Whether the password is correct. */ - public static function hashPassword(string $password, ?int $cost = null): string - { + public static function checkPassword( + #[\SensitiveParameter] + string $password, + User $member, + int $allowed_fallbacks, + bool $flood_check = true, + bool $update = true, + ): bool { + if (self::hashVerifyPassword($password, $member->passwd)) { + return true; + } + + // Let's be cautious, no hacking please. thanx. + if ($flood_check) { + self::validatePasswordFlood($member->id, $member->username, $member->passwd_flood); + } + + // If the forum was recently upgraded, password might be encrypted + // using a different algorithm. + $is_correct = self::checkPasswordFallbacks($password, $member, $allowed_fallbacks); + + // Whichever encryption it was using, let's make it use SMF's now ;). + if ($is_correct && $update) { + $member->passwd = self::hashPassword($password); + $member->password_salt = bin2hex(random_bytes(16)); + $member->passwd_flood = ''; + + $member->save(); + } + + return $is_correct; + } + + /** + * Hashes the user's password. + * + * @param string $password The plain text password. + * @param int|null $cost The cost. + * @return string The hashed password. + */ + public static function hashPassword( + #[\SensitiveParameter] + string $password, + ?int $cost = null, + ): string { $cost = empty($cost) ? (empty(Config::$modSettings['bcrypt_hash_cost']) ? 10 : Config::$modSettings['bcrypt_hash_cost']) : $cost; return password_hash($password, PASSWORD_BCRYPT, ['cost' => $cost]); @@ -45,20 +164,23 @@ public static function hashPassword(string $password, ?int $cost = null): string /** * Verifies a raw SMF password against the encrypted string * - * @param string $password The password - * @param string $hash The hashed string - * @return bool Whether the hashed password matches the string + * @param string $password The plain text password. + * @param string $hash The hashed string. + * @return bool Whether the hashed password matches the string. */ - public static function hashVerifyPassword(string $password, string $hash): bool - { + public static function hashVerifyPassword( + #[\SensitiveParameter] + string $password, + string $hash, + ): bool { return password_verify($password, $hash); } /** - * Benchmarks the server to figure out an appropriate cost factor (minimum 9) + * Benchmarks the server to figure out an appropriate cost factor. * - * @param float $hashTime Time to target, in seconds - * @return int The cost + * @param float $hashTime Time to target, in seconds. + * @return int The cost. */ public static function hashBenchmark(float $hashTime = 0.2): int { @@ -125,8 +247,12 @@ public static function minimumPasswordLength(): int * part of the password (email address, username, etc.) * @return null|string Null if valid or a string indicating the problem. */ - public static function validatePassword(string $password, string $username, array $restrict_in = []): ?string - { + public static function validatePassword( + #[\SensitiveParameter] + string $password, + string $username, + array $restrict_in = [], + ): ?string { // Perform basic requirements first. if (Utils::entityStrlen($password) < self::minimumPasswordLength()) { return 'short'; @@ -695,6 +821,81 @@ public static function spamProtection(string $error_type, bool $only_return_resu return false; } + /** + * This protects against brute force attacks on a member's password. + * Importantly, even if the password was right we DON'T TELL THEM! + * + * @param int $id_member The ID of the member. + * @param string $member_name The name of the member. + * @param bool|string $password_flood_value False if we don't have a flood + * value, otherwise a string with a timestamp and number of tries + * separated by a `|` character. + * @param bool $was_correct Whether or not the password was correct. + * @param bool $tfa Whether we're validating for two-factor authentication. + */ + public static function validatePasswordFlood( + int $id_member, + string $member_name, + bool|string $password_flood_value = false, + bool $was_correct = false, + bool $tfa = false, + ): void { + // As this is only brute protection, we allow 5 attempts every 10 seconds. + + // Destroy any session or cookie data about this member, as they validated wrong. + // Only if they're not validating for 2FA + if (!$tfa) { + Cookie::setLoginCookie(-3600, 0); + + if (isset($_SESSION['login_' . Config::$cookiename])) { + unset($_SESSION['login_' . Config::$cookiename]); + } + } + + // We need a member! + if (!$id_member) { + // Redirect back! + Utils::redirectexit(); + + // Probably not needed, but still make sure... + ErrorHandler::fatalLang('no_access', false); + } + + // Right, have we got a flood value? + if ($password_flood_value !== false) { + @list($time_stamp, $number_tries) = explode('|', $password_flood_value); + } + + // Timestamp or number of tries invalid? + if (empty($number_tries) || empty($time_stamp)) { + $number_tries = 0; + $time_stamp = time(); + } + + // They've failed logging in already + if (!empty($number_tries)) { + // Give them less chances if they failed before + $number_tries = $time_stamp < time() - 20 ? 2 : $number_tries; + + // They are trying too fast, make them wait longer + if ($time_stamp < time() - 10) { + $time_stamp = time(); + } + } + + $number_tries++; + + // Broken the law? + if ($number_tries > 5) { + ErrorHandler::fatalLang('login_threshold_brute_fail', 'login', [$member_name]); + } + + // Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it! + $member = current(User::load($id_member, dataset: UserDataset::None)); + $member->passwd_flood = $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries; + $member->save(); + } + /** * Checks for the existence and security status of specific files and directories * required for the proper functioning of the system. Ensures that security measures @@ -1056,4 +1257,228 @@ public static function corsPolicyHeader(bool $set_header = true): void } } } + + /************************* + * Internal static methods + *************************/ + + /** + * Checks a user-supplied password against other possible encrypted strings. + * + * If a match is found, the old encrypted string is replaced with an updated + * version that uses modern encryption. + * + * This allows seamlessly updating the encryption after the forum has been + * upgraded or converted. + * + * @param string $password The password to check. + * @param User $member The member whose password we are checking. + * @param int $allowed_fallbacks Which fallbacks to check. + * Must be a bitmask of this class's PASSWORD_FALLBACK_* constants. + * @return bool Whether the supplied password was correct. + */ + protected static function checkPasswordFallbacks( + #[\SensitiveParameter] + string $password, + User $member, + int $allowed_fallbacks, + ): bool { + // Maybe we were too hasty... let's try some other authentication methods. + $other_passwords = []; + + // SMF 2.1 prepended the username before the password. + if ( + $allowed_fallbacks & self::PASSWORD_FALLBACK_SMF21 + && self::hashVerifyPassword(Utils::strtolower($member->username) . $password, $member->passwd) + ) { + $other_passwords[] = $member->passwd; + } + + // SMF 2.0 and 1.1. + if ( + $allowed_fallbacks & self::PASSWORD_FALLBACK_SMF20 + && \strlen($member->passwd) == 40 + ) { + // Maybe they are using a hash from before the password fix. + // This is also valid for SMF 1.1 to 2.0 style of hashing, changed to bcrypt in SMF 2.1 + $other_passwords[] = sha1(strtolower($member->username) . $password); + + // Perhaps we converted to UTF-8 and have a valid password being hashed differently. + if (!empty(Config::$modSettings['previousCharacterSet']) && Config::$modSettings['previousCharacterSet'] != 'utf8') { + // Try iconv first, for no particular reason. + if (\function_exists('iconv')) { + $other_passwords['iconv'] = sha1(strtolower(iconv('UTF-8', Config::$modSettings['previousCharacterSet'], $member->username)) . Utils::htmlspecialcharsDecode(iconv('UTF-8', Config::$modSettings['previousCharacterSet'], $password))); + } + + // Say it aint so, iconv failed! + if (empty($other_passwords['iconv']) && \function_exists('mb_convert_encoding')) { + $other_passwords[] = sha1(strtolower(mb_convert_encoding($member->username, 'UTF-8', Config::$modSettings['previousCharacterSet'])) . Utils::htmlspecialcharsDecode(mb_convert_encoding($password, 'UTF-8', Config::$modSettings['previousCharacterSet']))); + } + } + } + + // SMF 1.0 and YaBB SE. + if ( + $allowed_fallbacks & self::PASSWORD_FALLBACK_SMF10 + && \strlen($member->passwd) == 32 + && $member->password_salt == '' + ) { + $other_passwords[] = hash_hmac('md5', $password, strtolower($member->username)); + } + + // Other forum software packages, for the case of conversions. + if ( + $allowed_fallbacks & self::PASSWORD_FALLBACK_OTHER + && !empty(Config::$modSettings['enable_password_conversion']) + ) { + // None of the below cases will be used most of the time (because the salt is normally set.) + if ($member->password_salt == '') { + // Discus, MD5 (used a lot), SHA-1 (used some), IkonBoard, and none at all. + switch (\strlen($member->passwd)) { + case 13: + $other_passwords[] = crypt($password, substr($password, 0, 2)); + $other_passwords[] = crypt($password, substr($member->passwd, 0, 2)); + $other_passwords[] = crypt($password, $member->passwd); + + // This one is a strange one... MyPHP, crypt() on the MD5 hash. + $other_passwords[] = crypt(md5($password), md5($password)); + break; + + case 32: + $other_passwords[] = md5($password); + $other_passwords[] = md5($password . strtolower($member->username)); + $other_passwords[] = md5(md5($password)); + + // APBoard 2 Login Method. + $other_passwords[] = md5(crypt($password, 'CRYPT_MD5')); + break; + + case 34: + // phpBB3. + $other_passwords[] = self::phpBB3_password_check($password, $member->passwd); + break; + + case 40: + $other_passwords[] = sha1($password); + break; + + case 64: + // Snitz style - SHA-256. + $other_passwords[] = hash('sha256', $password); + break; + } + + $other_passwords[] = $password; + } + // If the salt is set let's try some other options + else { + switch (\strlen($member->passwd)) { + case 32: + // MyBB + $other_passwords[] = md5(md5($member->password_salt) . md5($password)); + + // vBulletin 3 style hashing? Let's welcome them with open arms \o/. + $other_passwords[] = md5(md5($password) . stripslashes($member->password_salt)); + + // Hmm.. p'raps it's Invision 2 style? + $other_passwords[] = md5(md5($member->password_salt) . md5($password)); + + // Some common md5 ones. + $other_passwords[] = md5($member->password_salt . $password); + $other_passwords[] = md5($password . $member->password_salt); + break; + + case 40: + // BurningBoard3 style of hashing. + $other_passwords[] = sha1($member->password_salt . sha1($member->password_salt . sha1($password))); + // PunBB + $other_passwords[] = sha1($member->password_salt . sha1($password)); + break; + + case 64: + // PHP-Fusion + $other_passwords[] = hash_hmac('sha256', $password, $member->password_salt); + break; + } + } + } + + // Allows mods to easily extend the $other_passwords array + if ($allowed_fallbacks & self::PASSWORD_FALLBACK_OTHER) { + IntegrationHook::call('integrate_other_passwords', [&$other_passwords]); + } + + // Did anything match? + // Check all options with hash_equals() to prevent timing attacks. + foreach ($other_passwords as $other_password) { + $is_correct = ($is_correct ?? 0) | hash_equals($member->passwd, $other_password); + } + + return (bool) $is_correct; + } + + /** + * Custom encryption for phpBB3 based passwords. + * + * @param string $passwd The password to check. + * @param string $passwd_hash The hashed password stored in the database. + * @return ?string The hashed version of $passwd. + */ + protected static function phpBB3_password_check( + #[\SensitiveParameter] + string $passwd, + string $passwd_hash, + ): ?string { + // Too long or too short? + if (\strlen($passwd_hash) != 34) { + return null; + } + + // Range of characters allowed. + $range = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + // Tests + $strpos = strpos($range, $passwd_hash[3]); + $count = 1 << $strpos; + $salt = substr($passwd_hash, 4, 8); + + $hash = md5($salt . $passwd, true); + + for (; $count != 0; --$count) { + $hash = md5($hash . $passwd, true); + } + + $output = substr($passwd_hash, 0, 12); + $i = 0; + + while ($i < 16) { + $value = \ord($hash[$i++]); + $output .= $range[$value & 0x3f]; + + if ($i < 16) { + $value |= \ord($hash[$i]) << 8; + } + + $output .= $range[($value >> 6) & 0x3f]; + + if ($i++ >= 16) { + break; + } + + if ($i < 16) { + $value |= \ord($hash[$i]) << 16; + } + + $output .= $range[($value >> 12) & 0x3f]; + + if ($i++ >= 16) { + break; + } + + $output .= $range[($value >> 18) & 0x3f]; + } + + // Return now. + return $output; + } } diff --git a/Sources/ServerSideIncludes.php b/Sources/ServerSideIncludes.php index e7352f416bb..a8f1ce292a5 100644 --- a/Sources/ServerSideIncludes.php +++ b/Sources/ServerSideIncludes.php @@ -2809,19 +2809,17 @@ public static function checkPassword( return false; } - $request = Db::$db->query( - 'SELECT passwd, member_name, is_activated - FROM {db_prefix}members - WHERE ' . ($is_username ? 'member_name' : 'id_member') . ' = {string:id} - LIMIT 1', - [ - 'id' => $id, - ], - ); - list($pass, $user, $active) = Db::$db->fetch_row($request); - Db::$db->free_result($request); + if (!$is_username) { + $id = (int) $id; + } + + $member = current(User::load($id, $is_username ? User::LOAD_BY_NAME : User::LOAD_BY_ID, UserDataset::Minimal)); + + if (!($member instanceof User) || $member->is_activated !== User::ACTIVATED) { + return false; + } - return Security::hashVerifyPassword($password, $pass) && $active == User::ACTIVATED; + return Security::checkPassword($password, $member, Security::PASSWORD_FALLBACK_NONE); } /** diff --git a/Sources/Subs-Compat.php b/Sources/Subs-Compat.php index 4db46c840cf..ad8bdae69fc 100644 --- a/Sources/Subs-Compat.php +++ b/Sources/Subs-Compat.php @@ -10372,7 +10372,7 @@ function validatePasswordFlood( bool $was_correct = false, bool $tfa = false, ): void { - SMF\Actions\Login2::validatePasswordFlood( + SMF\Security::validatePasswordFlood( $id_member, $member_name, $password_flood_value, @@ -11425,7 +11425,7 @@ function ssi_recentEvents(int $max_events = 7, string $output_method = 'echo'): * @return bool Whether or not the password is correct. */ function ssi_checkPassword( - ?int $id = null, + int|string|null $id = null, ?string $password = null, bool $is_username = false, ): bool { diff --git a/Sources/Tasks/RemoveTempAttachments.php b/Sources/Tasks/RemoveTempAttachments.php index 96703ff4845..baf86ca2eb1 100644 --- a/Sources/Tasks/RemoveTempAttachments.php +++ b/Sources/Tasks/RemoveTempAttachments.php @@ -39,28 +39,64 @@ class RemoveTempAttachments extends ScheduledTask public function execute(): bool { // We need to know where this thing is going. - if (!empty(Config::$modSettings['currentAttachmentUploadDir'])) { - if (!\is_array(Config::$modSettings['attachmentUploadDir'])) { - Config::$modSettings['attachmentUploadDir'] = Utils::jsonDecode(Config::$modSettings['attachmentUploadDir'], true); - } + if (!isset(Config::$modSettings['attachmentUploadDir'])) { + $this->error(null); + + return true; + } - // Just use the current path for temp files. + // Is it a simple file path? + if ( + !\is_array(Config::$modSettings['attachmentUploadDir']) + && is_dir(Config::$modSettings['attachmentUploadDir']) + ) { + $attach_dirs = [1 => Config::$modSettings['attachmentUploadDir']]; + } + // Is it an array of file paths? + elseif (\is_array(Config::$modSettings['attachmentUploadDir'])) { $attach_dirs = Config::$modSettings['attachmentUploadDir']; - } else { - $attach_dirs = [Config::$modSettings['attachmentUploadDir']]; + } + // Is it a JSON string? + elseif ( + \is_array( + @Utils::jsonDecode( + Config::$modSettings['attachmentUploadDir'], + associative: true, + should_log: false, + ), + ) + ) { + $attach_dirs = Utils::jsonDecode( + Config::$modSettings['attachmentUploadDir'], + associative: true, + should_log: false, + ); + } + // Is it a serialized string? + elseif ( + \is_array( + @Utils::safeUnserialize( + Config::$modSettings['attachmentUploadDir'], + ), + ) + ) { + $attach_dirs = Utils::safeUnserialize(Config::$modSettings['attachmentUploadDir']); + } + // Invalid. + else { + $this->error(Config::$modSettings['attachmentUploadDir']); + + return true; } + // Now that we have all our attachment directories, clean them. foreach ($attach_dirs as $attach_dir) { $dir = @opendir($attach_dir); if (!$dir) { - Theme::loadEssential(); - - Utils::$context['scheduled_errors']['remove_temp_attachments'][] = Lang::getTxt('cant_access_upload_path', ['path' => $attach_dir], file: 'Post'); - - ErrorHandler::log(Lang::getTxt('cant_access_upload_path', ['path' => $attach_dir], file: 'Post'), 'critical'); + $this->error($attach_dir); - return true; + continue; } while ($file = readdir($dir)) { @@ -81,4 +117,36 @@ public function execute(): bool return true; } + + /****************** + * Internal methods + ******************/ + + /** + * undocumented method + * + * @param ?string $attach_dir + */ + private function error(?string $attach_dir): void + { + Theme::loadEssential(); + + if ($attach_dir === null) { + $error_message = Lang::getTxt( + 'attach_directory_admin_warning', + ['attach_dir' => 'null'], + file: 'Post', + ); + } else { + $error_message = Lang::getTxt( + 'cant_access_upload_path', + ['path' => $attach_dir], + file: 'Post', + ); + } + + Utils::$context['scheduled_errors']['remove_temp_attachments'][] = $error_message; + + ErrorHandler::log($error_message, 'critical'); + } } diff --git a/Sources/User.php b/Sources/User.php index d8399a01a80..0ac1367c45a 100644 --- a/Sources/User.php +++ b/Sources/User.php @@ -17,7 +17,6 @@ use SMF\Actions\Admin\ACP; use SMF\Actions\Admin\Bans; -use SMF\Actions\Login2; use SMF\Actions\Logout; use SMF\Actions\Moderation\ReportedContent; use SMF\Cache\CacheApi; @@ -4512,7 +4511,7 @@ protected function verifyPassword(): void $id = self::$my_id; self::$my_id = 0; - Login2::validatePasswordFlood( + Security::validatePasswordFlood( $id, self::$profiles[$id]['member_name'], self::$profiles[$id]['passwd_flood'], @@ -4732,10 +4731,9 @@ protected function initializeGuest(): void throw new \LogicException('Called ' . __METHOD__ . ' for a user that is not ' . __CLASS__ . '::$me'); } - // This is what a guest's variables should be. - if (self::$profiles[0]['dataset'] === UserDataset::Minimal) { - self::$profiles[0] = self::processRawUserData(self::$profiles[0]); - self::$profiles[0]['dataset'] = UserDataset::Basic; + // Ensure the guest profile has been loaded. + if (!isset(self::$profiles[0])) { + self::loadUserData([0]); } // If they gave us a bad cookie, discard it. @@ -5304,6 +5302,7 @@ protected static function loadUserData(array $users, int $type = self::LOAD_BY_I }; } + self::$profiles[0] = self::processRawUserData(self::$profiles[0]); self::$profiles[0]['dataset'] = UserDataset::Minimal; $loaded_ids[] = 0; diff --git a/Themes/default/InstallTemplate.php b/Themes/default/InstallTemplate.php index d2e4651c55b..8ed1be04fd5 100644 --- a/Themes/default/InstallTemplate.php +++ b/Themes/default/InstallTemplate.php @@ -19,6 +19,7 @@ use SMF\Db\DatabaseApi as Db; use SMF\Lang; use SMF\Maintenance\Maintenance; +use SMF\Utils; /** * Template for Installer @@ -45,16 +46,16 @@ public static function upper(): void */ public static function lower(): void { - if (!empty(Maintenance::$context['continue']) || !empty(Maintenance::$context['skip'])) { + if (!empty(Utils::$context['continue']) || !empty(Utils::$context['skip'])) { echo '
'; - if (!empty(Maintenance::$context['continue'])) { + if (!empty(Utils::$context['continue'])) { echo ' '; } - if (!empty(Maintenance::$context['skip'])) { + if (!empty(Utils::$context['skip'])) { echo ' '; } @@ -123,14 +124,14 @@ public static function checkFilesWritable(): void

', Lang::getTxt('ftp_setup_why_info', file: 'Maintenance'), '

'; - if (isset(Maintenance::$context['systemos'], Maintenance::$context['detected_path']) && Maintenance::$context['systemos'] == 'linux') { + if (isset(Utils::$context['systemos'], Utils::$context['detected_path']) && Utils::$context['systemos'] == 'linux') { echo '

', Lang::getTxt('chmod_linux_info', file: 'Maintenance'), '

- # chmod a+w ', implode(' ' . Maintenance::$context['detected_path'] . '/', Maintenance::$context['chmod_files']), ''; + # chmod a+w ', implode(' ' . Utils::$context['detected_path'] . '/', Utils::$context['chmod_files']), ''; } // This is serious! @@ -144,16 +145,16 @@ public static function checkFilesWritable(): void

', Lang::getTxt('ftp_setup_info', file: 'Maintenance'), '

'; - if (!empty(Maintenance::$context['ftp_errors'])) { + if (!empty(Utils::$context['ftp_errors'])) { echo '
', Lang::getTxt('error_ftp_no_connect', file: 'Maintenance'), '

- ', implode('
', Maintenance::$context['ftp_errors']), '
+ ', implode('
', Utils::$context['ftp_errors']), '
'; } echo ' -
+
@@ -161,16 +162,16 @@ public static function checkFilesWritable(): void
- +
- +
', Lang::getTxt('ftp_server_info', file: 'Maintenance'), '
- +
', Lang::getTxt('ftp_username_info', file: 'Maintenance'), '
@@ -184,15 +185,15 @@ public static function checkFilesWritable(): void
- -
', Maintenance::$context['chmod']['path_msg'] ?? '', '
+ +
', Utils::$context['chmod']['path_msg'] ?? '', '
- ', Lang::getTxt('ftp_setup_again', ['url' => Maintenance::$context['form_url']], file: 'Maintenance'); + ', Lang::getTxt('ftp_setup_again', ['url' => Utils::$context['form_url']], file: 'Maintenance'); } /** @@ -209,7 +210,7 @@ public static function databaseSettings(): void
'; // More than one database type? - if (\count(Maintenance::$context['databases']) > 1) { + if (\count(Utils::$context['databases']) > 1) { echo '
@@ -217,7 +218,7 @@ public static function databaseSettings(): void
+
'; } @@ -238,35 +239,35 @@ public static function databaseSettings(): void
- +
', Lang::getTxt('db_settings_server_info', file: 'Maintenance'), '
- +
', Lang::getTxt('db_settings_port_info', file: 'Maintenance'), '
- +
', Lang::getTxt('db_settings_username_info', file: 'Maintenance'), '
- +
', Lang::getTxt('db_settings_password_info', file: 'Maintenance'), '
- +
', Lang::getTxt('db_settings_database_info', file: 'Maintenance'), ' ', Lang::getTxt('db_settings_database_info_note', file: 'Maintenance'), ' @@ -276,7 +277,7 @@ public static function databaseSettings(): void
- +
', Lang::getTxt('db_settings_prefix_info', file: 'Maintenance'), '
'; @@ -318,7 +319,7 @@ public static function forumSettings(): void
- +
', Lang::getTxt('install_settings_url_info', file: 'Maintenance'), '
@@ -345,7 +346,7 @@ public static function forumSettings(): void
-
', Maintenance::$context['test_dbsession'] ? Lang::getTxt('install_settings_dbsession_info1', file: 'Maintenance') : Lang::getTxt('install_settings_dbsession_info2', file: 'Maintenance'), '
+
', Utils::$context['test_dbsession'] ? Lang::getTxt('install_settings_dbsession_info1', file: 'Maintenance') : Lang::getTxt('install_settings_dbsession_info2', file: 'Maintenance'), '
', Lang::getTxt('install_settings_stats', file: 'Maintenance'), ':
@@ -355,8 +356,8 @@ public static function forumSettings(): void
', Lang::getTxt('force_ssl', file: 'Maintenance'), ':
- +
', Lang::getTxt('force_ssl_info', file: 'Maintenance'), '
@@ -370,21 +371,21 @@ public static function forumSettings(): void public static function databasePopulation(): void { echo ' -

', !empty(Maintenance::$context['was_refresh']) ? Lang::getTxt('user_refresh_install_desc', file: 'Maintenance') : Lang::getTxt('db_populate_info', file: 'Maintenance'), '

'; +

', !empty(Utils::$context['was_refresh']) ? Lang::getTxt('user_refresh_install_desc', file: 'Maintenance') : Lang::getTxt('db_populate_info', file: 'Maintenance'), '

'; - if (!empty(Maintenance::$context['sql_results'])) { + if (!empty(Utils::$context['sql_results'])) { echo ' '; } - if (!empty(Maintenance::$context['failures'])) { + if (!empty(Utils::$context['failures'])) { echo '
', Lang::getTxt('error_db_queries', file: 'Maintenance'), '