Example #1
0
 /**
  * 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');
         }
     }
 }
Example #2
0
 /**
  * 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);
 }