SMF 2.1 |
---|
SMF 2.1.5 to SMF 2.1.6 |
SMF 2.1.4 to SMF 2.1.5 |
SMF 2.1.3 to SMF 2.1.4 |
SMF 2.1.2 to SMF 2.1.3 |
SMF 2.1.1 to SMF 2.1.2 |
SMF 2.1.0 to SMF 2.1.1 |
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
define('SMF_SOFTWARE_YEAR', '2023');
define('SMF_SOFTWARE_YEAR', '2025');
// Just in case there's a problem...
set_error_handler('smf_error_handler_cron');
// Just in case there's a problem...
set_error_handler('smf_error_handler_cron');
set_exception_handler('smf_exception_handler_cron');
die('No direct access...');
}
/**
* 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());
}
}
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
define('SMF_SOFTWARE_YEAR', '2023');
define('SMF_SOFTWARE_YEAR', '2025');
$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.
$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.
// Version: 2.1.0; ManagePermissions
// Version: 2.1.5; ManagePermissions
$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.';
$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.';
$txt['custom_profile_desc'] = 'From this page you can create your own custom profile fields that fit in with your own forums requirements';
$txt['custom_profile_desc'] = 'From this page you can create your own custom profile fields that fit in with your own forum\'s requirements';
$txt['uninstall_modification'] = 'Uninstall Mod';
$txt['uninstall_language'] = 'Uninstall Language';
$txt['uninstall_avatar'] = 'Uninstall Avatar Pack';
$txt['uninstall_unknown'] = 'Uninstall Package';
$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';
$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';
$txt['notify_important_email'] = 'Receive forum newsletters, announcements and important notifications by email.';
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// Loop until we're out of data.
while ($data != '')
{
// Find and remove the next tag.
preg_match('/\A<([\w\-:]+)((?:\s+.+?)?)([\s]?\/)?' . '>/', $data, $match);
// 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);
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
|| !in_array($columns[$key]['type'], array('text', 'mediumntext', 'largetext', 'varchar', 'char'))
|| !in_array($columns[$key]['type'], array('text', 'mediumtext', 'largetext', 'varchar', 'char'))
while ($row = $smcFunc['db_fetch_assoc']($request))
{
$row = array_change_key_case($row, CASE_LOWER);
// 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'))
// 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'))
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// 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;
// 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);
loadTheme();
}
// 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);
// Load the language file, only if it needs to be reloaded
if ($reload_lang_file)
// Load the language file, only if it needs to be reloaded
if ($reload_lang_file && !empty($txt[$error]))
die('No direct access...');
}
/**
* 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);
}
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
$fix_utf8mb4 = function($string) use ($utf8, $smcFunc)
{
if (!$utf8 || $smcFunc['db_mb4'])
return $string;
$smcFunc['fix_utf8mb4'] = function($string) use ($utf8, $smcFunc)
{
if (!$utf8 || $smcFunc['db_mb4'])
return $string;
$string = (string) $string;
'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)));
'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)));
'convert_case' => function($string, $case, $simple = false, $form = 'c') use (&$smcFunc, $utf8, $ent_check, $fix_utf8mb4, $sourcedir)
'convert_case' => function($string, $case, $simple = false, $form = 'c') use (&$smcFunc, $utf8, $ent_check, $sourcedir)
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
trigger_error(sprintf($txt['logActions_not_array'], $log['action']), E_USER_NOTICE);
throw new \TypeError(sprintf($txt['logActions_not_array'], $log['action']));
trigger_error($txt['logActions_topic_not_numeric'], E_USER_NOTICE);
throw new \TypeError($txt['logActions_topic_not_numeric']);
trigger_error($txt['logActions_message_not_numeric'], E_USER_NOTICE);
throw new \TypeError($txt['logActions_message_not_numeric']);
trigger_error($txt['logActions_member_not_numeric'], E_USER_NOTICE);
throw new \TypeError($txt['logActions_member_not_numeric']);
trigger_error($txt['logActions_board_not_numeric'], E_USER_NOTICE);
throw new \TypeError($txt['logActions_board_not_numeric']);
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// 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=">"> ';
$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,
// 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',
'position' => 'above_table_headers',
'position' => 'above_column_headers',
// Does a hook want to display their attachments better?
call_integration_hook('integrate_attachments_browse', array(&$listOptions, &$titles, &$list_title));
$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=">"> ';
$list_title .= '<a href="' . $scripturl . $details[0] . '">' . $details[1] . '</a>';
}
$listOptions['title'] = $list_title;
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
'db' => 'notes',
'class' => 'smalltext',
'db' => 'notes',
'class' => 'smalltext word_break',
'db' => 'reason',
'class' => 'smalltext',
'db' => 'reason',
'class' => 'smalltext word_break',
'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',
'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',
call_integration_hook('integrate_edit_bans', array(&$ban_info, empty($_REQUEST['bg'])));
// Limit 'reason' characters
$ban_info['reason'] = $smcFunc['truncate']($ban_info['reason'], 255);
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// Don't do anything with files we don't understand.
if (!in_array($extension, array('php', 'jpg', 'gif', 'jpeg', 'png', 'txt')))
// Don't do anything with files we don't understand.
if (!in_array($extension, array('php', 'jpg', 'gif', 'jpeg', 'png', 'txt', 'webp')))
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// Private PM/email subjects and similar shouldn't be shown in the mailbox area.
if (!empty($row['private']))
$row['subject'] = $txt['personal_message'];
// 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']);
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
createToken($context['not_done_token']);
createToken($context['not_done_token']);
$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>';
$hooks_filters[] = '<option' . ($current_filter == $hook ? ' selected ' : '') . ' value="' . $hook . '">' . $hook . '</option>';
return $instance . $data['real_function'];
},
'class' => 'word_break',
</ul>'
),
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',
),
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
'position' => 'above_table_headers',
'position' => 'above_column_headers',
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
'profile_upload_avatar',
'profile_remote_avatar',
'profile_gravatar',
'profile_remote_avatar' => array(false, 'profile'),
'profile_gravatar' => array(false, 'profile'),
// Hide Likes/Mentions permissions...
// Hide Likes/Mentions/Gravatar permissions...
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// Set the right tab to be selected.
$context[$context['admin_menu_name']]['current_subsection'] = 'editsets';
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
// Set the right tab to be selected.
$context[$context['admin_menu_name']]['current_subsection'] = 'editsets';
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg', 'webp');
array(
'position' => 'above_table_headers',
array(
'position' => 'above_column_headers',
// Some useful arrays... types we allow - and ports we don't!
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
// Some useful arrays... types we allow - and ports we don't!
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg', 'webp');
$context['smileys_dir'] = empty($modSettings['smileys_dir']) ? $boarddir . '/Smileys' : $modSettings['smileys_dir'];
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
$context['smileys_dir'] = empty($modSettings['smileys_dir']) ? $boarddir . '/Smileys' : $modSettings['smileys_dir'];
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg', 'webp');
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
'body' => $smcFunc['htmlspecialchars']($row['body']),
// Redo htmlspecialchars for the sake of old data that might have incorrectly encoded entities.
'body' => $smcFunc['htmlspecialchars'](un_htmlspecialchars($row['body'])),
// Safety first.
$_POST['template_title'] = $smcFunc['htmlspecialchars']($_POST['template_title']);
// Safety first.
$_POST['template_title'] = $smcFunc['htmlspecialchars']($_POST['template_title']);
$_POST['template_body'] = $smcFunc['htmlspecialchars']($_POST['template_body']);
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// Replace tokens with links in the reason.
$reason_replacements = array(
$txt['movetopic_auto_board'] => '[url="' . $scripturl . '?board=' . $_POST['toboard'] . '.0"]' . $board_name . '[/url]',
// Replace tokens with links in the reason.
$reason_replacements = array(
$txt['movetopic_auto_board'] => '[url="' . $scripturl . '?board=' . $_POST['toboard'] . '.0"]' . $board_name . '[/url]',
// 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]',
// 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]',
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
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)
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 )
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)
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)
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)
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)
);
$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)
);
$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)
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)
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)
$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))';
// 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')";
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)
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)
// 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']));
// 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']));
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// 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')))
// 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')
// 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')))
// 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')))
$context['modification_types'] = array('modification', 'avatar', 'language', 'unknown');
$context['modification_types'] = array('modification', 'avatar', 'language', 'unknown', 'smiley');
while ($entry = readdir($dh))
{
// Bypass directory abbreviations altogether...
if ($entry == '.' || $entry == '..')
continue;
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// 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']) ? '' : ' ' . $txt['last_edit_reason'] . ': ' . $row['modified_reason'];
// 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']));
'attachID' => $attachment['id_attach'],
'href' => $scripturl . '?action=dlattach;attach=' . $attachment['id_attach'],
text_max_size_progress: ' . JavaScriptEscape('{currentRemain} ' . ($modSettings[$type] >= 1024 ? $txt['megabyte'] : $txt['kilobyte']) . ' / {currentTotal} ' . ($modSettings[$type] >= 1024 ? $txt['megabyte'] : $txt['kilobyte'])) . ',
text_max_size_progress: ' . JavaScriptEscape('{currentRemain} ' . ($modSettings['attachmentPostLimit'] >= 1024 ? $txt['megabyte'] : $txt['kilobyte']) . ' / {currentTotal} ' . ($modSettings['attachmentPostLimit'] >= 1024 ? $txt['megabyte'] : $txt['kilobyte'])) . ',
'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']), ': ') : '',
),
);
}
// 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>',
),
);
'icon' => isset($_REQUEST['icon']) ? preg_replace('~[\./\\\\*\':"<>]~', '', $_REQUEST['icon']) : null,
'modify_reason' => (isset($_POST['modify_reason']) ? $_POST['modify_reason'] : ''),
'icon' => isset($_REQUEST['icon']) ? preg_replace('~[\./\\\\*\':"<>]~', '', $_REQUEST['icon']) : null,
// 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'];
// 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'] : '');
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
include_once(implode(DIRECTORY_SEPARATOR, array($sourcedir, 'minify', 'src', 'Minify.php')));
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')));
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg');
$allowedTypes = array('gif', 'png', 'jpg', 'jpeg', 'tiff', 'svg', 'webp');
// We are removing board preferences
elseif (isset($_POST['remove_notify_board']) && !empty($_POST['notify_boards']))
// We are removing board preferences
elseif (isset($_POST['remove_notify_boards']) && !empty($_POST['notify_boards']))
$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)))
$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)))
// 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)
// 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)
WHERE id_alert IN({array_int:toMark})',
array(
WHERE id_alert IN({array_int:toMark})
AND id_member = {int:memID}',
array(
'memID' => $memID,
WHERE id_alert IN({array_int:toDelete})',
array(
WHERE id_alert IN({array_int:toDelete})
AND id_member = {int:memID}',
array(
'memID' => $memID,
WHERE id_alert IN ({array_int:alerts})',
array(
WHERE id_alert IN ({array_int:alerts})
AND id_member = {int:member}',
array(
'member' => $memID,
// Because of the way this stuff works, we want to do this ourselves.
if (isset($_POST['edit_notify_boards']) || isset($_POSt['remove_notify_boards']))
// Because of the way this stuff works, we want to do this ourselves.
if (isset($_POST['edit_notify_boards']) || isset($_POST['remove_notify_boards']))
'allow_gravatar' => !empty($modSettings['gravatarEnabled']),
'allow_gravatar' => !empty($modSettings['gravatarEnabled']) && allowedTo('profile_gravatar'),
$mime_valid = check_mime_type($profile_vars['avatar'], 'image/', true);
$mime_type = get_mime_type($profile_vars['avatar'], true);
$mime_valid = strpos($mime_type, 'image/') === 0;
elseif (empty($mime_valid))
return 'bad_avatar';
// 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';
}
$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)
$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)
'6' => 'bmp'
);
$extension = isset($extensions[$sizes[2]]) ? $extensions[$sizes[2]] : 'bmp';
$mime_type = 'image/' . ($extension === 'jpg' ? 'jpeg' : ($extension === 'bmp' ? 'x-ms-bmp' : $extension));
'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);
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// 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'];
// 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'];
// Basic sanitation.
$memID = (int) $memID;
$unread = $to_fetch === false;
// Basic sanitation.
$memID = (int) $memID;
$unread = $to_fetch === false;
// Did a mod already take care of this one?
if (!empty($alerts[$id_alert]['text']))
continue;
// For developer convenience.
$alert = &$alerts[$id_alert];
// 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;
$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];
$alert['extra']['user_name'] = $user_profile[$alert['extra']['user_id']]['real_name'];
}
'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'],
'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'],
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// 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 . '&', $buffer);
// 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 . '&', $buffer);
// Let's do something special for session ids!
if (defined('SID') && SID != '')
$buffer = preg_replace_callback(
'~"' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&))((?:board|topic)=[^#"]+?)(#[^"]*?)?"~',
function($m)
{
global $scripturl;
return '"' . $scripturl . "/" . strtr("$m[1]", '&;=', '//,') . ".html?" . SID . (isset($m[2]) ? $m[2] : "") . '"';
// Let's do something special for session ids!
if (isset($sid) && $sid != '')
$buffer = preg_replace_callback(
'~"' . preg_quote($scripturl, '~') . '\?(?:' . $sid . '(?:;|&|&))((?:board|topic)=[^#"]+?)(#[^"]*?)?"~',
function($m)
{
global $scripturl;
return '"' . $scripturl . "/" . strtr("$m[1]", '&;=', '//,') . ".html?" . $sid . (isset($m[2]) ? $m[2] : "") . '"';
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
$group_clause = '1=1';
// Fetch all the board names.
$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);
'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!
);
$profiles[] = $row['id_profile'];
}
$smcFunc['db_free_result']($request);
foreach ($boards as $id => $board)
if ($board['profile'] == $row['id_profile'])
$board_permissions[$id][$row['id_group']][$row['permission']] = $row['add_deny'];
$board_permissions[$row['id_profile']][$row['id_group']][$row['permission']] = $row['add_deny'];
// 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');
$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');
// 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];
}
// Set the data for this group to be the local permission.
$curData[$id_group] = $group_permissions[$ID_PERM];
}
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// 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,
// 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),
);
}
$smcFunc['db_free_result']($request);
'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);
}
// Send each email, yea!
$failed_emails = array();
foreach ($emails as $email)
{
// 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.
// Hopefully it sent?
if (!$result)
$failed_emails[] = array($email['to'], $email['body'], $email['subject'], $email['headers'], $email['send_html'], $email['time_sent'], $email['private']);
// 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']);
}
// 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'),
// 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'),
array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
);
// 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()
);
// 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)
// 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)
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
if (empty($search_params['topic']) && empty($search_params['show_complete']))
{
$main_query['select']['id_topic'] = 't.id_topic';
if (empty($search_params['topic']) && empty($search_params['show_complete']))
{
$main_query['group_by'][] = 't.id_topic';
}
else
{
// This is outrageous!
$main_query['select']['id_topic'] = 'm.id_msg AS id_topic';
$main_query['group_by'][] = 't.id_topic';
}
else
{
// *** 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
// *** 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
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
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) . '%';
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) . '%';
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// 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);
// 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...');
// If we get here, something's gone wrong.... but let's try anyway.
trigger_error('No direct access...', E_USER_ERROR);
// If we get here, something's gone wrong.... but let's try anyway.
die('No direct access...');
// We really should never fall through here, for very important reasons. Let's make sure.
trigger_error('No direct access...', E_USER_ERROR);
// We really should never fall through here, for very important reasons. Let's make sure.
die('No direct access...');
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
global $modSettings, $boardurl, $sc, $smcFunc, $cache_enable;
global $context, $modSettings, $boardurl, $sc, $smcFunc, $cache_enable;
session_set_save_handler('sessionOpen', 'sessionClose', 'sessionRead', 'sessionWrite', 'sessionDestroy', 'sessionGC');
$context['session_handler'] = new SmfSessionHandler();
session_set_save_handler($context['session_handler'], true);
* 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);
* 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;
}
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
$file['etag'] = $file['exists'] ? sha1_file($file['filePath']) : '';
$file['filename'] = un_htmlspecialchars($file['filename']);
// 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']))
// 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']))
// 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'))))
// 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'))))
// 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)))
// 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)))
// 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')))
// 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')))
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// Replace tokens with links in the reason.
$reason_replacements = array(
$txt['movetopic_auto_topic'] => '[iurl="' . $scripturl . '?topic=' . $id_topic . '.0"]' . $target_subject . '[/iurl]',
// Replace tokens with links in the reason.
$reason_replacements = array(
$txt['movetopic_auto_topic'] => '[iurl="' . $scripturl . '?topic=' . $id_topic . '.0"]' . $target_subject . '[/iurl]',
// 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]',
// 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]',
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
$_SESSION['temp_attachments'][$attachID]['type'] = 'image/' . $context['valid_image_types'][$size[2]];
}
}
// 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;
}
}
list ($attachmentOptions['width'], $attachmentOptions['height']) = $size;
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;
}
'href' => $scripturl . '?action=dlattach;attach=' . $attachment['id_thumb'] . ';image',
'href' => $scripturl . '?action=dlattach;attach=' . $attachment['id_thumb'] . ';image;thumb',
$attachmentData[$i]['downloads']++;
// 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'] . ') ';
}
}
global $context, $modSettings, $smcFunc;
global $context, $modSettings, $smcFunc, $sourcedir;
// 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.
// 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.
foreach ($rows as $row)
{
// 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;}');
}
}
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
trigger_error(sprintf($txt['modify_board_incorrect_move_to'], $boardOptions['move_to']), E_USER_ERROR);
throw new \Exception(sprintf($txt['modify_board_incorrect_move_to'], $boardOptions['move_to']));
// 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,
)
);
// 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,
)
);
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);
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');
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
$cal_date = ($start_object >= $low_object) ? $start_object : $low_object;
$cal_date = ($start_object >= $low_object) ? (clone $start_object) : (clone $low_object);
'allowed_groups' => explode(',', $row['member_groups']),
'allowed_groups' => isset($row['member_groups']) ? explode(',', $row['member_groups']) : array(),
hr: "' . $txt['hour_short'] . '",
hrs: "' . $txt['hours_short'] . '",
// Find all possible variants of AM and PM for this language.
$replacements[strtolower($txt['time_am'])] = 'AM';
$replacements[strtolower($txt['time_pm'])] = 'PM';
// 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';
}
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
* @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');
* @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');
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
return sprintf('\'%1$s\'', mysqli_real_escape_string($connection, $replacement));
return sprintf('\'%1$s\'', mysqli_real_escape_string($connection, isset($smcFunc['fix_utf8mb4']) ? $smcFunc['fix_utf8mb4']($replacement) : $replacement));
$replacement[$key] = sprintf('\'%1$s\'', mysqli_real_escape_string($connection, $value));
$replacement[$key] = sprintf('\'%1$s\'', mysqli_real_escape_string($connection, isset($smcFunc['fix_utf8mb4']) ? $smcFunc['fix_utf8mb4']($value) : $value));
// 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]];
// 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]];
?>
/**
* 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);
}
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
trigger_error($txt['postgres_id_not_int'], E_USER_ERROR);
throw new \TypeError('postgres_id_not_int');
?>
/**
* 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);
}
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
fclose($fp);
return true;
}
/**
* 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;
}
'6' => 'bmp',
'15' => 'wbmp'
'6' => 'bmp',
'15' => 'wbmp',
'18' => 'webp'
$success = imagewbmp($dst_img, $destName);
elseif (!empty($preferred_format) && ($preferred_format == 18) && function_exists('imagewebp'))
$success = imagewebp($dst_img, $destName);
?>
/**
* 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));
}
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
$menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$sa]['disabled'] = true;
}
// 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;
}
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// Not a directory and doesn't exist already...
if (substr($current['filename'], -1, 1) != '/' && $destination !== null && !file_exists($destination . '/' . $current['filename']))
// 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']))
if ($destination !== null && !file_exists($destination) && !$single_file)
if ($destination !== null && (substr($destination, 0, 2) != '*/') && !file_exists($destination) && !$single_file)
// If this is a file, and it doesn't exist.... happy days!
if ($is_file)
// 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)
'description' => $action->fetch('.')
'description' => $action->fetch('.'),
'error' => $action->exists('@error') ? $action->fetch('@error') : 'fail'
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
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;
$headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . ($message_id ?? 0) . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break;
if (substr($server_response, 0, 3) != $code)
{
log_error($txt['smtp_error'] . $server_response);
$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);
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
'ts' => strtotime('1995-05-27T17:00:00+0000'),
'ts' => strtotime('1995-05-27T16:00:00+0000'),
'ts' => strtotime('2002-04-30T20:00:00+0000'),
'ts' => strtotime('2002-04-30T19:00:00+0000'),
'tz' => strtotime('1999-03-27T21:00:00+0000'),
'tzid' => 'Etc/GMT-5'
'tz' => strtotime('1999-03-27T21:00:00+0000'),
'tzid' => 'Asia/Aqtau'
'tzid' => 'Asia/Almaty',
),
),
// 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',
),
),
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
static $today;
$log_time = min(max($log_time, PHP_INT_MIN), PHP_INT_MAX);
function smf_strftime(string $format, int $timestamp = null, string $tzid = null)
function smf_strftime(string $format, $timestamp = null, $tzid = null)
if (!isset($tzid))
$tzid = date_default_timezone_get();
$timestamp = min(max($timestamp, PHP_INT_MIN), PHP_INT_MAX);
function smf_gmstrftime(string $format, int $timestamp = null)
function smf_gmstrftime(string $format, $timestamp = null)
// Image.
if (!empty($currentAttachment['is_image']))
{
// Image.
if (!empty($currentAttachment['is_image']))
{
// Just viewing the page shouldn't increase the download count for embedded images.
$currentAttachment['href'] .= ';preview';
'before' => '<div class="centertext">',
'after' => '</div>',
'before' => '<div class="centertext"><div class="inline_block">',
'after' => '</div></div>',
'before' => '<div class="righttext">',
'after' => '</div>',
'before' => '<div class="righttext"><div class="inline_block">',
'after' => '</div></div>',
// 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']))
// 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']))
// The value may be quoted for some tags - check.
if (isset($tag['quoted']))
{
$quoted = substr($message, $pos1, 6) == '"';
if ($tag['quoted'] != 'optional' && !$quoted)
continue;
if ($quoted)
$pos1 += 6;
}
else
$quoted = false;
$pos2 = strpos($message, $quoted == false ? ']' : '"]', $pos1);
// The value may be quoted for some tags - check.
if (isset($tag['quoted']))
{
// Anything passed through the preparser will use ",
// but we need to handle raw quotation marks too.
$quot = substr($message, $pos1, 1) === '"' ? '"' : '"';
$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);
substr($message, $pos2 + ($quoted == false ? 1 : 7), $pos3 - ($pos2 + ($quoted == false ? 1 : 7))),
substr($message, $pos2 + ($quoted == false ? 1 : 1 + strlen($quot)), $pos3 - ($pos2 + ($quoted == false ? 1 : 1 + strlen($quot)))),
// The value may be quoted for some tags - check.
if (isset($tag['quoted']))
{
$quoted = substr($message, $pos1, 6) == '"';
if ($tag['quoted'] != 'optional' && !$quoted)
continue;
if ($quoted)
$pos1 += 6;
}
else
$quoted = false;
if ($quoted)
{
$end_of_value = strpos($message, '"]', $pos1);
$nested_tag = strpos($message, '="', $pos1);
// Check so this is not just an quoted url ending with a =
if ($nested_tag && substr($message, $nested_tag, 8) == '="]')
$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 ? ']' : '"]', $pos1) + 6;
}
$pos2 = strpos($message, $quoted == false ? ']' : '"]', isset($nested_tag_pos) ? $nested_tag_pos : $pos1);
// The value may be quoted for some tags - check.
if (isset($tag['quoted']))
{
// Will normally be '"' but might be '"'.
$quot = substr($message, $pos1, 1) === '"' ? '"' : '"';
$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);
$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 7));
$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 1 + strlen($quot)));
$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('\'' => ''', '<code>' => '', '</code>' => ''));
$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, ['\'' => ''']);
// 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 . '(?:;|&|&))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
function($m) use ($scripturl)
{
return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
// 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 . '(?:;|&|&))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
function($m) use ($scripturl)
{
return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . $sid . (isset($m[2]) ? "$m[2]" : "");
$setLocation
);
}
// The request was from ajax/xhr/other api call, append ajax ot the url.
if (!empty($context['from_ajax']))
$setLocation .= (strpos($setLocation, '?') ? ';' : '?') . 'ajax';
// Remember this URL in case someone doesn't like sending HTTP_REFERER.
if ($should_log)
// Remember this URL in case someone doesn't like sending HTTP_REFERER.
if ($should_log && !isset($_REQUEST['xml']))
$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
$returned_words[] = $max_chars === null ? $word : $smcFunc['truncate']($word, $max_chars);
// Cleanup enabled/disabled variants before taking action.
$current_functions = array_diff($current_functions, array($enabled_call, $disabled_call));
// Cleanup enabled/disabled variants before taking action.
$current_functions = array_diff($current_functions, array($enabled_call, $disabled_call));
// 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);
// 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]);
$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
$protocol = !empty($_SERVER['SERVER_PROTOCOL']) && preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
function InstallDir()
{
global $themedir, $themeurl, $context;
$_REQUEST['theme_dir'] = rtrim($_REQUEST['theme_dir'], '\\/');
global $context, $scripturl, $boarddir, $smcFunc, $txt;
global $context, $scripturl, $boarddir, $smcFunc, $txt, $sourcedir;
// 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);
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']);
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
* @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved.
* @license MIT License
*/
class Converter
* @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved
* @license MIT License
*/
class Converter implements ConverterInterface
*/
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;
* @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;
// deal with different operating systems' directory structure
$path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/');
// 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);
* @param string $path The relative path that needs to be converted.
*
* @return string The new relative path.
* @param string $path The relative path that needs to be converted
*
* @return string The new relative path
namespace MatthiasMullie\Minify;
use MatthiasMullie\Minify\Exceptions\FileImportException;
use MatthiasMullie\PathConverter\Converter;
/**
* 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;
* @var int
*/
protected $maxImportSize = 5;
/**
* @var string[]
* @var int maximum inport size in kB
*/
protected $maxImportSize = 5;
/**
* @var string[] valid import extensions
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'xbm' => 'image/x-xbitmap',
'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',
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;
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, ';');
* @import's will be loaded and their content merged into the original file,
* to save HTTP requests.
* Import statements will be loaded and their content merged into the original
* file, to save HTTP requests.
(?P<path>
# do not fetch data uris or external sources
(?!(
["\']?
(data|https?):
))
.+?
)
(?P<path>.+?)
\s+
# open path enclosure
(?P<quotes>["\'])
# fetch path
(?P<path>
# do not fetch data uris or external sources
(?!(
["\']?
(data|https?):
))
.+?
)
\s+
# open path enclosure
(?P<quotes>["\'])
# fetch path
(?P<path>.+?)
// 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;
// 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);
$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];
$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;
// build replacement
$search[] = $match[0];
$replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
// build replacement
$search[] = $match[0];
$replace[] = 'url(' . $this->importExtensions[$extension] . ';base64,' . $importContent . ')';
* @param string[optional] $path Path to write the data to
* @param string[] $parents Parent paths, for circular reference checks
* @param string[optional] $path Path to write the data to
* @param string[] $parents Parent paths, for circular reference checks
// 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);
// 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);
* of the move code...)
*/
$converter = new Converter($source, $path ?: $source);
* of the move code, which also addresses url() & @import syntax...)
*/
$converter = $this->getPathConverter($source, $path ?: $source);
* @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)
* @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)
(?P<quotes>["\'])?
# fetch path
(?P<path>
# do not fetch data uris or external sources
(?!(
\s?
["\']?
(data|https?):
))
.+?
)
(?P<quotes>["\'])?
# fetch path
(?P<path>.+?)
# 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?):
))
.+?
)
# condition above will already catch these
# open path enclosure
(?P<quotes>["\'])
# fetch path
(?P<path>.+?)
// 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;
// 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);
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(
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
);
return preg_replace_callback(
'/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i',
// or the other way around
'WHITE' => '#fff',
'BLACK' => '#000',
);
return preg_replace_callback(
'/(?<=[: ])(' . implode('|', array_keys($colors)) . ')(?=[; }])/i',
return $match[1].$weights[$match[2]];
};
return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content);
return $match[1] . $weights[$match[2]];
};
return preg_replace_callback('/(font-weight\s*:\s*)(' . implode('|', array_keys($weights)) . ')(?=[;}])/', $callback, $content);
protected function shortenZeroes($content)
{
// 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
// 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.
// 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.
return preg_replace('/(^|\}|;)[^\{\};]+\{\s*\}/', '\\1', $content);
$content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content);
$content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content);
return $content;
$this->registerPattern('/\/\*.*?\*\//s', '');
$this->stripMultilineComments();
// 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);
// 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);
return trim($content);
}
/**
* 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;
}
);
}
return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
}
/**
* 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);
namespace MatthiasMullie\Minify;
/**
/**
* Base Exception.
*
* @deprecated Use Exceptions\BasicException instead
*
* @author Matthias Mullie <[email protected]>
*/
namespace MatthiasMullie\Minify;
/**
* Base Exception Class.
*
namespace MatthiasMullie\Minify\Exceptions;
use MatthiasMullie\Minify\Exception;
/**
/**
* 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.
*
namespace MatthiasMullie\Minify\Exceptions;
/**
/**
* 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.
*
namespace MatthiasMullie\Minify\Exceptions;
/**
/**
* 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.
*
namespace MatthiasMullie\Minify;
/**
* JavaScript minifier.
/**
* 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.
* Note: Most operators are fine, we've only removed !, ++ and --.
* There can't be a newline separating ! and whatever it is negating.
* Note: Most operators are fine, we've only removed ++ and --.
* Note: Most operators are fine, we've only removed ), ], ++ and --.
* Note: Most operators are fine, we've only removed ), ], ++, --, ! and ~.
* There can't be a newline separating ! or ~ and whatever it is negating.
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);
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);
$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;";
}
$content = '';
$content = $this->replace($content);
$content = $this->propertyNotation($content);
$content = $this->shortenBools($content);
$content = $this->stripWhitespace($content);
// 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);
// single-line comments
$this->registerPattern('/\/\/.*$/m', '');
// multi-line comments
$this->registerPattern('/\/\*.*?\*\//s', '');
$this->stripMultilineComments();
// single-line comments
$this->registerPattern('/\/\/.*$/m', '');
$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);
$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);
// (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);
// (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);
'/('.implode('|', $operatorsBefore).')\s+/',
'/\s+('.implode('|', $operatorsAfter).')/',
), '\\1', $content
'/(' . implode('|', $operatorsBefore) . ')\s+/',
'/\s+(' . implode('|', $operatorsAfter) . ')/',
),
'\\1',
$content
), '\\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);
),
'\\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);
$content = preg_replace('/('.implode('|', $operatorsDiffBefore).')[^\S\n]+/', '\\1', $content);
$content = preg_replace('/[^\S\n]+('.implode('|', $operatorsDiffAfter).')/', '\\1', $content);
$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);
* 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);
* 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);
// escape operators for use in regex
$delimiter = array_fill(0, count($operators), $delimiter);
$escaped = array_map('preg_quote', $operators, $delimiter);
// escape operators for use in regex
$delimiters = array_fill(0, count($operators), $delimiter);
$escaped = array_map('preg_quote', $operators, $delimiters);
// don't confuse = with other assignment shortcuts (e.g. +=)
$chars = preg_quote('+-*\=<>%&|');
$operators['='] = '(?<!['.$chars.'])\=';
// don't confuse = with other assignment shortcuts (e.g. +=)
$chars = preg_quote('+-*\=<>%&|', $delimiter);
$operators['='] = '(?<![' . $chars . '])\=';
// add word boundaries
array_walk($keywords, function ($value) {
return '\b'.$value.'\b';
// add word boundaries
array_walk($keywords, function ($value) {
return '\b' . $value . '\b';
if (!preg_match('/^'.$minifier::REGEX_VARIABLE.'$/u', $property)) {
return $match[0];
}
return '.'.$property;
if (!preg_match('/^' . $minifier::REGEX_VARIABLE . '$/u', $property)) {
return $match[0];
}
return '.' . $property;
$keywords = '(?<!'.implode(')(?<!', $keywords).')';
return preg_replace_callback('/(?<='.$previousChar.'|\])'.$keywords.'\[\s*(([\'"])[0-9]+\\2)\s*\]/u', $callback, $content);
$keywords = '(?<!' . implode(')(?<!', $keywords) . ')';
return preg_replace_callback('/(?<=' . $previousChar . '|\])' . $keywords . '\[\s*(([\'"])[0-9]+\\2)\s*\]/u', $callback, $content);
$content = preg_replace('/\btrue\b(?!:)/', '!0', $content);
$content = preg_replace('/\bfalse\b(?!:)/', '!1', $content);
// for(;;) is exactly the same as while(true)
/*
* '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 :)
namespace MatthiasMullie\Minify;
/**
* 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
*/
* without having to worry about potential "code-like" characters inside.
*
* @internal
*
// store data
$this->data[$key] = $value;
}
// 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;
* Register a pattern to execute against the source content.
*
* 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.
*
$this->patterns[] = array($pattern, $replacement);
}
/**
* 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', '');
}
$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);
}
$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);
*/
protected function extractStrings($chars = '\'"')
{
// PHP only supports $this inside anonymous functions since 5.4
$minifier = $this;
$callback = function ($match) use ($minifier) {
* @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) {
$placeholder = $match[1].$count.$match[1];
$minifier->extracted[$placeholder] = $match[1].$match[2].$match[1];
$placeholder = $match[1] . $placeholderPrefix . $count . $match[1];
$minifier->extracted[$placeholder] = $match[1] . $match[2] . $match[1];
$this->registerPattern('/(['.$chars.'])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback);
$this->registerPattern('/([' . $chars . '])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback);
protected function canImportFile($path)
{
$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;
}
if (($handler = @fopen($path, 'w')) === false) {
throw new IOException('The file "'.$path.'" could not be opened for writing. Check if PHP has enough permissions.');
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.');
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.');
}
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;
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
$this->members['mentioned'] = Mentions::getMentionsByContent('msg', $msgOptions['id'], array_keys($msgOptions['mentioned_members']));
$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);
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})',
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}',
// 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)
// 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()))
unset($row['id_group'], $row['id_post_group'], $row['additional_groups']);
}
// 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'];
}
}
// 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)
// 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()))
$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;
$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';
}
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
// 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">
// 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">
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
url: form.prop("action") + ";ajax",
url: form.prop("action") + (form.prop("action").indexOf("?") !== -1 ? ";" : "?") + "ajax",
window.location.reload();';
if ($(data).find(".roundframe").length > 0 && $(data).find("body").length == 0) {
form.parent().html($(data).find(".roundframe").html());
}
else {
window.location.reload();
}';
$.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;
});';
$.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;
});';
}
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
<input type="text" id="ban_name" name="ban_name" value="', $context['ban']['name'], '" size="45" maxlength="60">
<input type="text" id="ban_name" name="ban_name" value="', $context['ban']['name'], '" size="20" maxlength="20">
<input type="email" name="email" value="', $context['ban_suggestions']['email'], '" size="44" onfocus="document.getElementById(\'email_check\').checked = true;">
<input type="text" name="email" value="', $context['ban_suggestions']['email'], '" size="44" onfocus="document.getElementById(\'email_check\').checked = true;">
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
<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="">
<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'], '">
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
document.getElementById(\'warn_body\').value = "', strtr($type['body'], array('"' => "'", "\n" => '\\n', "\r" => '')), '";';
document.getElementById(\'warn_body\').value = ', JavaScriptEscape($type['body']), ';';
<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'), '
<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'), '
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
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>
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>
</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>
</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>
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
<div class="righttext"><input type="submit" value="', $txt['save'], '" tabindex="', $context['tabindex']++, '" class="button" onclick="return resetAgreementConfirm()" />
<input type="submit" value="', $txt['save'], '" tabindex="', $context['tabindex']++, '" class="button" onclick="return resetAgreementConfirm()" />
* @copyright 2022 Simple Machines and individual contributors
* @copyright 2025 Simple Machines and individual contributors
@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;
}
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;
// 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)$");
// 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)$");
* @copyright 2023 Simple Machines and individual contributors
* @copyright 2024 Simple Machines and individual contributors
appendEmoticon: function (code, emoticon, description) {
var base = this;
line = $('<div>');
base = this;
line = $('<div>');
var base = this;
if ($(".sceditor-smileyPopup").length > 0)
{
$(".sceditor-smileyPopup").fadeIn('fast');
var smileyPopup = base.editorMainWrapper.querySelector(".sceditor-smileyPopup");
if (smileyPopup)
{
$(smileyPopup).fadeIn('fast');
$(".sceditor-smileyPopup").fadeOut('fast');
$(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');
.appendTo($('.sceditor-container'));
$('.sceditor-smileyPopup').animaDrag({
.appendTo(base.editorMainWrapper);
$(base.editorMainWrapper.querySelector('.sceditor-smileyPopup')).animaDrag({
$(".sceditor-toolbar").append(content);
if (typeof moreButton !== "undefined")
content.append($('<center/>').append(moreButton));
}
};
var createFn = sceditor.create;
var isPatched = false;
$(base.editorMainWrapper.querySelector(".sceditor-toolbar")).append(content);
if (typeof moreButton !== "undefined")
content.append($('<center/>').append(moreButton));
}
};
var createFn = sceditor.create;
// 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);
// 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');
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;
instance.editorMainWrapper.removeAttribute("style");
instance.editorMainWrapper.querySelector("textarea").style.height = options.height;
instance.editorMainWrapper.querySelector("textarea").style.flexBasis = options.height;
instance.isPatched = true;
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>';
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>';
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;
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;
}
}
}
// 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),
// 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),
// This file has reached the max total size per post.
if (totalKB > 0 && currentlyUsedKB > totalKB) {
// This file has reached the max total size per post.
if (totalKB > 0 && (currentlyUsedKB + uploadedFileKB) > totalKB) {
// 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');
// 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');
auto_1.php |
This file should not be able to execute standalone.You may have to run the following queries manually.
Query: Select <?php
|
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/". |