Files
mrbs-equbao-2026/web/upgrade.inc
T
人事系统开发 48092cab42
Docker image / push (push) Canceled after 0s
MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、登录页JS修复等全部改动
2026-09-08 21:19:47 +08:00

463 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace MRBS;
use MRBS\DB\DB;
use MRBS\DB\DBException;
use MRBS\DB\DBFactory;
use MRBS\Errors\Errors;
use MRBS\Form\ElementFieldset;
use MRBS\Form\FieldInputPassword;
use MRBS\Form\FieldInputSubmit;
use MRBS\Form\FieldInputText;
use MRBS\Form\Form;
// This file checks to see whether the database schema versions (both global and local)
// are up-to-date, and if not performs the necessary upgrades.
// Get all the sub-site configuration files in this installation. Returns an array of site
// names indexed by configuration file. (We do it this way round in case the site names are
// integer-like, eg "1", "2", "3", etc., which will cause difficulties with PHP associative arrays.)
function get_site_config_files() : array
{
global $multisite;
$result = [];
if ($multisite)
{
$sites_dir = 'sites';
if (!is_readable($sites_dir))
{
trigger_error("The directory '$sites_dir' either does not exist or is not readable.", E_USER_WARNING);
return $result;
}
// Use scandir() rather than the DirectoryIterator in order to ensure that
// the sites are sorted.
$dirs = array_diff(scandir($sites_dir), array('.', '..'));
foreach ($dirs as $dir)
{
// Ignore anything that's not a directory
if (!is_dir($sites_dir . '/' . $dir))
{
continue;
}
// And ignore any directory that doesn't have a site config file
$site_config = $sites_dir . '/' . $dir . '/config.inc.php';
if (!is_readable($site_config))
{
continue;
}
$result[$site_config] = $dir;
}
}
return $result;
}
// Get the database config settings for a given $site_config.
// A NULL argument gets the main site config settings.
// Returns an array of settings.
function get_db_config(?string $site_config=null) : array
{
$result = [];
$vars = ['dbsys', 'db_host', 'db_database', 'db_port', 'db_schema', 'db_tbl_prefix', 'db_options'];
// Include the site's config files. No need to include areadefaults.inc.php as it doesn't contain DB settings.
include 'systemdefaults.inc.php';
include 'config.inc.php';
if (isset($site_config))
{
include $site_config;
}
foreach ($vars as $var)
{
$result[$var] = $$var ?? null;
}
return $result;
}
// Upgrade between database schema versions.
// Returns FALSE on error, TRUE if successful
// $local is a boolean specifying whether the upgrades are global MRBS ones ($local === false)
// or local upgrades ($local === true);
// $upgrade_handle is the database handle to use for the upgrade. It will typically
// have admin rights (eg CREATE and ALTER). [NB: the variable $upgrade_handle is used in
// the upgrade/*/post.inc files, so do not rename it without also editing those files.]
function upgrade_tables(bool $local, int $from, int $to, DB $upgrade_handle) : bool
{
global $dbsys;
$sql_type = $dbsys;
if ($sql_type == 'mysqli')
{
$sql_type = 'mysql';
}
for ($ver = ($from+1); $ver <= $to; $ver++)
{
echo "<p>\n";
$tag = ($local) ? get_vocab('upgrade_to_local_version') : get_vocab('upgrade_to_version');
echo get_vocab($tag) . ": $ver<br>\n";
if ($local)
{
$filename = "upgrade/local/$ver/$sql_type.sql";
$php_filename = "upgrade/local/$ver/post.inc";
}
else
{
$filename = "upgrade/$ver/$sql_type.sql";
$php_filename = "upgrade/$ver/post.inc";
}
$handle = fopen($filename, 'r');
if (!$handle)
{
// No need to localise, should never happen!
echo "Fatal error: Failed to open '$filename' for reading.\n";
return false;
}
$file_size = filesize($filename);
$sql = (!empty($file_size)) ? fread($handle, filesize($filename)) : '';
fclose($handle);
// PostgreSQL databases can have multiple schemas and so need a qualified
// table name when referring to the table name. However the table prefix is also
// used to make, for example, constraint, index and trigger names unique. These
// are unique within the schema and do not need to be schema-qualified. Also, when
// the tables were created from tables.pg.sql they would just have had "mrbs_"
// replaced by the unqualified table prefix.
$sql = str_replace('%DB_TBL_PREFIX%', _tbl('', true), $sql);
$sql = str_replace('%DB_TBL_PREFIX_SHORT%', _tbl('', false), $sql);
foreach (explode(";", $sql) as $query)
{
// Skip any empty query (so that last semicolon doesn't run
// an empty query)
if (preg_match("/\S/", $query))
{
$res = $upgrade_handle->query($query);
}
}
if ($ver > 1)
{
$variable_name = ($local) ? "local_db_version" : "db_version";
$upgrade_handle->command("UPDATE " . _tbl('variables') . " SET variable_content = ? ".
"WHERE variable_name = ?", array($ver, $variable_name));
}
// Now execute the PHP file if there is one
if (is_readable($php_filename))
{
include($php_filename);
}
echo get_vocab('ok');
echo "</p>\n";
}
return true;
}
// Upgrades the database tables defined by the current config context
function do_upgrade(DB $upgrade_handle) : bool
{
$result = true;
if (!$upgrade_handle->table_exists(_tbl('entry')))
{
echo "<p>" . escape_html(get_vocab('no_tables_found')) . "</p>\n";
}
else
{
$current_db_schema_version = db_schema_version($upgrade_handle);
$current_db_schema_version_local = db_schema_version($upgrade_handle, true);
// Do any MRBS upgrades first
if ($result && ($current_db_schema_version < DB::DB_SCHEMA_VERSION))
{
$result = $result && upgrade_tables(false, $current_db_schema_version, DB::DB_SCHEMA_VERSION, $upgrade_handle);
}
else
{
echo "<p>" . escape_html(get_vocab('already_at_version', $current_db_schema_version)) . "</p>\n";
}
// Then any local upgrades
if ($result && ($current_db_schema_version_local < DB::DB_SCHEMA_VERSION_LOCAL))
{
$result = $result && upgrade_tables(true, $current_db_schema_version_local, DB::DB_SCHEMA_VERSION_LOCAL, $upgrade_handle);
}
}
return $result;
}
function upgrade_site(array &$admin_handles, ?string $config_file=null) : bool
{
global $dbsys, $db_host, $db_database, $db_port, $db_schema, $db_tbl_prefix, $db_options;
global $db_admin_username, $db_admin_password;
// Get the site's DB config settings
$settings = get_db_config($config_file);
foreach ($settings as $var => $value)
{
$$var = $value;
}
// Form the new DSN
$dsn = DBFactory::createDsn($dbsys, $db_host, $db_database, $db_port ?? null);
// Try and get a handle for the site
if (!isset($admin_handles[$dsn]))
{
// Try and get a new connection using the login details we've already been given
try {
$admin_handle = DBFactory::create(
$dbsys,
$db_host,
$db_admin_username,
$db_admin_password,
$db_database,
false,
$db_port,
$db_options
);
$admin_handles[$dsn] = $admin_handle;
}
catch (DBException $e) {
// Don't do anything: we were only trying to see if we could connect using the same details
}
}
if (isset($admin_handles[$dsn]))
{
return do_upgrade($admin_handles[$dsn]);
}
echo "<p>" . escape_html(get_vocab('no_connection')) . "</p>\n";
return false;
}
function upgrade_report(bool $main_site_result, array $failed_sites) : void
{
global $multisite;
// Summarise
if (!$multisite)
{
// Just report on success. Failure will be obvious.
if ($main_site_result) {
echo "<p>" . escape_html(get_vocab('upgrade_completed')) . "</p>\n";
}
}
else
{
echo "<h2>" . escape_html(get_vocab('upgrade_summary')) . "</h2>\n";
if ($main_site_result && empty($failed_sites)) {
echo "<p>" . escape_html(get_vocab('upgrade_completed')) . "</p>\n";
}
else {
// Report on the main site
if (!$main_site_result) {
echo "<p>" . escape_html(get_vocab('main_site_failed')) . "</p>\n";
}
// Report on failed sub-sites
if (!empty($failed_sites)) {
echo "<p>" . escape_html(get_vocab('failed_sites')) . "</p>\n";
echo "<ul>\n";
foreach ($failed_sites as $failed_site) {
echo "<li>" . escape_html($failed_site) . "</li>\n";
}
echo "</ul>\n";
}
// Advice on what to do next
echo "<p>" . escape_html(get_vocab('retry_from_failing')) . "</p>\n";
}
}
}
// Get a database username and password
function db_get_userpass() : void
{
print_header();
$form = new Form(Form::METHOD_POST);
$form->setAttributes(array('class' => 'standard',
'id' => 'db_logon',
'action' => multisite(this_page())));
$fieldset = new ElementFieldset();
$fieldset->addLegend(get_vocab('database_login'));
// The username field
$field = new FieldInputText();
$field->setLabel('Database username')
->setControlAttributes(array('id' => 'form_username',
'name' => 'form_username',
'required' => true));
$fieldset->addElement($field);
// The password field
$field = new FieldInputPassword();
$field->setLabel('Database password')
->setControlAttributes(array('id' => 'form_password',
'name' => 'form_password'));
$fieldset->addElement($field);
// The submit button
$field = new FieldInputSubmit();
$field->setControlAttributes(array('value' => get_vocab('login')));
$fieldset->addElement($field);
$form->addElement($fieldset);
$form->render();
// Print footer and exit
print_footer(true);
}
// Sanity check: check that we can access the MRBS tables. If we can't, it's
// either because they don't exist or we don't have permission.
if (!db()->table_exists(_tbl('entry')))
{
Errors::fatalError(get_vocab('fatal_no_tables'));
}
// Check the database schema versions
$current_db_schema_version = db_schema_version(db());
$current_db_schema_version_local = db_schema_version(db(), true);
// Although we could check whether the database schema version is higher than
// that expected by MRBS, we don't. That's because if we do it prevents us
// performing backwards compatible database upgrades in the background, because
// the production site will stop working unnecessarily once the database has
// been upgraded.
// If either of the database schema version numbers are out of date, then
// upgrade the database - provided of course that the entry table exists.
if (($current_db_schema_version < DB::DB_SCHEMA_VERSION) ||
($current_db_schema_version_local < DB::DB_SCHEMA_VERSION_LOCAL))
{
// Upgrade needed
// Upgrades can take some time, so turn on implicit flushing so that status
// updates appear as they happen. Also turn off output compression because
// that will involve some buffering.
// Note that this won't always result in streamed output as (a) the web server
// (b) the browser may implement their own buffering. Solution: use Ajax calls?
ini_set('zlib.output_compression', '0');
ob_end_flush();
ob_implicit_flush();
// Just use a simple header as the normal header may (a) use features
// which are not available until after the database upgrade or (b) use
// functions which are not available until after dbsys has run.
print_simple_header();
echo '<h1>' . get_vocab('mrbs') . "</h1>\n";
echo '<p><strong>' . get_vocab('upgrade_required') . "</strong></p>\n";
$admin_handle = null;
// We need to open a connection to the database with a database
// username that has admin rights.
// TODO: The while loop isn't doing anything. Ideally we want to offer the user a
// TODO: chance to try a new username and password after a connection failure.
while (empty($admin_handle))
{
$db_admin_username = get_form_var('form_username', 'string');
$db_admin_password = get_form_var('form_password', 'string');
if (!isset($db_admin_username) || !isset($db_admin_password))
{
// Get a username and password if we haven't got them
echo '<p>' . get_vocab('supply_userpass') . "</p>\n";
echo '<p>' . get_vocab('contact_admin', $mrbs_admin) . "</p>\n";
db_get_userpass();
}
else
{
try {
$admin_handle = DBFactory::create(
$dbsys,
$db_host,
$db_admin_username,
$db_admin_password,
$db_database,
false,
$db_port,
$db_options
);
}
catch (DBException $e) {
trigger_error($e->getMessage(), E_USER_NOTICE);
Errors::fatalError(get_vocab('no_connection'));
}
}
}
// Check the CSRF token before we make any changes
Form::checkToken();
// If we're using multisite then the various sites could be using different databases, or even
// different database systems (MySQL or PostgreSQL). In that case we'll need different
// handles to access them, so we store the handles indexed by DSN (which will give enough detail
// to distinguish them).
$dsn = DBFactory::createDsn($dbsys, $db_host, $db_database, $db_port);
$admin_handles = [$dsn => $admin_handle];
// Get all the sub-site config files in this installation
$site_config_files = get_site_config_files();
// Upgrade each of the sub-sites
$failed_sites = [];
foreach ($site_config_files as $site_config_file => $dir)
{
// Do the upgrade for the sub-site
echo "<h2>" . escape_html(get_vocab('upgrading_site', $dir)) . "</h2>\n";
if (!upgrade_site($admin_handles, $site_config_file))
{
$failed_sites[] = $dir;
}
}
// Then upgrade the main site
// We only need this heading if we're in multi-site mode
if ($multisite)
{
echo "<h2>" . escape_html(get_vocab('upgrading_main_site')) . "</h2>\n";
}
$main_site_result = upgrade_site($admin_handles);
// Report on the upgrade
upgrade_report($main_site_result, $failed_sites);
// Close the database connections that have admin rights
foreach ($admin_handles as $admin_handle)
{
unset($admin_handle);
}
// Restore the DB config settings for the site we're supposed to be running
$settings = get_db_config('site_config.inc');
foreach ($settings as $var => $value)
{
$$var = $value;
}
echo '<p class="upgrade_nav"><a href="' . escape_html(multisite('index.php')) . '">' . get_vocab('returncal') . '</a>.</p>';
print_footer(true);
}