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'), '
- ', implode('
- - ', Maintenance::$context['chmod_files']), '
+ - ', Utils::$context['chmod_files']), '
';
- 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 '
-
- ', 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
-