/** * Update the Settings.php file. * * Typically this method is used from admin screens, just like this entire class. * They're also available for addons and integrations. * * What it does: * - 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. * - does not escape or quote values. * - preserves case, formatting, and additional options in file. * - writes nothing if the resulting file would be less than 10 lines * in length (sanity check for read lock.) * - check for changes to db_last_error and passes those off to a separate handler * - attempts to create a backup file and will use it should the writing of the * new settings file fail * * @param mixed[] $config_vars */ public static function save_file($config_vars) { global $context; // Some older code is trying to updating the db_last_error, // then don't mess around with Settings.php if (count($config_vars) === 1 && isset($config_vars['db_last_error'])) { require_once SUBSDIR . '/Admin.subs.php'; updateDbLastError($config_vars['db_last_error']); return; } // When was Settings.php last changed? $last_settings_change = filemtime(BOARDDIR . '/Settings.php'); // Load the settings file. $settingsArray = trim(file_get_contents(BOARDDIR . '/Settings.php')); // Break it up based on \r or \n, and then clean out extra characters. if (strpos($settingsArray, "\n") !== false) { $settingsArray = explode("\n", $settingsArray); } elseif (strpos($settingsArray, "\r") !== false) { $settingsArray = explode("\r", $settingsArray); } else { return; } // Presumably, the file has to have stuff in it for this function to be called :P. if (count($settingsArray) < 10) { return; } // remove any /r's that made there way in here foreach ($settingsArray as $k => $dummy) { $settingsArray[$k] = strtr($dummy, array("\r" => '')) . "\n"; } // go line by line and see whats changing for ($i = 0, $n = count($settingsArray); $i < $n; $i++) { // Don't trim or bother with it if it's not a variable. if (substr($settingsArray[$i], 0, 1) != '$') { continue; } $settingsArray[$i] = trim($settingsArray[$i]) . "\n"; // Look through the variables to set.... foreach ($config_vars as $var => $val) { // be sure someone is not updating db_last_error this with a group if ($var === 'db_last_error') { updateDbLastError($val); unset($config_vars[$var]); } elseif (strncasecmp($settingsArray[$i], '$' . $var, 1 + strlen($var)) == 0) { $comment = strstr(substr($settingsArray[$i], strpos($settingsArray[$i], ';')), '#'); $settingsArray[$i] = '$' . $var . ' = ' . $val . ';' . ($comment == '' ? '' : "\t\t" . rtrim($comment)) . "\n"; // This one's been 'used', so to speak. unset($config_vars[$var]); } } // End of the file ... maybe if (substr(trim($settingsArray[$i]), 0, 2) == '?' . '>') { $end = $i; } } // This should never happen, but apparently it is happening. if (empty($end) || $end < 10) { $end = count($settingsArray) - 1; } // Still more variables to go? Then lets add them at the end. if (!empty($config_vars)) { if (trim($settingsArray[$end]) == '?' . '>') { $settingsArray[$end++] = ''; } else { $end++; } // Add in any newly defined vars that were passed foreach ($config_vars as $var => $val) { $settingsArray[$end++] = '$' . $var . ' = ' . $val . ';' . "\n"; } } else { $settingsArray[$end] = trim($settingsArray[$end]); } // Sanity error checking: the file needs to be at least 12 lines. if (count($settingsArray) < 12) { return; } // Try to avoid a few pitfalls: // - like a possible race condition, // - or a failure to write at low diskspace // // Check before you act: if cache is enabled, we can do a simple write test // to validate that we even write things on this filesystem. if ((!defined('CACHEDIR') || !file_exists(CACHEDIR)) && file_exists(BOARDDIR . '/cache')) { $tmp_cache = BOARDDIR . '/cache'; } else { $tmp_cache = CACHEDIR; } $test_fp = @fopen($tmp_cache . '/settings_update.tmp', 'w+'); if ($test_fp) { fclose($test_fp); $written_bytes = file_put_contents($tmp_cache . '/settings_update.tmp', 'test', LOCK_EX); @unlink($tmp_cache . '/settings_update.tmp'); if ($written_bytes !== 4) { // Oops. Low disk space, perhaps. Don't mess with Settings.php then. // No means no. :P return; } } // Protect me from what I want! :P clearstatcache(); if (filemtime(BOARDDIR . '/Settings.php') === $last_settings_change) { // Save the old before we do anything $settings_backup_fail = !@is_writable(BOARDDIR . '/Settings_bak.php') || !@copy(BOARDDIR . '/Settings.php', BOARDDIR . '/Settings_bak.php'); $settings_backup_fail = !$settings_backup_fail ? !file_exists(BOARDDIR . '/Settings_bak.php') || filesize(BOARDDIR . '/Settings_bak.php') === 0 : $settings_backup_fail; // Write out the new $write_settings = implode('', $settingsArray); $written_bytes = file_put_contents(BOARDDIR . '/Settings.php', $write_settings, LOCK_EX); // Survey says ... if ($written_bytes !== strlen($write_settings) && !$settings_backup_fail) { // Well this is not good at all, lets see if we can save this $context['settings_message'] = 'settings_error'; if (file_exists(BOARDDIR . '/Settings_bak.php')) { @copy(BOARDDIR . '/Settings_bak.php', BOARDDIR . '/Settings.php'); } } // And ensure we are going to read the correct file next time if (function_exists('opcache_invalidate')) { opcache_invalidate(BOARDDIR . '/Settings.php'); } } }
/** * Database error. * Backtrace, log, try to fix. * * @param string $db_string * @param resource|null $connection = null */ public function error($db_string, $connection = null) { global $txt, $context, $webmaster_email, $modSettings; global $db_last_error, $db_persist; global $db_server, $db_user, $db_passwd, $db_name, $db_show_debug, $ssi_db_user, $ssi_db_passwd; // Get the file and line numbers. list($file, $line) = $this->error_backtrace('', '', 'return', __FILE__, __LINE__); // Decide which connection to use $connection = $connection === null ? $this->_connection : $connection; // This is the error message... $query_error = mysqli_error($connection); $query_errno = mysqli_errno($connection); // Error numbers: // 1016: Can't open file '....MYI' // 1030: Got error ??? from table handler. // 1034: Incorrect key file for table. // 1035: Old key file for table. // 1142: Command denied // 1205: Lock wait timeout exceeded. // 1213: Deadlock found. // 2006: Server has gone away. // 2013: Lost connection to server during query. // We cannot do something, try to find out what and act accordingly if ($query_errno == 1142) { $command = substr(trim($db_string), 0, 6); if ($command === 'DELETE' || $command === 'UPDATE' || $command === 'INSERT') { // We can try to ignore it (warning the admin though it's a thing to do) // and serve the page just SELECTing $_SESSION['query_command_denied'][$command] = $query_error; // Let the admin know there is a command denied issue if (function_exists('log_error')) { log_error($txt['database_error'] . ': ' . $query_error . (!empty($modSettings['enableErrorQueryLogging']) ? "\n\n{$db_string}" : ''), 'database', $file, $line); } return false; } } // Log the error. if ($query_errno != 1213 && $query_errno != 1205 && function_exists('log_error')) { log_error($txt['database_error'] . ': ' . $query_error . (!empty($modSettings['enableErrorQueryLogging']) ? "\n\n{$db_string}" : ''), 'database', $file, $line); } // Database error auto fixing ;). if (function_exists('cache_get_data') && (!isset($modSettings['autoFixDatabase']) || $modSettings['autoFixDatabase'] == '1')) { // Force caching on, just for the error checking. $old_cache = isset($modSettings['cache_enable']) ? $modSettings['cache_enable'] : null; $modSettings['cache_enable'] = '1'; if (($temp = cache_get_data('db_last_error', 600)) !== null) { $db_last_error = max(@$db_last_error, $temp); } if (@$db_last_error < time() - 3600 * 24 * 3) { // We know there's a problem... but what? Try to auto detect. if ($query_errno == 1030 && strpos($query_error, ' 127 ') !== false) { preg_match_all('~(?:[\\n\\r]|^)[^\']+?(?:FROM|JOIN|UPDATE|TABLE) ((?:[^\\n\\r(]+?(?:, )?)*)~s', $db_string, $matches); $fix_tables = array(); foreach ($matches[1] as $tables) { $tables = array_unique(explode(',', $tables)); foreach ($tables as $table) { // Now, it's still theoretically possible this could be an injection. So backtick it! if (trim($table) != '') { $fix_tables[] = '`' . strtr(trim($table), array('`' => '')) . '`'; } } } $fix_tables = array_unique($fix_tables); } elseif ($query_errno == 1016) { if (preg_match('~\'([^\\.\']+)~', $query_error, $match) != 0) { $fix_tables = array('`' . $match[1] . '`'); } } elseif ($query_errno == 1034 || $query_errno == 1035) { preg_match('~\'([^\']+?)\'~', $query_error, $match); $fix_tables = array('`' . $match[1] . '`'); } } // Check for errors like 145... only fix it once every three days, and send an email. (can't use empty because it might not be set yet...) if (!empty($fix_tables)) { // subs/Admin.subs.php for updateDbLastError(), subs/Mail.subs.php for sendmail(). require_once SUBSDIR . '/Admin.subs.php'; require_once SUBSDIR . '/Mail.subs.php'; // Make a note of the REPAIR... cache_put_data('db_last_error', time(), 600); if (($temp = cache_get_data('db_last_error', 600)) === null) { updateDbLastError(time()); } // Attempt to find and repair the broken table. foreach ($fix_tables as $table) { $this->query('', "\n\t\t\t\t\t\tREPAIR TABLE {$table}", false, false); } // And send off an email! sendmail($webmaster_email, $txt['database_error'], $txt['tried_to_repair']); $modSettings['cache_enable'] = $old_cache; // Try the query again...? $ret = $this->query('', $db_string, false, false); if ($ret !== false) { return $ret; } } else { $modSettings['cache_enable'] = $old_cache; } // Check for the "lost connection" or "deadlock found" errors - and try it just one more time. if (in_array($query_errno, array(1205, 1213, 2006, 2013))) { if (in_array($query_errno, array(2006, 2013)) && $this->_connection == $connection) { // Are we in SSI mode? If so try that username and password first if (ELK == 'SSI' && !empty($ssi_db_user) && !empty($ssi_db_passwd)) { $new_connection = @mysqli_connect((!empty($db_persist) ? 'p:' : '') . $db_server, $ssi_db_user, $ssi_db_passwd, $db_name); } // Fall back to the regular username and password if need be if (!$new_connection) { $new_connection = @mysqli_connect((!empty($db_persist) ? 'p:' : '') . $db_server, $db_user, $db_passwd, $db_name); } } if ($new_connection) { $this->_connection = $new_connection; // Try a deadlock more than once more. for ($n = 0; $n < 4; $n++) { $ret = $this->query('', $db_string, false, false); $new_errno = mysqli_errno($new_connection); if ($ret !== false || in_array($new_errno, array(1205, 1213))) { break; } } // If it failed again, shucks to be you... we're not trying it over and over. if ($ret !== false) { return $ret; } } } elseif ($query_errno == 1030 && (strpos($query_error, ' -1 ') !== false || strpos($query_error, ' 28 ') !== false || strpos($query_error, ' 12 ') !== false)) { if (!isset($txt)) { $query_error .= ' - check database storage space.'; } else { if (!isset($txt['mysql_error_space'])) { loadLanguage('Errors'); } $query_error .= !isset($txt['mysql_error_space']) ? ' - check database storage space.' : $txt['mysql_error_space']; } } } // Nothing's defined yet... just die with it. if (empty($context) || empty($txt)) { die($query_error); } // Show an error message, if possible. $context['error_title'] = $txt['database_error']; if (allowedTo('admin_forum')) { $context['error_message'] = nl2br($query_error) . '<br />' . $txt['file'] . ': ' . $file . '<br />' . $txt['line'] . ': ' . $line; } else { $context['error_message'] = $txt['try_again']; } // Add database version that we know of, for the admin to know. (and ask for support) if (allowedTo('admin_forum')) { $context['error_message'] .= '<br /><br />' . sprintf($txt['database_error_versions'], $modSettings['elkVersion']); } if (allowedTo('admin_forum') && isset($db_show_debug) && $db_show_debug === true) { $context['error_message'] .= '<br /><br />' . nl2br($db_string); } // It's already been logged... don't log it again. fatal_error($context['error_message'], false); }