Update to SMF 2.1.5 - Installation Instructions for 2.1.4

Update to SMF 2.1.5
This will update your forum to SMF 2.1.5.

File Edits ALT + Click to collapse all the operations

./LICENSE

Find: Select
Copyright © 2023 Simple Machines. All rights reserved.
Replace With: Select
Copyright © 2025 Simple Machines. All rights reserved.

./SSI.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
define('SMF_VERSION', '2.1.4');
Replace With: Select
define('SMF_VERSION', '2.1.5');
Find: Select
define('SMF_SOFTWARE_YEAR', '2023');
Replace With: Select
define('SMF_SOFTWARE_YEAR', '2025');

./cron.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
define('SMF_VERSION', '2.1.4');
Replace With: Select
define('SMF_VERSION', '2.1.5');
Find: Select
define('SMF_SOFTWARE_YEAR', '2023');
Replace With: Select
define('SMF_SOFTWARE_YEAR', '2025');
Find: Select
// Just in case there's a problem...
set_error_handler('smf_error_handler_cron');
Replace With: Select
// Just in case there's a problem...
set_error_handler('smf_error_handler_cron');
set_exception_handler('smf_exception_handler_cron');
Find: Select

die('No direct access...');
}
Add After: Select

/**
* Generic handler for uncaught exceptions.
*
* Always ends execution.
*
* @param \Throwable $e The uncaught exception.
*/
function smf_exception_handler_cron(\Throwable $e)
{
global $modSettings, $txt;

loadLanguage('Errors');

$message = $txt[$e->getMessage()] ?? $e->getMessage();

if (!empty($modSettings['enableErrorLogging'])) {
log_error($message, 'cron', $e->getFile(), $e->getLine());
}
}

./index.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
define('SMF_VERSION', '2.1.4');
Replace With: Select
define('SMF_VERSION', '2.1.5');
Find: Select
define('SMF_SOFTWARE_YEAR', '2023');
Replace With: Select
define('SMF_SOFTWARE_YEAR', '2025');
Find: Select
// Register an error handler.
set_error_handler('smf_error_handler');
Replace With: Select
// Register an error handler.
set_error_handler('smf_error_handler');
set_exception_handler('smf_exception_handler');
Find: Select

'suggest' => true,
Add After: Select

'uploadAttach' => true,

./proxy.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
define('SMF_VERSION', '2.1.4');
Replace With: Select
define('SMF_VERSION', '2.1.5');
Find: Select
define('SMF_SOFTWARE_YEAR', '2023');
Replace With: Select
define('SMF_SOFTWARE_YEAR', '2025');

./Themes/default/languages/Errors.english.php

Find: Select
// Version: 2.1.4; Errors
Replace With: Select
// Version: 2.1.5; Errors
Find: Select

$txt['ban_name_empty'] = 'The name of the ban was left empty';
Add After: Select

$txt['ban_name_is_too_long'] = 'The selected name is too long. Use no more than 20 characters.';
Find (at the end of the file): Select
?>
Add Before: Select

$txt['unicode_update_failed'] = 'A new version of Unicode is available, but SMF could not update to it. Please make sure %1$s and all the files in it are writable. SMF will try to update its Unicode data files again automatically.';

./Themes/default/languages/Help.english.php

Find: Select
// Version: 2.1.3; Help
Replace With: Select
// Version: 2.1.5; Help
Find: Select
$helptxt['coppaAge'] = 'The value specified in this box will determine the minimum age that new members must be in order to be granted immediate access to the forums.
On registration they will be prompted to confirm whether they are over this age, and if not will either have their application rejected or suspended awaiting parental approval - dependant on the type of restriction chosen.
Replace With: Select
$helptxt['coppaAge'] = 'The value specified in this box will determine the minimum age that new members must be in order to be granted immediate access to the forum.
On registration they will be prompted to confirm whether they are over this age, and if not will either have their application rejected or suspended awaiting parental approval - dependent on the type of restriction chosen.

./Themes/default/languages/ManageMaintenance.english.php

Find: Select
// Version: 2.1.0; ManageMaintenance
Replace With: Select
// Version: 2.1.5; ManageMaintenance
Find: Select
$txt['maintain_backup_info'] = 'Download a backup copy of your forums database in case of emergency.';
Replace With: Select
$txt['maintain_backup_info'] = 'Download a backup copy of your forum\'s database in case of emergency.';

./Themes/default/languages/ManagePermissions.english.php

Find: Select
// Version: 2.1.0; ManagePermissions
Replace With: Select
// Version: 2.1.5; ManagePermissions
Find: Select

$txt['permissionhelp_profile_remote_avatar'] = 'Because avatars might influence the page creation time negatively, it is possible to disallow certain membergroups to use avatars from external servers.';
Add After: Select

$txt['permissionname_profile_gravatar'] = 'Choose a Gravatar';
$txt['permissionhelp_profile_gravatar'] = 'Because Gravatars might influence the page creation time negatively, it is possible to disallow certain membergroups to use Gravatars.';

./Themes/default/languages/ManageSettings.english.php

Find: Select
// Version: 2.1.0; ManageSettings
Replace With: Select
// Version: 2.1.5; ManageSettings
Find: Select
$txt['custom_profile_desc'] = 'From this page you can create your own custom profile fields that fit in with your own forums requirements';
Replace With: Select
$txt['custom_profile_desc'] = 'From this page you can create your own custom profile fields that fit in with your own forum\'s requirements';

./Themes/default/languages/Packages.english.php

Find: Select
// Version: 2.1.0; Packages
Replace With: Select
// Version: 2.1.5; Packages
Find: Select
$txt['uninstall_modification'] = 'Uninstall Mod';
$txt['uninstall_language'] = 'Uninstall Language';
$txt['uninstall_avatar'] = 'Uninstall Avatar Pack';
$txt['uninstall_unknown'] = 'Uninstall Package';
Replace With: Select
$txt['install_smiley'] = 'Install Smiley Pack';
$txt['uninstall_modification'] = 'Uninstall Mod';
$txt['uninstall_language'] = 'Uninstall Language';
$txt['uninstall_avatar'] = 'Uninstall Avatar Pack';
$txt['uninstall_unknown'] = 'Uninstall Package';
$txt['uninstall_smiley'] = 'Uninstall Smiley Pack';
Find: Select

$txt['unknown_package'] = 'Unknown packages';
Add After: Select

$txt['smiley_package'] = 'Smiley packages';

./Themes/default/languages/Post.english.php

Find: Select
// Version: 2.1.4; Post
Replace With: Select
// Version: 2.1.5; Post
Find: Select

$txt['attached_insert_height'] = 'Insert height (px):';
Add After: Select

$txt['attached_insert_placeholder'] = 'auto';

./Themes/default/languages/Profile.english.php

Find: Select
// Version: 2.1.3; Profile
Replace With: Select
// Version: 2.1.5; Profile
Find: Select
$txt['notify_important_email'] = 'Receive forum newsletters, announcements and important notifications by email.';
$txt['auto_notify'] = 'Turn notification on when you post or reply to a topic';
Replace With: Select
$txt['notify_important_email'] = 'Receive forum newsletters, announcements and important notifications by email.';

./Themes/default/languages/Reports.english.php

Find: Select
// Version: 2.1.0; Reports
Replace With: Select
// Version: 2.1.5; Reports
Find: Select

$txt['group_perms_name_profile_remote_avatar'] = 'Choose a remote avatar';
Add After: Select

$txt['group_perms_name_profile_gravatar'] = 'Use a Gravatar';

./Themes/default/languages/Timezones.english.php

Find: Select
// Version: 2.1.3; Timezones
Replace With: Select
// Version: 2.1.5; Timezones
Find: Select

$txt['America/Chihuahua'] = 'Chihuahua';
Add After: Select

$txt['America/Ciudad_Juarez'] = 'Ciudad Juárez';

./Themes/default/languages/index.english.php

Find: Select
// Version: 2.1.3; index
Replace With: Select
// Version: 2.1.5; index
Find: Select
// Short form of hours
$txt['hours_short'] = 'hrs';
// Descimal sign
Replace With: Select
// Short form of hours
$txt['hours_short'] = 'hrs';
// Decimal sign

