$txt['support_versions_gd'], 'version' => $temp['GD Version']); } // Why not have a look at ImageMagick? If it's installed, we should show version information for it too. if (in_array('imagemagick', $checkFor) && (class_exists('Imagick') || function_exists('MagickGetVersionString'))) { if (class_exists('Imagick')) { $temp = New Imagick; $temp2 = $temp->getVersion(); $im_version = $temp2['versionString']; $extension_version = 'Imagick ' . phpversion('Imagick'); } else { $im_version = MagickGetVersionString(); $extension_version = 'MagickWand ' . phpversion('MagickWand'); } // We already know it's ImageMagick and the website isn't needed... $im_version = str_replace(array('ImageMagick ', ' https://www.imagemagick.org'), '', $im_version); $versions['imagemagick'] = array('title' => $txt['support_versions_imagemagick'], 'version' => $im_version . ' (' . $extension_version . ')'); } // Now lets check for the Database. if (in_array('db_server', $checkFor)) { db_extend(); if (!isset($db_connection) || $db_connection === false) { loadLanguage('Errors'); trigger_error($txt['get_server_versions_no_database'], E_USER_NOTICE); } else { $versions['db_engine'] = array( 'title' => sprintf($txt['support_versions_db_engine'], $smcFunc['db_title']), 'version' => $smcFunc['db_get_vendor'](), ); $versions['db_server'] = array( 'title' => sprintf($txt['support_versions_db'], $smcFunc['db_title']), 'version' => $smcFunc['db_get_version'](), ); } } // Check to see if we have any accelerators installed. require_once($sourcedir . '/ManageServer.php'); $detected = loadCacheAPIs(); /* @var CacheApiInterface $cache_api */ foreach ($detected as $class_name => $cache_api) { $class_name_txt_key = strtolower($cache_api->getImplementationClassKeyName()); if (in_array($class_name_txt_key, $checkFor)) $versions[$class_name_txt_key] = array( 'title' => isset($txt[$class_name_txt_key . '_cache']) ? $txt[$class_name_txt_key . '_cache'] : $class_name, 'version' => $cache_api->getVersion(), ); } if (in_array('php', $checkFor)) $versions['php'] = array( 'title' => 'PHP', 'version' => PHP_VERSION, 'more' => '?action=admin;area=serversettings;sa=phpinfo', ); if (in_array('server', $checkFor)) $versions['server'] = array( 'title' => $txt['support_versions_server'], 'version' => $_SERVER['SERVER_SOFTWARE'], ); return $versions; } /** * Search through source, theme and language files to determine their version. * Get detailed version information about the physical SMF files on the server. * * - the input parameter allows to set whether to include SSI.php and whether * the results should be sorted. * - returns an array containing information on source files, templates and * language files found in the default theme directory (grouped by language). * * @param array &$versionOptions An array of options. Can contain one or more of 'include_ssi', 'include_subscriptions', 'include_tasks' and 'sort_results' * @return array An array of file version info. */ function getFileVersions(&$versionOptions) { global $boarddir, $sourcedir, $settings, $tasksdir; // Default place to find the languages would be the default theme dir. $lang_dir = $settings['default_theme_dir'] . '/languages'; $version_info = array( 'file_versions' => array(), 'default_template_versions' => array(), 'template_versions' => array(), 'default_language_versions' => array(), 'tasks_versions' => array(), ); // Find the version in SSI.php's file header. if (!empty($versionOptions['include_ssi']) && file_exists($boarddir . '/SSI.php')) { $fp = fopen($boarddir . '/SSI.php', 'rb'); $header = fread($fp, 4096); fclose($fp); // The comment looks rougly like... that. if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) $version_info['file_versions']['SSI.php'] = $match[1]; // Not found! This is bad. else $version_info['file_versions']['SSI.php'] = '??'; } // Do the paid subscriptions handler? if (!empty($versionOptions['include_subscriptions']) && file_exists($boarddir . '/subscriptions.php')) { $fp = fopen($boarddir . '/subscriptions.php', 'rb'); $header = fread($fp, 4096); fclose($fp); // Found it? if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) $version_info['file_versions']['subscriptions.php'] = $match[1]; // If we haven't how do we all get paid? else $version_info['file_versions']['subscriptions.php'] = '??'; } // Load all the files in the Sources directory, except this file and the redirect. $sources_dir = dir($sourcedir); while ($entry = $sources_dir->read()) { if (substr($entry, -4) === '.php' && !is_dir($sourcedir . '/' . $entry) && $entry !== 'index.php') { // Read the first 4k from the file.... enough for the header. $fp = fopen($sourcedir . '/' . $entry, 'rb'); $header = fread($fp, 4096); fclose($fp); // Look for the version comment in the file header. if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) $version_info['file_versions'][$entry] = $match[1]; // It wasn't found, but the file was... show a '??'. else $version_info['file_versions'][$entry] = '??'; } } $sources_dir->close(); // Load all the files in the tasks directory. if (!empty($versionOptions['include_tasks'])) { $tasks_dir = dir($tasksdir); while ($entry = $tasks_dir->read()) { if (substr($entry, -4) === '.php' && !is_dir($tasksdir . '/' . $entry) && $entry !== 'index.php') { // Read the first 4k from the file.... enough for the header. $fp = fopen($tasksdir . '/' . $entry, 'rb'); $header = fread($fp, 4096); fclose($fp); // Look for the version comment in the file header. if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) $version_info['tasks_versions'][$entry] = $match[1]; // It wasn't found, but the file was... show a '??'. else $version_info['tasks_versions'][$entry] = '??'; } } $tasks_dir->close(); } // Load all the files in the default template directory - and the current theme if applicable. $directories = array('default_template_versions' => $settings['default_theme_dir']); if ($settings['theme_id'] != 1) $directories += array('template_versions' => $settings['theme_dir']); foreach ($directories as $type => $dirname) { $this_dir = dir($dirname); while ($entry = $this_dir->read()) { if (substr($entry, -12) == 'template.php' && !is_dir($dirname . '/' . $entry)) { // Read the first 768 bytes from the file.... enough for the header. $fp = fopen($dirname . '/' . $entry, 'rb'); $header = fread($fp, 768); fclose($fp); // Look for the version comment in the file header. if (preg_match('~\*\s@version\s+(.+)[\s]{2}~i', $header, $match) == 1) $version_info[$type][$entry] = $match[1]; // It wasn't found, but the file was... show a '??'. else $version_info[$type][$entry] = '??'; } } $this_dir->close(); } // Load up all the files in the default language directory and sort by language. $this_dir = dir($lang_dir); while ($entry = $this_dir->read()) { if (substr($entry, -4) == '.php' && $entry != 'index.php' && !is_dir($lang_dir . '/' . $entry)) { // Read the first 768 bytes from the file.... enough for the header. $fp = fopen($lang_dir . '/' . $entry, 'rb'); $header = fread($fp, 768); fclose($fp); // Split the file name off into useful bits. list ($name, $language) = explode('.', $entry); // Look for the version comment in the file header. if (preg_match('~(?://|/\*)\s*Version:\s+(.+?);\s*' . preg_quote($name, '~') . '(?:[\s]{2}|\*/)~i', $header, $match) == 1) $version_info['default_language_versions'][$language][$name] = $match[1]; // It wasn't found, but the file was... show a '??'. else $version_info['default_language_versions'][$language][$name] = '??'; } } $this_dir->close(); // Sort the file versions by filename. if (!empty($versionOptions['sort_results'])) { ksort($version_info['file_versions']); ksort($version_info['default_template_versions']); ksort($version_info['template_versions']); ksort($version_info['default_language_versions']); ksort($version_info['tasks_versions']); // For languages sort each language too. foreach ($version_info['default_language_versions'] as $language => $dummy) ksort($version_info['default_language_versions'][$language]); } return $version_info; } /** * Describes properties of all known Settings.php variables and other content. * Helper for updateSettingsFile(); also called by saveSettings(). * * @return array Descriptions of all known Settings.php content */ function get_settings_defs() { /* * A big, fat array to define properties of all the Settings.php variables * and other content like code blocks. * * - String keys are used to identify actual variables. * * - Integer keys are used for content not connected to any particular * variable, such as code blocks or the license block. * * - The content of the 'text' element is simply printed out, if it is used * at all. Use it for comments or to insert code blocks, etc. * * - The 'default' element, not surprisingly, gives a default value for * the variable. * * - The 'type' element defines the expected variable type or types. If * more than one type is allowed, this should be an array listing them. * Types should match the possible types returned by gettype(). * * - If 'raw_default' is true, the default should be printed directly, * rather than being handled as a string. Use it if the default contains * code, e.g. 'dirname(__FILE__)' * * - If 'required' is true and a value for the variable is undefined, * the update will be aborted. (The only exception is during the SMF * installation process.) * * - If 'auto_delete' is 1 or true and the variable is empty, the variable * will be deleted from Settings.php. If 'auto_delete' is 0/false/null, * the variable will never be deleted. If 'auto_delete' is 2, behaviour * depends on $rebuild: if $rebuild is true, 'auto_delete' == 2 behaves * like 'auto_delete' == 1; if $rebuild is false, 'auto_delete' == 2 * behaves like 'auto_delete' == 0. * * - The 'is_password' element indicates that a value is a password. This * is used primarily to tell SMF how to interpret input when the value * is being set to a new value. * * - The optional 'search_pattern' element defines a custom regular * expression to search for the existing entry in the file. This is * primarily useful for code blocks rather than variables. * * - The optional 'replace_pattern' element defines a custom regular * expression to decide where the replacement entry should be inserted. * Note: 'replace_pattern' should be avoided unless ABSOLUTELY necessary. */ $settings_defs = array( array( 'text' => implode("\n", array( '', '/**', ' * The settings file contains all of the basic settings that need to be present when a database/cache is not available.', ' *', ' * Simple Machines Forum (SMF)', ' *', ' * @package SMF', ' * @author Simple Machines https://www.simplemachines.org', ' * @copyright ' . SMF_SOFTWARE_YEAR . ' Simple Machines and individual contributors', ' * @license https://www.simplemachines.org/about/smf/license.php BSD', ' *', ' * @version ' . SMF_VERSION, ' */', '', )), 'search_pattern' => '~/\*\*.*?@package\h+SMF\b.*?\*/\n{0,2}~s', ), 'maintenance' => array( 'text' => implode("\n", array( '', '########## Maintenance ##########', '/**', ' * The maintenance "mode"', ' * Set to 1 to enable Maintenance Mode, 2 to make the forum untouchable. (you\'ll have to make it 0 again manually!)', ' * 0 is default and disables maintenance mode.', ' *', ' * @var int 0, 1, 2', ' * @global int $maintenance', ' */', )), 'default' => 0, 'type' => 'integer', ), 'mtitle' => array( 'text' => implode("\n", array( '/**', ' * Title for the Maintenance Mode message.', ' *', ' * @var string', ' * @global int $mtitle', ' */', )), 'default' => 'Maintenance Mode', 'type' => 'string', ), 'mmessage' => array( 'text' => implode("\n", array( '/**', ' * Description of why the forum is in maintenance mode.', ' *', ' * @var string', ' * @global string $mmessage', ' */', )), 'default' => 'Okay faithful users...we\'re attempting to restore an older backup of the database...news will be posted once we\'re back!', 'type' => 'string', ), 'mbname' => array( 'text' => implode("\n", array( '', '########## Forum Info ##########', '/**', ' * The name of your forum.', ' *', ' * @var string', ' */', )), 'default' => 'My Community', 'type' => 'string', ), 'language' => array( 'text' => implode("\n", array( '/**', ' * The default language file set for the forum.', ' *', ' * @var string', ' */', )), 'default' => 'english', 'type' => 'string', ), 'boardurl' => array( 'text' => implode("\n", array( '/**', ' * URL to your forum\'s folder. (without the trailing /!)', ' *', ' * @var string', ' */', )), 'default' => 'http://127.0.0.1/smf', 'type' => 'string', ), 'webmaster_email' => array( 'text' => implode("\n", array( '/**', ' * Email address to send emails from. (like noreply@yourdomain.com.)', ' *', ' * @var string', ' */', )), 'default' => 'noreply@myserver.com', 'type' => 'string', ), 'cookiename' => array( 'text' => implode("\n", array( '/**', ' * Name of the cookie to set for authentication.', ' *', ' * @var string', ' */', )), 'default' => 'SMFCookie11', 'type' => 'string', ), 'auth_secret' => array( 'text' => implode("\n", array( '/**', ' * Secret key used to create and verify cookies, tokens, etc.', ' * Do not change this unless absolutely necessary, and NEVER share it.', ' *', ' * Note: Changing this will immediately log out all members of your forum', ' * and break the token-based links in all previous email notifications,', ' * among other possible effects.', ' *', ' * @var string', ' */', )), 'default' => null, 'auto_delete' => 1, 'type' => 'string', ), 'db_type' => array( 'text' => implode("\n", array( '', '########## Database Info ##########', '/**', ' * The database type', ' * Default options: mysql, postgresql', ' *', ' * @var string', ' */', )), 'default' => 'mysql', 'type' => 'string', ), 'db_port' => array( 'text' => implode("\n", array( '/**', ' * The database port', ' * 0 to use default port for the database type', ' *', ' * @var int', ' */', )), 'default' => 0, 'type' => 'integer', ), 'db_server' => array( 'text' => implode("\n", array( '/**', ' * The server to connect to (or a Unix socket)', ' *', ' * @var string', ' */', )), 'default' => 'localhost', 'required' => true, 'type' => 'string', ), 'db_name' => array( 'text' => implode("\n", array( '/**', ' * The database name', ' *', ' * @var string', ' */', )), 'default' => 'smf', 'required' => true, 'type' => 'string', ), 'db_user' => array( 'text' => implode("\n", array( '/**', ' * Database username', ' *', ' * @var string', ' */', )), 'default' => 'root', 'required' => true, 'type' => 'string', ), 'db_passwd' => array( 'text' => implode("\n", array( '/**', ' * Database password', ' *', ' * @var string', ' */', )), 'default' => '', 'required' => true, 'type' => 'string', 'is_password' => true, ), 'ssi_db_user' => array( 'text' => implode("\n", array( '/**', ' * Database user for when connecting with SSI', ' *', ' * @var string', ' */', )), 'default' => '', 'type' => 'string', ), 'ssi_db_passwd' => array( 'text' => implode("\n", array( '/**', ' * Database password for when connecting with SSI', ' *', ' * @var string', ' */', )), 'default' => '', 'type' => 'string', 'is_password' => true, ), 'db_prefix' => array( 'text' => implode("\n", array( '/**', ' * A prefix to put in front of your table names.', ' * This helps to prevent conflicts', ' *', ' * @var string', ' */', )), 'default' => 'smf_', 'required' => true, 'type' => 'string', ), 'db_persist' => array( 'text' => implode("\n", array( '/**', ' * Use a persistent database connection', ' *', ' * @var bool', ' */', )), 'default' => false, 'type' => 'boolean', ), 'db_error_send' => array( 'text' => implode("\n", array( '/**', ' * Send emails on database connection error', ' *', ' * @var bool', ' */', )), 'default' => false, 'type' => 'boolean', ), 'db_mb4' => array( 'text' => implode("\n", array( '/**', ' * Override the default behavior of the database layer for mb4 handling', ' * null keep the default behavior untouched', ' *', ' * @var null|bool', ' */', )), 'default' => null, 'type' => array('NULL', 'boolean'), ), 'cache_accelerator' => array( 'text' => implode("\n", array( '', '########## Cache Info ##########', '/**', ' * Select a cache system. You want to leave this up to the cache area of the admin panel for', ' * proper detection of memcached, output_cache, or smf file system', ' * (you can add more with a mod).', ' *', ' * @var string', ' */', )), 'default' => '', 'type' => 'string', ), 'cache_enable' => array( 'text' => implode("\n", array( '/**', ' * The level at which you would like to cache. Between 0 (off) through 3 (cache a lot).', ' *', ' * @var int', ' */', )), 'default' => 0, 'type' => 'integer', ), 'cache_memcached' => array( 'text' => implode("\n", array( '/**', ' * This is only used for memcache / memcached. Should be a string of \'server:port,server:port\'', ' *', ' * @var array', ' */', )), 'default' => '', 'type' => 'string', ), 'cachedir' => array( 'text' => implode("\n", array( '/**', ' * This is only for the \'smf\' file cache system. It is the path to the cache directory.', ' * It is also recommended that you place this in /tmp/ if you are going to use this.', ' *', ' * @var string', ' */', )), 'default' => 'dirname(__FILE__) . \'/cache\'', 'raw_default' => true, 'type' => 'string', ), 'cachedir_sqlite' => array( 'text' => implode("\n", array( '/**', ' * This is only for SQLite3 cache system. It is the path to the directory where the SQLite3', ' * database file will be saved.', ' *', ' * @var string', ' */', )), 'default' => '', 'auto_delete' => 2, 'type' => 'string', ), 'image_proxy_enabled' => array( 'text' => implode("\n", array( '', '########## Image Proxy ##########', '# This is done entirely in Settings.php to avoid loading the DB while serving the images', '/**', ' * Whether the proxy is enabled or not', ' *', ' * @var bool', ' */', )), 'default' => true, 'type' => 'boolean', ), 'image_proxy_secret' => array( 'text' => implode("\n", array( '/**', ' * Secret key to be used by the proxy', ' *', ' * @var string', ' */', )), 'default' => 'smfisawesome', 'type' => 'string', ), 'image_proxy_maxsize' => array( 'text' => implode("\n", array( '/**', ' * Maximum file size (in KB) for individual files', ' *', ' * @var int', ' */', )), 'default' => 5192, 'type' => 'integer', ), 'boarddir' => array( 'text' => implode("\n", array( '', '########## Directories/Files ##########', '# Note: These directories do not have to be changed unless you move things.', '/**', ' * The absolute path to the forum\'s folder. (not just \'.\'!)', ' *', ' * @var string', ' */', )), 'default' => 'dirname(__FILE__)', 'raw_default' => true, 'type' => 'string', ), 'sourcedir' => array( 'text' => implode("\n", array( '/**', ' * Path to the Sources directory.', ' *', ' * @var string', ' */', )), 'default' => 'dirname(__FILE__) . \'/Sources\'', 'raw_default' => true, 'type' => 'string', ), 'packagesdir' => array( 'text' => implode("\n", array( '/**', ' * Path to the Packages directory.', ' *', ' * @var string', ' */', )), 'default' => 'dirname(__FILE__) . \'/Packages\'', 'raw_default' => true, 'type' => 'string', ), 'tasksdir' => array( 'text' => implode("\n", array( '/**', ' * Path to the tasks directory.', ' *', ' * @var string', ' */', )), 'default' => '$sourcedir . \'/tasks\'', 'raw_default' => true, 'type' => 'string', ), array( 'text' => implode("\n", array( '', '# Make sure the paths are correct... at least try to fix them.', 'if (!is_dir(realpath($boarddir)) && file_exists(dirname(__FILE__) . \'/agreement.txt\'))', ' $boarddir = dirname(__FILE__);', 'if (!is_dir(realpath($sourcedir)) && is_dir($boarddir . \'/Sources\'))', ' $sourcedir = $boarddir . \'/Sources\';', 'if (!is_dir(realpath($tasksdir)) && is_dir($sourcedir . \'/tasks\'))', ' $tasksdir = $sourcedir . \'/tasks\';', 'if (!is_dir(realpath($packagesdir)) && is_dir($boarddir . \'/Packages\'))', ' $packagesdir = $boarddir . \'/Packages\';', 'if (!is_dir(realpath($cachedir)) && is_dir($boarddir . \'/cache\'))', ' $cachedir = $boarddir . \'/cache\';', )), 'search_pattern' => '~\n?(#[^\n]+)?(?:\n\h*if\s*\((?:\!file_exists\(\$(?'.'>boarddir|sourcedir|tasksdir|packagesdir|cachedir)\)|\!is_dir\(realpath\(\$(?'.'>boarddir|sourcedir|tasksdir|packagesdir|cachedir)\)\))[^;]+\n\h*\$(?'.'>boarddir|sourcedir|tasksdir|packagesdir|cachedir)[^\n]+;)+~sm', ), 'db_character_set' => array( 'text' => implode("\n", array( '', '######### Legacy Settings #########', '# UTF-8 is now the only character set supported in 2.1.', )), 'default' => 'utf8', 'type' => 'string', ), 'db_show_debug' => array( 'text' => implode("\n", array( '', '######### Developer Settings #########', '# Show debug info.', )), 'default' => false, 'auto_delete' => 2, 'type' => 'boolean', ), array( 'text' => implode("\n", array( '', '########## Error-Catching ##########', '# Note: You shouldn\'t touch these settings.', 'if (file_exists((isset($cachedir) ? $cachedir : dirname(__FILE__)) . \'/db_last_error.php\'))', ' include((isset($cachedir) ? $cachedir : dirname(__FILE__)) . \'/db_last_error.php\');', '', 'if (!isset($db_last_error))', '{', ' // File does not exist so lets try to create it', ' file_put_contents((isset($cachedir) ? $cachedir : dirname(__FILE__)) . \'/db_last_error.php\', \'<\' . \'?\' . "php\n" . \'$db_last_error = 0;\' . "\n" . \'?\' . \'>\');', ' $db_last_error = 0;', '}', )), // Designed to match both 2.0 and 2.1 versions of this code. 'search_pattern' => '~\n?#+ Error.Catching #+\n[^\n]*?settings\.\n(?:\$db_last_error = \d{1,11};|if \(file_exists.*?\$db_last_error = 0;(?' . '>\s*}))(?=\n|\?' . '>|$)~s', ), // Temporary variable used during the upgrade process. 'upgradeData' => array( 'default' => '', 'auto_delete' => 1, 'type' => 'string', ), // This should be removed if found. 'db_last_error' => array( 'default' => 0, 'auto_delete' => 1, 'type' => 'integer', ), ); // Allow mods the option to define comments, defaults, etc., for their settings. // Check if function exists, in case we are calling from installer or upgrader. if (function_exists('call_integration_hook')) call_integration_hook('integrate_update_settings_file', array(&$settings_defs)); return $settings_defs; } /** * Update the Settings.php file. * * The most important function in this file for mod makers happens to be the * updateSettingsFile() function, but it shouldn't be used often anyway. * * - Updates the Settings.php file with the changes supplied in config_vars. * * - Expects config_vars to be an associative array, with the keys as the * variable names in Settings.php, and the values the variable values. * * - Correctly formats the values using smf_var_export(). * * - Restores standard formatting of the file, if $rebuild is true. * * - Checks for changes to db_last_error and passes those off to a separate * handler. * * - Creates a backup file and will use it should the writing of the * new settings file fail. * * - Tries to intelligently trim quotes and remove slashes from string values. * This is done for backwards compatibility purposes (old versions of this * function expected strings to have been manually escaped and quoted). This * behaviour can be controlled by the $keep_quotes parameter. * * MOD AUTHORS: If you are adding a setting to Settings.php, you should use the * integrate_update_settings_file hook to define it in get_settings_defs(). * * @param array $config_vars An array of one or more variables to update. * @param bool|null $keep_quotes Whether to strip slashes & trim quotes from string values. Defaults to auto-detection. * @param bool $rebuild If true, attempts to rebuild with standard format. Default false. * @return bool True on success, false on failure. */ function updateSettingsFile($config_vars, $keep_quotes = null, $rebuild = false) { // In this function we intentionally don't declare any global variables. // This allows us to work with everything cleanly. static $mtime; // Should we try to unescape the strings? if (empty($keep_quotes)) { foreach ($config_vars as $var => $val) { if (is_string($val) && ($keep_quotes === false || strpos($val, '\'') === 0 && strrpos($val, '\'') === strlen($val) - 1)) $config_vars[$var] = trim(stripcslashes($val), '\''); } } // Updating the db_last_error, then don't mess around with Settings.php if (isset($config_vars['db_last_error'])) { updateDbLastError($config_vars['db_last_error']); if (count($config_vars) === 1 && empty($rebuild)) return true; // Make sure we delete this from Settings.php, if present. $config_vars['db_last_error'] = 0; } // Rebuilding should not be undertaken lightly, so we're picky about the parameter. if (!is_bool($rebuild)) $rebuild = false; $mtime = isset($mtime) ? (int) $mtime : (defined('TIME_START') ? TIME_START : $_SERVER['REQUEST_TIME']); /***************** * PART 1: Setup * *****************/ // Typically Settings.php is in $boarddir, but maybe this is a custom setup... foreach (get_included_files() as $settingsFile) if (basename($settingsFile) === 'Settings.php') break; // Fallback in case Settings.php isn't loaded (e.g. while installing) if (basename($settingsFile) !== 'Settings.php') $settingsFile = (!empty($GLOBALS['boarddir']) && @realpath($GLOBALS['boarddir']) ? $GLOBALS['boarddir'] : (!empty($_SERVER['SCRIPT_FILENAME']) ? dirname($_SERVER['SCRIPT_FILENAME']) : dirname(__DIR__))) . '/Settings.php'; // File not found? Attempt an emergency on-the-fly fix! if (!file_exists($settingsFile)) @touch($settingsFile); // When was Settings.php last changed? $last_settings_change = filemtime($settingsFile); // Get the current values of everything in Settings.php. $settings_vars = get_current_settings($mtime, $settingsFile); // If Settings.php is empty for some reason, see if we can use the backup. if (empty($settings_vars) && file_exists(dirname($settingsFile) . '/Settings_bak.php')) $settings_vars = get_current_settings($mtime, dirname($settingsFile) . '/Settings_bak.php'); // False means there was a problem with the file and we can't safely continue. if ($settings_vars === false) return false; // It works best to set everything afresh. $new_settings_vars = array_merge($settings_vars, $config_vars); // Are we using UTF-8? $utf8 = isset($GLOBALS['context']['utf8']) ? $GLOBALS['context']['utf8'] : (isset($GLOBALS['utf8']) ? $GLOBALS['utf8'] : (isset($settings_vars['db_character_set']) ? $settings_vars['db_character_set'] === 'utf8' : false)); // Get our definitions for all known Settings.php variables and other content. $settings_defs = get_settings_defs(); // If Settings.php is empty or invalid, try to recover using whatever is in $GLOBALS. if ($settings_vars === array()) { foreach ($settings_defs as $var => $setting_def) if (isset($GLOBALS[$var])) $settings_vars[$var] = $GLOBALS[$var]; $new_settings_vars = array_merge($settings_vars, $config_vars); } // During install/upgrade, don't set anything until we're ready for it. if (defined('SMF_INSTALLING') && empty($rebuild)) { foreach ($settings_defs as $var => $setting_def) if (!in_array($var, array_keys($new_settings_vars)) && !is_int($var)) unset($settings_defs[$var]); } /******************************* * PART 2: Build substitutions * *******************************/ $type_regex = array( 'string' => '(?:' . // match the opening quotation mark... '(["\'])' . // then any number of other characters or escaped quotation marks... '(?:.(?!\\1)|\\\(?=\\1))*.?' . // then the closing quotation mark. '\\1' . // Maybe there's a second string concatenated to this one. '(?:\s*\.\s*)*' . ')+', // Some numeric values might have been stored as strings. 'integer' => '["\']?[+-]?\d+["\']?', 'double' => '["\']?[+-]?\d+\.\d+([Ee][+-]\d+)?["\']?', // Some boolean values might have been stored as integers. 'boolean' => '(?i:TRUE|FALSE|(["\']?)[01]\b\\1)', 'NULL' => '(?i:NULL)', // These use a PCRE subroutine to match nested arrays. 'array' => 'array\s*(\((?'.'>[^()]|(?1))*\))', 'object' => '\w+::__set_state\(array\s*(\((?'.'>[^()]|(?1))*\))\)', ); /* * The substitutions take place in one of two ways: * * 1: The search_pattern regex finds a string in Settings.php, which is * temporarily replaced by a placeholder. Once all the placeholders * have been inserted, each is replaced by the final replacement string * that we want to use. This is the standard method. * * 2: The search_pattern regex finds a string in Settings.php, which is * then deleted by replacing it with an empty placeholder. Then after * all the real placeholders have been dealt with, the replace_pattern * regex finds where to insert the final replacement string that we * want to use. This method is for special cases. */ $prefix = mt_rand() . '-'; $neg_index = -1; $substitutions = array( $neg_index-- => array( 'search_pattern' => '~^\s*<\?(php\b)?\n?~', 'placeholder' => '', 'replace_pattern' => '~^~', 'replacement' => '<' . "?php\n", ), $neg_index-- => array( 'search_pattern' => '~\S\K\s*(\?' . '>)?\s*$~', 'placeholder' => "\n" . md5($prefix . '?' . '>'), 'replacement' => "\n\n?" . '>', ), // Remove the code that redirects to the installer. $neg_index-- => array( 'search_pattern' => '~^if\s*\(file_exists\(dirname\(__FILE__\)\s*\.\s*\'/install\.php\'\)\)\s*(?:({(?'.'>[^{}]|(?1))*})\h*|header(\((?' . '>[^()]|(?2))*\));\n)~m', 'placeholder' => '', ), ); if (defined('SMF_INSTALLING')) $substitutions[$neg_index--] = array( 'search_pattern' => '~/\*.*?SMF\s+1\.\d.*?\*/~s', 'placeholder' => '', ); foreach ($settings_defs as $var => $setting_def) { $placeholder = md5($prefix . $var); $replacement = ''; if (!empty($setting_def['text'])) { // Special handling for the license block: always at the beginning. if (strpos($setting_def['text'], "* @package SMF\n") !== false) { $substitutions[$var]['search_pattern'] = $setting_def['search_pattern']; $substitutions[$var]['placeholder'] = ''; $substitutions[-1]['replacement'] .= $setting_def['text'] . "\n"; } // Special handling for the Error-Catching block: always at the end. elseif (strpos($setting_def['text'], 'Error-Catching') !== false) { $errcatch_var = $var; $substitutions[$var]['search_pattern'] = $setting_def['search_pattern']; $substitutions[$var]['placeholder'] = ''; $substitutions[-2]['replacement'] = "\n" . $setting_def['text'] . $substitutions[-2]['replacement']; } // The text is the whole thing (code blocks, etc.) elseif (is_int($var)) { // Remember the path correcting code for later. if (strpos($setting_def['text'], '# Make sure the paths are correct') !== false) $pathcode_var = $var; if (!empty($setting_def['search_pattern'])) $substitutions[$var]['search_pattern'] = $setting_def['search_pattern']; else $substitutions[$var]['search_pattern'] = '~' . preg_quote($setting_def['text'], '~') . '~'; $substitutions[$var]['placeholder'] = $placeholder; $replacement .= $setting_def['text'] . "\n"; } // We only include comments when rebuilding. elseif (!empty($rebuild)) $replacement .= $setting_def['text'] . "\n"; } if (is_string($var)) { // Ensure the value is good. if (in_array($var, array_keys($new_settings_vars))) { // Objects without a __set_state method need a fallback. if (is_object($new_settings_vars[$var]) && !method_exists($new_settings_vars[$var], '__set_state')) { if (method_exists($new_settings_vars[$var], '__toString')) $new_settings_vars[$var] = (string) $new_settings_vars[$var]; else $new_settings_vars[$var] = (array) $new_settings_vars[$var]; } // Normalize the type if necessary. if (isset($setting_def['type'])) { $expected_types = (array) $setting_def['type']; $var_type = gettype($new_settings_vars[$var]); // Variable is not of an expected type. if (!in_array($var_type, $expected_types)) { // Passed in an unexpected array. if ($var_type == 'array') { $temp = reset($new_settings_vars[$var]); // Use the first element if there's only one and it is a scalar. if (count($new_settings_vars[$var]) === 1 && is_scalar($temp)) $new_settings_vars[$var] = $temp; // Or keep the old value, if that is good. elseif (isset($settings_vars[$var]) && in_array(gettype($settings_vars[$var]), $expected_types)) $new_settings_vars[$var] = $settings_vars[$var]; // Fall back to the default else $new_settings_vars[$var] = $setting_def['default']; } // Cast it to whatever type was expected. // Note: the order of the types in this loop matters. foreach (array('boolean', 'integer', 'double', 'string', 'array') as $to_type) { if (in_array($to_type, $expected_types)) { settype($new_settings_vars[$var], $to_type); break; } } } } } // Abort if a required one is undefined (unless we're installing). elseif (!empty($setting_def['required']) && !defined('SMF_INSTALLING')) return false; // Create the search pattern. if (!empty($setting_def['search_pattern'])) $substitutions[$var]['search_pattern'] = $setting_def['search_pattern']; else { $var_pattern = array(); if (isset($setting_def['type'])) { foreach ((array) $setting_def['type'] as $type) $var_pattern[] = $type_regex[$type]; } if (in_array($var, array_keys($config_vars))) { $var_pattern[] = @$type_regex[gettype($config_vars[$var])]; if (is_string($config_vars[$var]) && strpos($config_vars[$var], dirname($settingsFile)) === 0) $var_pattern[] = '(?:__DIR__|dirname\(__FILE__\)) . \'' . (preg_quote(str_replace(dirname($settingsFile), '', $config_vars[$var]), '~')) . '\''; } if (in_array($var, array_keys($settings_vars))) { $var_pattern[] = @$type_regex[gettype($settings_vars[$var])]; if (is_string($settings_vars[$var]) && strpos($settings_vars[$var], dirname($settingsFile)) === 0) $var_pattern[] = '(?:__DIR__|dirname\(__FILE__\)) . \'' . (preg_quote(str_replace(dirname($settingsFile), '', $settings_vars[$var]), '~')) . '\''; } if (!empty($setting_def['raw_default']) && $setting_def['default'] !== '') { $var_pattern[] = preg_replace('/\s+/', '\s+', preg_quote($setting_def['default'], '~')); if (strpos($setting_def['default'], 'dirname(__FILE__)') !== false) $var_pattern[] = preg_replace('/\s+/', '\s+', preg_quote(str_replace('dirname(__FILE__)', '__DIR__', $setting_def['default']), '~')); if (strpos($setting_def['default'], '__DIR__') !== false) $var_pattern[] = preg_replace('/\s+/', '\s+', preg_quote(str_replace('__DIR__', 'dirname(__FILE__)', $setting_def['default']), '~')); } $var_pattern = array_unique($var_pattern); $var_pattern = count($var_pattern) > 1 ? '(?:' . (implode('|', $var_pattern)) . ')' : $var_pattern[0]; $substitutions[$var]['search_pattern'] = '~(?<=^|\s)\h*\$' . preg_quote($var, '~') . '\s*=\s*' . $var_pattern . ';~' . (!empty($utf8) ? 'u' : ''); } // Next create the placeholder or replace_pattern. if (!empty($setting_def['replace_pattern'])) $substitutions[$var]['replace_pattern'] = $setting_def['replace_pattern']; else $substitutions[$var]['placeholder'] = $placeholder; // Now create the replacement. // A setting to delete. if (!empty($setting_def['auto_delete']) && empty($new_settings_vars[$var])) { if ($setting_def['auto_delete'] === 2 && empty($rebuild) && in_array($var, array_keys($new_settings_vars))) { $replacement .= '$' . $var . ' = ' . ($new_settings_vars[$var] === $setting_def['default'] && !empty($setting_def['raw_default']) ? sprintf($new_settings_vars[$var]) : smf_var_export($new_settings_vars[$var], true)) . ";"; } else { $replacement = ''; $substitutions[$var]['placeholder'] = ''; // This is just for cosmetic purposes. Removes the blank line. $substitutions[$var]['search_pattern'] = str_replace('(?<=^|\s)', '\n?', $substitutions[$var]['search_pattern']); } } // Add this setting's value. elseif (in_array($var, array_keys($new_settings_vars))) { $replacement .= '$' . $var . ' = ' . ($new_settings_vars[$var] === $setting_def['default'] && !empty($setting_def['raw_default']) ? sprintf($new_settings_vars[$var]) : smf_var_export($new_settings_vars[$var], true)) . ";"; } // Fall back to the default value. elseif (isset($setting_def['default'])) { $replacement .= '$' . $var . ' = ' . (!empty($setting_def['raw_default']) ? sprintf($setting_def['default']) : smf_var_export($setting_def['default'], true)) . ';'; } // This shouldn't happen, but we've got nothing. else $replacement .= '$' . $var . ' = null;'; } $substitutions[$var]['replacement'] = $replacement; // We're done with this one. unset($new_settings_vars[$var]); } // Any leftovers to deal with? foreach ($new_settings_vars as $var => $val) { $var_pattern = array(); if (in_array($var, array_keys($config_vars))) $var_pattern[] = $type_regex[gettype($config_vars[$var])]; if (in_array($var, array_keys($settings_vars))) $var_pattern[] = $type_regex[gettype($settings_vars[$var])]; $var_pattern = array_unique($var_pattern); $var_pattern = count($var_pattern) > 1 ? '(?:' . (implode('|', $var_pattern)) . ')' : $var_pattern[0]; $placeholder = md5($prefix . $var); $substitutions[$var]['search_pattern'] = '~(?<=^|\s)\h*\$' . preg_quote($var, '~') . '\s*=\s*' . $var_pattern . ';~' . (!empty($utf8) ? 'u' : ''); $substitutions[$var]['placeholder'] = $placeholder; $substitutions[$var]['replacement'] = '$' . $var . ' = ' . smf_var_export($val, true) . ";"; } // During an upgrade, some of the path variables may not have been declared yet. if (defined('SMF_INSTALLING') && empty($rebuild)) { preg_match_all('~^\h*\$(\w+)\s*=\s*~m', $substitutions[$pathcode_var]['replacement'], $matches); $missing_pathvars = array_diff($matches[1], array_keys($substitutions)); if (!empty($missing_pathvars)) { foreach ($missing_pathvars as $var) { $substitutions[$pathcode_var]['replacement'] = preg_replace('~\nif[^\n]+\$' . $var . '[^\n]+\n\h*\$' . $var . ' = [^\n]+~', '', $substitutions[$pathcode_var]['replacement']); } } } // It's important to do the numbered ones before the named ones, or messes happen. uksort( $substitutions, function($a, $b) { if (is_int($a) && is_int($b)) return $a > $b ? 1 : ($a < $b ? -1 : 0); elseif (is_int($a)) return -1; elseif (is_int($b)) return 1; else return strcasecmp($b, $a); } ); /****************************** * PART 3: Content processing * ******************************/ /* 3.a: Get the content of Settings.php and make sure it is good. */ // Retrieve the contents of Settings.php and normalize the line endings. $settingsText = trim(strtr(file_get_contents($settingsFile), array("\r\n" => "\n", "\r" => "\n"))); // If Settings.php is empty or corrupt for some reason, see if we can recover. if ($settingsText == '' || substr($settingsText, 0, 5) !== '<' . '?php') { // Try restoring from the backup. if (file_exists(dirname($settingsFile) . '/Settings_bak.php')) $settingsText = strtr(file_get_contents(dirname($settingsFile) . '/Settings_bak.php'), array("\r\n" => "\n", "\r" => "\n")); // Backup is bad too? Our only option is to create one from scratch. if ($settingsText == '' || substr($settingsText, 0, 5) !== '<' . '?php' || substr($settingsText, -2) !== '?' . '>') { $settingsText = '<' . "?php\n"; foreach ($settings_defs as $var => $setting_def) { if (is_string($var) && !empty($setting_def['text']) && strpos($substitutions[$var]['replacement'], $setting_def['text']) === false) $substitutions[$var]['replacement'] = $setting_def['text'] . "\n" . $substitutions[$var]['replacement']; $settingsText .= $substitutions[$var]['replacement'] . "\n"; } $settingsText .= "\n\n?" . '>'; $rebuild = true; } } // Settings.php is unlikely to contain any heredocs, but just in case... if (preg_match_all('/<<<([\'"]?)(\w+)\1\R(.*?)\R\h*\2;$/ms', $settingsText, $matches)) { foreach ($matches[0] as $mkey => $heredoc) { if (!empty($matches[1][$mkey]) && $matches[1][$mkey] === '\'') $heredoc_replacements[$heredoc] = var_export($matches[3][$mkey], true) . ';'; else $heredoc_replacements[$heredoc] = '"' . strtr(substr(var_export($matches[3][$mkey], true), 1, -1), array("\\'" => "'", '"' => '\"')) . '";'; } $settingsText = strtr($settingsText, $heredoc_replacements); } /* 3.b: Loop through all our substitutions to insert placeholders, etc. */ $last_var = null; $bare_settingsText = $settingsText; $force_before_pathcode = array(); foreach ($substitutions as $var => $substitution) { $placeholders[$var] = $substitution['placeholder']; if (!empty($substitution['placeholder'])) { $simple_replacements[$substitution['placeholder']] = $substitution['replacement']; } elseif (!empty($substitution['replace_pattern'])) { $replace_patterns[$var] = $substitution['replace_pattern']; $replace_strings[$var] = $substitution['replacement']; } if (strpos($substitutions[$pathcode_var]['replacement'], '$' . $var . ' = ') !== false) $force_before_pathcode[] = $var; // Look before you leap. preg_match_all($substitution['search_pattern'], $bare_settingsText, $matches); if ((is_string($var) || $var === $pathcode_var) && count($matches[0]) !== 1 && $substitution['replacement'] !== '') { // More than one instance of the variable = not good. if (count($matches[0]) > 1) { if (is_string($var)) { // Maybe we can try something more interesting? $sp = substr($substitution['search_pattern'], 1); if (strpos($sp, '(?<=^|\s)') === 0) $sp = substr($sp, 9); if (strpos($sp, '^') === 0 || strpos($sp, '(?<') === 0) return false; // See if we can exclude `if` blocks, etc., to narrow down the matches. // @todo Multiple layers of nested brackets might confuse this. $sp = '~(?:^|//[^\n]+c\n|\*/|[;}]|' . implode('|', array_filter($placeholders)) . ')\s*' . (strpos($sp, '\K') === false ? '\K' : '') . $sp; preg_match_all($sp, $settingsText, $matches); } else $sp = $substitution['search_pattern']; // Found at least some that are simple assignment statements. if (count($matches[0]) > 0) { // Remove any duplicates. if (count($matches[0]) > 1) $settingsText = preg_replace($sp, '', $settingsText, count($matches[0]) - 1); // Insert placeholder for the last one. $settingsText = preg_replace($sp, $substitution['placeholder'], $settingsText, 1); } // All instances are inside more complex code structures. else { // Only safe option at this point is to skip it. unset($substitutions[$var], $new_settings_vars[$var], $settings_defs[$var], $simple_replacements[$substitution['placeholder']], $replace_patterns[$var], $replace_strings[$var]); continue; } } // No matches found. elseif (count($matches[0]) === 0) { $found = false; $in_c = in_array($var, array_keys($config_vars)); $in_s = in_array($var, array_keys($settings_vars)); // Is it in there at all? if (!preg_match('~(^|\s)\$' . preg_quote($var, '~') . '\s*=\s*~', $bare_settingsText)) { // It's defined by Settings.php, but not by code in the file. // Probably done via an include or something. Skip it. if ($in_s) unset($substitutions[$var], $settings_defs[$var]); // Admin is explicitly trying to set this one, so we'll handle // it as if it were a new custom setting being added. elseif ($in_c) $new_settings_vars[$var] = $config_vars[$var]; continue; } // It's in there somewhere, so check if the value changed type. foreach (array('scalar', 'object', 'array') as $type) { // Try all the other scalar types first. if ($type == 'scalar') $sp = '(?:' . (implode('|', array_diff_key($type_regex, array($in_c ? gettype($config_vars[$var]) : ($in_s ? gettype($settings_vars[$var]) : PHP_INT_MAX) => '', 'array' => '', 'object' => '')))) . ')'; // Maybe it's an object? (Probably not, but we should check.) elseif ($type == 'object') { if (strpos($settingsText, '__set_state') === false) continue; $sp = $type_regex['object']; } // Maybe it's an array? else $sp = $type_regex['array']; if (preg_match('~(^|\s)\$' . preg_quote($var, '~') . '\s*=\s*' . $sp . '~', $bare_settingsText, $derp)) { $settingsText = preg_replace('~(^|\s)\$' . preg_quote($var, '~') . '\s*=\s*' . $sp . '~', $substitution['placeholder'], $settingsText); $found = true; break; } } // Something weird is going on. Better just leave it alone. if (!$found) { // $var? What $var? Never heard of it. unset($substitutions[$var], $new_settings_vars[$var], $settings_defs[$var], $simple_replacements[$substitution['placeholder']], $replace_patterns[$var], $replace_strings[$var]); continue; } } } // Good to go, so insert our placeholder. else $settingsText = preg_replace($substitution['search_pattern'], $substitution['placeholder'], $settingsText); // Once the code blocks are done, we want to compare to a version without comments. if (is_int($last_var) && is_string($var)) $bare_settingsText = strip_php_comments($settingsText); $last_var = $var; } // Rebuilding requires more work. if (!empty($rebuild)) { // Strip out the leading and trailing placeholders to prevent duplication. $settingsText = str_replace(array($substitutions[-1]['placeholder'], $substitutions[-2]['placeholder']), '', $settingsText); // Strip out all our standard comments. foreach ($settings_defs as $var => $setting_def) { if (isset($setting_def['text'])) $settingsText = strtr($settingsText, array($setting_def['text'] . "\n" => '', $setting_def['text'] => '',)); } // We need to refresh $bare_settingsText at this point. $bare_settingsText = strip_php_comments($settingsText); // Fix up whitespace to make comparison easier. foreach ($placeholders as $placeholder) { $bare_settingsText = str_replace(array($placeholder . "\n\n", $placeholder), $placeholder . "\n", $bare_settingsText); } $bare_settingsText = preg_replace('/\h+$/m', '', rtrim($bare_settingsText)); /* * Divide the existing content into sections. * The idea here is to make sure we don't mess with the relative position * of any code blocks in the file, since that could break things. Within * each section, however, we'll reorganize the content to match the * default layout as closely as we can. */ $sections = array(array()); $section_num = 0; $trimmed_placeholders = array_filter(array_map('trim', $placeholders)); $newsection_placeholders = array(); $all_custom_content = ''; foreach ($substitutions as $var => $substitution) { if (is_int($var) && ($var === -2 || $var > 0) && isset($trimmed_placeholders[$var]) && strpos($bare_settingsText, $trimmed_placeholders[$var]) !== false) $newsection_placeholders[$var] = $trimmed_placeholders[$var]; } foreach (preg_split('~(?<=' . implode('|', $trimmed_placeholders) . ')|(?=' . implode('|', $trimmed_placeholders) . ')~', $bare_settingsText) as $part) { $part = trim($part); if (empty($part)) continue; // Build a list of placeholders for this section. if (in_array($part, $trimmed_placeholders) && !in_array($part, $newsection_placeholders)) { $sections[$section_num][] = $part; } // Custom content and newsection_placeholders get their own sections. else { if (!empty($sections[$section_num])) ++$section_num; $sections[$section_num][] = $part; ++$section_num; if (!in_array($part, $trimmed_placeholders)) $all_custom_content .= "\n" . $part; } } // And now, rebuild the content! $new_settingsText = ''; $done_defs = array(); $sectionkeys = array_keys($sections); foreach ($sections as $sectionkey => $section) { // Custom content needs to be preserved. if (count($section) === 1 && !in_array($section[0], $trimmed_placeholders)) { $prev_section_end = $sectionkey < 1 ? 0 : strpos($settingsText, end($sections[$sectionkey - 1])) + strlen(end($sections[$sectionkey - 1])); $next_section_start = $sectionkey == end($sectionkeys) ? strlen($settingsText) : strpos($settingsText, $sections[$sectionkey + 1][0]); $new_settingsText .= "\n" . substr($settingsText, $prev_section_end, $next_section_start - $prev_section_end) . "\n"; } // Put the placeholders in this section into canonical order. else { $section_parts = array_flip($section); $pathcode_reached = false; foreach ($settings_defs as $var => $setting_def) { if ($var === $pathcode_var) $pathcode_reached = true; // Already did this setting, so move on to the next. if (in_array($var, $done_defs)) continue; // Stop when we hit a setting definition that will start a later section. if (isset($newsection_placeholders[$var]) && count($section) !== 1) break; // Stop when everything in this section is done, unless it's the last. // This helps maintain the relative position of any custom content. if (empty($section_parts) && $sectionkey < (count($sections) - 1)) break; $p = trim($substitutions[$var]['placeholder']); // Can't do anything with an empty placeholder. if ($p === '') continue; // Does this need to be inserted before the path correction code? if (strpos($new_settingsText, trim($substitutions[$pathcode_var]['placeholder'])) !== false && in_array($var, $force_before_pathcode)) { $new_settingsText = strtr($new_settingsText, array($substitutions[$pathcode_var]['placeholder'] => $p . "\n" . $substitutions[$pathcode_var]['placeholder'])); $bare_settingsText .= "\n" . $substitutions[$var]['placeholder']; $done_defs[] = $var; unset($section_parts[trim($substitutions[$var]['placeholder'])]); } // If it's in this section, add it to the new text now. elseif (in_array($p, $section)) { $new_settingsText .= "\n" . $substitutions[$var]['placeholder']; $done_defs[] = $var; unset($section_parts[trim($substitutions[$var]['placeholder'])]); } // Perhaps it is safe to reposition it anyway. elseif (is_string($var) && strpos($new_settingsText, $p) === false && strpos($all_custom_content, '$' . $var) === false) { $new_settingsText .= "\n" . $substitutions[$var]['placeholder']; $done_defs[] = $var; unset($section_parts[trim($substitutions[$var]['placeholder'])]); } // If this setting is missing entirely, fix it. elseif (strpos($bare_settingsText, $p) === false) { // Special case if the path code is missing. Put it near the end, // and also anything else that is missing that normally follows it. if (!isset($newsection_placeholders[$pathcode_var]) && $pathcode_reached === true && $sectionkey < (count($sections) - 1)) break; $new_settingsText .= "\n" . $substitutions[$var]['placeholder']; $bare_settingsText .= "\n" . $substitutions[$var]['placeholder']; $done_defs[] = $var; unset($section_parts[trim($substitutions[$var]['placeholder'])]); } } } } $settingsText = $new_settingsText; // Restore the leading and trailing placeholders as necessary. foreach (array(-1, -2) as $var) { if (!empty($substitutions[$var]['placeholder']) && strpos($settingsText, $substitutions[$var]['placeholder']) === false); { $settingsText = ($var == -1 ? $substitutions[$var]['placeholder'] : '') . $settingsText . ($var == -2 ? $substitutions[$var]['placeholder'] : ''); } } } // Even if not rebuilding, there are a few variables that may need to be moved around. else { $pathcode_pos = strpos($settingsText, $substitutions[$pathcode_var]['placeholder']); if ($pathcode_pos !== false) { foreach ($force_before_pathcode as $var) { if (!empty($substitutions[$var]['placeholder']) && strpos($settingsText, $substitutions[$var]['placeholder']) > $pathcode_pos) { $settingsText = strtr($settingsText, array( $substitutions[$var]['placeholder'] => '', $substitutions[$pathcode_var]['placeholder'] => $substitutions[$var]['placeholder'] . "\n" . $substitutions[$pathcode_var]['placeholder'], )); } } } } /* 3.c: Replace the placeholders with the final values */ // Where possible, perform simple substitutions. $settingsText = strtr($settingsText, $simple_replacements); // Deal with any complicated ones. if (!empty($replace_patterns)) $settingsText = preg_replace($replace_patterns, $replace_strings, $settingsText); // Make absolutely sure that the path correction code is included. if (strpos($settingsText, $substitutions[$pathcode_var]['replacement']) === false) $settingsText = preg_replace('~(?=\n#+ Error.Catching #+)~', "\n" . $substitutions[$pathcode_var]['replacement'] . "\n", $settingsText); // If we did not rebuild, do just enough to make sure the thing is viable. if (empty($rebuild)) { // We need to refresh $bare_settingsText again, and remove the code blocks from it. $bare_settingsText = $settingsText; foreach ($substitutions as $var => $substitution) { if (!is_int($var)) break; if (isset($substitution['replacement'])) $bare_settingsText = str_replace($substitution['replacement'], '', $bare_settingsText); } $bare_settingsText = strip_php_comments($bare_settingsText); // Now insert any defined settings that are missing. $pathcode_reached = false; foreach ($settings_defs as $var => $setting_def) { if ($var === $pathcode_var) $pathcode_reached = true; if (is_int($var)) continue; // Do nothing if it is already in there. if (preg_match($substitutions[$var]['search_pattern'], $bare_settingsText)) continue; // Insert it either before or after the path correction code, whichever is appropriate. if (!$pathcode_reached || in_array($var, $force_before_pathcode)) { $settingsText = preg_replace($substitutions[$pathcode_var]['search_pattern'], $substitutions[$var]['replacement'] . "\n\n$0", $settingsText); } else { $settingsText = preg_replace($substitutions[$pathcode_var]['search_pattern'], "$0\n\n" . $substitutions[$var]['replacement'], $settingsText); } } } // If we have any brand new settings to add, do so. foreach ($new_settings_vars as $var => $val) { if (isset($substitutions[$var]) && !preg_match($substitutions[$var]['search_pattern'], $settingsText)) { if (!isset($settings_defs[$var]) && strpos($settingsText, '# Custom Settings #') === false) $settingsText = preg_replace('~(?=\n#+ Error.Catching #+)~', "\n\n######### Custom Settings #########\n", $settingsText); $settingsText = preg_replace('~(?=\n#+ Error.Catching #+)~', $substitutions[$var]['replacement'] . "\n", $settingsText); } } // This is just cosmetic. Get rid of extra lines of whitespace. $settingsText = preg_replace('~\n\s*\n~', "\n\n", $settingsText); /************************************** * PART 4: Check syntax before saving * **************************************/ $temp_sfile = tempnam(sm_temp_dir(), md5($prefix . 'Settings.php')); file_put_contents($temp_sfile, $settingsText); $result = get_current_settings(filemtime($temp_sfile), $temp_sfile); unlink($temp_sfile); // If the syntax is borked, try rebuilding to see if that fixes it. if ($result === false) return empty($rebuild) ? updateSettingsFile($config_vars, $keep_quotes, true) : false; /****************************************** * PART 5: Write updated settings to file * ******************************************/ $success = safe_file_write($settingsFile, $settingsText, dirname($settingsFile) . '/Settings_bak.php', $last_settings_change); // Remember this in case updateSettingsFile is called twice. $mtime = filemtime($settingsFile); return $success; } /** * Retrieves a copy of the current values of all settings defined in Settings.php. * * Importantly, it does this without affecting our actual global variables at all, * and it performs safety checks before acting. The result is an array of the * values as recorded in the settings file. * * @param int $mtime Timestamp of last known good configuration. Defaults to time SMF started. * @param string $settingsFile The settings file. Defaults to SMF's standard Settings.php. * @return array An array of name/value pairs for all the settings in the file. */ function get_current_settings($mtime = null, $settingsFile = null) { $mtime = is_null($mtime) ? (defined('TIME_START') ? TIME_START : $_SERVER['REQUEST_TIME']) : (int) $mtime; if (!is_file($settingsFile)) { foreach (get_included_files() as $settingsFile) if (basename($settingsFile) === 'Settings.php') break; if (basename($settingsFile) !== 'Settings.php') return false; } // If the file has been changed since the last known good configuration, bail out. clearstatcache(); if (filemtime($settingsFile) > $mtime) return false; // Strip out opening and closing PHP tags. $settingsText = trim(file_get_contents($settingsFile)); if (substr($settingsText, 0, 5) == '<' . '?php') $settingsText = substr($settingsText, 5); if (substr($settingsText, -2) == '?' . '>') $settingsText = substr($settingsText, 0, -2); // Since we're using eval, we need to manually replace these with strings. $settingsText = strtr($settingsText, array( '__FILE__' => var_export($settingsFile, true), '__DIR__' => var_export(dirname($settingsFile), true), )); // Prevents warnings about constants that are already defined. $settingsText = preg_replace_callback( '~\bdefine\s*\(\s*(["\'])(\w+)\1~', function ($matches) { return 'define(\'' . md5(mt_rand()) . '\''; }, $settingsText ); // Handle eval errors gracefully in both PHP 5 and PHP 7 try { if($settingsText !== '' && @eval($settingsText) === false) throw new ErrorException('eval error'); unset($mtime, $settingsFile, $settingsText); $defined_vars = get_defined_vars(); } catch (Throwable $e) {} catch (ErrorException $e) {} if (isset($e)) return false; return $defined_vars; } /** * Writes data to a file, optionally making a backup, while avoiding race conditions. * * @param string $file The filepath of the file where the data should be written. * @param string $data The data to be written to $file. * @param string $backup_file The filepath where the backup should be saved. Default null. * @param int $mtime If modification time of $file is more recent than this Unix timestamp, the write operation will abort. Defaults to time that the script started execution. * @param bool $append If true, the data will be appended instead of overwriting the existing content of the file. Default false. * @return bool Whether the write operation succeeded or not. */ function safe_file_write($file, $data, $backup_file = null, $mtime = null, $append = false) { // Sanity checks. if (!file_exists($file) && !is_dir(dirname($file))) return false; if (!is_int($mtime)) $mtime = $_SERVER['REQUEST_TIME']; $temp_dir = sm_temp_dir(); // Our temp files. $temp_sfile = tempnam($temp_dir, pathinfo($file, PATHINFO_FILENAME) . '.'); if (!empty($backup_file)) $temp_bfile = tempnam($temp_dir, pathinfo($backup_file, PATHINFO_FILENAME) . '.'); // We need write permissions. $failed = false; foreach (array($file, $backup_file) as $sf) { if (empty($sf)) continue; if (!file_exists($sf)) touch($sf); elseif (!is_file($sf)) $failed = true; if (!$failed) $failed = !smf_chmod($sf); } // Now let's see if writing to a temp file succeeds. if (!$failed && file_put_contents($temp_sfile, $data, LOCK_EX) !== strlen($data)) $failed = true; // Tests passed, so it's time to do the job. if (!$failed) { // Back up the backup, just in case. if (file_exists($backup_file)) $temp_bfile_saved = @copy($backup_file, $temp_bfile); // Make sure no one changed the file while we weren't looking. clearstatcache(); if (filemtime($file) <= $mtime) { // Attempt to open the file. $sfhandle = @fopen($file, 'c'); // Let's do this thing! if ($sfhandle !== false) { // Immediately get a lock. flock($sfhandle, LOCK_EX); // Make sure the backup works before we do anything more. $temp_sfile_saved = @copy($file, $temp_sfile); // Now write our data to the file. if ($temp_sfile_saved) { if (empty($append)) { ftruncate($sfhandle, 0); rewind($sfhandle); } $failed = fwrite($sfhandle, $data) !== strlen($data); } else $failed = true; // If writing failed, put everything back the way it was. if ($failed) { if (!empty($temp_sfile_saved)) @rename($temp_sfile, $file); if (!empty($temp_bfile_saved)) @rename($temp_bfile, $backup_file); } // It worked, so make our temp backup the new permanent backup. elseif (!empty($backup_file)) @rename($temp_sfile, $backup_file); // And we're done. flock($sfhandle, LOCK_UN); fclose($sfhandle); } } } // We're done with these. @unlink($temp_sfile); @unlink($temp_bfile); if ($failed) return false; // Even though on normal installations the filemtime should invalidate any cached version // it seems that there are times it might not. So let's MAKE it dump the cache. if (function_exists('opcache_invalidate')) opcache_invalidate($file, true); return true; } /** * A wrapper around var_export whose output matches SMF coding conventions. * * @todo Add special handling for objects? * * @param mixed $var The variable to export * @return mixed A PHP-parseable representation of the variable's value */ function smf_var_export($var) { /* * Old versions of updateSettingsFile couldn't handle multi-line values. * Even though technically we can now, we'll keep arrays on one line for * the sake of backwards compatibility. */ if (is_array($var)) { $return = array(); foreach ($var as $key => $value) $return[] = var_export($key, true) . ' => ' . smf_var_export($value); return 'array(' . implode(', ', $return) . ')'; } // For the same reason, replace literal returns and newlines with "\r" and "\n" elseif (is_string($var) && (strpos($var, "\n") !== false || strpos($var, "\r") !== false)) { return strtr( preg_replace_callback( '/[\r\n]+/', function($m) { return '\' . "' . strtr($m[0], array("\r" => '\r', "\n" => '\n')) . '" . \''; }, var_export($var, true) ), array("'' . " => '', " . ''" => '') ); } // We typically use lowercase true/false/null. elseif (in_array(gettype($var), array('boolean', 'NULL'))) return strtolower(var_export($var, true)); // Nothing special. else return var_export($var, true); }; /** * Deletes all PHP comments from a string. * * @param string $code_str A string containing PHP code. * @return string A string of PHP code with no comments in it. */ function strip_php_comments($code_str) { // This is the faster, better way. if (is_callable('token_get_all')) { $tokens = token_get_all($code_str); $parts = array(); foreach ($tokens as $token) { if (is_string($token)) $parts[] = $token; else { list($id, $text) = $token; switch ($id) { case T_COMMENT: case T_DOC_COMMENT: end($parts); $prev_part = key($parts); // For the sake of tider output, trim any horizontal // whitespace that immediately preceded the comment. $parts[$prev_part] = rtrim($parts[$prev_part], "\t "); // For 'C' style comments, also trim one preceding // line break, if present. if (strpos($text, '/*') === 0) { if (substr($parts[$prev_part], -2) === "\r\n") $parts[$prev_part] = substr($parts[$prev_part], 0, -2); elseif (in_array(substr($parts[$prev_part], -1), array("\r", "\n"))) $parts[$prev_part] = substr($parts[$prev_part], 0, -1); } break; default: $parts[] = $text; break; } } } $code_str = implode('', $parts); return $code_str; } // If the tokenizer extension has been disabled, do the job manually. // Leave any heredocs alone. if (preg_match_all('/<<<([\'"]?)(\w+)\1?\R(.*?)\R\h*\2;$/ms', $code_str, $matches)) { $heredoc_replacements = array(); foreach ($matches[0] as $mkey => $heredoc) $heredoc_replacements[$heredoc] = var_export(md5($matches[3][$mkey]), true) . ';'; $code_str = strtr($code_str, $heredoc_replacements); } // Split before everything that could possibly delimit a comment or a string. $parts = preg_split('~(?=#+|/(?=/|\*)|\*/|\R|(? $part) { $one_char = substr($part, 0, 1); $two_char = substr($part, 0, 2); $to_remove = 0; /* * Meaning of $in_string values: * 0: not in a string * 1: in a single quote string * 2: in a double quote string */ if ($one_char == "'") { if (!empty($in_comment)) $in_string = 0; elseif (in_array($in_string, array(0, 1))) $in_string = ($in_string ^ 1); } elseif ($one_char == '"') { if (!empty($in_comment)) $in_string = 0; elseif (in_array($in_string, array(0, 2))) $in_string = ($in_string ^ 2); } /* * Meaning of $in_comment values: * 0: not in a comment * 1: in a single line comment * 2: in a multi-line comment */ elseif ($one_char == '#' || $two_char == '//') { $in_comment = !empty($in_string) ? 0 : (empty($in_comment) ? 1 : $in_comment); if ($in_comment == 1) { $parts[$partkey - 1] = rtrim($parts[$partkey - 1], "\t "); if (substr($parts[$partkey - 1], -2) === "\r\n") $parts[$partkey - 1] = substr($parts[$partkey - 1], 0, -2); elseif (in_array(substr($parts[$partkey - 1], -1), array("\r", "\n"))) $parts[$partkey - 1] = substr($parts[$partkey - 1], 0, -1); } } elseif ($two_char === "\r\n" || $one_char === "\r" || $one_char === "\n") { if ($in_comment == 1) $in_comment = 0; } elseif ($two_char == '/*') { $in_comment = !empty($in_string) ? 0 : (empty($in_comment) ? 2 : $in_comment); if ($in_comment == 2) { $parts[$partkey - 1] = rtrim($parts[$partkey - 1], "\t "); if (substr($parts[$partkey - 1], -2) === "\r\n") $parts[$partkey - 1] = substr($parts[$partkey - 1], 0, -2); elseif (in_array(substr($parts[$partkey - 1], -1), array("\r", "\n"))) $parts[$partkey - 1] = substr($parts[$partkey - 1], 0, -1); } } elseif ($two_char == '*/') { if ($in_comment == 2) { $in_comment = 0; // Delete the comment closing. $to_remove = 2; } } if (empty($in_comment)) $parts[$partkey] = strlen($part) > $to_remove ? substr($part, $to_remove) : ''; else $parts[$partkey] = ''; } $code_str = implode('', $parts); if (!empty($heredoc_replacements)) $code_str = strtr($code_str, array_flip($heredoc_replacements)); return $code_str; } /** * Saves the time of the last db error for the error log * - Done separately from updateSettingsFile to avoid race conditions * which can occur during a db error * - If it fails Settings.php will assume 0 * * @param int $time The timestamp of the last DB error * @param bool True If we should update the current db_last_error context as well. This may be useful in cases where the current context needs to know a error was logged since the last check. * @return bool True If we could succesfully put the file or not. */ function updateDbLastError($time, $update = true) { global $boarddir, $cachedir, $db_last_error; // Write out the db_last_error file with the error timestamp if (!empty($cachedir) && is_writable($cachedir)) $errorfile = $cachedir . '/db_last_error.php'; elseif (file_exists(dirname(__DIR__) . '/cache')) $errorfile = dirname(__DIR__) . '/cache/db_last_error.php'; else $errorfile = dirname(__DIR__) . '/db_last_error.php'; $result = file_put_contents($errorfile, '<' . '?' . "php\n" . '$db_last_error = ' . $time . ';' . "\n" . '?' . '>', LOCK_EX); @touch($boarddir . '/' . 'Settings.php'); // Unless requested, we should update $db_last_error as well. if ($update) $db_last_error = $time; // We do a loose match here rather than strict (!==) as 0 is also false. return $result != false; } /** * Saves the admin's current preferences to the database. */ function updateAdminPreferences() { global $options, $context, $smcFunc, $settings, $user_info; // This must exist! if (!isset($context['admin_preferences'])) return false; // This is what we'll be saving. $options['admin_preferences'] = $smcFunc['json_encode']($context['admin_preferences']); // Just check we haven't ended up with something theme exclusive somehow. $smcFunc['db_query']('', ' DELETE FROM {db_prefix}themes WHERE id_theme != {int:default_theme} AND variable = {string:admin_preferences}', array( 'default_theme' => 1, 'admin_preferences' => 'admin_preferences', ) ); // Update the themes table. $smcFunc['db_insert']('replace', '{db_prefix}themes', array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'), array($user_info['id'], 1, 'admin_preferences', $options['admin_preferences']), array('id_member', 'id_theme', 'variable') ); // Make sure we invalidate any cache. cache_put_data('theme_settings-' . $settings['theme_id'] . ':' . $user_info['id'], null, 0); } /** * Send all the administrators a lovely email. * - loads all users who are admins or have the admin forum permission. * - uses the email template and replacements passed in the parameters. * - sends them an email. * * @param string $template Which email template to use * @param array $replacements An array of items to replace the variables in the template * @param array $additional_recipients An array of arrays of info for additional recipients. Should have 'id', 'email' and 'name' for each. */ function emailAdmins($template, $replacements = array(), $additional_recipients = array()) { global $smcFunc, $sourcedir, $language, $modSettings; // We certainly want this. require_once($sourcedir . '/Subs-Post.php'); // Load all members which are effectively admins. require_once($sourcedir . '/Subs-Members.php'); $members = membersAllowedTo('admin_forum'); // Load their alert preferences require_once($sourcedir . '/Subs-Notify.php'); $prefs = getNotifyPrefs($members, 'announcements', true); $request = $smcFunc['db_query']('', ' SELECT id_member, member_name, real_name, lngfile, email_address FROM {db_prefix}members WHERE id_member IN({array_int:members})', array( 'members' => $members, ) ); $emails_sent = array(); while ($row = $smcFunc['db_fetch_assoc']($request)) { if (empty($prefs[$row['id_member']]['announcements'])) continue; // Stick their particulars in the replacement data. $replacements['IDMEMBER'] = $row['id_member']; $replacements['REALNAME'] = $row['member_name']; $replacements['USERNAME'] = $row['real_name']; // Load the data from the template. $emaildata = loadEmailTemplate($template, $replacements, empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']); // Then send the actual email. sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, $template, $emaildata['is_html'], 1); // Track who we emailed so we don't do it twice. $emails_sent[] = $row['email_address']; } $smcFunc['db_free_result']($request); // Any additional users we must email this to? if (!empty($additional_recipients)) foreach ($additional_recipients as $recipient) { if (in_array($recipient['email'], $emails_sent)) continue; $replacements['IDMEMBER'] = $recipient['id']; $replacements['REALNAME'] = $recipient['name']; $replacements['USERNAME'] = $recipient['name']; // Load the template again. $emaildata = loadEmailTemplate($template, $replacements, empty($recipient['lang']) || empty($modSettings['userLanguage']) ? $language : $recipient['lang']); // Send off the email. sendmail($recipient['email'], $emaildata['subject'], $emaildata['body'], null, $template, $emaildata['is_html'], 1); } } /** * Locates the most appropriate temp directory. * * Systems using `open_basedir` restrictions may receive errors with * `sys_get_temp_dir()` due to misconfigurations on servers. Other * cases sys_temp_dir may not be set to a safe value. Additionally * `sys_get_temp_dir` may use a readonly directory. This attempts to * find a working temp directory that is accessible under the * restrictions and is writable to the web service account. * * Directories checked against `open_basedir`: * * - `sys_get_temp_dir()` * - `upload_tmp_dir` * - `session.save_path` * - `cachedir` * * @return string */ function sm_temp_dir() { global $cachedir; static $temp_dir = null; // Already did this. if (!empty($temp_dir)) return $temp_dir; // Temp Directory options order. $temp_dir_options = array( 0 => 'sys_get_temp_dir', 1 => 'upload_tmp_dir', 2 => 'session.save_path', 3 => 'cachedir' ); // Determine if we should detect a restriction and what restrictions that may be. $open_base_dir = ini_get('open_basedir'); $restriction = !empty($open_base_dir) ? explode(':', $open_base_dir) : false; // Prevent any errors as we search. $old_error_reporting = error_reporting(0); // Search for a working temp directory. foreach ($temp_dir_options as $id_temp => $temp_option) { switch ($temp_option) { case 'cachedir': $possible_temp = rtrim($cachedir, '/'); break; case 'session.save_path': $possible_temp = rtrim(ini_get('session.save_path'), '/'); break; case 'upload_tmp_dir': $possible_temp = rtrim(ini_get('upload_tmp_dir'), '/'); break; default: $possible_temp = sys_get_temp_dir(); break; } // Check if we have a restriction preventing this from working. if ($restriction) { foreach ($restriction as $dir) { if (strpos($possible_temp, $dir) !== false && is_writable($possible_temp)) { $temp_dir = $possible_temp; break; } } } // No restrictions, but need to check for writable status. elseif (is_writable($possible_temp)) { $temp_dir = $possible_temp; break; } } // Fall back to sys_get_temp_dir even though it won't work, so we have something. if (empty($temp_dir)) $temp_dir = sys_get_temp_dir(); // Fix the path. $temp_dir = substr($temp_dir, -1) === '/' ? $temp_dir : $temp_dir . '/'; // Put things back. error_reporting($old_error_reporting); return $temp_dir; } ?>