./Sources/Class-Package.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
// Loop until we're out of data.
while ($data != '')
{
// Find and remove the next tag.
preg_match('/\A<([\w\-:]+)((?:\s+.+?)?)([\s]?\/)?' . '>/', $data, $match);
Replace With: Select
// Loop until we're out of data.
while ($data !== '')
{
// Find and remove the next tag.
preg_match('/\A<([\w\-:]+)((?:\s+[\s\S]+?)?)([\s]?\/)?' . '>/', $data, $match);

./Sources/DbPackages-mysql.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
|| !in_array($columns[$key]['type'], array('text', 'mediumntext', 'largetext', 'varchar', 'char'))
Replace With: Select
|| !in_array($columns[$key]['type'], array('text', 'mediumtext', 'largetext', 'varchar', 'char'))
Find: Select

while ($row = $smcFunc['db_fetch_assoc']($request))
{
Add After: Select

$row = array_change_key_case($row, CASE_LOWER);
Find: Select
// If a size was already specified, we won't be able to match it anyways.
if (
!isset($cols[$c])
|| !in_array($cols[$c]['type'], array('text', 'mediumntext', 'largetext', 'varchar', 'char'))
Replace With: Select
// If a size was already specified, we won't be able to match it anyways.
if (
!isset($cols[$c])
|| !in_array($cols[$c]['type'], array('text', 'mediumtext', 'largetext', 'varchar', 'char'))

./Sources/Errors.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.2
Replace With: Select
* @version 2.1.5
Find: Select
// Allow the hook to change the error_type and know about the error.
call_integration_hook('integrate_error_types', array(&$other_error_types, &$error_type, $error_message, $file, $line));
$known_error_types += $other_error_types;
Replace With: Select
// Allow the hook to change the error_type and know about the error.
call_integration_hook('integrate_error_types', array(&$other_error_types, &$error_type, $error_message, $file, $line));
$known_error_types = array_merge($known_error_types, $other_error_types);
Find: Select

loadTheme();
}
Add After: Select

// Attempt to load the text string.
loadLanguage('Errors');
if (empty($txt[$error]))
$error_message = $error;
else
$error_message = empty($sprintf) ? $txt[$error] : vsprintf($txt[$error], $sprintf);

// Send a custom header if we have a custom message.
if (isset($_REQUEST['js']) || isset($_REQUEST['xml']) || isset($_RQEUEST['ajax']))
header('X-SMF-errormsg: ' . $error_message);
Find: Select
// Load the language file, only if it needs to be reloaded
if ($reload_lang_file)
Replace With: Select
// Load the language file, only if it needs to be reloaded
if ($reload_lang_file && !empty($txt[$error]))
Find: Select

die('No direct access...');
}
Add After: Select

/**
* Generic handler for uncaught exceptions.
*
* Always ends execution.
*
* @param \Throwable $e The uncaught exception.
*/
function smf_exception_handler(\Throwable $e)
{
global $modSettings, $txt;

loadLanguage('Errors');

$message = $txt[$e->getMessage()] ?? $e->getMessage();

if (!empty($modSettings['enableErrorLogging'])) {
log_error($message, 'general', $e->getFile(), $e->getLine());
}

fatal_error($message, false);
}
Find: Select
*/
trigger_error('No direct access...', E_USER_ERROR);
Replace With: Select
*/
die('No direct access...');

./Sources/Likes.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select

protected $_js = false;
Add After: Select

/**
* @var string The sub action sent in $_GET['sa'].
*/
protected $_sa = null;
Find: Select
// Insert the like.
$smcFunc['db_insert']('insert',
Replace With: Select
// Insert the like.
$smcFunc['db_insert']('ignore',

./Sources/Load.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
$fix_utf8mb4 = function($string) use ($utf8, $smcFunc)
{
if (!$utf8 || $smcFunc['db_mb4'])
return $string;
Replace With: Select
$smcFunc['fix_utf8mb4'] = function($string) use ($utf8, $smcFunc)
{
if (!$utf8 || $smcFunc['db_mb4'])
return $string;

$string = (string) $string;
Find: Select
'htmlspecialchars' => function($string, $quote_style = ENT_COMPAT, $charset = 'ISO-8859-1') use ($ent_check, $utf8, $fix_utf8mb4, &$smcFunc)
{
$string = $smcFunc['normalize']($string);

return $fix_utf8mb4($ent_check(htmlspecialchars($string, $quote_style, $utf8 ? 'UTF-8' : $charset)));
Replace With: Select
'htmlspecialchars' => function($string, $quote_style = ENT_COMPAT, $charset = 'ISO-8859-1') use ($ent_check, $utf8, &$smcFunc)
{
$string = $smcFunc['normalize']($string);

return $smcFunc['fix_utf8mb4']($ent_check(htmlspecialchars($string, $quote_style, $utf8 ? 'UTF-8' : $charset)));
Find: Select
'convert_case' => function($string, $case, $simple = false, $form = 'c') use (&$smcFunc, $utf8, $ent_check, $fix_utf8mb4, $sourcedir)
Replace With: Select
'convert_case' => function($string, $case, $simple = false, $form = 'c') use (&$smcFunc, $utf8, $ent_check, $sourcedir)
Find: Select
return $fix_utf8mb4($string);
Replace With: Select
return $smcFunc['fix_utf8mb4']($string);
Find: Select
IMAGETYPE_IFF => 'iff'
Replace With: Select
IMAGETYPE_IFF => 'iff',
IMAGETYPE_WEBP => 'webp'

./Sources/LogInOut.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
trigger_error($txt['login_no_session_cookie'], E_USER_ERROR);
Replace With: Select
throw new \Exception('login_no_session_cookie');

./Sources/Logging.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
trigger_error(sprintf($txt['logActions_not_array'], $log['action']), E_USER_NOTICE);
Replace With: Select
throw new \TypeError(sprintf($txt['logActions_not_array'], $log['action']));
Find: Select
trigger_error($txt['logActions_topic_not_numeric'], E_USER_NOTICE);
Replace With: Select
throw new \TypeError($txt['logActions_topic_not_numeric']);
Find: Select
trigger_error($txt['logActions_message_not_numeric'], E_USER_NOTICE);
Replace With: Select
throw new \TypeError($txt['logActions_message_not_numeric']);
Find: Select
trigger_error($txt['logActions_member_not_numeric'], E_USER_NOTICE);
Replace With: Select
throw new \TypeError($txt['logActions_member_not_numeric']);
Find: Select
trigger_error($txt['logActions_board_not_numeric'], E_USER_NOTICE);
Replace With: Select
throw new \TypeError($txt['logActions_board_not_numeric']);
Find: Select
trigger_error($txt['logActions_board_to_not_numeric'], E_USER_NOTICE);
Replace With: Select
throw new \TypeError($txt['logActions_board_to_not_numeric']);
Find: Select
$memID = $user_info['id'];
Replace With: Select
$memID = $user_info['id'] ?? $log['extra']['member'] ?? 0;

./Sources/ManageAttachments.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
// Attachments or avatars?
$context['browse_type'] = isset($_REQUEST['avatars']) ? 'avatars' : (isset($_REQUEST['thumbs']) ? 'thumbs' : 'attachments');

$titles = array(
'attachments' => array('?action=admin;area=manageattachments;sa=browse', $txt['attachment_manager_attachments']),
'avatars' => array('?action=admin;area=manageattachments;sa=browse;avatars', $txt['attachment_manager_avatars']),
'thumbs' => array('?action=admin;area=manageattachments;sa=browse;thumbs', $txt['attachment_manager_thumbs']),
);

$list_title = $txt['attachment_manager_browse_files'] . ': ';
foreach ($titles as $browse_type => $details)
{
if ($browse_type != 'attachments')
$list_title .= ' | ';

if ($context['browse_type'] == $browse_type)
$list_title .= '<img src="' . $settings['images_url'] . '/selected.png" alt="&gt;"> ';

$list_title .= '<a href="' . $scripturl . $details[0] . '">' . $details[1] . '</a>';
}

// Set the options for the list component.
$listOptions = array(
'id' => 'file_list',
'title' => $list_title,
Replace With: Select
// Attachments or avatars?
$context['browse_type'] = isset($_REQUEST['avatars']) ? 'avatars' : (isset($_REQUEST['thumbs']) ? 'thumbs' : 'attachments');

// Set the options for the list component.
$listOptions = array(
'id' => 'file_list',
Find: Select
'position' => 'above_table_headers',
Replace With: Select
'position' => 'above_column_headers',
Find: Select
// Does a hook want to display their attachments better?
call_integration_hook('integrate_attachments_browse', array(&$listOptions, &$titles, &$list_title));
Replace With: Select
$titles = array(
'attachments' => array('?action=admin;area=manageattachments;sa=browse', $txt['attachment_manager_attachments']),
'avatars' => array('?action=admin;area=manageattachments;sa=browse;avatars', $txt['attachment_manager_avatars']),
'thumbs' => array('?action=admin;area=manageattachments;sa=browse;thumbs', $txt['attachment_manager_thumbs']),
);

$list_title = $txt['attachment_manager_browse_files'] . ': ';

// Does a hook want to display their attachments better?
call_integration_hook('integrate_attachments_browse', array(&$listOptions, &$titles));

foreach ($titles as $browse_type => $details)
{
if ($browse_type != 'attachments')
$list_title .= ' | ';

if ($context['browse_type'] == $browse_type)
$list_title .= '<img src="' . $settings['images_url'] . '/selected.png" alt="&gt;"> ';

$list_title .= '<a href="' . $scripturl . $details[0] . '">' . $details[1] . '</a>';
}

$listOptions['title'] = $list_title;

./Sources/ManageBans.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
'db' => 'notes',
'class' => 'smalltext',
Replace With: Select
'db' => 'notes',
'class' => 'smalltext word_break',
Find: Select
'db' => 'reason',
'class' => 'smalltext',
Replace With: Select
'db' => 'reason',
'class' => 'smalltext word_break',
Find: Select
'position' => 'above_table_headers',
'value' => '
<input type="submit" name="remove_selection" value="' . $txt['ban_remove_selected_triggers'] . '" class="button"> <a class="button" href="' . $scripturl . '?action=admin;area=ban;sa=edittrigger;bg=' . $ban_group_id . '">' . $txt['ban_add_trigger'] . '</a>',
'style' => 'text-align: right;',
),
array(
'position' => 'above_table_headers',
Replace With: Select
'position' => 'above_column_headers',
'value' => '
<input type="submit" name="remove_selection" value="' . $txt['ban_remove_selected_triggers'] . '" class="button"> <a class="button" href="' . $scripturl . '?action=admin;area=ban;sa=edittrigger;bg=' . $ban_group_id . '">' . $txt['ban_add_trigger'] . '</a>',
'style' => 'text-align: right;',
),
array(
'position' => 'above_column_headers',
Find: Select

call_integration_hook('integrate_edit_bans', array(&$ban_info, empty($_REQUEST['bg'])));
Add After: Select

// Limit 'reason' characters
$ban_info['reason'] = $smcFunc['truncate']($ban_info['reason'], 255);
Find: Select

function updateBanGroup($ban_info = array())
{
global $smcFunc, $context;

if (empty($ban_info['name']))
$context['ban_errors'][] = 'ban_name_empty';
Add After: Select

if ($smcFunc['strlen']($ban_info['name']) > 20)
$context['ban_errors'][] = 'ban_name_is_too_long';
Find: Select

function insertBanGroup($ban_info = array())
{
global $smcFunc, $context;

if (empty($ban_info['name']))
$context['ban_errors'][] = 'ban_name_empty';
Add After: Select

if ($smcFunc['strlen']($ban_info['name']) > 20)
$context['ban_errors'][] = 'ban_name_is_too_long';

./Sources/ManageErrors.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
'href' => ';filter=' . $_GET['filter'] . ';value=' . $_GET['value'],
Replace With: Select
'href' => ';filter=' . $_GET['filter'] . ';value=' . urlencode($_GET['value']),

./Sources/ManageLanguages.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
// Don't do anything with files we don't understand.
if (!in_array($extension, array('php', 'jpg', 'gif', 'jpeg', 'png', 'txt')))
Replace With: Select
// Don't do anything with files we don't understand.
if (!in_array($extension, array('php', 'jpg', 'gif', 'jpeg', 'png', 'txt', 'webp')))

./Sources/ManageMail.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
// Private PM/email subjects and similar shouldn't be shown in the mailbox area.
if (!empty($row['private']))
$row['subject'] = $txt['personal_message'];
Replace With: Select
// Private PM/email subjects and similar shouldn't be shown in the mailbox area.
if (!empty($row['private']))
$row['subject'] = $txt['personal_message'];
else
$row['subject'] = mb_decode_mimeheader($row['subject']);

./Sources/ManageMaintenance.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
createToken($context['not_done_token']);
Replace With: Select
createToken($context['not_done_token']);
Find: Select
$hooks_filters[] = '<option' . ($current_filter == $hook ? ' selected ' : '') . ' value="' . $hook . '">' . $hook . '</option>';

if (!empty($hooks_filters))
$context['insert_after_template'] .= '
<script>
var hook_name_header = document.getElementById(\'header_list_integration_hooks_hook_name\');
hook_name_header.innerHTML += ' . JavaScriptEscape('<select style="margin-left:15px;" onchange="window.location=(\'' . $scripturl . '?action=admin;area=maintain;sa=hooks\' + (this.value ? \';filter=\' + this.value : \'\'));"><option value="">' . $txt['hooks_reset_filter'] . '</option>' . implode('', $hooks_filters) . '</select>') . ';
</script>';
Replace With: Select
$hooks_filters[] = '<option' . ($current_filter == $hook ? ' selected ' : '') . ' value="' . $hook . '">' . $hook . '</option>';
Find: Select

return $instance . $data['real_function'];
},
Add After: Select

'class' => 'word_break',
Find: Select

'db' => 'file_name',
Add After: Select

'class' => 'word_break',
Find: Select

</ul>'
),
Add After: Select

array(
'position' => 'above_column_headers',
'value' => '
<select onchange="window.location=(\'' . $scripturl . '?action=admin;area=maintain;sa=hooks\' + (this.value ? \';filter=\' + this.value : \'\'));">
<option value="">' . $txt['hooks_reset_filter'] . '</option>
' . implode('', $hooks_filters) . '
</select>',
'class' => 'floatright',
),
Find: Select
// Handle hooks pointing outside the sources directory.
$absPath_clean = rtrim($hookParsedData['absPath'], '!');
Replace With: Select
// Handle hooks pointing outside the sources directory.
$absPath_clean = rtrim($hookParsedData['absPath'], '!');
Find: Select

$hookData['call'] = $hookData['pureFunc'] = $modFunc;
Add After: Select

$hookData['call'] = ltrim($hookData['call'], '\\');

./Sources/ManageMembergroups.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
'position' => 'above_table_headers',
Replace With: Select
'position' => 'above_column_headers',
Find: Select
// There might have been some post group changes.
updateStats('postgroups');
Replace With: Select
// There might have been some post group changes.
if ($_POST['min_posts'] != -1)
updateStats('postgroups');
Find: Select
// Get a list of all the image formats we can select.
$imageExts = array('png', 'jpg', 'jpeg', 'bmp', 'gif');
Replace With: Select
// Get a list of all the image formats we can select.
$imageExts = array('png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp');

./Sources/ManagePaid.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
'position' => 'above_table_headers',
Replace With: Select
'position' => 'above_column_headers',

./Sources/ManagePermissions.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select

'profile_upload_avatar',
'profile_remote_avatar',
Add After: Select

'profile_gravatar',
Find: Select

'profile_remote_avatar' => array(false, 'profile'),
Add After: Select

'profile_gravatar' => array(false, 'profile'),
Find: Select
// Hide Likes/Mentions permissions...
Replace With: Select
// Hide Likes/Mentions/Gravatar permissions...
Find: Select

$hiddenPermissions[] = 'mention';
}
Add After: Select

if (empty($modSettings['gravatarEnabled']))
{
$hiddenPermissions[] = 'profile_gravatar';
}
Find: Select

'profile_remote_avatar',
'profile_server_avatar',
Add After: Select

'profile_gravatar',

./Sources/ManageSearchEngines.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.2
Replace With: Select
* @version 2.1.5
Find: Select
SELECT COUNT(*) AS offset
Replace With: Select
SELECT COUNT(*)

./Sources/ManageSmileys.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
// Set the right tab to be selected.
$context[$context['admin_menu_name']]['current_subsection'] = 'editsets';

$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
Replace With: Select
// Set the right tab to be selected.
$context[$context['admin_menu_name']]['current_subsection'] = 'editsets';

$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg', 'webp');
Find: Select
array(
'position' => 'above_table_headers',
Replace With: Select
array(
'position' => 'above_column_headers',
Find: Select
// Some useful arrays... types we allow - and ports we don't!
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
Replace With: Select
// Some useful arrays... types we allow - and ports we don't!
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg', 'webp');
Find: Select
$context['smileys_dir'] = empty($modSettings['smileys_dir']) ? $boarddir . '/Smileys' : $modSettings['smileys_dir'];

$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
Replace With: Select
$context['smileys_dir'] = empty($modSettings['smileys_dir']) ? $boarddir . '/Smileys' : $modSettings['smileys_dir'];

$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg', 'webp');
Find: Select
$writeErrors[] = $set['path'];
Replace With: Select
$writeErrors[] = $set;
Find: Select
fatal_lang_error('smiley_set_unable_to_import', false);

$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
Replace With: Select
fatal_lang_error('smiley_set_unable_to_import', false);

$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg', 'webp');

./Sources/ModerationCenter.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
'body' => $smcFunc['htmlspecialchars']($row['body']),
Replace With: Select
// Redo htmlspecialchars for the sake of old data that might have incorrectly encoded entities.
'body' => $smcFunc['htmlspecialchars'](un_htmlspecialchars($row['body'])),
Find: Select
// Safety first.
$_POST['template_title'] = $smcFunc['htmlspecialchars']($_POST['template_title']);
Replace With: Select
// Safety first.
$_POST['template_title'] = $smcFunc['htmlspecialchars']($_POST['template_title']);
$_POST['template_body'] = $smcFunc['htmlspecialchars']($_POST['template_body']);

./Sources/MoveTopic.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
// Replace tokens with links in the reason.
$reason_replacements = array(
$txt['movetopic_auto_board'] => '[url="' . $scripturl . '?board=' . $_POST['toboard'] . '.0"]' . $board_name . '[/url]',
Replace With: Select
// Replace tokens with links in the reason.
$reason_replacements = array(
$txt['movetopic_auto_board'] => '[url=&quot;' . $scripturl . '?board=' . $_POST['toboard'] . '.0&quot;]' . $board_name . '[/url]',
Find: Select
// Make sure we catch both languages in the reason.
$reason_replacements += array(
$txt['movetopic_auto_board'] => '[url="' . $scripturl . '?board=' . $_POST['toboard'] . '.0"]' . $board_name . '[/url]',
Replace With: Select
// Make sure we catch both languages in the reason.
$reason_replacements += array(
$txt['movetopic_auto_board'] => '[url=&quot;' . $scripturl . '?board=' . $_POST['toboard'] . '.0&quot;]' . $board_name . '[/url]',

./Sources/News.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.2
Replace With: Select
* @version 2.1.5
Find: Select
INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
Replace With: Select
INNER JOIN {db_prefix}messages AS m ON (t.id_first_msg = m.id_msg)
INNER JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board)
LEFT JOIN {db_prefix}members AS mem ON (m.id_member = mem.id_member )
Find: Select
INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
Replace With: Select
INNER JOIN {db_prefix}boards AS b ON (m.id_board = b.id_board)
INNER JOIN {db_prefix}topics AS t ON (m.id_topic = t.id_topic)
Find: Select
INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
LEFT JOIN {db_prefix}members AS memf ON (memf.id_member = mf.id_member)
Replace With: Select
INNER JOIN {db_prefix}topics AS t ON (m.id_topic = t.id_topic)
INNER JOIN {db_prefix}messages AS mf ON (t.id_first_msg = mf.id_msg)
INNER JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board)
LEFT JOIN {db_prefix}members AS mem ON (m.id_member = mem.id_member)
LEFT JOIN {db_prefix}members AS memf ON (mf.id_member = memf.id_member)
Find: Select
);
$data = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
{
// If any control characters slipped in somehow, kill the evil things
$row = filter_var($row, FILTER_CALLBACK, array('options' => 'cleanXml'));

// Limit the length of the message, if the option is set.
if (!empty($modSettings['xmlnews_maxlen']) && $smcFunc['strlen'](str_replace('<br>', "\n", $row['body'])) > $modSettings['xmlnews_maxlen'])
$row['body'] = strtr($smcFunc['substr'](str_replace('<br>', "\n", $row['body']), 0, $modSettings['xmlnews_maxlen'] - 3), array("\n" => '<br>')) . '...';

$row['body'] = parse_bbc($row['body'], $row['smileys_enabled'], $row['id_msg']);

censorText($row['body']);
censorText($row['subject']);

// Do we want to include any attachments?
if (!empty($modSettings['attachmentEnable']) && !empty($modSettings['xmlnews_attachments']) && allowedTo('view_attachments', $row['id_board']))
{
$attach_request = $smcFunc['db_query']('', '
SELECT
a.id_attach, a.filename, COALESCE(a.size, 0) AS filesize, a.mime_type, a.downloads, a.approved, m.id_topic AS topic
FROM {db_prefix}attachments AS a
LEFT JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
Replace With: Select
);
$data = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
{
// If any control characters slipped in somehow, kill the evil things
$row = filter_var($row, FILTER_CALLBACK, array('options' => 'cleanXml'));

// Limit the length of the message, if the option is set.
if (!empty($modSettings['xmlnews_maxlen']) && $smcFunc['strlen'](str_replace('<br>', "\n", $row['body'])) > $modSettings['xmlnews_maxlen'])
$row['body'] = strtr($smcFunc['substr'](str_replace('<br>', "\n", $row['body']), 0, $modSettings['xmlnews_maxlen'] - 3), array("\n" => '<br>')) . '...';

$row['body'] = parse_bbc($row['body'], $row['smileys_enabled'], $row['id_msg']);

censorText($row['body']);
censorText($row['subject']);

// Do we want to include any attachments?
if (!empty($modSettings['attachmentEnable']) && !empty($modSettings['xmlnews_attachments']) && allowedTo('view_attachments', $row['id_board']))
{
$attach_request = $smcFunc['db_query']('', '
SELECT
a.id_attach, a.filename, COALESCE(a.size, 0) AS filesize, a.mime_type, a.downloads, a.approved, m.id_topic AS topic
FROM {db_prefix}attachments AS a
LEFT JOIN {db_prefix}messages AS m ON (a.id_msg = m.id_msg)
Find: Select
if (!empty($modSettings['attachmentEnable']) && !empty($modSettings['xmlnews_attachments']))
{
$attach_request = $smcFunc['db_query']('', '
SELECT
a.id_attach, a.filename, COALESCE(a.size, 0) AS filesize, a.mime_type, a.downloads, a.approved, m.id_topic AS topic
FROM {db_prefix}attachments AS a
LEFT JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
Replace With: Select
if (!empty($modSettings['attachmentEnable']) && !empty($modSettings['xmlnews_attachments']))
{
$attach_request = $smcFunc['db_query']('', '
SELECT
a.id_attach, a.filename, COALESCE(a.size, 0) AS filesize, a.mime_type, a.downloads, a.approved, m.id_topic AS topic
FROM {db_prefix}attachments AS a
LEFT JOIN {db_prefix}messages AS m ON (a.id_msg = m.id_msg)
Find: Select
$select_id_members_to = $smcFunc['db_title'] === POSTGRE_TITLE ? "string_agg(pmr.id_member::text, ',')" : 'GROUP_CONCAT(pmr.id_member)';

$select_to_names = $smcFunc['db_title'] === POSTGRE_TITLE ? "string_agg(COALESCE(mem.real_name, mem.member_name), ',')" : 'GROUP_CONCAT(COALESCE(mem.real_name, mem.member_name))';
Replace With: Select
// Use a private-use Unicode character to separate member names.
// This ensures that the separator will not occur in the names themselves.
$separator = "\xEE\x88\xA0";

$select_id_members_to = $smcFunc['db_title'] === POSTGRE_TITLE ? "string_agg(pmr.id_member::text, ',')" : 'GROUP_CONCAT(pmr.id_member)';

$select_to_names = $smcFunc['db_title'] === POSTGRE_TITLE ? "string_agg(COALESCE(mem.real_name, mem.member_name), '$separator')" : "GROUP_CONCAT(COALESCE(mem.real_name, mem.member_name) SEPARATOR '$separator')";
Find: Select
INNER JOIN {db_prefix}members AS mem ON (mem.id_member = pmr.id_member)
LEFT JOIN {db_prefix}members AS memf ON (memf.id_member = pm2.id_member_from)
Replace With: Select
INNER JOIN {db_prefix}members AS mem ON (pmr.id_member = mem.id_member)
LEFT JOIN {db_prefix}members AS memf ON (pm2.id_member_from = memf.id_member)
Find: Select
) AS nis ON nis.id_pm = pm.id_pm
Replace With: Select
) AS nis ON pm.id_pm = nis.id_pm
Find: Select
// If using our own format, we want both the raw and the parsed content.
$row[$xml_format === 'smf' ? 'body_html' : 'body'] = parse_bbc($row['body']);

$recipients = array_combine(explode(',', $row['id_members_to']), explode(',', $row['to_names']));
Replace With: Select
// If using our own format, we want both the raw and the parsed content.
$row[$xml_format === 'smf' ? 'body_html' : 'body'] = parse_bbc($row['body']);

$recipients = array_combine(explode(',', $row['id_members_to']), explode($separator, $row['to_names']));

./Sources/Packages.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
// Don't fail if a file/directory we're trying to create doesn't exist...
if (isset($action['filename']) && !file_exists($file) && !in_array($action['type'], array('create-dir', 'create-file')))
Replace With: Select
// Don't fail if a file/directory we're trying to create doesn't exist...
if (isset($action['filename']) && !file_exists($file) && !in_array($action['type'], array('create-dir', 'create-file')) && $action['error'] != 'ignore')
Find: Select
// Let the unpacker do the work.... but make sure we handle images properly.
if (in_array(strtolower(strrchr($_REQUEST['file'], '.')), array('.bmp', '.gif', '.jpeg', '.jpg', '.png')))
Replace With: Select
// Let the unpacker do the work.... but make sure we handle images properly.
if (in_array(strtolower(strrchr($_REQUEST['file'], '.')), array('.bmp', '.gif', '.jpeg', '.jpg', '.png', '.webp')))
Find: Select
$context['modification_types'] = array('modification', 'avatar', 'language', 'unknown');
Replace With: Select
$context['modification_types'] = array('modification', 'avatar', 'language', 'unknown', 'smiley');
Find: Select

'unknown' => 1,
Add After: Select

'smiley' => 1,
Find: Select

while ($entry = readdir($dh))
{
Add After: Select

// Bypass directory abbreviations altogether...
if ($entry == '.' || $entry == '..')
continue;
Find: Select
// It's a directory - we're interested one way or another, probably...
elseif ($entry != '.' && $entry != '..')
Replace With: Select
// It's a directory - we're interested one way or another, probably...
else
Find: Select

while ($entry = readdir($dh))
{
Add After: Select

// Bypass directory abbreviations altogether...
if ($entry == '.' || $entry == '..')
continue;

./Sources/PersonalMessage.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
trigger_error($txt['pm_invalid_validation_type'], E_USER_ERROR);
Replace With: Select
throw new \Exception('pm_invalid_validation_type');

./Sources/Post.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
// When was it last modified?
if (!empty($row['modified_time']))
{
$context['last_modified'] = timeformat($row['modified_time']);
$context['last_modified_reason'] = censorText($row['modified_reason']);
$context['last_modified_text'] = sprintf($txt['last_edit_by'], $context['last_modified'], $row['modified_name']) . empty($row['modified_reason']) ? '' : '&nbsp;' . $txt['last_edit_reason'] . ':&nbsp;' . $row['modified_reason'];
Replace With: Select
// When was it last modified?
if (!empty($row['modified_time']))
{
$modified_reason = $row['modified_reason'];
$context['last_modified'] = timeformat($row['modified_time']);
$context['last_modified_reason'] = censorText($row['modified_reason']);
$context['last_modified_name'] = $row['modified_name'];
$context['last_modified_text'] = sprintf($txt['last_edit_by'], $context['last_modified'], $row['modified_name']) . (empty($row['modified_reason']) ? '' : ' ' . sprintf($txt['last_edit_reason'], $row['modified_reason']));
Find: Select

'attachID' => $attachment['id_attach'],
Add After: Select

'href' => $scripturl . '?action=dlattach;attach=' . $attachment['id_attach'],
Find: Select
text_max_size_progress: ' . JavaScriptEscape('{currentRemain} ' . ($modSettings[$type] >= 1024 ? $txt['megabyte'] : $txt['kilobyte']) . ' / {currentTotal} ' . ($modSettings[$type] >= 1024 ? $txt['megabyte'] : $txt['kilobyte'])) . ',
Replace With: Select
text_max_size_progress: ' . JavaScriptEscape('{currentRemain} ' . ($modSettings['attachmentPostLimit'] >= 1024 ? $txt['megabyte'] : $txt['kilobyte']) . ' / {currentTotal} ' . ($modSettings['attachmentPostLimit'] >= 1024 ? $txt['megabyte'] : $txt['kilobyte'])) . ',
Find: Select
'value' => isset($context['last_modified_reason']) ? $context['last_modified_reason'] : '',
),
),
);

// If this message has been edited in the past - display when it was.
if (!empty($context['last_modified_text']))
{
$context['posting_fields']['modified_time'] = array(
'label' => array(
'text' => $txt['modified_time'],
),
'input' => array(
'type' => '',
'html' => !empty($context['last_modified_text']) ? ltrim(preg_replace('~<span[^>]*>[^<]*</span>~u', '', $context['last_modified_text']), ': ') : '',
),
);
}
Replace With: Select
// If same user is editing again, keep the previous edit reason by default.
'value' => isset($modified_reason) && isset($context['last_modified_name']) && $context['last_modified_name'] === $user_info['name'] ? $modified_reason : '',
),
// If message has been edited before, show info about that.
'after' => empty($context['last_modified_text']) ? '' : '<div class="smalltext em">' . $context['last_modified_text'] . '</div>',
),
);
Find: Select
'icon' => isset($_REQUEST['icon']) ? preg_replace('~[\./\\\\*\':"<>]~', '', $_REQUEST['icon']) : null,
'modify_reason' => (isset($_POST['modify_reason']) ? $_POST['modify_reason'] : ''),
Replace With: Select
'icon' => isset($_REQUEST['icon']) ? preg_replace('~[\./\\\\*\':"<>]~', '', $_REQUEST['icon']) : null,
Find: Select
// Only consider marking as editing if they have edited the subject, message or icon.
if ((isset($_POST['subject']) && $_POST['subject'] != $row['subject']) || (isset($_POST['message']) && $_POST['message'] != $row['body']) || (isset($_REQUEST['icon']) && $_REQUEST['icon'] != $row['icon']))
{
// And even then only if the time has passed...
if (time() - $row['poster_time'] > $modSettings['edit_wait_time'] || $user_info['id'] != $row['id_member'])
{
$msgOptions['modify_time'] = time();
$msgOptions['modify_name'] = $user_info['name'];
Replace With: Select
// Only consider marking as editing if they have edited the subject, modify reason, message or icon.
if ((isset($_POST['subject']) && $_POST['subject'] != $row['subject']) || (isset($_POST['message']) && $_POST['message'] != $row['body']) || (isset($_REQUEST['icon']) && $_REQUEST['icon'] != $row['icon']) || (isset($_POST['modify_reason']) && $_POST['modify_reason'] != $row['modified_reason']))
{
// And even then only if the time has passed...
if (time() - $row['poster_time'] > $modSettings['edit_wait_time'] || $user_info['id'] != $row['id_member'])
{
$msgOptions['modify_time'] = time();
$msgOptions['modify_name'] = $user_info['name'];
$msgOptions['modify_reason'] = (isset($_POST['modify_reason']) ? $_POST['modify_reason'] : '');
Find: Select
'reason' => $msgOptions['modify_reason'],
Replace With: Select
'reason' => isset($msgOptions['modify_reason']) ? $msgOptions['modify_reason'] : '',

./Sources/Profile-Actions.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
'body' => $row['body'],
Replace With: Select
// un_htmlspecialchars because this will be passed through JavaScriptEscape()
'body' => un_htmlspecialchars($row['body']),

./Sources/Profile-Export.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.2
Replace With: Select
* @version 2.1.5
Find: Select

include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'src', 'Minify.php')));
Add After: Select

include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'path-converter', 'src', 'ConverterInterface.php')));
include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'path-converter', 'src', 'NoConverter.php')));
Find: Select

'svg' => 'image/svg+xml',
Add After: Select

'webp' => 'image/webp',

./Sources/Profile-Modify.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
Replace With: Select
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg', 'webp');
Find: Select
// We are removing board preferences
elseif (isset($_POST['remove_notify_board']) && !empty($_POST['notify_boards']))
Replace With: Select
// We are removing board preferences
elseif (isset($_POST['remove_notify_boards']) && !empty($_POST['notify_boards']))
Find: Select
$valueReference = un_htmlspecialchars($value);

// Try and avoid some checks. '0' could be a valid non-empty value.
if (empty($value) && !is_numeric($value))
$value = '';

if ($row['mask'] == 'nohtml' && ($valueReference != strip_tags($valueReference) || $value != $smcFunc['htmlspecialchars']($value, ENT_NOQUOTES) || preg_match('/<(.+?)[\s]*\/?[\s]*>/si', $valueReference)))
Replace With: Select
$valueReference = html_entity_decode($value);

// Try and avoid some checks. '0' could be a valid non-empty value.
if (empty($value) && !is_numeric($value))
$value = '';

if ($row['mask'] == 'nohtml' && ($valueReference != strip_tags($valueReference) || $valueReference != htmlspecialchars($valueReference, ENT_NOQUOTES) || preg_match('/<(.+?)[\s]*\/?[\s]*>/si', $valueReference)))
Find: Select
// Make sure it is an image.
if (strcasecmp($extension, 'gif') != 0 && strcasecmp($extension, 'jpg') != 0 && strcasecmp($extension, 'jpeg') != 0 && strcasecmp($extension, 'png') != 0 && strcasecmp($extension, 'bmp') != 0)
Replace With: Select
// Make sure it is an image.
if (strcasecmp($extension, 'gif') != 0 && strcasecmp($extension, 'jpg') != 0 && strcasecmp($extension, 'jpeg') != 0 && strcasecmp($extension, 'png') != 0 && strcasecmp($extension, 'bmp') != 0 && strcasecmp($extension, 'webp') != 0)
Find: Select
WHERE id_alert IN({array_int:toMark})',
array(
Replace With: Select
WHERE id_alert IN({array_int:toMark})
AND id_member = {int:memID}',
array(
'memID' => $memID,
Find: Select
WHERE id_alert IN({array_int:toDelete})',
array(
Replace With: Select
WHERE id_alert IN({array_int:toDelete})
AND id_member = {int:memID}',
array(
'memID' => $memID,
Find: Select
WHERE id_alert IN ({array_int:alerts})',
array(
Replace With: Select
WHERE id_alert IN ({array_int:alerts})
AND id_member = {int:member}',
array(
'member' => $memID,
Find: Select
// Because of the way this stuff works, we want to do this ourselves.
if (isset($_POST['edit_notify_boards']) || isset($_POSt['remove_notify_boards']))
Replace With: Select
// Because of the way this stuff works, we want to do this ourselves.
if (isset($_POST['edit_notify_boards']) || isset($_POST['remove_notify_boards']))
Find: Select
'allow_gravatar' => !empty($modSettings['gravatarEnabled']),
Replace With: Select
'allow_gravatar' => !empty($modSettings['gravatarEnabled']) && allowedTo('profile_gravatar'),
Find: Select
$mime_valid = check_mime_type($profile_vars['avatar'], 'image/', true);
Replace With: Select
$mime_type = get_mime_type($profile_vars['avatar'], true);
$mime_valid = strpos($mime_type, 'image/') === 0;
Find: Select

elseif (empty($mime_valid))
return 'bad_avatar';
Add After: Select

// SVGs are special.
elseif ($mime_type === 'image/svg+xml')
{
$safe = false;

if (($tempfile = @tempnam($uploadDir, 'tmp_')) !== false && ($svg_content = @fetch_web_data($profile_vars['avatar'])) !== false && (file_put_contents($tempfile, $svg_content)) !== false)
{
$safe = checkSvgContents($tempfile);
@unlink($tempfile);
}

if (!$safe)
return 'bad_avatar';
}
Find: Select
$mime_valid = check_mime_type($_FILES['attachment']['tmp_name'], 'image/', true);
$sizes = empty($mime_valid) ? false : @getimagesize($_FILES['attachment']['tmp_name']);

// No size, then it's probably not a valid pic.
if ($sizes === false)
Replace With: Select
$mime_type = get_mime_type($_FILES['attachment']['tmp_name'], true);
$mime_valid = strpos($mime_type, 'image/') === 0;
$sizes = empty($mime_valid) ? false : @getimagesize($_FILES['attachment']['tmp_name']);

// SVGs are special.
if ($mime_type === 'image/svg+xml')
{
if ((checkSvgContents($_FILES['attachment']['tmp_name'])) === false)
{
@unlink($_FILES['attachment']['tmp_name']);
return 'bad_avatar';
}

$extension = 'svg';
$destName = 'avatar_' . $memID . '_' . time() . '.' . $extension;
extract(getSvgSize($_FILES['attachment']['tmp_name']));
$file_hash = '';

removeAttachments(array('id_member' => $memID));

$cur_profile['id_attach'] = $smcFunc['db_insert']('',
'{db_prefix}attachments',
array(
'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'fileext' => 'string', 'size' => 'int',
'width' => 'int', 'height' => 'int', 'mime_type' => 'string', 'id_folder' => 'int',
),
array(
$memID, 1, $destName, $file_hash, $extension, filesize($_FILES['attachment']['tmp_name']),
(int) $width, (int) $height, $mime_type, $id_folder,
),
array('id_attach'),
1
);

$cur_profile['filename'] = $destName;
$cur_profile['attachment_type'] = 1;

$destinationPath = $uploadDir . '/' . $destName;
if (!rename($_FILES['attachment']['tmp_name'], $destinationPath))
{
removeAttachments(array('id_member' => $memID));
fatal_lang_error('attach_timeout', 'critical');
}

smf_chmod($destinationPath, 0644);
}
// No size, then it's probably not a valid pic.
elseif ($sizes === false)
Find: Select
'6' => 'bmp'
);

$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
$mime_type = 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension === 'bmp' ? 'x-ms-bmp' : $extension));
Replace With: Select
'6' => 'bmp',
'18' => 'webp'
);

$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
$mime_type = str_replace('image/bmp', 'image/x-ms-bmp', $mime_type);
Find: Select
// Attempt to chmod it.
smf_chmod($uploadDir . '/' . $destinationPath, 0644);
Replace With: Select
// Attempt to chmod it.
smf_chmod($destinationPath, 0644);

./Sources/Profile-View.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
// Are they hidden?
$context['member']['is_hidden'] = empty($user_profile[$memID]['show_online']);
$context['member']['show_last_login'] = allowedTo('admin_forum') || !$context['member']['is_hidden'];
Replace With: Select
// Are they hidden?
$context['member']['is_hidden'] = empty($user_profile[$memID]['show_online']);
$context['member']['show_last_login'] = allowedTo('moderate_forum') || !$context['member']['is_hidden'];
Find: Select
// Basic sanitation.
$memID = (int) $memID;
$unread = $to_fetch === false;
Replace With: Select
// Basic sanitation.
$memID = (int) $memID;
$unread = $to_fetch === false;
Find: Select
// Did a mod already take care of this one?
if (!empty($alerts[$id_alert]['text']))
continue;

// For developer convenience.
$alert = &$alerts[$id_alert];
Replace With: Select
// For developer convenience.
$alert = &$alerts[$id_alert];

// If we loaded the sender's profile, we may as well use it.
$sender_id = !empty($alert['sender_id']) ? $alert['sender_id'] : 0;
if (isset($user_profile[$sender_id]))
$alert['sender_name'] = $user_profile[$sender_id]['real_name'];

// If requested, include the sender's avatar data.
if ($with_avatar && !empty($senders[$sender_id]))
$alert['sender'] = $senders[$sender_id];

// Did a mod already take care of this one?
if (!empty($alerts[$id_alert]['text']))
continue;
Find: Select
$alert['extra']['user_name'] = $user_profile[$alert['extra']['user_id']]['real_name'];
}

// If we loaded the sender's profile, we may as well use it.
$sender_id = !empty($alert['sender_id']) ? $alert['sender_id'] : 0;
if (isset($user_profile[$sender_id]))
$alert['sender_name'] = $user_profile[$sender_id]['real_name'];

// If requested, include the sender's avatar data.
if ($with_avatar && !empty($senders[$sender_id]))
$alert['sender'] = $senders[$sender_id];
Replace With: Select
$alert['extra']['user_name'] = $user_profile[$alert['extra']['user_id']]['real_name'];
}
Find: Select
'url' => 'https://lacnic.net/cgi-bin/lacnic/whois?query=' . $context['ip'],
),
'ripe' => array(
'name' => $txt['whois_ripe'],
'url' => 'https://apps.db.ripe.net/search/query.html?searchtext=' . $context['ip'],
Replace With: Select
'url' => 'https://query.milacnic.lacnic.net/search?id=' . $context['ip'],
),
'ripe' => array(
'name' => $txt['whois_ripe'],
'url' => 'https://apps.db.ripe.net/db-web-ui/query?searchtext=' . $context['ip'],

./Sources/QueryString.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
// If $scripturl is set to nothing, or the SID is not defined (SSI?) just quit.
if ($scripturl == '' || !defined('SID'))
return $buffer;

// Do nothing if the session is cookied, or they are a crawler - guests are caught by redirectexit(). This doesn't work below PHP 4.3.0, because it makes the output buffer bigger.
// @todo smflib
if (empty($_COOKIE) && SID != '' && !isBrowser('possibly_robot'))
$buffer = preg_replace('/(?<!<link rel="canonical" href=)"' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', '"' . $scripturl . '?' . SID . '&amp;', $buffer);
Replace With: Select
// PHP 8.4 deprecated SID. A better long-term solution is needed, but this works for now.
$sid = defined('SID') ? @constant('SID') : null;

// If $scripturl is set to nothing, or the SID is not defined (SSI?) just quit.
if ($scripturl == '' || !isset($sid))
return $buffer;

// Do nothing if the session is cookied, or they are a crawler - guests are caught by redirectexit(). This doesn't work below PHP 4.3.0, because it makes the output buffer bigger.
// @todo smflib
if (empty($_COOKIE) && $sid != '' && !isBrowser('possibly_robot'))
$buffer = preg_replace('/(?<!<link rel="canonical" href=)"' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote($sid, '/') . ')\\??/', '"' . $scripturl . '?' . $sid . '&amp;', $buffer);
Find: Select
// Let's do something special for session ids!
if (defined('SID') && SID != '')
$buffer = preg_replace_callback(
'~"' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#"]+?)(#[^"]*?)?"~',
function($m)
{
global $scripturl;

return '"' . $scripturl . "/" . strtr("$m[1]", '&;=', '//,') . ".html?" . SID . (isset($m[2]) ? $m[2] : "") . '"';
Replace With: Select
// Let's do something special for session ids!
if (isset($sid) && $sid != '')
$buffer = preg_replace_callback(
'~"' . preg_quote($scripturl, '~') . '\?(?:' . $sid . '(?:;|&|&amp;))((?:board|topic)=[^#"]+?)(#[^"]*?)?"~',
function($m)
{
global $scripturl;

return '"' . $scripturl . "/" . strtr("$m[1]", '&;=', '//,') . ".html?" . $sid . (isset($m[2]) ? $m[2] : "") . '"';

./Sources/ReCaptcha/ReCaptcha.php

Find: Select
public function __construct($secret, RequestMethod $requestMethod = null)
Replace With: Select
public function __construct($secret, ?RequestMethod $requestMethod = null)

./Sources/ReCaptcha/RequestMethod/CurlPost.php

Find: Select
public function __construct(Curl $curl = null, $siteVerifyUrl = null)
Replace With: Select
public function __construct(?Curl $curl = null, $siteVerifyUrl = null)

./Sources/ReCaptcha/RequestMethod/SocketPost.php

Find: Select
public function __construct(Socket $socket = null, $siteVerifyUrl = null)
Replace With: Select
public function __construct(?Socket $socket = null, $siteVerifyUrl = null)

./Sources/Reports.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
$group_clause = '1=1';

// Fetch all the board names.
Replace With: Select
$group_clause = '1=1';

$request = $smcFunc['db_query']('', '
SELECT id_profile, profile_name
FROM {db_prefix}permission_profiles');
$board_perms_names = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
$board_perms_names[$row['id_profile']] = $row['profile_name'];
$smcFunc['db_free_result']($request);
Find: Select
'mod_groups' => array(),
);
$profiles[] = $row['id_profile'];
}
$smcFunc['db_free_result']($request);

// Get the ids of any groups allowed to moderate this board
// Limit it to any boards and/or groups we're looking at
$request = $smcFunc['db_query']('', '
SELECT id_board, id_group
FROM {db_prefix}moderator_groups
WHERE ' . $board_clause . ' AND ' . $group_clause,
array(
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
$boards[$row['id_board']]['mod_groups'][] = $row['id_group'];
}
$smcFunc['db_free_result']($request);

// Get all the possible membergroups, except admin!
Replace With: Select
);
$profiles[] = $row['id_profile'];
}
$smcFunc['db_free_result']($request);
Find: Select
foreach ($boards as $id => $board)
if ($board['profile'] == $row['id_profile'])
$board_permissions[$id][$row['id_group']][$row['permission']] = $row['add_deny'];
Replace With: Select
$board_permissions[$row['id_profile']][$row['id_group']][$row['permission']] = $row['add_deny'];
Find: Select
// Now cycle through the board permissions array... lots to do ;)
foreach ($board_permissions as $board => $groups)
{
// Create the table for this board first.
newTable($boards[$board]['name'], 'x', 'all', 100, 'center', 200, 'left');
Replace With: Select
$board_names = array_reduce(
$boards,
function (array $accumulator, array $board)
{
$accumulator[$board['profile']][] = $board['name'];

return $accumulator;
},
[]
);

// Now cycle through the board permissions array... lots to do ;)
foreach ($board_permissions as $id_profile => $groups)
{
// Create the table for this board first.
newTable($board_perms_names[$id_profile] . ' (' . implode(', ', $board_names[$id_profile]) . ')', 'x', 'all', 100, 'center', 200, 'left');
Find: Select
// Set the data for this group to be the local permission.
$curData[$id_group] = $group_permissions[$ID_PERM];
}
// Is it inherited from Moderator?
elseif (in_array($id_group, $boards[$board]['mod_groups']) && !empty($groups[3]) && isset($groups[3][$ID_PERM]))
{
$curData[$id_group] = $groups[3][$ID_PERM];
}
Replace With: Select
// Set the data for this group to be the local permission.
$curData[$id_group] = $group_permissions[$ID_PERM];
}

./Sources/ScheduledTasks.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
// Now we know how many we're sending, let's send them.
$request = $smcFunc['db_query']('', '
SELECT id_mail, recipient, body, subject, headers, send_html, time_sent, private
FROM {db_prefix}mail_queue
ORDER BY priority ASC, id_mail ASC
LIMIT {int:limit}',
array(
'limit' => $number,
Replace With: Select
// Now we know how many we're sending, let's send them.
$request = $smcFunc['db_query']('', '
SELECT id_mail, recipient, body, subject, headers, send_html, time_sent, private, priority
FROM {db_prefix}mail_queue
ORDER BY priority ASC, id_mail ASC
LIMIT {int:limit}',
array(
'limit' => ceil($number * 0.7),
Find: Select
);
}
$smcFunc['db_free_result']($request);
Replace With: Select
'priority' => $row['priority'],
);
}
$smcFunc['db_free_result']($request);

// Random emails from the queue..
if (!empty($ids)) {
$request = $smcFunc['db_query']('', '
SELECT id_mail, recipient, body, subject, headers, send_html, time_sent, private, priority
FROM {db_prefix}mail_queue
WHERE id_mail NOT IN ({array_int:ids})
ORDER BY RAND()
LIMIT {int:limit}',
array(
'ids' => $ids,
'limit' => ceil($number * 0.3),
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
// We want to delete these from the database ASAP, so just get the data and go.
$ids[] = $row['id_mail'];
$emails[] = array(
'to' => $row['recipient'],
'body' => $row['body'],
'subject' => $row['subject'],
'headers' => $row['headers'],
'send_html' => $row['send_html'],
'time_sent' => $row['time_sent'],
'private' => $row['private'],
'priority' => $row['priority'],
);
}
$smcFunc['db_free_result']($request);
}
Find: Select
// Send each email, yea!
$failed_emails = array();
foreach ($emails as $email)
{
Replace With: Select
// Send each email, yea!
$failed_emails = array();
$max_priority = 127;
$smtp_expire = 259200;
$priority_offset = 8;
foreach ($emails as $email)
{
// First, figure out when the next send attempt should happen based on the current priority.
$next_send_time = $email['time_sent'];
for ($i = 0; $i < $email['priority']; $i++) {
$next_send_time += 20 * max(0, $email['priority'] - $priority_offset);
}

// If the email is too old, discard it.
if ($next_send_time > $email['time_sent'] + $smtp_expire) {
continue;
}

$email['priority'] = max($priority_offset, $email['priority'], min(ceil((time() - $email['time_sent']) / $smtp_expire * ($max_priority - $priority_offset)) + $priority_offset, $max_priority));

// Don't send if it's too soon. Also, if we've already failed a few times, only send on every fourth attempt so that we don't DOS some poor mail server.
if (time() < $next_send_time || ($email['priority'] >= $priority_offset && $email['priority'] % 4 !== 0)) {
if ($email['priority'] < $max_priority) {
$failed_emails[] = array($email['to'], $email['body'], $email['subject'], $email['headers'], $email['send_html'], $email['time_sent'], $email['private'], $email['priority']);
}

continue;
}

// Try to send.
Find: Select
// Hopefully it sent?
if (!$result)
$failed_emails[] = array($email['to'], $email['body'], $email['subject'], $email['headers'], $email['send_html'], $email['time_sent'], $email['private']);
Replace With: Select
// If we failed, and we haven't already hit the limit, schedule this for another attempt.
if (empty($result) && $email['priority'] < $max_priority) {
$failed_emails[] = array($email['to'], $email['body'], $email['subject'], $email['headers'], $email['send_html'], $email['time_sent'], $email['private'], $email['priority']);
}
Find: Select
// Add our email back to the queue, manually.
$smcFunc['db_insert']('insert',
'{db_prefix}mail_queue',
array('recipient' => 'string', 'body' => 'string', 'subject' => 'string', 'headers' => 'string', 'send_html' => 'string', 'time_sent' => 'string', 'private' => 'int'),
Replace With: Select
// Add our email back to the queue, manually.
$smcFunc['db_insert']('insert',
'{db_prefix}mail_queue',
array('recipient' => 'string', 'body' => 'string', 'subject' => 'string', 'headers' => 'string', 'send_html' => 'string', 'time_sent' => 'string', 'private' => 'int', 'priority' => 'int'),
Find: Select

array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
);
Add After: Select

// Ensure Unicode data files are up to date
$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
array('$sourcedir/tasks/UpdateUnicode.php', 'Update_Unicode', '', 0), array()
);
Find: Select
// Send the actual email.
if ($notifyPrefs[$row['id_member']] & 0x02)
sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, 'paid_sub_remind', $emaildata['is_html'], 2);

if ($notifyPrefs[$row['id_member']] & 0x01)
Replace With: Select
// Check notification prefs.
$subs_notify = isset($notifyPrefs[$row['id_member']]['paidsubs_expiring']) ? $notifyPrefs[$row['id_member']]['paidsubs_expiring'] : 0;

// Send the actual email.
if ($subs_notify & 0x02)
sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, 'paid_sub_remind', $emaildata['is_html'], 2);

if ($subs_notify & 0x01)

./Sources/Search.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select

'relevance' => '0',
Add After: Select

'id_topic' => 't.id_topic',
Find: Select
if (empty($search_params['topic']) && empty($search_params['show_complete']))
{
$main_query['select']['id_topic'] = 't.id_topic';
Replace With: Select
if (empty($search_params['topic']) && empty($search_params['show_complete']))
{
Find: Select
$main_query['group_by'][] = 't.id_topic';
}
else
{
// This is outrageous!
$main_query['select']['id_topic'] = 'm.id_msg AS id_topic';
Replace With: Select
$main_query['group_by'][] = 't.id_topic';
}
else
{
Find: Select
// *** Retrieve the results to be shown on the page
$participants = array();
$request = $smcFunc['db_search_query']('', '
SELECT ' . (empty($search_params['topic']) ? 'lsr.id_topic' : $search_params['topic'] . ' AS id_topic') . ', lsr.id_msg, lsr.relevance, lsr.num_matches
Replace With: Select
// *** Retrieve the results to be shown on the page
$participants = array();
$request = $smcFunc['db_search_query']('', '
SELECT lsr.id_topic, lsr.id_msg, lsr.relevance, lsr.num_matches

./Sources/SearchAPI-Custom.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
foreach ($query_params['excluded_phrases'] as $phrase)
{
$query_where[] = 'subject NOT ' . $query_match_type . ' {string:exclude_subject_words_' . $count . '}';

if ($is_search_regex)
$query_params['exclude_subject_words_' . $count++] = $word_boundary_wrapper($escape_sql_regex($excludedWord));
else
$query_params['exclude_subject_words_' . $count++] = '%' . $smcFunc['db_escape_wildcard_string']($excludedWord) . '%';
Replace With: Select
foreach ($query_params['excluded_phrases'] as $phrase)
{
$query_where[] = 'subject NOT ' . $query_match_type . ' {string:exclude_subject_words_' . $count . '}';

if ($is_search_regex)
$query_params['exclude_subject_words_' . $count++] = $word_boundary_wrapper($escape_sql_regex($phrase));
else
$query_params['exclude_subject_words_' . $count++] = '%' . $smcFunc['db_escape_wildcard_string']($phrase) . '%';

./Sources/Security.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
// We should never get to this point, but if we did we wouldn't know the user isn't a guest.
trigger_error('No direct access...', E_USER_ERROR);
Replace With: Select
// We should never get to this point, but if we did we wouldn't know the user isn't a guest.
die('No direct access...');
Find: Select
// If we get here, something's gone wrong.... but let's try anyway.
trigger_error('No direct access...', E_USER_ERROR);
Replace With: Select
// If we get here, something's gone wrong.... but let's try anyway.
die('No direct access...');
Find: Select
// We really should never fall through here, for very important reasons. Let's make sure.
trigger_error('No direct access...', E_USER_ERROR);
Replace With: Select
// We really should never fall through here, for very important reasons. Let's make sure.
die('No direct access...');
Find: Select
// Getting this far is a really big problem, but let's try our best to prevent any cases...
trigger_error('No direct access...', E_USER_ERROR);
Replace With: Select
// Getting this far is a really big problem, but let's try our best to prevent any cases...
die('No direct access...');
Find: Select
($check_access ? ' AND {query_see_board}' : ''),
Replace With: Select
($check_access ? ' AND {query_see_board}' : '') . '
ORDER BY b.board_order',

./Sources/Session.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
global $modSettings, $boardurl, $sc, $smcFunc, $cache_enable;
Replace With: Select
global $context, $modSettings, $boardurl, $sc, $smcFunc, $cache_enable;
Find: Select
session_set_save_handler('sessionOpen', 'sessionClose', 'sessionRead', 'sessionWrite', 'sessionDestroy', 'sessionGC');
Replace With: Select

$context['session_handler'] = new SmfSessionHandler();
session_set_save_handler($context['session_handler'], true);
Find: Select
* Implementation of sessionOpen() replacing the standard open handler.
* It simply returns true.
*
* @param string $save_path The path to save the session to
* @param string $session_name The name of the session
* @return boolean Always returns true
*/
function sessionOpen($save_path, $session_name)
{
return true;
}

/**
* Implementation of sessionClose() replacing the standard close handler.
* It simply returns true.
*
* @return boolean Always returns true
*/
function sessionClose()
{
return true;
}

/**
* Implementation of sessionRead() replacing the standard read handler.
*
* @param string $session_id The session ID
* @return string The session data
*/
function sessionRead($session_id)
{
global $smcFunc;

if (preg_match('~^[A-Za-z0-9,-]{16,64}$~', $session_id) == 0)
return '';

// Look for it in the database.
$result = $smcFunc['db_query']('', '
SELECT data
FROM {db_prefix}sessions
WHERE session_id = {string:session_id}
LIMIT 1',
array(
'session_id' => $session_id,
)
);
list ($sess_data) = $smcFunc['db_fetch_row']($result);
$smcFunc['db_free_result']($result);

return $sess_data != null ? $sess_data : '';
}

/**
* Implementation of sessionWrite() replacing the standard write handler.
*
* @param string $session_id The session ID
* @param string $data The data to write to the session
* @return boolean Whether the info was successfully written
*/
function sessionWrite($session_id, $data)
{
global $smcFunc, $db_connection, $db_server, $db_name, $db_user, $db_passwd;
global $db_prefix, $db_persist, $db_port, $db_mb4;

if (preg_match('~^[A-Za-z0-9,-]{16,64}$~', $session_id) == 0)
return false;

// php < 7.0 need this
if (empty($db_connection))
{
$db_options = array();

// Add in the port if needed
if (!empty($db_port))
$db_options['port'] = $db_port;

if (!empty($db_mb4))
$db_options['db_mb4'] = $db_mb4;

$options = array_merge($db_options, array('persist' => $db_persist, 'dont_select_db' => SMF == 'SSI'));

$db_connection = smf_db_initiate($db_server, $db_name, $db_user, $db_passwd, $db_prefix, $options);
}

// If an insert fails due to a dupe, replace the existing session...
$session_update = $smcFunc['db_insert']('replace',
'{db_prefix}sessions',
array('session_id' => 'string', 'data' => 'string', 'last_update' => 'int'),
array($session_id, $data, time()),
array('session_id')
);

return ($smcFunc['db_affected_rows']() == 0 ? false : true);
}

/**
* Implementation of sessionDestroy() replacing the standard destroy handler.
*
* @param string $session_id The session ID
* @return boolean Whether the session was successfully destroyed
*/
function sessionDestroy($session_id)
{
global $smcFunc;

if (preg_match('~^[A-Za-z0-9,-]{16,64}$~', $session_id) == 0)
return false;

// Just delete the row...
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}sessions
WHERE session_id = {string:session_id}',
array(
'session_id' => $session_id,
)
);

return true;
}

/**
* Implementation of sessionGC() replacing the standard gc handler.
* Callback for garbage collection.
*
* @param int $max_lifetime The maximum lifetime (in seconds) - prevents deleting of sessions older than this
* @return boolean Whether the option was successful
*/
function sessionGC($max_lifetime)
{
global $modSettings, $smcFunc;

// Just set to the default or lower? Ignore it for a higher value. (hopefully)
if (!empty($modSettings['databaseSession_lifetime']) && ($max_lifetime <= 1440 || $modSettings['databaseSession_lifetime'] > $max_lifetime))
$max_lifetime = max($modSettings['databaseSession_lifetime'], 60);

// Clean up after yerself ;).
$session_update = $smcFunc['db_query']('', '
DELETE FROM {db_prefix}sessions
WHERE last_update < {int:last_update}',
array(
'last_update' => time() - $max_lifetime,
)
);

return ($smcFunc['db_affected_rows']() == 0 ? false : true);
Replace With: Select
* Class SmfSessionHandler
*
* An implementation of the SessionHandler
*
* Note: To support PHP 8.x, we use the attribute ReturnTypeWillChange. When
* 8.1 is the miniumn, this can be removed.
*
* Note: To support PHP 7.x, we do not use type hints as SessionHandlerInterface
* does not have them.
*/
class SmfSessionHandler extends SessionHandler implements SessionHandlerInterface, SessionIdInterface
{
/**
* Implementation of SessionHandler::open() replacing the standard open handler.
* It simply returns true.
*
* @param string $path The path to save the session to
* @param string $name The name of the session
* @return boolean Always returns true
*/
function open(/*PHP 8.0 string*/$path, /*PHP 8.0 string*/$name): bool
{
return true;
}

/**
* Implementation of SessionHandler::close() replacing the standard close handler.
* It simply returns true.
*
* @return boolean Always returns true
*/
public function close(): bool
{
return true;
}

/**
* Implementation of SessionHandler::read() replacing the standard read handler.
*
* @param string $id The session ID
* @return string The session data
*/
#[\ReturnTypeWillChange]
public function read(/*PHP 8.0 string*/$id)/*PHP 8.0: string|false*/
{
global $smcFunc;

if (!$this->isValidSessionID($id))
return '';

// Look for it in the database.
$result = $smcFunc['db_query']('', '
SELECT data
FROM {db_prefix}sessions
WHERE session_id = {string:session_id}
LIMIT 1',
array(
'session_id' => $id,
)
);
list ($sess_data) = $smcFunc['db_fetch_row']($result);
$smcFunc['db_free_result']($result);

return $sess_data != null ? $sess_data : '';
}

/**
* Implementation of SessionHandler::write() replacing the standard write handler.
*
* @param string $id The session ID
* @param string $data The data to write to the session
* @return boolean Whether the info was successfully written
*/
#[\ReturnTypeWillChange]
public function write(/*PHP 8.0 string*/$id,/*PHP 8.0 string */ $data): bool
{
global $smcFunc;

if (!$this->isValidSessionID($id))
return false;

// If an insert fails due to a dupe, replace the existing session...
$session_update = $smcFunc['db_insert']('replace',
'{db_prefix}sessions',
array('session_id' => 'string', 'data' => 'string', 'last_update' => 'int'),
array($id, $data, time()),
array('session_id')
);

return ($smcFunc['db_affected_rows']() == 0 ? false : true);
}

/**
* Implementation of SessionHandler::destroy() replacing the standard destroy handler.
*
* @param string $session_id The session ID
* @return boolean Whether the session was successfully destroyed
*/
public function destroy(/*PHP 8.0 string*/$id): bool
{
global $smcFunc;

if (!$this->isValidSessionID($id))
return false;

// Just delete the row...
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}sessions
WHERE session_id = {string:session_id}',
array(
'session_id' => $id,
)
);

return true;
}

/**
* Implementation of SessionHandler::GC() replacing the standard gc handler.
* Callback for garbage collection.
*
* @param int $max_lifetime The maximum lifetime (in seconds) - prevents deleting of sessions older than this
* @return boolean Whether the option was successful
*/
#[\ReturnTypeWillChange]
public function gc(/*PHP 8.0 int*/$max_lifetime)/*PHP 8.1 : int|false*/
{
global $modSettings, $smcFunc;

// Just set to the default or lower? Ignore it for a higher value. (hopefully)
if (!empty($modSettings['databaseSession_lifetime']) && ($max_lifetime <= 1440 || $modSettings['databaseSession_lifetime'] > $max_lifetime))
$max_lifetime = max($modSettings['databaseSession_lifetime'], 60);

// Clean up after yerself ;).
$session_update = $smcFunc['db_query']('', '
DELETE FROM {db_prefix}sessions
WHERE last_update < {int:last_update}',
array(
'last_update' => time() - $max_lifetime,
)
);

return $smcFunc['db_affected_rows']();
}

/**
* Validates a given string conforms to our testing for a valid session id.
*
* @param string $id The session ID
* @return boolean Whether the string is valid format or not
*/
private function isValidSessionID(string $id): bool
{
return preg_match('~^[A-Za-z0-9,-]{16,64}$~', $id) === 1;
}

./Sources/ShowAttachments.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select

$file['etag'] = $file['exists'] ? sha1_file($file['filePath']) : '';
Add After: Select

$file['filename'] = un_htmlspecialchars($file['filename']);
Find: Select
// Update the download counter (unless it's a thumbnail or resuming an incomplete download).
if ($file['attachment_type'] != 3 && empty($showThumb) && $range === 0 && empty($context['skip_downloads_increment']))
Replace With: Select
// Update the download counter (unless it's a thumbnail or resuming an incomplete download).
if ($file['attachment_type'] != 3 && empty($showThumb) && empty($_REQUEST['preview']) && $range === 0 && empty($context['skip_downloads_increment']))
Find: Select
// Does this have a mime type?
elseif (!empty($file['mime_type']) && (isset($_REQUEST['image']) || !in_array($file['fileext'], array('jpg', 'gif', 'jpeg', 'x-ms-bmp', 'png', 'psd', 'tiff', 'iff'))))
Replace With: Select
// Does this have a mime type?
elseif (!empty($file['mime_type']) && (isset($_REQUEST['image']) || !in_array($file['fileext'], array('jpg', 'gif', 'jpeg', 'x-ms-bmp', 'png', 'psd', 'tiff', 'iff', 'webp'))))
Find: Select
// On mobile devices, audio and video should be served inline so the browser can play them.
if (isset($_REQUEST['image']) || (isBrowser('is_mobile') && (strpos($file['mime_type'], 'audio/') !== 0 || strpos($file['mime_type'], 'video/') !== 0)))
Replace With: Select
// On mobile devices, audio and video should be served inline so the browser can play them.
if (isset($_REQUEST['image']) || (isBrowser('is_mobile') && (strpos($file['mime_type'], 'audio/') === 0 || strpos($file['mime_type'], 'video/') === 0)))
Find: Select
// If this has an "image extension" - but isn't actually an image - then ensure it isn't cached cause of silly IE.
if (!isset($_REQUEST['image']) && in_array($file['fileext'], array('gif', 'jpg', 'bmp', 'png', 'jpeg', 'tiff')))
Replace With: Select
// If this has an "image extension" - but isn't actually an image - then ensure it isn't cached cause of silly IE.
if (!isset($_REQUEST['image']) && in_array($file['fileext'], array('gif', 'jpg', 'bmp', 'png', 'jpeg', 'tiff', 'webp')))

./Sources/SplitTopics.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
// Replace tokens with links in the reason.
$reason_replacements = array(
$txt['movetopic_auto_topic'] => '[iurl="' . $scripturl . '?topic=' . $id_topic . '.0"]' . $target_subject . '[/iurl]',
Replace With: Select
// Replace tokens with links in the reason.
$reason_replacements = array(
$txt['movetopic_auto_topic'] => '[iurl=&quot;' . $scripturl . '?topic=' . $id_topic . '.0&quot;]' . $target_subject . '[/iurl]',
Find: Select
// Make sure we catch both languages in the reason.
$reason_replacements += array(
$txt['movetopic_auto_topic'] => '[iurl="' . $scripturl . '?topic=' . $id_topic . '.0"]' . $target_subject . '[/iurl]',
Replace With: Select
// Make sure we catch both languages in the reason.
$reason_replacements += array(
$txt['movetopic_auto_topic'] => '[iurl=&quot;' . $scripturl . '?topic=' . $id_topic . '.0&quot;]' . $target_subject . '[/iurl]',

./Sources/Subs-Admin.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.2
Replace With: Select
* @version 2.1.5
Find: Select
// Back up the backup, just in case.
if (file_exists($backup_file))
Replace With: Select
// Back up the backup, just in case.
if (!empty($backup_file) && file_exists($backup_file))
Find: Select
// We're done with these.
@unlink($temp_sfile);
@unlink($temp_bfile);
Replace With: Select
// We're done with these.
@unlink($temp_sfile);

if (!empty($temp_bfile))
@unlink($temp_bfile);

./Sources/Subs-Attachments.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select

$_SESSION['temp_attachments'][$attachID]['type'] = 'image/' . $context['valid_image_types'][$size[2]];
}
}
Add After: Select

// SVGs have their own set of security checks.
elseif ($_SESSION['temp_attachments'][$attachID]['type'] === 'image/svg+xml')
{
require_once($sourcedir . '/Subs-Graphics.php');
if (!checkSvgContents($_SESSION['temp_attachments'][$attachID]['tmp_name']))
{
$_SESSION['temp_attachments'][$attachID]['errors'][] = 'bad_attachment';
return false;
}
}
Find: Select

list ($attachmentOptions['width'], $attachmentOptions['height']) = $size;
Add After: Select

if (!empty($attachmentOptions['mime_type']) && $attachmentOptions['mime_type'] === 'image/svg+xml')
{
foreach (getSvgSize($attachmentOptions['tmp_name']) as $key => $value)
$attachmentOptions[$key] = $value === INF ? 0 : $value;
}
Find: Select
'href' => $scripturl . '?action=dlattach;attach=' . $attachment['id_thumb'] . ';image',
Replace With: Select
'href' => $scripturl . '?action=dlattach;attach=' . $attachment['id_thumb'] . ';image;thumb',
Find: Select

$attachmentData[$i]['downloads']++;
Add After: Select


// Describe undefined dimensions as "unknown".
// This can happen if an uploaded SVG is missing some key data.
foreach (array('real_width', 'real_height') as $key)
{
if (!isset($attachmentData[$i][$key]) || $attachmentData[$i][$key] === INF)
{
loadLanguage('Admin');
$attachmentData[$i][$key] = ' (' . $txt['unknown'] . ') ';
}
}
Find: Select
global $context, $modSettings, $smcFunc;
Replace With: Select
global $context, $modSettings, $smcFunc, $sourcedir;
Find: Select
// Where clause - there may or may not be msg ids, & may or may not be attachs to preview,
// depending on post vs edit, inserted or not, preview or not, post error or not, etc.
Replace With: Select
// Where clause - there may or may not be msg ids, & may or may not be attachs to preview,
// depending on post vs edit, inserted or not, preview or not, post error or not, etc.
Find: Select

foreach ($rows as $row)
{
Add After: Select

// SVGs are special.
if ($row['mime_type'] === 'image/svg+xml')
{
if (empty($row['width']) || empty($row['height']))
{
require_once($sourcedir . '/Subs-Graphics.php');

$row = array_merge($row, getSvgSize(getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'])));
}

// SVG is its own thumbnail.
if (isset($row['id_thumb']))
{
$row['id_thumb'] = $row['id_attach'];

// For SVGs, we don't need to calculate thumbnail size precisely.
$row['thumb_width'] = min($row['width'], !empty($modSettings['attachmentThumbWidth']) ? $modSettings['attachmentThumbWidth'] : 1000);
$row['thumb_height'] = min($row['height'], !empty($modSettings['attachmentThumbHeight']) ? $modSettings['attachmentThumbHeight'] : 1000);

// Must set the thumbnail's CSS dimensions manually.
addInlineCss('img#thumb_' . $row['id_thumb'] . ':not(.original_size) {width: ' . $row['thumb_width'] . 'px; height: ' . $row['thumb_height'] . 'px;}');
}
}

./Sources/Subs-Auth.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
// We MUST exit at this point, because otherwise we CANNOT KNOW that the user is privileged.
trigger_error('No direct access...', E_USER_ERROR);
Replace With: Select
// We MUST exit at this point, because otherwise we CANNOT KNOW that the user is privileged.
die('No direct access...');
Find: Select
'httponly' => $httponly,
Replace With: Select
'httponly' => $httponly,

./Sources/Subs-Boards.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
trigger_error(sprintf($txt['modify_board_incorrect_move_to'], $boardOptions['move_to']), E_USER_ERROR);
Replace With: Select
throw new \Exception(sprintf($txt['modify_board_incorrect_move_to'], $boardOptions['move_to']));
Find: Select
// Before we add new access_groups or deny_groups, remove all of the old entries
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}board_permissions_view
WHERE id_board = {int:selected_board}',
array(
'selected_board' => $board_id,
)
);
Replace With: Select
// Before we add new access_groups or deny_groups, remove all of the old entries
if (isset($boardOptions['access_groups']) || isset($boardOptions['deny_groups']))
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}board_permissions_view
WHERE id_board = {int:selected_board}',
array(
'selected_board' => $board_id,
)
);
Find: Select
trigger_error($txt['create_board_missing_options'], E_USER_ERROR);
}

if (in_array($boardOptions['move_to'], array('child', 'before', 'after')) && !isset($boardOptions['target_board']))
{
loadLanguage('Errors');
trigger_error($txt['move_board_no_target'], E_USER_ERROR);
Replace With: Select
throw new \Exception('create_board_missing_options');
}

if (in_array($boardOptions['move_to'], array('child', 'before', 'after')) && !isset($boardOptions['target_board']))
{
loadLanguage('Errors');
throw new \Exception('move_board_no_target');

./Sources/Subs-Calendar.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.2
Replace With: Select
* @version 2.1.5
Find: Select
$cal_date = ($start_object >= $low_object) ? $start_object : $low_object;
Replace With: Select
$cal_date = ($start_object >= $low_object) ? (clone $start_object) : (clone $low_object);
Find: Select
'allowed_groups' => explode(',', $row['member_groups']),
Replace With: Select
'allowed_groups' => isset($row['member_groups']) ? explode(',', $row['member_groups']) : array(),
Find: Select

hr: "' . $txt['hour_short'] . '",
Add After: Select

hrs: "' . $txt['hours_short'] . '",
Find: Select
// Find all possible variants of AM and PM for this language.
$replacements[strtolower($txt['time_am'])] = 'AM';
$replacements[strtolower($txt['time_pm'])] = 'PM';
Replace With: Select
// Find all possible variants of AM and PM for this language.
if (trim($txt['time_am']) !== '' && trim($txt['time_pm']) !== '')
{
$replacements[strtolower($txt['time_am'])] = 'AM';
$replacements[strtolower($txt['time_pm'])] = 'PM';
}

./Sources/Subs-Categories.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
trigger_error($txt['create_category_no_name'], E_USER_ERROR);
Replace With: Select
throw new \Exception('create_category_no_name');
Find: Select
trigger_error($txt['cannot_move_to_deleted_category'], E_USER_ERROR);
Replace With: Select
throw new \Exception('cannot_move_to_deleted_category');

./Sources/Subs-Charset.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/

if (!defined('SMF'))
die('No direct access...');

require_once($sourcedir . '/Unicode/Metadata.php');
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.5
*/

if (!defined('SMF'))
die('No direct access...');

// If this file is missing, we're using an old version of Unicode.
if (!@include_once($sourcedir . '/Unicode/Metadata.php'))
define('SMF_UNICODE_VERSION', '14.0.0.0');
Find: Select
// Soft Hyphen.
'\x{AD}',
// Khmer Vowel Inherent AQ and Khmer Vowel Inherent AA.
// Unicode Standard ch. 16 says: "they are insufficient for [their]
// purpose and should be considered errors in the encoding."
'\x{17B4}-\x{17B5}',
Replace With: Select
// Soft Hyphen.
'\x{AD}',

./Sources/Subs-Db-mysql.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
'db_error' => 'mysqli_error',
Replace With: Select
'db_error' => 'smf_db_errormsg',
Find: Select
return sprintf('\'%1$s\'', mysqli_real_escape_string($connection, $replacement));
Replace With: Select
return sprintf('\'%1$s\'', mysqli_real_escape_string($connection, isset($smcFunc['fix_utf8mb4']) ? $smcFunc['fix_utf8mb4']($replacement) : $replacement));
Find: Select
$replacement[$key] = sprintf('\'%1$s\'', mysqli_real_escape_string($connection, $value));
Replace With: Select
$replacement[$key] = sprintf('\'%1$s\'', mysqli_real_escape_string($connection, isset($smcFunc['fix_utf8mb4']) ? $smcFunc['fix_utf8mb4']($value) : $value));
Find: Select
// the inserted value already exists we need to find the pk
else
{
$where_string = '';
$count2 = count($keys);
for ($x = 0; $x < $count2; $x++)
{
$keyPos = array_search($keys[$x], array_keys($columns));
$where_string .= $keys[$x] . ' = ' . $data[$i][$keyPos];
if (($x + 1) < $count2)
$where_string .= ' AND ';
}

$request = $smcFunc['db_query']('', '
SELECT `' . $keys[0] . '` FROM ' . $table . '
WHERE ' . $where_string . ' LIMIT 1',
array()
);

if ($request !== false && $smcFunc['db_num_rows']($request) == 1)
{
$row = $smcFunc['db_fetch_assoc']($request);
$ai = $row[$keys[0]];
Replace With: Select
// the inserted value already exists we need to find the pk
else
{
$where_string = [];

foreach ($columns as $column_name => $type)
{
if (strpos($type, 'string-') !== false)
$where_string[] = $column_name . ' = ' . sprintf('SUBSTRING({string:%1$s}, 1, ' . substr($type, 7) . ')', $column_name);
else
$where_string[] = $column_name . ' = ' . sprintf('{%1$s:%2$s}', $type, $column_name);
}

$where_string = implode(' AND ', $where_string);

$request = $smcFunc['db_query']('', '
SELECT ' . $keys[0] . '
FROM ' . $table . '
WHERE ' . $where_string . '
LIMIT 1',
array_combine($indexed_columns, $data[$i])
);

if ($request !== false && $smcFunc['db_num_rows']($request) == 1)
{
$row = $smcFunc['db_fetch_assoc']($request);
$ai = (int) $row[$keys[0]];
Find (at the end of the file): Select
?>
Add Before: Select

/**
* Wrapper to handle null errors
*
* @param null|mysqli $connection = null The connection to use (null to use $db_connection)
* @return string escaped string
*/
function smf_db_errormsg($connection = null)
{
global $db_connection;

if ($connection === null && $db_connection === null)
return '';

return mysqli_error($connection === null ? $db_connection : $connection);
}

./Sources/Subs-Db-postgresql.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
'db_error' => 'pg_last_error',
Replace With: Select
'db_error' => 'smf_db_errormsg',
Find: Select
trigger_error($txt['postgres_id_not_int'], E_USER_ERROR);
Replace With: Select
throw new \TypeError('postgres_id_not_int');
Find (at the end of the file): Select
?>
Add Before: Select

/**
* Wrapper to handle null errors
*
* @param null|PgSql\Connection $connection = null The connection to use (null to use $db_connection)
* @return string escaped string
*/
function smf_db_errormsg($connection = null)
{
global $db_connection;

if ($connection === null && $db_connection === null)
return '';

return pg_last_error($connection === null ? $db_connection : $connection);
}

./Sources/Subs-Graphics.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.2
Replace With: Select
* @version 2.1.5
Find: Select

fclose($fp);

return true;
}
Add After: Select

/**
* Searches through an SVG file to see if there's potentially harmful content.
*
* @param string $fileName The path to the file.
* @return bool Whether the image appears to be safe.
*/
function checkSvgContents($fileName)
{
$fp = fopen($fileName, 'rb');
if (!$fp)
fatal_lang_error('attach_timeout');

$patterns = array(
// No external or embedded scripts allowed.
'/<(\S*:)?script\b/i',
'/\b(\S:)?href\s*=\s*["\']\s*javascript:/i',

// No SVG event attributes allowed, since they execute scripts.
'/\bon\w+\s*=\s*["\']/',
'/<(\S*:)?set\b[^>]*\battributeName\s*=\s*(["\'])\s*on\w+\\1/i',

// No XML Events allowed, since they execute scripts.
'~\bhttp://www\.w3\.org/2001/xml-events\b~i',

// No data URIs allowed, since they contain arbitrary data.
'/\b(\S*:)?href\s*=\s*["\']\s*data:/i',

// No foreignObjects allowed, since they allow embedded HTML.
'/<(\S*:)?foreignObject\b/i',

// No custom entities allowed, since they can be used for entity
// recursion attacks.
'/<!ENTITY\b/',

// Embedded external images can't have custom cross-origin rules.
'/<\b(\S*:)?image\b[^>]*\bcrossorigin\s*=/',

// No embedded PHP tags allowed.
// Harmless if the SVG is just the src of an img element, but very bad
// if the SVG is embedded inline into the HTML document.
'/<[?](php|=|\s)/i',
);

$prev_chunk = '';
while (!feof($fp))
{
$cur_chunk = fread($fp, 8192);

foreach ($patterns as $pattern)
{
if (preg_match($pattern, $prev_chunk . $cur_chunk))
{
fclose($fp);
return false;
}
}

$prev_chunk = $cur_chunk;
}
fclose($fp);

return true;
}
Find: Select
'6' => 'bmp',
'15' => 'wbmp'
Replace With: Select
'6' => 'bmp',
'15' => 'wbmp',
'18' => 'webp'
Find: Select
'15' => 'wbmp'
Replace With: Select
'15' => 'wbmp',
'18' => 'webp'
Find: Select

$success = imagewbmp($dst_img, $destName);
Add After: Select

elseif (!empty($preferred_format) && ($preferred_format == 18) && function_exists('imagewebp'))
$success = imagewebp($dst_img, $destName);
Find (at the end of the file): Select
?>
Add Before: Select

/**
* Gets the dimensions of an SVG image (specifically, of its viewport).
*
* See https://www.w3.org/TR/SVG11/coords.html#IntrinsicSizing
*
* @param string $filepath The path to the SVG file.
* @return array The width and height of the SVG image in pixels.
*/
function getSvgSize($filepath)
{
if (!preg_match('/<svg\b[^>]*>/', file_get_contents($filepath, false, null, 0, 480), $matches))
{
return array('width' => 0, 'height' => 0);
}

$svg = $matches[0];

// If the SVG has width and height attributes, use those.
// If attribute is missing, SVG spec says the default is '100%'.
// If no unit is supplied, spec says unit defaults to px.
foreach (array('width', 'height') as $dimension)
{
if (preg_match("/\b$dimension\s*=\s*([\"'])\s*([\d.]+)([\D\S]*)\s*\\1/", $svg, $matches))
{
$$dimension = $matches[2];
$unit = !empty($matches[3]) ? $matches[3] : 'px';
}
else
{
$$dimension = 100;
$unit = '%';
}

// Resolve unit.
switch ($unit)
{
// Already pixels, so do nothing.
case 'px':
break;

// Points.
case 'pt':
$$dimension *= 0.75;
break;

// Picas.
case 'pc':
$$dimension *= 16;
break;

// Inches.
case 'in':
$$dimension *= 96;
break;

// Centimetres.
case 'cm':
$$dimension *= 37.8;
break;

// Millimetres.
case 'mm':
$$dimension *= 3.78;
break;

// Font height.
// Assume browser default of 1em = 1pc.
case 'em':
$$dimension *= 16;
break;

// Font x-height.
// Assume half of font height.
case 'ex':
$$dimension *= 8;
break;

// Font '0' character width.
// Assume a typical monospace font at 1em = 1pc.
case 'ch':
$$dimension *= 9.6;
break;

// Percentage.
// SVG spec says to use viewBox dimensions in this case.
default:
unset($$dimension);
break;
}
}

// Width and/or height is missing or a percentage, so try the viewBox attribute.
if ((!isset($width) || !isset($height)) && preg_match('/\bviewBox\s*=\s*(["\'])\s*[\d.]+[,\s]+[\d.]+[,\s]+([\d.]+)[,\s]+([\d.]+)\s*\\1/', $svg, $matches))
{
$vb_width = $matches[2];
$vb_height = $matches[3];

// No dimensions given, so use viewBox dimensions.
if (!isset($width) && !isset($height))
{
$width = $vb_width;
$height = $vb_height;
}
// Width but no height, so calculate height.
elseif (isset($width))
{
$height = $width * $vb_height / $vb_width;
}
// Height but no width, so calculate width.
elseif (isset($height))
{
$width = $height * $vb_width / $vb_height;
}
}

// Viewport undefined, so call it infinite.
if (!isset($width) && !isset($height))
{
$width = INF;
$height = INF;
}

return array('width' => round($width), 'height' => round($height));
}

./Sources/Subs-Membergroups.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
return false;
}
Replace With: Select
return false;
}
Find: Select
trigger_error(sprintf($txt['add_members_to_group_invalid_type'], $type), E_USER_WARNING);
Replace With: Select
throw new \TypeError(sprintf($txt['add_members_to_group_invalid_type'], $type));

./Sources/Subs-Menu.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select

$menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$sa]['disabled'] = true;
}
Add After: Select

// If permissions removed/disabled for all submenu items, remove the menu item
if (empty($first_sa) && empty($last_sa))
{
unset($menu_context['sections'][$section_id]['areas'][$area_id]);
continue;
}

./Sources/Subs-MessageIndex.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.2
Replace With: Select
* @version 2.1.5
Find: Select
trigger_error($txt['get_board_list_cannot_include_and_exclude'], E_USER_ERROR);
Replace With: Select
throw new \Exception('get_board_list_cannot_include_and_exclude');

./Sources/Subs-Package.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
// Not a directory and doesn't exist already...
if (substr($current['filename'], -1, 1) != '/' && $destination !== null && !file_exists($destination . '/' . $current['filename']))
Replace With: Select
// If hunting for a file in subdirectories, pass to subsequent write test...
if ($single_file && $destination !== null && (substr($destination, 0, 2) == '*/'))
$write_this = true;
// Not a directory and doesn't exist already...
elseif (substr($current['filename'], -1, 1) != '/' && $destination !== null && !file_exists($destination . '/' . $current['filename']))
Find: Select
if ($destination !== null && !file_exists($destination) && !$single_file)
Replace With: Select
if ($destination !== null && (substr($destination, 0, 2) != '*/') && !file_exists($destination) && !$single_file)
Find: Select
// If this is a file, and it doesn't exist.... happy days!
if ($is_file)
Replace With: Select
// If hunting for a file in subdirectories, pass to subsequent write test...
if ($single_file && $destination !== null && (substr($destination, 0, 2) == '*/'))
$write_this = true;
// If this is a file, and it doesn't exist.... happy days!
elseif ($is_file)
Find: Select
'description' => $action->fetch('.')
Replace With: Select
'description' => $action->fetch('.'),
'error' => $action->exists('@error') ? $action->fetch('@error') : 'fail'
Find: Select
// The file that was supposed to be deleted couldn't be found.
else
$failure = true;
Replace With: Select
// The file that was supposed to be deleted couldn't be found.
else
$failure = $action['error'] != 'ignore';
Find: Select
trigger_error($txt['parse_path_filename_required'], E_USER_ERROR);
Replace With: Select
throw new \Exception('parse_path_filename_required');

./Sources/Subs-Post.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select

if ($message_id !== null && empty($modSettings['mail_no_message_id']))
$headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . $message_id . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break;
Replace With: Select
$headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . ($message_id ?? 0) . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break;
Find: Select
if (substr($server_response, 0, 3) != $code)
{
log_error($txt['smtp_error'] . $server_response);
Replace With: Select
$response_code = (int) substr($server_response, 0, 3);
if ($response_code != $code)
{
// Ignoreable errors that we can't fix should not be logged.
/*
* 550 - cPanel rejected sending due to DNS issues
* 450 - DNS Routing issues
* 451 - cPanel "Temporary local problem - please try later"
*/
if ($response_code < 500 && !in_array($response_code, array(450, 451)))
log_error($txt['smtp_error'] . $server_response);

./Sources/Subs-Themes.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
unlink($path . '/' . $object);
}
}

reset($objects);
rmdir($path);
Replace With: Select
@unlink($path . '/' . $object);
}
}

reset($objects);
@rmdir($path);
Find: Select
'is_image' => preg_match('~\.(jpg|jpeg|gif|bmp|png)$~', $entry) != 0,
Replace With: Select
'is_image' => preg_match('~\.(jpg|jpeg|gif|bmp|png|webp)$~', $entry) != 0,

./Sources/Subs-Timezones.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select

'America/Monterrey',
Add After: Select

'America/Ciudad_Juarez',
Find: Select
'ts' => strtotime('1995-05-27T17:00:00+0000'),
Replace With: Select
'ts' => strtotime('1995-05-27T16:00:00+0000'),
Find: Select
'ts' => strtotime('2002-04-30T20:00:00+0000'),
Replace With: Select
'ts' => strtotime('2002-04-30T19:00:00+0000'),
Find: Select

'tz' => strtotime('1999-03-27T21:00:00+0000'),
'tzid' => 'Etc/GMT-5'
Replace With: Select

'tz' => strtotime('1999-03-27T21:00:00+0000'),
'tzid' => 'Asia/Aqtau'
Find: Select

'tzid' => 'Asia/Almaty',
),
),
Add After: Select


// Diverged from America/Ojinaga in version 2022g.
'America/Ciudad_Juarez' => array(
array(
'ts' => PHP_INT_MIN,
'tzid' => '',
),
array(
'ts' => strtotime('1922-01-01T07:00:00+0000'),
'tzid' => 'America/Ojinaga',
),
array(
'ts' => strtotime('2022-11-30T06:00:00+0000'),
'tzid' => 'America/Denver',
),
),

./Sources/Subs.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select

static $today;
Add After: Select

$log_time = min(max($log_time, PHP_INT_MIN), PHP_INT_MAX);
Find: Select
function smf_strftime(string $format, int $timestamp = null, string $tzid = null)
Replace With: Select
function smf_strftime(string $format, $timestamp = null, $tzid = null)
Find: Select

if (!isset($tzid))
$tzid = date_default_timezone_get();
Add After: Select

$timestamp = min(max($timestamp, PHP_INT_MIN), PHP_INT_MAX);
Find: Select
function smf_gmstrftime(string $format, int $timestamp = null)
Replace With: Select
function smf_gmstrftime(string $format, $timestamp = null)
Find: Select
// Image.
if (!empty($currentAttachment['is_image']))
{
Replace With: Select
// Image.
if (!empty($currentAttachment['is_image']))
{
// Just viewing the page shouldn't increase the download count for embedded images.
$currentAttachment['href'] .= ';preview';
Find: Select
'before' => '<div class="centertext">',
'after' => '</div>',
Replace With: Select
'before' => '<div class="centertext"><div class="inline_block">',
'after' => '</div></div>',
Find: Select
'before' => '<div class="righttext">',
'after' => '</div>',
Replace With: Select
'before' => '<div class="righttext"><div class="inline_block">',
'after' => '</div></div>',
Find: Select
// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
Replace With: Select
// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]], $message[$pos + 2]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
Find: Select
// The value may be quoted for some tags - check.
if (isset($tag['quoted']))
{
$quoted = substr($message, $pos1, 6) == '&quot;';
if ($tag['quoted'] != 'optional' && !$quoted)
continue;

if ($quoted)
$pos1 += 6;
}
else
$quoted = false;

$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
Replace With: Select
// The value may be quoted for some tags - check.
if (isset($tag['quoted']))
{
// Anything passed through the preparser will use &quot;,
// but we need to handle raw quotation marks too.
$quot = substr($message, $pos1, 1) === '"' ? '"' : '&quot;';

$quoted = substr($message, $pos1, strlen($quot)) == $quot;
if ($tag['quoted'] != 'optional' && !$quoted)
continue;

if ($quoted)
$pos1 += strlen($quot);
}
else
$quoted = false;

$pos2 = strpos($message, $quoted == false ? ']' : $quot . ']', $pos1);
Find: Select
substr($message, $pos2 + ($quoted == false ? 1 : 7), $pos3 - ($pos2 + ($quoted == false ? 1 : 7))),
Replace With: Select
substr($message, $pos2 + ($quoted == false ? 1 : 1 + strlen($quot)), $pos3 - ($pos2 + ($quoted == false ? 1 : 1 + strlen($quot)))),
Find: Select
// The value may be quoted for some tags - check.
if (isset($tag['quoted']))
{
$quoted = substr($message, $pos1, 6) == '&quot;';
if ($tag['quoted'] != 'optional' && !$quoted)
continue;

if ($quoted)
$pos1 += 6;
}
else
$quoted = false;

if ($quoted)
{
$end_of_value = strpos($message, '&quot;]', $pos1);
$nested_tag = strpos($message, '=&quot;', $pos1);
// Check so this is not just an quoted url ending with a =
if ($nested_tag && substr($message, $nested_tag, 8) == '=&quot;]')
$nested_tag = false;
if ($nested_tag && $nested_tag < $end_of_value)
// Nested tag with quoted value detected, use next end tag
$nested_tag_pos = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1) + 6;
}

$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', isset($nested_tag_pos) ? $nested_tag_pos : $pos1);
Replace With: Select
// The value may be quoted for some tags - check.
if (isset($tag['quoted']))
{
// Will normally be '&quot;' but might be '"'.
$quot = substr($message, $pos1, 1) === '"' ? '"' : '&quot;';

$quoted = substr($message, $pos1, strlen($quot)) == $quot;
if ($tag['quoted'] != 'optional' && !$quoted)
continue;

if ($quoted)
$pos1 += strlen($quot);
}
else
$quoted = false;

if ($quoted)
{
$end_of_value = strpos($message, $quot . ']', $pos1);
$nested_tag = strpos($message, '=' . $quot, $pos1);
// Check so this is not just an quoted url ending with a =
if ($nested_tag && substr($message, $nested_tag, 2 + strlen($quot)) == '=' . $quot . ']')
$nested_tag = false;
if ($nested_tag && $nested_tag < $end_of_value)
// Nested tag with quoted value detected, use next end tag
$nested_tag_pos = strpos($message, $quoted == false ? ']' : $quot . ']', $pos1) + strlen($quot);
}

$pos2 = strpos($message, $quoted == false ? ']' : $quot . ']', isset($nested_tag_pos) ? $nested_tag_pos : $pos1);
Find: Select
$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 7));
Replace With: Select
$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 1 + strlen($quot)));
Find: Select
$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));

error_reporting($oldlevel);

// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);

return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
Replace With: Select
$buffer = str_replace(array("\n", "\r"), array('<br />', ''), @highlight_string($code, true));

error_reporting($oldlevel);

// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\(\);~', '<span style="white-space: pre-wrap;">' . "\t" . '</span>', $buffer);

// PHP 8.3 changed the returned HTML.
$buffer = preg_replace('/^(<pre>)?<code[^>]*>|<\/code>(<\/pre>)?$/', '', $buffer);

// Remove line breaks inserted before & after the actual code in php < 8.3
$buffer = preg_replace('/^(<span\s[^>]*>)<br \/>/', '$1', $buffer);
$buffer = preg_replace('/<br \/>(<\/span[^>]*>)<br \/>$/', '$1', $buffer);

return strtr($buffer, ['\'' => '&#039;']);
Find: Select
// Put the session ID in.
if (defined('SID') && SID != '')
$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
// Keep that debug in their for template debugging!
elseif (isset($_GET['debug']))
$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);

if (!empty($modSettings['queryless_urls']) && (empty($context['server']['is_cgi']) || ini_get('cgi.fix_pathinfo') == 1 || @get_cfg_var('cgi.fix_pathinfo') == 1) && (!empty($context['server']['is_apache']) || !empty($context['server']['is_lighttpd']) || !empty($context['server']['is_litespeed'])))
{
if (defined('SID') && SID != '')
$setLocation = preg_replace_callback(
'~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
function($m) use ($scripturl)
{
return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
Replace With: Select
// PHP 8.4 deprecated SID. A better long-term solution is needed, but this works for now.
$sid = defined('SID') ? @constant('SID') : null;

// Put the session ID in.
if (isset($sid) && $sid != '')
$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote($sid, '/') . ')\\??/', $scripturl . '?' . $sid . ';', $setLocation);
// Keep that debug in their for template debugging!
elseif (isset($_GET['debug']))
$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);

if (!empty($modSettings['queryless_urls']) && (empty($context['server']['is_cgi']) || ini_get('cgi.fix_pathinfo') == 1 || @get_cfg_var('cgi.fix_pathinfo') == 1) && (!empty($context['server']['is_apache']) || !empty($context['server']['is_lighttpd']) || !empty($context['server']['is_litespeed'])))
{
if (isset($sid) && $sid != '')
$setLocation = preg_replace_callback(
'~^' . preg_quote($scripturl, '~') . '\?(?:' . $sid . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
function($m) use ($scripturl)
{
return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . $sid . (isset($m[2]) ? "$m[2]" : "");
Find: Select

$setLocation
);
}
Add After: Select

// The request was from ajax/xhr/other api call, append ajax ot the url.
if (!empty($context['from_ajax']))
$setLocation .= (strpos($setLocation, '?') ? ';' : '?') . 'ajax';
Find: Select
// Remember this URL in case someone doesn't like sending HTTP_REFERER.
if ($should_log)
Replace With: Select
// Remember this URL in case someone doesn't like sending HTTP_REFERER.
if ($should_log && !isset($_REQUEST['xml']))
Find: Select
$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
Replace With: Select
$returned_words[] = $max_chars === null ? $word : $smcFunc['truncate']($word, $max_chars);
Find: Select
// Cleanup enabled/disabled variants before taking action.
$current_functions = array_diff($current_functions, array($enabled_call, $disabled_call));
Replace With: Select
// Cleanup enabled/disabled variants before taking action.
$current_functions = array_diff($current_functions, array($enabled_call, $disabled_call));
Find: Select
// The substring 'O:' is used to serialize objects.
// If it is not present, then there are none in the serialized data.
if (strpos($str, 'O:') === false)
return unserialize($str);
Replace With: Select
// The substrings 'O' and 'C' are used to serialize objects.
// If they are not present, then there are none in the serialized data.
if (strpos($str, 'O:') === false && strpos($str, 'C:') === false)
return unserialize($str, ['allowed_classes' => false]);
Find: Select
$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
Replace With: Select
$protocol = !empty($_SERVER['SERVER_PROTOCOL']) && preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';

./Sources/Themes.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select

function InstallDir()
{
global $themedir, $themeurl, $context;
Add After: Select

$_REQUEST['theme_dir'] = rtrim($_REQUEST['theme_dir'], '\\/');
Find: Select
global $context, $scripturl, $boarddir, $smcFunc, $txt;
Replace With: Select
global $context, $scripturl, $boarddir, $smcFunc, $txt, $sourcedir;
Find: Select
// Check for a parse error!
if (substr($_REQUEST['filename'], -13) == '.template.php' && is_writable($currentTheme['theme_dir']) && ini_get('display_errors'))
{
$fp = fopen($currentTheme['theme_dir'] . '/tmp_' . session_id() . '.php', 'w');
fwrite($fp, $_POST['entire_file']);
fclose($fp);
Replace With: Select
require_once($sourcedir . '/Subs-Admin.php');

// Check for a parse error!
if (substr($_REQUEST['filename'], -13) == '.template.php' && is_writable($currentTheme['theme_dir']) && ini_get('display_errors'))
{
safe_file_write($currentTheme['theme_dir'] . '/tmp_' . session_id() . '.php', $_POST['entire_file']);
Find: Select
$fp = fopen($currentTheme['theme_dir'] . '/' . $_REQUEST['filename'], 'w');
fwrite($fp, $_POST['entire_file']);
fclose($fp);
Replace With: Select
safe_file_write($currentTheme['theme_dir'] . '/' . $_REQUEST['filename'], $_POST['entire_file']);

./Sources/Who.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
// Lead Developer
'Shawn Bulen',
Replace With: Select
// Lead Developer
'Jon "Sesquipedalian" Stovell',
Find: Select

'Selman "[SiNaN]" Eser',
Add After: Select

'Shawn Bulen',
Find: Select

'Paul_Pauline',
Add After: Select

'Rock Lee',

./Sources/minify/path-converter/src/Converter.php

Find: Select
* @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved.
* @license MIT License
*/
class Converter
Replace With: Select
* @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved
* @license MIT License
*/
class Converter implements ConverterInterface
Find: Select
*/
public function __construct($from, $to)
{
$shared = $this->shared($from, $to);
if ($shared === '') {
// when both paths have nothing in common, one of them is probably
// absolute while the other is relative
$cwd = getcwd();
$from = strpos($from, $cwd) === 0 ? $from : $cwd.'/'.$from;
$to = strpos($to, $cwd) === 0 ? $to : $cwd.'/'.$to;

// or traveling the tree via `..`
// attempt to resolve path, or assume it's fine if it doesn't exist
$from = realpath($from) ?: $from;
$to = realpath($to) ?: $to;
Replace With: Select
* @param string $root Root directory (defaults to `getcwd`)
*/
public function __construct($from, $to, $root = '')
{
$shared = $this->shared($from, $to);
if ($shared === '') {
// when both paths have nothing in common, one of them is probably
// absolute while the other is relative
$root = $root ?: getcwd();
$from = strpos($from, $root) === 0 ? $from : preg_replace('/\/+/', '/', $root.'/'.$from);
$to = strpos($to, $root) === 0 ? $to : preg_replace('/\/+/', '/', $root.'/'.$to);

// or traveling the tree via `..`
// attempt to resolve path, or assume it's fine if it doesn't exist
$from = @realpath($from) ?: $from;
$to = @realpath($to) ?: $to;
Find: Select
// deal with different operating systems' directory structure
$path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/');
Replace With: Select
// deal with different operating systems' directory structure
$path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/');

// remove leading current directory.
if (substr($path, 0, 2) === './') {
$path = substr($path, 2);
}

// remove references to current directory in the path.
$path = str_replace('/./', '/', $path);
Find: Select
* @param string $path The relative path that needs to be converted.
*
* @return string The new relative path.
Replace With: Select
* @param string $path The relative path that needs to be converted
*
* @return string The new relative path
Find: Select
// add .. for every directory that needs to be traversed to new path
$to = str_repeat('../', mb_substr_count($to, '/'));
Replace With: Select
// add .. for every directory that needs to be traversed to new path
$to = str_repeat('../', count(array_filter(explode('/', $to))));
Find: Select
public function dirname($path)
{
if (is_file($path)) {
return dirname($path);
}

if (is_dir($path)) {
Replace With: Select
protected function dirname($path)
{
if (@is_file($path)) {
return dirname($path);
}

if (@is_dir($path)) {

./Sources/minify/src/CSS.php

Find: Select
namespace MatthiasMullie\Minify;

use MatthiasMullie\Minify\Exceptions\FileImportException;
use MatthiasMullie\PathConverter\Converter;
Replace With: Select
/**
* CSS Minifier.
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @author Matthias Mullie <[email protected]>
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
* @license MIT License
*/

namespace MatthiasMullie\Minify;

use MatthiasMullie\Minify\Exceptions\FileImportException;
use MatthiasMullie\PathConverter\Converter;
use MatthiasMullie\PathConverter\ConverterInterface;
Find: Select
* @var int
*/
protected $maxImportSize = 5;

/**
* @var string[]
Replace With: Select
* @var int maximum inport size in kB
*/
protected $maxImportSize = 5;

/**
* @var string[] valid import extensions
Find: Select
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'xbm' => 'image/x-xbitmap',
Replace With: Select
'woff2' => 'data:application/x-font-woff2',
'avif' => 'data:image/avif',
'apng' => 'data:image/apng',
'webp' => 'data:image/webp',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'xbm' => 'image/x-xbitmap',
'webp' => 'image/webp',
Find: Select
if (preg_match_all('/@import[^;]+;/', $content, $matches)) {
// remove from content
foreach ($matches[0] as $import) {
$content = str_replace($import, '', $content);
}

// add to top
$content = implode('', $matches[0]).$content;
Replace With: Select
if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) {
// remove from content
foreach ($matches[0] as $import) {
$content = str_replace($import, '', $content);
}

// add to top
$content = implode(';', $matches[2]) . ';' . trim($content, ';');
Find: Select
* @import's will be loaded and their content merged into the original file,
* to save HTTP requests.
Replace With: Select
* Import statements will be loaded and their content merged into the original
* file, to save HTTP requests.
Find: Select
(?P<path>

# do not fetch data uris or external sources
(?!(
["\']?
(data|https?):
))

.+?
)
Replace With: Select
(?P<path>.+?)
Find: Select
\s+

# open path enclosure
(?P<quotes>["\'])

# fetch path
(?P<path>

# do not fetch data uris or external sources
(?!(
["\']?
(data|https?):
))

.+?
)
Replace With: Select
\s+

# open path enclosure
(?P<quotes>["\'])

# fetch path
(?P<path>.+?)
Find: Select
// loop the matches
foreach ($matches as $match) {
// get the path for the file that will be imported
$importPath = dirname($source).'/'.$match['path'];

// only replace the import with the content if we can grab the
// content of the file
if ($this->canImportFile($importPath)) {
// check if current file was not imported previously in the same
// import chain.
if (in_array($importPath, $parents)) {
throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.');
}

// grab referenced file & minify it (which may include importing
// yet other @import statements recursively)
$minifier = new static($importPath);
$importContent = $minifier->execute($source, $parents);

// check if this is only valid for certain media
if (!empty($match['media'])) {
$importContent = '@media '.$match['media'].'{'.$importContent.'}';
}

// add to replacement array
$search[] = $match[0];
$replace[] = $importContent;
}
}

// replace the import statements
$content = str_replace($search, $replace, $content);

return $content;
Replace With: Select
// loop the matches
foreach ($matches as $match) {
// get the path for the file that will be imported
$importPath = dirname($source) . '/' . $match['path'];

// only replace the import with the content if we can grab the
// content of the file
if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) {
continue;
}

// check if current file was not imported previously in the same
// import chain.
if (in_array($importPath, $parents)) {
throw new FileImportException('Failed to import file "' . $importPath . '": circular reference detected.');
}

// grab referenced file & minify it (which may include importing
// yet other @import statements recursively)
$minifier = new self($importPath);
$minifier->setMaxImportSize($this->maxImportSize);
$minifier->setImportExtensions($this->importExtensions);
$importContent = $minifier->execute($source, $parents);

// check if this is only valid for certain media
if (!empty($match['media'])) {
$importContent = '@media ' . $match['media'] . '{' . $importContent . '}';
}

// add to replacement array
$search[] = $match[0];
$replace[] = $importContent;
}

// replace the import statements
return str_replace($search, $replace, $content);
Find: Select
$extensions = array_keys($this->importExtensions);
$regex = '/url\((["\']?)((?!["\']?data:).*?\.('.implode('|', $extensions).'))\\1\)/i';
if ($extensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
$search = array();
$replace = array();

// loop the matches
foreach ($matches as $match) {
// get the path for the file that will be imported
$path = $match[2];
$path = dirname($source).'/'.$path;
$extension = $match[3];
Replace With: Select
$regex = '/url\((["\']?)(.+?)\\1\)/i';
if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
$search = array();
$replace = array();

// loop the matches
foreach ($matches as $match) {
$extension = substr(strrchr($match[2], '.'), 1);
if ($extension && !array_key_exists($extension, $this->importExtensions)) {
continue;
}

// get the path for the file that will be imported
$path = $match[2];
$path = dirname($source) . '/' . $path;
Find: Select
// build replacement
$search[] = $match[0];
$replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
Replace With: Select
// build replacement
$search[] = $match[0];
$replace[] = 'url(' . $this->importExtensions[$extension] . ';base64,' . $importContent . ')';
Find: Select
* @param string[optional] $path Path to write the data to
* @param string[] $parents Parent paths, for circular reference checks
Replace With: Select
* @param string[optional] $path Path to write the data to
* @param string[] $parents Parent paths, for circular reference checks
Find: Select
// loop css data (raw data and files)
foreach ($this->data as $source => $css) {
/*
* Let's first take out strings & comments, since we can't just remove
* whitespace anywhere. If whitespace occurs inside a string, we should
* leave it alone. E.g.:
* p { content: "a test" }
*/
$this->extractStrings();
$this->stripComments();
$css = $this->replace($css);

$css = $this->stripWhitespace($css);
$css = $this->shortenHex($css);
Replace With: Select
// loop CSS data (raw data and files)
foreach ($this->data as $source => $css) {
/*
* Let's first take out strings & comments, since we can't just
* remove whitespace anywhere. If whitespace occurs inside a string,
* we should leave it alone. E.g.:
* p { content: "a test" }
*/
$this->extractStrings();
$this->stripComments();
$this->extractMath();
$this->extractCustomProperties();
$css = $this->replace($css);

$css = $this->stripWhitespace($css);
$css = $this->shortenColors($css);
Find: Select
* of the move code...)
*/
$converter = new Converter($source, $path ?: $source);
Replace With: Select
* of the move code, which also addresses url() & @import syntax...)
*/
$converter = $this->getPathConverter($source, $path ?: $source);
Find: Select
* @param Converter $converter Relative path converter
* @param string $content The CSS content to update relative urls for
*
* @return string
*/
protected function move(Converter $converter, $content)
Replace With: Select
* @param ConverterInterface $converter Relative path converter
* @param string $content The CSS content to update relative urls for
*
* @return string
*/
protected function move(ConverterInterface $converter, $content)
Find: Select
(?P<quotes>["\'])?

# fetch path
(?P<path>

# do not fetch data uris or external sources
(?!(
\s?
["\']?
(data|https?):
))

.+?
)
Replace With: Select
(?P<quotes>["\'])?

# fetch path
(?P<path>.+?)
Find: Select
# condition above will already catch these

# open path enclosure
(?P<quotes>["\'])

# fetch path
(?P<path>

# do not fetch data uris or external sources
(?!(
["\']?
(data|https?):
))

.+?
)
Replace With: Select
# condition above will already catch these

# open path enclosure
(?P<quotes>["\'])

# fetch path
(?P<path>.+?)
Find: Select
// determine if it's a url() or an @import match
$type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');

// attempting to interpret GET-params makes no sense, so let's discard them for awhile
$params = strrchr($match['path'], '?');
$url = $params ? substr($match['path'], 0, -strlen($params)) : $match['path'];

// fix relative url
$url = $converter->convert($url);

// now that the path has been converted, re-apply GET-params
$url .= $params;

// build replacement
$search[] = $match[0];
if ($type == 'url') {
$replace[] = 'url('.$url.')';
} elseif ($type == 'import') {
$replace[] = '@import "'.$url.'"';
}
}

// replace urls
$content = str_replace($search, $replace, $content);

return $content;
Replace With: Select
// determine if it's a url() or an @import match
$type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');

$url = $match['path'];
if ($this->canImportByPath($url)) {
// attempting to interpret GET-params makes no sense, so let's discard them for awhile
$params = strrchr($url, '?');
$url = $params ? substr($url, 0, -strlen($params)) : $url;

// fix relative url
$url = $converter->convert($url);

// now that the path has been converted, re-apply GET-params
$url .= $params;
}

/*
* Urls with control characters above 0x7e should be quoted.
* According to Mozilla's parser, whitespace is only allowed at the
* end of unquoted urls.
* Urls with `)` (as could happen with data: uris) should also be
* quoted to avoid being confused for the url() closing parentheses.
* And urls with a # have also been reported to cause issues.
* Urls with quotes inside should also remain escaped.
*
* @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation
* @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378
* @see https://github.com/matthiasmullie/minify/issues/193
*/
$url = trim($url);
if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) {
$url = $match['quotes'] . $url . $match['quotes'];
}

// build replacement
$search[] = $match[0];
if ($type === 'url') {
$replace[] = 'url(' . $url . ')';
} elseif ($type === 'import') {
$replace[] = '@import "' . $url . '"';
}
}

// replace urls
return str_replace($search, $replace, $content);
Find: Select
protected function shortenHex($content)
{
$content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?=[; }])/i', '#$1$2$3', $content);

// we can shorten some even more by replacing them with their color name
$colors = array(
Replace With: Select
protected function shortenColors($content)
{
$content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content);

// remove alpha channel if it's pointless...
$content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content);
$content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content);

$colors = array(
// we can shorten some even more by replacing them with their color name
Find: Select
);

return preg_replace_callback(
'/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i',
Replace With: Select
// or the other way around
'WHITE' => '#fff',
'BLACK' => '#000',
);

return preg_replace_callback(
'/(?<=[: ])(' . implode('|', array_keys($colors)) . ')(?=[; }])/i',
Find: Select
return $match[1].$weights[$match[2]];
};

return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content);
Replace With: Select
return $match[1] . $weights[$match[2]];
};

return preg_replace_callback('/(font-weight\s*:\s*)(' . implode('|', array_keys($weights)) . ')(?=[;}])/', $callback, $content);
Find: Select

protected function shortenZeroes($content)
{
Add After: Select

// we don't want to strip units in `calc()` expressions:
// `5px - 0px` is valid, but `5px - 0` is not
// `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but
// `10 * 0` is invalid
// we've extracted calcs earlier, so we don't need to worry about this
Find: Select
// practice, Webkit (especially Safari) seems to stumble over at least
// 0%, potentially other units as well. Only stripping 'px' for now.
// @see https://github.com/matthiasmullie/minify/issues/60
$content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content);

// strip 0-digits (.0 -> 0)
$content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content);
// strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
$content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content);
// strip trailing 0: 50.00 -> 50, 50.00px -> 50px
$content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content);
// strip leading 0: 0.1 -> .1, 01.1 -> 1.1
$content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content);

// strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
$content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);

// remove zeroes where they make no sense in calc: e.g. calc(100px - 0)
// the 0 doesn't have any effect, and this isn't even valid without unit
// strip all `+ 0` or `- 0` occurrences: calc(10% + 0) -> calc(10%)
// looped because there may be multiple 0s inside 1 group of parentheses
do {
$previous = $content;
$content = preg_replace('/\(([^\(\)]+)\s+[\+\-]\s+0(\s+[^\(\)]+)?\)/', '(\\1\\2)', $content);
} while ($content !== $previous);
// strip all `0 +` occurrences: calc(0 + 10%) -> calc(10%)
$content = preg_replace('/\(\s*0\s+\+\s+([^\(\)]+)\)/', '(\\1)', $content);
// strip all `0 -` occurrences: calc(0 - 10%) -> calc(-10%)
$content = preg_replace('/\(\s*0\s+\-\s+([^\(\)]+)\)/', '(-\\1)', $content);
// I'm not going to attempt to optimize away `x * 0` instances:
// it's dumb enough code already that it likely won't occur, and it's
// too complex to do right (order of operations would have to be
// respected etc)
// what I cared about most here was fixing incorrectly truncated units

return $content;
}

/**
* Strip comments from source code.
Replace With: Select
// practice, Webkit (especially Safari) seems to stumble over at least
// 0%, potentially other units as well. Only stripping 'px' for now.
// @see https://github.com/matthiasmullie/minify/issues/60
$content = preg_replace('/' . $before . '(-?0*(\.0+)?)(?<=0)px' . $after . '/', '\\1', $content);

// strip 0-digits (.0 -> 0)
$content = preg_replace('/' . $before . '\.0+' . $units . '?' . $after . '/', '0\\1', $content);
// strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
$content = preg_replace('/' . $before . '(-?[0-9]+\.[0-9]+)0+' . $units . '?' . $after . '/', '\\1\\2', $content);
// strip trailing 0: 50.00 -> 50, 50.00px -> 50px
$content = preg_replace('/' . $before . '(-?[0-9]+)\.0+' . $units . '?' . $after . '/', '\\1\\2', $content);
// strip leading 0: 0.1 -> .1, 01.1 -> 1.1
$content = preg_replace('/' . $before . '(-?)0+([0-9]*\.[0-9]+)' . $units . '?' . $after . '/', '\\1\\2\\3', $content);

// strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
$content = preg_replace('/' . $before . '-?0+' . $units . '?' . $after . '/', '0\\1', $content);

// IE doesn't seem to understand a unitless flex-basis value (correct -
// it goes against the spec), so let's add it in again (make it `%`,
// which is only 1 char: 0%, 0px, 0 anything, it's all just the same)
// @see https://developer.mozilla.org/nl/docs/Web/CSS/flex
$content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content);
$content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content);

return $content;
}

/**
* Strip empty tags from source code.
Find: Select
return preg_replace('/(^|\}|;)[^\{\};]+\{\s*\}/', '\\1', $content);
Replace With: Select
$content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content);
$content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content);

return $content;
Find: Select
$this->registerPattern('/\/\*.*?\*\//s', '');
Replace With: Select
$this->stripMultilineComments();
Find: Select
// remove whitespace around meta characters
// inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
$content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
$content = preg_replace('/([\[(:])\s+/', '$1', $content);
$content = preg_replace('/\s+([\]\)])/', '$1', $content);
$content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);

// whitespace around + and - can only be stripped in selectors, like
// :nth-child(3+2n), not in things like calc(3px + 2px) or shorthands
// like 3px -2px
$content = preg_replace('/\s*([+-])\s*(?=[^}]*{)/', '$1', $content);
Replace With: Select
// remove whitespace around meta characters
// inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
$content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
$content = preg_replace('/([\[(:>\+])\s+/', '$1', $content);
$content = preg_replace('/\s+([\]\)>\+])/', '$1', $content);
$content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);

// whitespace around + and - can only be stripped inside some pseudo-
// classes, like `:nth-child(3+2n)`
// not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or
// selectors like `div.weird- p`
$pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type');
$content = preg_replace('/:(' . implode('|', $pseudos) . ')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content);
Find: Select

return trim($content);
}
Add After: Select

/**
* Replace all occurrences of functions that may contain math, where
* whitespace around operators needs to be preserved (e.g. calc, clamp).
*/
protected function extractMath()
{
$functions = array('calc', 'clamp', 'min', 'max');
$pattern = '/\b(' . implode('|', $functions) . ')(\(.+?)(?=$|;|})/m';

// PHP only supports $this inside anonymous functions since 5.4
$minifier = $this;
$callback = function ($match) use ($minifier, $pattern, &$callback) {
$function = $match[1];
$length = strlen($match[2]);
$expr = '';
$opened = 0;

// the regular expression for extracting math has 1 significant problem:
// it can't determine the correct closing parenthesis...
// instead, it'll match a larger portion of code to where it's certain that
// the calc() musts have ended, and we'll figure out which is the correct
// closing parenthesis here, by counting how many have opened
for ($i = 0; $i < $length; ++$i) {
$char = $match[2][$i];
$expr .= $char;
if ($char === '(') {
++$opened;
} elseif ($char === ')' && --$opened === 0) {
break;
}
}

// now that we've figured out where the calc() starts and ends, extract it
$count = count($minifier->extracted);
$placeholder = 'math(' . $count . ')';
$minifier->extracted[$placeholder] = $function . '(' . trim(substr($expr, 1, -1)) . ')';

// and since we've captured more code than required, we may have some leftover
// calc() in here too - go recursive on the remaining but of code to go figure
// that out and extract what is needed
$rest = $minifier->str_replace_first($function . $expr, '', $match[0]);
$rest = preg_replace_callback($pattern, $callback, $rest);

return $placeholder . $rest;
};

$this->registerPattern($pattern, $callback);
}

/**
* Replace custom properties, whose values may be used in scenarios where
* we wouldn't want them to be minified (e.g. inside calc).
*/
protected function extractCustomProperties()
{
// PHP only supports $this inside anonymous functions since 5.4
$minifier = $this;
$this->registerPattern(
'/(?<=^|[;}{])\s*(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m',
function ($match) use ($minifier) {
$placeholder = '--custom-' . count($minifier->extracted) . ':0';
$minifier->extracted[$placeholder] = $match[1] . ':' . trim($match[2]);

return $placeholder;
}
);
}
Find: Select

return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
Add After: Select

}

/**
* Check if file a file can be imported, going by the path.
*
* @param string $path
*
* @return bool
*/
protected function canImportByPath($path)
{
return preg_match('/^(data:|https?:|\\/)/', $path) === 0;
}

/**
* Return a converter to update relative paths to be relative to the new
* destination.
*
* @param string $source
* @param string $target
*
* @return ConverterInterface
*/
protected function getPathConverter($source, $target)
{
return new Converter($source, $target);

./Sources/minify/src/Exception.php

Find: Select
namespace MatthiasMullie\Minify;

/**
Replace With: Select
/**
* Base Exception.
*
* @deprecated Use Exceptions\BasicException instead
*
* @author Matthias Mullie <[email protected]>
*/

namespace MatthiasMullie\Minify;

/**
* Base Exception Class.
*

./Sources/minify/src/Exceptions/BasicException.php

Find: Select
namespace MatthiasMullie\Minify\Exceptions;

use MatthiasMullie\Minify\Exception;

/**
Replace With: Select
/**
* Basic exception.
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @author Matthias Mullie <[email protected]>
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
* @license MIT License
*/

namespace MatthiasMullie\Minify\Exceptions;

use MatthiasMullie\Minify\Exception;

/**
* Basic Exception Class.
*

./Sources/minify/src/Exceptions/FileImportException.php

Find: Select
namespace MatthiasMullie\Minify\Exceptions;

/**
Replace With: Select
/**
* File Import Exception.
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @author Matthias Mullie <[email protected]>
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
* @license MIT License
*/

namespace MatthiasMullie\Minify\Exceptions;

/**
* File Import Exception Class.
*

./Sources/minify/src/Exceptions/IOException.php

Find: Select
namespace MatthiasMullie\Minify\Exceptions;

/**
Replace With: Select
/**
* IO Exception.
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @author Matthias Mullie <[email protected]>
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
* @license MIT License
*/

namespace MatthiasMullie\Minify\Exceptions;

/**
* IO Exception Class.
*

./Sources/minify/src/JS.php

Find: Select
namespace MatthiasMullie\Minify;

/**
* JavaScript minifier.
Replace With: Select
/**
* JavaScript minifier.
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @author Matthias Mullie <[email protected]>
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
* @license MIT License
*/

namespace MatthiasMullie\Minify;

/**
* JavaScript Minifier Class.
Find: Select

* pattern modifier (/u) set.
*
Add After: Select

* @internal
*
Find: Select
* Note: Most operators are fine, we've only removed !, ++ and --.
* There can't be a newline separating ! and whatever it is negating.
Replace With: Select
* Note: Most operators are fine, we've only removed ++ and --.
Find: Select
* Note: Most operators are fine, we've only removed ), ], ++ and --.
Replace With: Select
* Note: Most operators are fine, we've only removed ), ], ++, --, ! and ~.
* There can't be a newline separating ! or ~ and whatever it is negating.
Find: Select
call_user_func_array(array('parent', '__construct'), func_get_args());

$dataDir = __DIR__.'/../data/js/';
$options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
$this->keywordsReserved = file($dataDir.'keywords_reserved.txt', $options);
$this->keywordsBefore = file($dataDir.'keywords_before.txt', $options);
$this->keywordsAfter = file($dataDir.'keywords_after.txt', $options);
$this->operators = file($dataDir.'operators.txt', $options);
$this->operatorsBefore = file($dataDir.'operators_before.txt', $options);
$this->operatorsAfter = file($dataDir.'operators_after.txt', $options);
Replace With: Select
call_user_func_array(array('\\MatthiasMullie\Minify\\Minify', '__construct'), func_get_args());

$dataDir = __DIR__ . '/../data/js/';
$options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
$this->keywordsReserved = file($dataDir . 'keywords_reserved.txt', $options);
$this->keywordsBefore = file($dataDir . 'keywords_before.txt', $options);
$this->keywordsAfter = file($dataDir . 'keywords_after.txt', $options);
$this->operators = file($dataDir . 'operators.txt', $options);
$this->operatorsBefore = file($dataDir . 'operators_before.txt', $options);
$this->operatorsAfter = file($dataDir . 'operators_after.txt', $options);
Find: Select
$content = '';

// loop files
foreach ($this->data as $source => $js) {
/*
* Combine js: separating the scripts by a ;
* I'm also adding a newline: it will be eaten when whitespace is
* stripped, but we need to make sure we're not just appending
* a new script right after a previous script that ended with a
* singe-line comment on the last line (in which case it would also
* be seen as part of that comment)
*/
$content .= $js."\n;";
}
Replace With: Select
$content = '';
Find: Select
$content = $this->replace($content);

$content = $this->propertyNotation($content);
$content = $this->shortenBools($content);
$content = $this->stripWhitespace($content);
Replace With: Select

// loop files
foreach ($this->data as $source => $js) {
// take out strings, comments & regex (for which we've registered
// the regexes just a few lines earlier)
$js = $this->replace($js);

$js = $this->propertyNotation($js);
$js = $this->shortenBools($js);
$js = $this->stripWhitespace($js);

// combine js: separating the scripts by a ;
$content .= $js . ';';
}

// clean up leftover `;`s from the combination of multiple scripts
$content = ltrim($content, ';');
$content = (string) substr($content, 0, -1);
Find: Select
// single-line comments
$this->registerPattern('/\/\/.*$/m', '');

// multi-line comments
$this->registerPattern('/\/\*.*?\*\//s', '');
Replace With: Select
$this->stripMultilineComments();

// single-line comments
$this->registerPattern('/\/\/.*$/m', '');
Find: Select
$placeholder = '/'.$count.'/';
$minifier->extracted[$placeholder] = $match[0];

return $placeholder;
};

$pattern = '\/.+?(?<!\\\\)(\\\\\\\\)*\/[gimy]*(?![0-9a-zA-Z\/])';

// a regular expression can only be followed by a few operators or some
// of the RegExp methods (a `\` followed by a variable or value is
// likely part of a division, not a regex)
$after = '[\.,;\)\}]';
$methods = '\.(exec|test|match|search|replace|split)\(';
$this->registerPattern('/'.$pattern.'(?=\s*('.$after.'|'.$methods.'))/', $callback);
Replace With: Select
$placeholder = '"' . $count . '"';
$minifier->extracted[$placeholder] = $match[0];

return $placeholder;
};

// match all chars except `/` and `\`
// `\` is allowed though, along with whatever char follows (which is the
// one being escaped)
// this should allow all chars, except for an unescaped `/` (= the one
// closing the regex)
// then also ignore bare `/` inside `[]`, where they don't need to be
// escaped: anything inside `[]` can be ignored safely
$pattern = '\\/(?!\*)(?:[^\\[\\/\\\\\n\r]++|(?:\\\\.)++|(?:\\[(?:[^\\]\\\\\n\r]++|(?:\\\\.)++)++\\])++)++\\/[gimuy]*';

// a regular expression can only be followed by a few operators or some
// of the RegExp methods (a `\` followed by a variable or value is
// likely part of a division, not a regex)
$keywords = array('do', 'in', 'new', 'else', 'throw', 'yield', 'delete', 'return', 'typeof');
$before = '(^|[=:,;\+\-\*\?\/\}\(\{\[&\|!]|' . implode('|', $keywords) . ')\s*';
$propertiesAndMethods = array(
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties_2
'constructor',
'flags',
'global',
'ignoreCase',
'multiline',
'source',
'sticky',
'unicode',
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Methods_2
'compile(',
'exec(',
'test(',
'toSource(',
'toString(',
);
$delimiters = array_fill(0, count($propertiesAndMethods), '/');
$propertiesAndMethods = array_map('preg_quote', $propertiesAndMethods, $delimiters);
$after = '(?=\s*([\.,;:\)\}&\|+]|\/\/|$|\.(' . implode('|', $propertiesAndMethods) . ')))';
$this->registerPattern('/' . $before . '\K' . $pattern . $after . '/', $callback);

// regular expressions following a `)` are rather annoying to detect...
// quite often, `/` after `)` is a division operator & if it happens to
// be followed by another one (or a comment), it is likely to be
// confused for a regular expression
// however, it's perfectly possible for a regex to follow a `)`: after
// a single-line `if()`, `while()`, ... statement, for example
// since, when they occur like that, they're always the start of a
// statement, there's only a limited amount of ways they can be useful:
// by calling the regex methods directly
// if a regex following `)` is not followed by `.<property or method>`,
// it's quite likely not a regex
$before = '\)\s*';
$after = '(?=\s*\.(' . implode('|', $propertiesAndMethods) . '))';
$this->registerPattern('/' . $before . '\K' . $pattern . $after . '/', $callback);
Find: Select
// (https://github.com/matthiasmullie/minify/issues/56)
$operators = $this->getOperatorsForRegex($this->operatorsBefore, '/');
$operators += $this->getOperatorsForRegex($this->keywordsReserved, '/');
$this->registerPattern('/'.$pattern.'\s*\n(?=\s*('.implode('|', $operators).'))/', $callback);
Replace With: Select
// (https://github.com/matthiasmullie/minify/issues/56)
$operators = $this->getOperatorsForRegex($this->operatorsBefore, '/');
$operators += $this->getOperatorsForRegex($this->keywordsReserved, '/');
$after = '(?=\s*\n\s*(' . implode('|', $operators) . '))';
$this->registerPattern('/' . $pattern . $after . '/', $callback);
Find: Select
'/('.implode('|', $operatorsBefore).')\s+/',
'/\s+('.implode('|', $operatorsAfter).')/',
), '\\1', $content
Replace With: Select
'/(' . implode('|', $operatorsBefore) . ')\s+/',
'/\s+(' . implode('|', $operatorsAfter) . ')/',
),
'\\1',
$content
Find: Select
), '\\1', $content
);

// collapse whitespace around reserved words into single space
$content = preg_replace('/(^|[;\}\s])\K('.implode('|', $keywordsBefore).')\s+/', '\\2 ', $content);
$content = preg_replace('/\s+('.implode('|', $keywordsAfter).')(?=([;\{\s]|$))/', ' \\1', $content);
Replace With: Select
),
'\\1',
$content
);

// collapse whitespace around reserved words into single space
$content = preg_replace('/(^|[;\}\s])\K(' . implode('|', $keywordsBefore) . ')\s+/', '\\2 ', $content);
$content = preg_replace('/\s+(' . implode('|', $keywordsAfter) . ')(?=([;\{\s]|$))/', ' \\1', $content);
Find: Select
$content = preg_replace('/('.implode('|', $operatorsDiffBefore).')[^\S\n]+/', '\\1', $content);
$content = preg_replace('/[^\S\n]+('.implode('|', $operatorsDiffAfter).')/', '\\1', $content);
Replace With: Select
$content = preg_replace('/(' . implode('|', $operatorsDiffBefore) . ')[^\S\n]+/', '\\1', $content);
$content = preg_replace('/[^\S\n]+(' . implode('|', $operatorsDiffAfter) . ')/', '\\1', $content);

/*
* Whitespace after `return` can be omitted in a few occasions
* (such as when followed by a string or regex)
* Same for whitespace in between `)` and `{`, or between `{` and some
* keywords.
*/
$content = preg_replace('/\breturn\s+(["\'\/\+\-])/', 'return$1', $content);
$content = preg_replace('/\)\s+\{/', '){', $content);
$content = preg_replace('/}\n(else|catch|finally)\b/', '}$1', $content);
Find: Select
* semicolon), like: `for(i=1;i<3;i++);`
* Here, nothing happens during the loop; it's just used to keep
* increasing `i`. With that ; omitted, the next line would be expected
* to be the for-loop's body...
* I'm going to double that semicolon (if any) so after the next line,
* which strips semicolons here & there, we're still left with this one.
*/
$content = preg_replace('/(for\([^;]*;[^;]*;[^;\{]*\));(\}|$)/s', '\\1;;\\2', $content);
Replace With: Select
* semicolon), like: `for(i=1;i<3;i++);`, of `for(i in list);`
* Here, nothing happens during the loop; it's just used to keep
* increasing `i`. With that ; omitted, the next line would be expected
* to be the for-loop's body... Same goes for while loops.
* I'm going to double that semicolon (if any) so after the next line,
* which strips semicolons here & there, we're still left with this one.
* Note the special recursive construct in the three inner parts of the for:
* (\{([^\{\}]*(?-2))*[^\{\}]*\})? - it is intended to match inline
* functions bodies, e.g.: i<arr.map(function(e){return e}).length.
* Also note that the construct is applied only once and multiplied
* for each part of the for, otherwise it risks a catastrophic backtracking.
* The limitation is that it will not allow closures in more than one
* of the three parts for a specific for() case.
* REGEX throwing catastrophic backtracking: $content = preg_replace('/(for\([^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*;[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*;[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*\));(\}|$)/s', '\\1;;\\8', $content);
*/
$content = preg_replace('/(for\((?:[^;\{]*|[^;\{]*function[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*);[^;\{]*;[^;\{]*\));(\}|$)/s', '\\1;;\\4', $content);
$content = preg_replace('/(for\([^;\{]*;(?:[^;\{]*|[^;\{]*function[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*);[^;\{]*\));(\}|$)/s', '\\1;;\\4', $content);
$content = preg_replace('/(for\([^;\{]*;[^;\{]*;(?:[^;\{]*|[^;\{]*function[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*)\));(\}|$)/s', '\\1;;\\4', $content);

$content = preg_replace('/(for\([^;\{]+\s+in\s+[^;\{]+\));(\}|$)/s', '\\1;;\\2', $content);

/*
* Do the same for the if's that don't have a body but are followed by ;}
*/
$content = preg_replace('/(\bif\s*\([^{;]*\));\}/s', '\\1;;}', $content);

/*
* Below will also keep `;` after a `do{}while();` along with `while();`
* While these could be stripped after do-while, detecting this
* distinction is cumbersome, so I'll play it safe and make sure `;`
* after any kind of `while` is kept.
*/
$content = preg_replace('/(while\([^;\{]+\));(\}|$)/s', '\\1;;\\2', $content);
Find: Select
// escape operators for use in regex
$delimiter = array_fill(0, count($operators), $delimiter);
$escaped = array_map('preg_quote', $operators, $delimiter);
Replace With: Select
// escape operators for use in regex
$delimiters = array_fill(0, count($operators), $delimiter);
$escaped = array_map('preg_quote', $operators, $delimiters);
Find: Select
// don't confuse = with other assignment shortcuts (e.g. +=)
$chars = preg_quote('+-*\=<>%&|');
$operators['='] = '(?<!['.$chars.'])\=';
Replace With: Select
// don't confuse = with other assignment shortcuts (e.g. +=)
$chars = preg_quote('+-*\=<>%&|', $delimiter);
$operators['='] = '(?<![' . $chars . '])\=';
Find: Select
// add word boundaries
array_walk($keywords, function ($value) {
return '\b'.$value.'\b';
Replace With: Select
// add word boundaries
array_walk($keywords, function ($value) {
return '\b' . $value . '\b';
Find: Select
if (!preg_match('/^'.$minifier::REGEX_VARIABLE.'$/u', $property)) {
return $match[0];
}

return '.'.$property;
Replace With: Select
if (!preg_match('/^' . $minifier::REGEX_VARIABLE . '$/u', $property)) {
return $match[0];
}

return '.' . $property;
Find: Select
$keywords = '(?<!'.implode(')(?<!', $keywords).')';

return preg_replace_callback('/(?<='.$previousChar.'|\])'.$keywords.'\[\s*(([\'"])[0-9]+\\2)\s*\]/u', $callback, $content);
Replace With: Select
$keywords = '(?<!' . implode(')(?<!', $keywords) . ')';

return preg_replace_callback('/(?<=' . $previousChar . '|\])' . $keywords . '\[\s*(([\'"])[0-9]+\\2)\s*\]/u', $callback, $content);
Find: Select
$content = preg_replace('/\btrue\b(?!:)/', '!0', $content);
$content = preg_replace('/\bfalse\b(?!:)/', '!1', $content);

// for(;;) is exactly the same as while(true)
Replace With: Select
/*
* 'true' or 'false' could be used as property names (which may be
* followed by whitespace) - we must not replace those!
* Since PHP doesn't allow variable-length (to account for the
* whitespace) lookbehind assertions, I need to capture the leading
* character and check if it's a `.`
*/
$callback = function ($match) {
if (trim($match[1]) === '.') {
return $match[0];
}

return $match[1] . ($match[2] === 'true' ? '!0' : '!1');
};
$content = preg_replace_callback('/(^|.\s*)\b(true|false)\b(?!:)/', $callback, $content);

// for(;;) is exactly the same as while(true), but shorter :)

./Sources/minify/src/Minify.php

Find: Select
namespace MatthiasMullie\Minify;
Add Before: Select
/**
* Abstract minifier class.
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @author Matthias Mullie <[email protected]>
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
* @license MIT License
*/

Find: Select

* without having to worry about potential "code-like" characters inside.
*
Add After: Select

* @internal
*
Find: Select

* @param string|string[] $data
Add After: Select

*
* @return static
Find: Select
// store data
$this->data[$key] = $value;
}
Replace With: Select
// store data
$this->data[$key] = $value;
}

return $this;
}

/**
* Add a file to be minified.
*
* @param string|string[] $data
*
* @return static
*
* @throws IOException
*/
public function addFile($data /* $data = null, ... */)
{
// bogus "usage" of parameter $data: scrutinizer warns this variable is
// not used (we're using func_get_args instead to support overloading),
// but it still needs to be defined because it makes no sense to have
// this function without argument :)
$args = array($data) + func_get_args();

// this method can be overloaded
foreach ($args as $path) {
if (is_array($path)) {
call_user_func_array(array($this, 'addFile'), $path);
continue;
}

// redefine var
$path = (string) $path;

// check if we can read the file
if (!$this->canImportFile($path)) {
throw new IOException('The file "' . $path . '" could not be opened for reading. Check if PHP has enough permissions.');
}

$this->add($path);
}

return $this;
Find: Select

* Register a pattern to execute against the source content.
*
Add After: Select

* If $replacement is a string, it must be plain text. Placeholders like $1 or \2 don't work.
* If you need that functionality, use a callback instead.
*
Find: Select

$this->patterns[] = array($pattern, $replacement);
}
Add After: Select

/**
* Both JS and CSS use the same form of multi-line comment, so putting the common code here.
*/
protected function stripMultilineComments()
{
// First extract comments we want to keep, so they can be restored later
// PHP only supports $this inside anonymous functions since 5.4
$minifier = $this;
$callback = function ($match) use ($minifier) {
$count = count($minifier->extracted);
$placeholder = '/*'.$count.'*/';
$minifier->extracted[$placeholder] = $match[0];

return $placeholder;
};
$this->registerPattern('/
# optional newline
\n?

# start comment
\/\*

# comment content
(?:
# either starts with an !
!
|
# or, after some number of characters which do not end the comment
(?:(?!\*\/).)*?

# there is either a @license or @preserve tag
@(?:license|preserve)
)

# then match to the end of the comment
.*?\*\/\n?

/ixs', $callback);

// Then strip all other comments
$this->registerPattern('/\/\*.*?\*\//s', '');
}
Find: Select
$processed = '';
$positions = array_fill(0, count($this->patterns), -1);
$matches = array();

while ($content) {
// find first match for all patterns
foreach ($this->patterns as $i => $pattern) {
list($pattern, $replacement) = $pattern;

// no need to re-run matches that are still in the part of the
// content that hasn't been processed
if ($positions[$i] >= 0) {
continue;
}

$match = null;
if (preg_match($pattern, $content, $match)) {
$matches[$i] = $match;

// we'll store the match position as well; that way, we
// don't have to redo all preg_matches after changing only
// the first (we'll still know where those others are)
$positions[$i] = strpos($content, $match[0]);
} else {
// if the pattern couldn't be matched, there's no point in
// executing it again in later runs on this same content;
// ignore this one until we reach end of content
unset($matches[$i]);
$positions[$i] = strlen($content);
}
}

// no more matches to find: everything's been processed, break out
if (!$matches) {
$processed .= $content;
break;
}

// see which of the patterns actually found the first thing (we'll
// only want to execute that one, since we're unsure if what the
// other found was not inside what the first found)
$discardLength = min($positions);
$firstPattern = array_search($discardLength, $positions);
$match = $matches[$firstPattern][0];

// execute the pattern that matches earliest in the content string
list($pattern, $replacement) = $this->patterns[$firstPattern];
$replacement = $this->replacePattern($pattern, $replacement, $content);

// figure out which part of the string was unmatched; that's the
// part we'll execute the patterns on again next
$content = substr($content, $discardLength);
$unmatched = (string) substr($content, strpos($content, $match) + strlen($match));

// move the replaced part to $processed and prepare $content to
// again match batch of patterns against
$processed .= substr($replacement, 0, strlen($replacement) - strlen($unmatched));
$content = $unmatched;

// first match has been replaced & that content is to be left alone,
// the next matches will start after this replacement, so we should
// fix their offsets
foreach ($positions as $i => $position) {
$positions[$i] -= $discardLength + strlen($match);
}
}

return $processed;
}

/**
* This is where a pattern is matched against $content and the matches
* are replaced by their respective value.
* This function will be called plenty of times, where $content will always
* move up 1 character.
*
* @param string $pattern Pattern to match
* @param string|callable $replacement Replacement value
* @param string $content Content to match pattern against
*
* @return string
*/
protected function replacePattern($pattern, $replacement, $content)
{
if (is_callable($replacement)) {
return preg_replace_callback($pattern, $replacement, $content, 1, $count);
} else {
return preg_replace($pattern, $replacement, $content, 1, $count);
}
Replace With: Select
$contentLength = strlen($content);
$output = '';
$processedOffset = 0;
$positions = array_fill(0, count($this->patterns), -1);
$matches = array();

while ($processedOffset < $contentLength) {
// find first match for all patterns
foreach ($this->patterns as $i => $pattern) {
list($pattern, $replacement) = $pattern;

// we can safely ignore patterns for positions we've unset earlier,
// because we know these won't show up anymore
if (array_key_exists($i, $positions) == false) {
continue;
}

// no need to re-run matches that are still in the part of the
// content that hasn't been processed
if ($positions[$i] >= $processedOffset) {
continue;
}

$match = null;
if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) {
$matches[$i] = $match;

// we'll store the match position as well; that way, we
// don't have to redo all preg_matches after changing only
// the first (we'll still know where those others are)
$positions[$i] = $match[0][1];
} else {
// if the pattern couldn't be matched, there's no point in
// executing it again in later runs on this same content;
// ignore this one until we reach end of content
unset($matches[$i], $positions[$i]);
}
}

// no more matches to find: everything's been processed, break out
if (!$matches) {
// output the remaining content
$output .= substr($content, $processedOffset);
break;
}

// see which of the patterns actually found the first thing (we'll
// only want to execute that one, since we're unsure if what the
// other found was not inside what the first found)
$matchOffset = min($positions);
$firstPattern = array_search($matchOffset, $positions);
$match = $matches[$firstPattern];

// execute the pattern that matches earliest in the content string
list(, $replacement) = $this->patterns[$firstPattern];

// add the part of the input between $processedOffset and the first match;
// that content wasn't matched by anything
$output .= substr($content, $processedOffset, $matchOffset - $processedOffset);
// add the replacement for the match
$output .= $this->executeReplacement($replacement, $match);
// advance $processedOffset past the match
$processedOffset = $matchOffset + strlen($match[0][0]);
}

return $output;
}

/**
* If $replacement is a callback, execute it, passing in the match data.
* If it's a string, just pass it through.
*
* @param string|callable $replacement Replacement value
* @param array $match Match data, in PREG_OFFSET_CAPTURE form
*
* @return string
*/
protected function executeReplacement($replacement, $match)
{
if (!is_callable($replacement)) {
return $replacement;
}
// convert $match from the PREG_OFFSET_CAPTURE form to the form the callback expects
foreach ($match as &$matchItem) {
$matchItem = $matchItem[0];
}

return $replacement($match);
Find: Select
*/
protected function extractStrings($chars = '\'"')
{
// PHP only supports $this inside anonymous functions since 5.4
$minifier = $this;
$callback = function ($match) use ($minifier) {
Replace With: Select
* @param string[optional] $placeholderPrefix
*/
protected function extractStrings($chars = '\'"', $placeholderPrefix = '')
{
// PHP only supports $this inside anonymous functions since 5.4
$minifier = $this;
$callback = function ($match) use ($minifier, $placeholderPrefix) {
Find: Select
$placeholder = $match[1].$count.$match[1];
$minifier->extracted[$placeholder] = $match[1].$match[2].$match[1];
Replace With: Select
$placeholder = $match[1] . $placeholderPrefix . $count . $match[1];
$minifier->extracted[$placeholder] = $match[1] . $match[2] . $match[1];
Find: Select
$this->registerPattern('/(['.$chars.'])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback);
Replace With: Select
$this->registerPattern('/([' . $chars . '])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback);
Find: Select

protected function canImportFile($path)
{
Add After: Select

$parsed = parse_url($path);
if (
// file is elsewhere
isset($parsed['host']) ||
// file responds to queries (may change, or need to bypass cache)
isset($parsed['query'])
) {
return false;
}
Find: Select
if (($handler = @fopen($path, 'w')) === false) {
throw new IOException('The file "'.$path.'" could not be opened for writing. Check if PHP has enough permissions.');
Replace With: Select
if ($path === '' || ($handler = @fopen($path, 'w')) === false) {
throw new IOException('The file "' . $path . '" could not be opened for writing. Check if PHP has enough permissions.');
Find: Select
if (($result = @fwrite($handler, $content)) === false || ($result < strlen($content))) {
throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.');
}
Replace With: Select
if (
!is_resource($handler) ||
($result = @fwrite($handler, $content)) === false ||
($result < strlen($content))
) {
throw new IOException('The file "' . $path . '" could not be written to. Check your disk space and file permissions.');
}
}

protected static function str_replace_first($search, $replace, $subject)
{
$pos = strpos($subject, $search);
if ($pos !== false) {
return substr_replace($subject, $replace, $pos, strlen($search));
}

return $subject;

./Sources/tasks/CreatePost-Notify.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select

$this->members['mentioned'] = Mentions::getMentionsByContent('msg', $msgOptions['id'], array_keys($msgOptions['mentioned_members']));
Add After: Select

$group_permissions = array(
'allowed' => array(),
'denied' => array(),
);
$request = $smcFunc['db_query']('', '
SELECT id_group, deny
FROM {db_prefix}board_permissions_view
WHERE id_board = {int:current_board}',
array(
'current_board' => $topicOptions['board'],
)
);
while (list ($id_group, $deny) = $smcFunc['db_fetch_row']($request))
$group_permissions[$deny === '0' ? 'allowed' : 'denied'][] = $id_group;
$smcFunc['db_free_result']($request);
Find: Select
b.member_groups, t.id_member_started, t.id_member_updated
FROM {db_prefix}log_notify AS ln
INNER JOIN {db_prefix}members AS mem ON (ln.id_member = mem.id_member)
LEFT JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)
LEFT JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board OR b.id_board = t.id_board)
WHERE ln.id_member != {int:member}
AND (ln.id_topic = {int:topic} OR ln.id_board = {int:board})',
Replace With: Select
t.id_member_started, t.id_member_updated
FROM {db_prefix}log_notify AS ln
INNER JOIN {db_prefix}members AS mem ON (ln.id_member = mem.id_member)
LEFT JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)
WHERE ' . ($type == 'topic' ? 'ln.id_board = {int:board}' : 'ln.id_topic = {int:topic}') . '
AND ln.id_member != {int:member}',
Find: Select
// Skip members who aren't allowed to see this board
$groups = array_merge(array($row['id_group'], $row['id_post_group']), (empty($row['additional_groups']) ? array() : explode(',', $row['additional_groups'])));

$allowed_groups = explode(',', $row['member_groups']);

if (!in_array(1, $groups) && count(array_intersect($groups, $allowed_groups)) == 0)
Replace With: Select
// Skip members who aren't allowed to see this board
$groups = array_merge(array($row['id_group'], $row['id_post_group']), (empty($row['additional_groups']) ? array() : explode(',', $row['additional_groups'])));

$is_denied = array_intersect($group_permissions['denied'], $groups) != array();
if (!in_array(1, $groups) && ($is_denied || array_intersect($groups, $group_permissions['allowed']) == array()))
Find: Select

unset($row['id_group'], $row['id_post_group'], $row['additional_groups']);
}
Add After: Select

// If this user subscribes both to the topic and the board there will be two records returned.
// Copy board/topic data to the new record or it will be lost.
if (!empty($this->members['watching'][$row['id_member']])) {
if ($this->members['watching'][$row['id_member']]['id_board'] > 0) {
$row['id_board'] = $this->members['watching'][$row['id_member']]['id_board'];
}
if ($this->members['watching'][$row['id_member']]['id_topic'] > 0) {
$row['id_topic'] = $this->members['watching'][$row['id_member']]['id_topic'];
}
}
Find: Select
// Filter out mentioned and quoted members who can't see this board.
if (!empty($this->members['mentioned']) || !empty($this->members['quoted']))
{
// This won't be set yet if no one is watching this board or topic.
if (!isset($allowed_groups))
{
$request = $smcFunc['db_query']('', '
SELECT member_groups
FROM {db_prefix}boards
WHERE id_board = {int:board}',
array(
'board' => $topicOptions['board'],
)
);
list($allowed_groups) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
$allowed_groups = explode(',', $allowed_groups);
}

foreach (array('mentioned', 'quoted') as $member_type)
{
foreach ($this->members[$member_type] as $member_id => $member_data)
{
if (!in_array(1, $member_data['groups']) && count(array_intersect($member_data['groups'], $allowed_groups)) == 0)
Replace With: Select
// Filter out mentioned and quoted members who can't see this board.
if (!empty($this->members['mentioned']) || !empty($this->members['quoted']))
{
foreach (array('mentioned', 'quoted') as $member_type)
{
foreach ($this->members[$member_type] as $member_id => $member_data)
{
$is_denied = array_intersect($group_permissions['denied'], $member_data['groups']) != array();
if (!in_array(1, $member_data['groups']) && ($is_denied || array_intersect($member_data['groups'], $group_permissions['allowed']) == array()))
Find: Select
$message_type = !empty($frequency) ? 'notify_boards_once' : 'notify_boards';

if (empty($modSettings['disallow_sendBody']) && !empty($this->prefs[$member_id]['msg_receive_body']))
$message_type .= '_body';
}

// If neither of the above, this might be a redundant row due to the OR clause in our SQL query, skip
else
continue;
Replace With: Select
$message_type = !empty($frequency) && $frequency == 2 ? 'notify_boards_once' : 'notify_boards';

if (empty($modSettings['disallow_sendBody']) && !empty($this->prefs[$member_id]['msg_receive_body']))
$message_type .= '_body';
}
Find: Select
foreach ($this->members['quoted'] as $member_id => $member_data)
Add Before: Select
$members_info = $this->getMinUserInfo([$posterOptions['id']]);

Find: Select
'QUOTENAME' => $posterOptions['name'],
Replace With: Select
'QUOTENAME' => un_htmlspecialchars(isset($members_info[$posterOptions['id']]['name']) ? $members_info[$posterOptions['id']]['name'] : $posterOptions['name']),

./Themes/default/Admin.template.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
<td colspan="2">', str_replace(',', ', ', $setting), '</td>
Replace With: Select
<td colspan="2" class="word_break">', str_replace(',', ', ', $setting), '</td>

./Themes/default/GenericList.template.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
// Show the page index (if this list doesn't intend to show all items).
if (!empty($cur_list['items_per_page']) && !empty($cur_list['page_index']))
echo '
<div class="pagesection">
Replace With: Select
// Show the page index (if this list doesn't intend to show all items).
if (!empty($cur_list['items_per_page']) && !empty($cur_list['page_index']))
echo '
<div class="pagesection floatleft">

./Themes/default/Login.template.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
url: form.prop("action") + ";ajax",
Replace With: Select
url: form.prop("action") + (form.prop("action").indexOf("?") !== -1 ? ";" : "?") + "ajax",
Find: Select
window.location.reload();';
Replace With: Select
if ($(data).find(".roundframe").length > 0 && $(data).find("body").length == 0) {
form.parent().html($(data).find(".roundframe").html());
}
else {
window.location.reload();
}';
Find: Select

form = $("#frmTfa");';

if (!empty($context['from_ajax']))
Add After: Select

{
Find: Select
$.post(form.prop("action"), form.serialize(), function(data) {
if (data.indexOf("<bo" + "dy") > -1)
document.location = ', JavaScriptEscape(!empty($_SESSION['login_url']) ? $_SESSION['login_url'] : $scripturl), ';
else {
form.parent().html($(data).find(".roundframe").html());
}
});

return false;
});';
Replace With: Select
$.ajax({
url: form.prop("action") + (form.prop("action").indexOf("?") !== -1 ? ";" : "?") + "ajax",
method: "POST",
headers: {
"X-SMF-AJAX": 1
},
xhrFields: {
withCredentials: typeof allow_xhjr_credentials !== "undefined" ? allow_xhjr_credentials : false
},
data: form.serialize(),
success: function(data) {
if (data.indexOf("<bo" + "dy") > -1) {';

if (empty($context['valid_cors_found']) || $context['valid_cors_found'] == 'same')
echo '
document.location = ', JavaScriptEscape(!empty($_SESSION['login_url']) ? $_SESSION['login_url'] : $scripturl), ';';
else
echo '
window.location.reload();';

echo '
}
else {
window.location.reload();
}
},
error: function(xhr) {
var data = xhr.responseText;
if (data.indexOf("<bo" + "dy") > -1) {
document.open();
document.write(data);
document.close();
}
else
form.parent().html($(data).filter("#fatal_error").html());
}
});

return false;
});';
}

./Themes/default/ManageBans.template.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
<input type="text" id="ban_name" name="ban_name" value="', $context['ban']['name'], '" size="45" maxlength="60">
Replace With: Select
<input type="text" id="ban_name" name="ban_name" value="', $context['ban']['name'], '" size="20" maxlength="20">
Find: Select
<input type="email" name="email" value="', $context['ban_suggestions']['email'], '" size="44" onfocus="document.getElementById(\'email_check\').checked = true;">
Replace With: Select
<input type="text" name="email" value="', $context['ban_suggestions']['email'], '" size="44" onfocus="document.getElementById(\'email_check\').checked = true;">
Find: Select
if (!$context['ban']['is_new'] && empty($context['ban_suggestions']))
Replace With: Select
if (!$context['ban']['is_new'] && empty($context['ban_suggestions']) && !empty($context['ban_list']))

./Themes/default/Post.template.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
<input type="number" name="attached_BBC_width" min="0" value="">
</div>
<div class="attached_BBC_height">
<label for="attached_BBC_height">', $txt['attached_insert_height'], '</label>
<input type="number" name="attached_BBC_height" min="0" value="">
Replace With: Select
<input type="number" name="attached_BBC_width" min="0" value="" placeholder="', $txt['attached_insert_placeholder'], '">
</div>
<div class="attached_BBC_height">
<label for="attached_BBC_height">', $txt['attached_insert_height'], '</label>
<input type="number" name="attached_BBC_height" min="0" value="" placeholder="', $txt['attached_insert_placeholder'], '">

./Themes/default/Profile.template.php

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select
document.getElementById(\'warn_body\').value = "', strtr($type['body'], array('"' => "'", "\n" => '\\n', "\r" => '')), '";';
Replace With: Select
document.getElementById(\'warn_body\').value = ', JavaScriptEscape($type['body']), ';';
Find: Select
<input type="file" size="44" name="attachment" id="avatar_upload_box" value="" onchange="readfromUpload(this)" onfocus="selectRadioByName(document.forms.creator.avatar_choice, \'upload\');" accept="image/gif, image/jpeg, image/jpg, image/png">', template_max_size('upload'), '
Replace With: Select
<input type="file" size="44" name="attachment" id="avatar_upload_box" value="" onchange="readfromUpload(this)" onfocus="selectRadioByName(document.forms.creator.avatar_choice, \'upload\');" accept="image/gif, image/jpeg, image/jpg, image/png, image/webp">', template_max_size('upload'), '

./Themes/default/Recent.template.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
echo '
<div class="pagesection">
', !empty($context['recent_buttons']) ? template_button_strip($context['recent_buttons'], 'right') : '', '
', $context['menu_separator'], '
<div class="pagelinks floatleft">
<a href="#recent" class="button">', $txt['go_up'], '</a>
Replace With: Select
echo '
<div class="pagesection">
', !empty($context['recent_buttons']) ? template_button_strip($context['recent_buttons'], 'right') : '', '
', $context['menu_separator'], '
<div class="pagelinks floatleft">
<a href="#recent" class="button" id="bot">', $txt['go_up'], '</a>
Find: Select
</div><!-- #unreadreplies -->
<div class="pagesection">
', !empty($context['recent_buttons']) ? template_button_strip($context['recent_buttons'], 'right') : '', '
', $context['menu_separator'], '
<div class="pagelinks floatleft">
<a href="#recent" class="button">', $txt['go_up'], '</a>
Replace With: Select
</div><!-- #unreadreplies -->
<div class="pagesection">
', !empty($context['recent_buttons']) ? template_button_strip($context['recent_buttons'], 'right') : '', '
', $context['menu_separator'], '
<div class="pagelinks floatleft">
<a href="#recent" class="button" id="bot">', $txt['go_up'], '</a>

./Themes/default/Register.template.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.3
Replace With: Select
* @version 2.1.5
Find: Select
<div class="righttext"><input type="submit" value="', $txt['save'], '" tabindex="', $context['tabindex']++, '" class="button" onclick="return resetAgreementConfirm()" />
Replace With: Select
<input type="submit" value="', $txt['save'], '" tabindex="', $context['tabindex']++, '" class="button" onclick="return resetAgreementConfirm()" />

./Themes/default/Settings.template.php

Find: Select
* @copyright 2022 Simple Machines and individual contributors
Replace With: Select
* @copyright 2025 Simple Machines and individual contributors
Find: Select
* @version 2.1.0
Replace With: Select
* @version 2.1.5
Find: Select
'label' => $txt['no_new_reply_warning'],
'default' => true,
),
array(
'id' => 'auto_notify',
'label' => $txt['auto_notify'],
'default' => true,
),
Replace With: Select
'label' => $txt['no_new_reply_warning'],
'default' => true,
),
Find: Select
$txt['theme_opt_calendar'],
Replace With: Select
!empty($modSettings['cal_enabled']) ? $txt['theme_opt_calendar'] : '',

./Themes/default/css/attachments.css

Find: Select
overflow: auto;
Replace With: Select
overflow: visible;
Find: Select
justify-content: center;
width: 200px;
overflow: hidden;
Replace With: Select
justify-content: center;
width: 200px;
Find: Select

margin: 0;
box-shadow: none;
Add After: Select

z-index: 2;
Find: Select
border-radius: 0;
Replace With: Select
border-radius: 0 0 7px 7px;

./Themes/default/css/index.css

Find: Select
#quick_mod_jump_to_select {
width: 29ch;
Replace With: Select
#quick_mod_jump_to_select,
#list_integration_hooks select {
width: 29ch;
overflow: hidden;

./Themes/default/css/jquery-ui.datepicker.css

Find: Select
width: 17em;
Replace With: Select
width: 18em;

./Themes/default/css/jquery.sceditor.css

Find: Select
@media screen and (min-width: 1024px) {
div.sceditor-smileyPopup {
max-height: 50%;
width: 50%;
position: fixed;
}
#sceditor-popup {
height: 100%;
}
#sceditor-popup-smiley {
height: 90%;
overflow: auto;
}
}
@media screen and (max-width: 1024px) {
div.sceditor-smileyPopup {
width: 90%;
position: absolute;
}
Replace With: Select

div.sceditor-smileyPopup {
top: unset;
left: 0;
right: 0;
transform: unset;
position: absolute;
min-height: 100px;
min-width: 250px;
margin-inline: auto;
display: flex;
box-sizing: border-box;
max-height: 215px;
max-width: 450px;
}
div.sceditor-smileyPopup #sceditor-popup {
width: 100%;
display: flex;
flex-direction: column;
}
div.sceditor-smileyPopup #sceditor-popup div#sceditor-popup-smiley {
flex-grow: 1;
align-self: flex-start;
}
div.sceditor-smileyPopup #sceditor-popup span.button {
margin-top: 20px;
align-self: center;

./Themes/default/scripts/admin.js

Find: Select
// Make sure no sneaky people are trying to be sneaky
var regex = new RegExp("^/(" + smf_smiley_sets.split(",").join("|") + ")/[^.]+\.(gif|png|jpg|jpeg|tiff|svg)$");
Replace With: Select
// Make sure no sneaky people are trying to be sneaky
var regex = new RegExp("^/(" + smf_smiley_sets.split(",").join("|") + ")/[^.]+\.(gif|png|jpg|jpeg|tiff|svg|webp)$");

./Themes/default/scripts/jquery.sceditor.smf.js

Find: Select
* @copyright 2023 Simple Machines and individual contributors
Replace With: Select
* @copyright 2024 Simple Machines and individual contributors
Find: Select
* @version 2.1.4
Replace With: Select
* @version 2.1.5
Find: Select

appendEmoticon: function (code, emoticon, description) {
Add After: Select

var base = this;
Find: Select
line = $('<div>');
base = this;
Replace With: Select
line = $('<div>');
var base = this;
Find: Select
if ($(".sceditor-smileyPopup").length > 0)
{
$(".sceditor-smileyPopup").fadeIn('fast');
Replace With: Select
var smileyPopup = base.editorMainWrapper.querySelector(".sceditor-smileyPopup");
if (smileyPopup)
{
$(smileyPopup).fadeIn('fast');
Find: Select
$(".sceditor-smileyPopup").fadeOut('fast');
Replace With: Select
$(base.editorMainWrapper.querySelector(".sceditor-smileyPopup")).fadeOut('fast');
});
$(document).mouseup(function (e) {
if (allowHide && !popupContent.is(e.target) && popupContent.has(e.target).length === 0)
$(smileyPopup).fadeOut('fast');
}).keyup(function (e) {
if (e.keyCode === 27)
$(smileyPopup).fadeOut('fast');
Find: Select
.appendTo($('.sceditor-container'));

$('.sceditor-smileyPopup').animaDrag({
Replace With: Select
.appendTo(base.editorMainWrapper);

$(base.editorMainWrapper.querySelector('.sceditor-smileyPopup')).animaDrag({
Find: Select
$(".sceditor-toolbar").append(content);
if (typeof moreButton !== "undefined")
content.append($('<center/>').append(moreButton));
}
};

var createFn = sceditor.create;
var isPatched = false;
Replace With: Select
$(base.editorMainWrapper.querySelector(".sceditor-toolbar")).append(content);
if (typeof moreButton !== "undefined")
content.append($('<center/>').append(moreButton));
}
};

var createFn = sceditor.create;
Find: Select
// Constructor isn't exposed so get reference to it when
// creating the first instance and extend it then
var instance = sceditor.instance(textarea);
if (!isPatched && instance) {
sceditor.utils.extend(instance.constructor.prototype, extensionMethods);
Replace With: Select
// Constructor isn't exposed so get reference to it when
// creating the first instance and extend it then
var instance = sceditor.instance(textarea);
if (instance && typeof instance.isPatched == 'undefined') {
sceditor.utils.extend(instance.constructor.prototype, extensionMethods);
instance.editorMainWrapper = instance.getContentAreaContainer().closest('.sceditor-container');
Find: Select
document.querySelector(".sceditor-container").removeAttribute("style");
document.querySelector(".sceditor-container textarea").style.height = options.height;
document.querySelector(".sceditor-container textarea").style.flexBasis = options.height;

isPatched = true;
Replace With: Select
instance.editorMainWrapper.removeAttribute("style");
instance.editorMainWrapper.querySelector("textarea").style.height = options.height;
instance.editorMainWrapper.querySelector("textarea").style.flexBasis = options.height;

instance.isPatched = true;
Find: Select
var contentUrl = smf_scripturl +'?action=dlattach;attach='+ id + ';type=preview;thumb';
contentIMG = new Image();
contentIMG.src = contentUrl;
}

// If not an image, show a boring ol' link
if (typeof contentUrl === "undefined" || contentIMG.getAttribute('width') == 0)
return '<a href="' + smf_scripturl + '?action=dlattach;attach=' + id + ';type=preview;file"' + attribs + '>' + content + '</a>';
Replace With: Select
var contentUrl = smf_scripturl +'?action=dlattach;attach='+ id + ';preview;image';
contentIMG = new Image();
contentIMG.src = contentUrl;
}

// If not an image, show a boring ol' link
if (typeof contentUrl === "undefined" || contentIMG.getAttribute('width') == 0)
return '<a href="' + smf_scripturl + '?action=dlattach;attach=' + id + ';file"' + attribs + '>' + content + '</a>';

./Themes/default/scripts/quotedText.js

Find: Select
function getSelectedText(divID)
{
if (typeof divID == 'undefined' || divID == false)
return false;

var text = '',
selection,
found = 0,
container = document.createElement("div");

if (window.getSelection)
{
selection = window.getSelection();
text = selection.toString();
}
else if (document.selection && document.selection.type != 'Control')
{
selection = document.selection.createRange();
text = selection.text;
}

// Need to be sure the selected text does belong to the right div.
for (var i = 0; i < selection.rangeCount; i++) {
s = getClosest(selection.getRangeAt(i).startContainer, divID);
e = getClosest(selection.getRangeAt(i).endContainer, divID);

if (s !== null && e !== null)
{
found = 1;
container.appendChild(selection.getRangeAt(i).cloneContents());
text = container.innerHTML;
break;
}
}

return found === 1 ? text : false;
Replace With: Select
function getSelectedText(node) {
var selection = window.getSelection();
// Need to be sure the selected text includes the right div.
for (var i = 0; i < selection.rangeCount; i++) {
const range = selection.getRangeAt(i);

if (range.intersectsNode(node)) {
const
frag = range.cloneContents(),
s = getClosest(range.startContainer, node.id),
e = getClosest(range.endContainer, node.id);

if (s && e) {
const container = document.createElement("div");
container.appendChild(range.cloneContents());
return container.innerHTML;
} else {
const el = frag.getElementById(node.id);
return el?.innerHTML;
}
}
}
Find: Select
// Do a call to make sure this is a valid message.
$.ajax({
url: smf_prepareScriptUrl(smf_scripturl) + 'action=quotefast;quote=' + oOptions.msgID + ';xml;pb='+ oEditorID + ';mode=' + (oEditorObject.bRichTextEnabled ? 1 : 0),
Replace With: Select
// Do a call to make sure this is a valid message.
$.ajax({
url: smf_prepareScriptUrl(smf_scripturl) + 'action=quotefast;quote=' + oOptions.msgID + ';xml;pb='+ oEditorID + ';mode=' + (oEditorObject?.bRichTextEnabled ? 1 : 0),
Find: Select
// Get any selected text.
oSelected.text = getSelectedText(oSelected.divID);
Replace With: Select
// Get any selected text.
const node = this;
oSelected.text = getSelectedText(this);
Find: Select
// Delay the check a bit to allow the deselection to happen.
setTimeout(function() {
selectedText = getSelectedText(oSelected.divID);
Replace With: Select
// Delay the check a bit to allow the deselection to happen.
setTimeout(function() {
selectedText = getSelectedText(node);

./Themes/default/scripts/script.js

Find: Select
403: function() {
oPopup_body.html(banned_text);
Replace With: Select
403: function(res, status, xhr) {
let errorMsg = res.getResponseHeader('x-smf-errormsg');
oPopup_body.html(errorMsg ?? banned_text);
Find: Select

img.style.height = link.style.height;
Add After: Select

img.classList.toggle('original_size');

./Themes/default/scripts/smf_fileUpload.js

Find: Select
// This file has reached the max total size per post.
if (totalKB > 0 && currentlyUsedKB > totalKB) {
Replace With: Select
// This file has reached the max total size per post.
if (totalKB > 0 && (currentlyUsedKB + uploadedFileKB) > totalKB) {
Find: Select
// If the file is too small, it won't have a thumbnail, show the regular file.
else if (typeof file.isMock !== "undefined" && typeof file.attachID !== "undefined") {
myDropzone.emit('thumbnail', file, smf_prepareScriptUrl(smf_scripturl) + 'action=dlattach;attach=' + (file.thumbID > 0 ? file.thumbID : file.attachID) + ';type=preview');
Replace With: Select
// If the file is too small, it won't have a thumbnail, show the regular file.
else if (typeof file.isMock !== "undefined" && typeof file.attachID !== "undefined") {
myDropzone.emit('thumbnail', file, smf_prepareScriptUrl(smf_scripturl) + 'action=dlattach;attach=' + (file.thumbID > 0 ? file.thumbID : file.attachID) + ';preview');

./Themes/default/scripts/topic.js

Find: Select
return reqOverlayDiv(url, title, 'post/thumbup.png');
Replace With: Select
return reqOverlayDiv(url, title, 'like');

Code

auto_1.php
This file should not be able to execute standalone.You may have to run the following queries manually.
Query: Select
<?php
define('REQUIRED_PHP_VERSION', '7.1.0');
if (version_compare(PHP_VERSION, REQUIRED_PHP_VERSION, '<')) {
fatal_error('This update requires a minimum of PHP ' . REQUIRED_PHP_VERSION . ' in order to function. (You are currently running PHP ' . PHP_VERSION . ')');
}

// Update smfVersion.
updateSettings(array('smfVersion' => '2.1.5'));

// Fix indexes for search results and notifications.
if ($smcFunc['db_title'] === POSTGRE_TITLE)
{
$smcFunc['db_query']('', '
TRUNCATE TABLE {db_prefix}log_search_results',
array()
);

$smcFunc['db_query']('', '
ALTER TABLE {db_prefix}log_search_results
{raw:drop_pk}',
array(
'drop_pk' => $smcFunc['db_quote']('DROP CONSTRAINT {db_prefix}log_search_results_pkey', array())
)
);

$smcFunc['db_query']('', '
ALTER TABLE {db_prefix}log_search_results
ADD PRIMARY KEY (id_search, id_topic, id_msg)',
array()
);

$smcFunc['db_query']('', '
CREATE INDEX {db_prefix}log_notify_id_board ON {db_prefix}log_notify (id_board)',
array()
);
}
else
{
$smcFunc['db_query']('', '
TRUNCATE TABLE {db_prefix}log_search_results',
array()
);

$smcFunc['db_query']('', '
ALTER TABLE {db_prefix}log_search_results
DROP PRIMARY KEY',
array()
);

$smcFunc['db_query']('', '
ALTER TABLE {db_prefix}log_search_results
ADD PRIMARY KEY (id_search, id_topic, id_msg)',
array()
);

$smcFunc['db_query']('', '
ALTER TABLE {db_prefix}log_notify
ADD INDEX id_board (id_board)',
array()
);

}

File Operations

Move the included file "ConverterInterface.php" to "./Sources/minify/path-converter/src/".
Move the included file "NoConverter.php" to "./Sources/minify/path-converter/src/".
Move the included file "UpdateUnicode.php" to "./Sources/tasks/".
Advertisement: