Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ jobs:
[ -n "$integrationtestdb_container" ] || { echo "integrationtestdb container ID not found"; exit 1; }
timeout 180s bash -c "until [ \"\$(docker inspect --format '{{.State.Health.Status}}' \"${integrationtestdb_container}\" 2>/dev/null)\" = \"healthy\" ]; do sleep 3; done"

docker compose -f docker-compose-test.yml exec -T --workdir /var/www/public php php test/scripts/finalizeInstallSchemaVersion.php

# 2. Pre-clean the integration database
docker compose -f docker-compose-test.yml exec -T integrationtestdb mysql -udev -pdev -e "DROP DATABASE IF EXISTS cats_integrationtest; CREATE DATABASE cats_integrationtest;"

Expand Down
27 changes: 27 additions & 0 deletions ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
include_once(LEGACY_ROOT . '/lib/Session.php'); /* Depends: MRU, Users, DatabaseConnection. */
include_once(LEGACY_ROOT . '/lib/AJAXInterface.php');
include_once(LEGACY_ROOT . '/lib/CATSUtility.php');
include_once(LEGACY_ROOT . '/lib/SchemaMigrationStatus.php');


header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
Expand Down Expand Up @@ -117,6 +118,8 @@
}
}

$module = '';

if (strpos($_REQUEST['f'], ':') === false)
{
$function = preg_replace("/[^A-Za-z0-9]/", "", $_REQUEST['f']);
Expand All @@ -134,6 +137,30 @@
$filename = sprintf('modules/%s/ajax/%s.php', $module, $function);
}

/* Fresh installer AJAX remains available. On installed systems, only the
* existing maintenance action may pass the pending-migration AJAX gate.
*/
$maintenanceAJAXAllowed =
$installerActive ||
($module === 'install' && $function === 'maint');

if (isset($_SESSION['CATS']) &&
$_SESSION['CATS']->isLoggedIn() &&
!$maintenanceAJAXAllowed &&
SchemaMigrationStatus::hasPendingInstallMigrations())
{
header('Content-type: text/xml; charset=' . AJAX_ENCODING);
echo '<?xml version="1.0" encoding="', AJAX_ENCODING, '"?>', "\n";
echo(
"<data>\n" .
" <errorcode>-1</errorcode>\n" .
" <errormessage>Database maintenance is required.</errormessage>\n" .
"</data>\n"
);

die();
}

if (!is_readable($filename))
{
header('Content-type: text/xml; charset=' . AJAX_ENCODING);
Expand Down
36 changes: 36 additions & 0 deletions index.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,46 @@
}
}

$isPublicRequest =
(isset($careerPage) && $careerPage) ||
(isset($_GET['showCareerPortal']) && $_GET['showCareerPortal'] == '1') ||
(isset($rssPage) && $rssPage) ||
(isset($xmlPage) && $xmlPage);
$isMigrationGateExcluded =
(isset($_GET['m']) && $_GET['m'] === 'install' &&
isset($_GET['a']) && $_GET['a'] === 'maint') ||
(isset($_GET['m']) && ($_GET['m'] === 'login' || $_GET['m'] === 'logout'));

if ($_SESSION['CATS']->isLoggedIn() &&
!$isPublicRequest &&
!$isMigrationGateExcluded &&
SchemaMigrationStatus::hasPendingInstallMigrations())
{
$template = new Template();
$template->assign(
'isAdministrator',
$_SESSION['CATS']->getAccessLevel(ACL::SECOBJ_ROOT) >= ACCESS_LEVEL_SA
);
$template->display('./modules/login/PendingMigrations.tpl');
die();
}

/* Check to see if we are supposed to display the career page. */
if (((isset($careerPage) && $careerPage) ||
(isset($_GET['showCareerPortal']) && $_GET['showCareerPortal'] == '1')))
{
if (SchemaMigrationStatus::hasPendingInstallMigrations())
{
header('HTTP/1.1 503 Service Unavailable');
header('Content-Type: text/html; charset=UTF-8');

echo '<!DOCTYPE html>',
'<html><head><title>Career Portal Maintenance</title></head><body>',
'<p>The career portal is temporarily unavailable while system maintenance is in progress. Please try again later.</p>',
'</body></html>';
die();
}

ModuleUtility::loadModule('careers');
}

Expand Down
26 changes: 24 additions & 2 deletions js/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

var response;
var maxSteps;
var maintenanceOnly = false;


function setActiveStep(step)
Expand Down Expand Up @@ -119,9 +120,30 @@

response = http.responseText;

if (maintenanceOnly &&
(http.status < 200 ||
http.status >= 300 ||
AJAX_isPHPError(response) ||

Check notice on line 126 in js/install.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/install.js#L126

A function with a name starting with an uppercase letter should only be used as a constructor.
response.indexOf("<errorcode>-1</errorcode>") != -1 ||
response.indexOf("Query Error") != -1 ||
response.indexOf("Access denied.") != -1))
{
document.getElementById("maintenanceProgress").style.display = "none";
document.getElementById("maintenanceError").style.display = "";
document.getElementById("startMaintenance").disabled = false;
return;
}

if (response.indexOf("setProgressUpdating") == -1)
{
Installpage_populate("a=reindexResumes");
{
if (maintenanceOnly)
{
window.location = "index.php";
}
else
{
Installpage_populate("a=reindexResumes");
}
}
else
{
Expand Down
23 changes: 21 additions & 2 deletions lib/ModuleUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
* @package CATS
* @subpackage Library
*/
include_once(LEGACY_ROOT . '/lib/SchemaMigrationStatus.php');

class ModuleUtility
{
/* Prevent this class from being instantiated. */
Expand Down Expand Up @@ -441,6 +443,14 @@
*/
private static function processModuleSchema($moduleName, $schema)
{
global $maintPage;

if ($moduleName === 'install' &&
(!isset($maintPage) || $maintPage !== true))
{
return;
}

if( ini_get('safe_mode') )
{
//don't do anything in safe mode
Expand Down Expand Up @@ -501,7 +511,11 @@

if ($moduleName === 'install' && ($currentVersion === NULL || $currentVersion === ''))
{
/* A NULL install module version means the database came from cats_schema.sql and should not replay historical install migrations. */
/* This explicit installer/maintenance finalization is only for
* snapshot databases whose schema already matches the bundled
* baseline. It must not run during normal requests and is not
* proof that an unknown historical database state is current.
*/
$sql = sprintf(
"UPDATE
module_schema
Expand All @@ -514,6 +528,7 @@
);
$db->query($sql);

SchemaMigrationStatus::clearCache();

Check warning on line 531 in lib/ModuleUtility.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/ModuleUtility.php#L531

Avoid using static access to class 'SchemaMigrationStatus' in method 'processModuleSchema'.
return;
}

Expand All @@ -532,7 +547,6 @@
}

/* if maintPage, execute 1 query, output the next query and progress, and terminate. */
global $maintPage;
if ((isset($maintPage) && $maintPage === true))
{
if ($executedQuery == false)
Expand Down Expand Up @@ -586,6 +600,11 @@
$rs = $db->query($sql);

$currentVersion = $version;

if ($moduleName === 'install')
{
SchemaMigrationStatus::clearCache();
}
}
}
}
Expand Down
128 changes: 128 additions & 0 deletions lib/SchemaMigrationStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php
/**
* CATS
* Schema Migration Status Library
*
* @package CATS
* @subpackage Library
*/

include_once(LEGACY_ROOT . '/modules/install/Schema.php');

class SchemaMigrationStatus
{
private static $_latestInstallSchemaVersion = NULL;

Check notice on line 14 in lib/SchemaMigrationStatus.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/SchemaMigrationStatus.php#L14

Avoid excessively long variable names like $_latestInstallSchemaVersion. Keep variable name length under 20.
private static $_storedInstallSchemaVersion = NULL;

Check notice on line 15 in lib/SchemaMigrationStatus.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/SchemaMigrationStatus.php#L15

Avoid excessively long variable names like $_storedInstallSchemaVersion. Keep variable name length under 20.
private static $_storedInstallSchemaVersionLoaded = false;

Check notice on line 16 in lib/SchemaMigrationStatus.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/SchemaMigrationStatus.php#L16

Avoid excessively long variable names like $_storedInstallSchemaVersionLoaded. Keep variable name length under 20.
private static $_hasPendingInstallMigrations = NULL;

Check notice on line 17 in lib/SchemaMigrationStatus.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/SchemaMigrationStatus.php#L17

Avoid excessively long variable names like $_hasPendingInstallMigrations. Keep variable name length under 20.

/* Prevent this class from being instantiated. */
private function __construct() {}
private function __clone() {}

/**
* Returns the latest install schema version available in the code.
*
* @return integer
*/
public static function getLatestInstallSchemaVersion()
{
if (self::$_latestInstallSchemaVersion !== NULL)
{
return self::$_latestInstallSchemaVersion;
}

$schemaVersions = array_keys(CATSSchema::get());

Check warning on line 35 in lib/SchemaMigrationStatus.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/SchemaMigrationStatus.php#L35

Avoid using static access to class 'CATSSchema' in method 'getLatestInstallSchemaVersion'.
self::$_latestInstallSchemaVersion = empty($schemaVersions) ? 0 : (int) max($schemaVersions);

return self::$_latestInstallSchemaVersion;
}

/**
* Returns the install schema version stored in the database.
*
* NULL or an empty string is not a known applied schema version. It must
* remain unresolved until an explicit installer or maintenance flow
* finalizes it; this read-only checker must not normalize it.
*
* @return mixed Integer version, NULL or empty string if unresolved,
* or false if the row does not exist.
*/
public static function getStoredInstallSchemaVersion()
{
if (self::$_storedInstallSchemaVersionLoaded)
{
return self::$_storedInstallSchemaVersion;
}

$db = DatabaseConnection::getInstance();

Check warning on line 58 in lib/SchemaMigrationStatus.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/SchemaMigrationStatus.php#L58

Avoid using static access to class 'DatabaseConnection' in method 'getStoredInstallSchemaVersion'.

$sql = sprintf(
"SELECT
version AS version
FROM
module_schema
WHERE
name = %s",
$db->makeQueryString('install')
);
$rs = $db->getAssoc($sql);

if (empty($rs))
{
self::$_storedInstallSchemaVersion = false;
}
else
{
self::$_storedInstallSchemaVersion = $rs['version'];
}

self::$_storedInstallSchemaVersionLoaded = true;

return self::$_storedInstallSchemaVersion;
}

/**
* Returns whether install schema migrations are pending.
*
* @return boolean
*/
public static function hasPendingInstallMigrations()
{
if (self::$_hasPendingInstallMigrations !== NULL)
{
return self::$_hasPendingInstallMigrations;
}

$storedVersion = self::getStoredInstallSchemaVersion();

if ($storedVersion === false ||
$storedVersion === NULL ||
$storedVersion === '')
{
/* Unknown versions require explicit install or maintenance finalization. */
self::$_hasPendingInstallMigrations = true;
return true;
}

self::$_hasPendingInstallMigrations =
(int) $storedVersion < self::getLatestInstallSchemaVersion();

return self::$_hasPendingInstallMigrations;
}

/**
* Clears cached migration status after explicit maintenance updates.
*
* @return void
*/
public static function clearCache()
{
self::$_latestInstallSchemaVersion = NULL;
self::$_storedInstallSchemaVersion = NULL;
self::$_storedInstallSchemaVersionLoaded = false;
self::$_hasPendingInstallMigrations = NULL;
}
}

?>
28 changes: 28 additions & 0 deletions modules/install/CATSUI.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@

public function handleRequest()
{
if ($this->getAction() !== 'maint')
{
return;
}

if (!isset($_SESSION['CATS']) || !$_SESSION['CATS']->isLoggedIn())
{
CATSUtility::transferRelativeURI('m=login');

Check warning on line 51 in modules/install/CATSUI.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/install/CATSUI.php#L51

Avoid using static access to class 'CATSUtility' in method 'handleRequest'.
die();

Check warning on line 52 in modules/install/CATSUI.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/install/CATSUI.php#L52

The method handleRequest() contains an exit expression.
}

if ($_SESSION['CATS']->getAccessLevel(ACL::SECOBJ_ROOT) < ACCESS_LEVEL_SA)
{
header('HTTP/1.1 403 Forbidden');
CommonErrors::fatal(COMMONERROR_PERMISSION, $this);

Check warning on line 58 in modules/install/CATSUI.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/install/CATSUI.php#L58

Avoid using static access to class 'CommonErrors' in method 'handleRequest'.
}

if (!SchemaMigrationStatus::hasPendingInstallMigrations())

Check warning on line 61 in modules/install/CATSUI.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/install/CATSUI.php#L61

Avoid using static access to class 'SchemaMigrationStatus' in method 'handleRequest'.
{
CATSUtility::transferRelativeURI('');

Check warning on line 63 in modules/install/CATSUI.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/install/CATSUI.php#L63

Avoid using static access to class 'CATSUtility' in method 'handleRequest'.
die();
}

$this->_template->assign(
'csrfToken',
$_SESSION['CATS']->getCSRFToken()
);
$this->_template->display('./modules/install/Maintenance.tpl');
}
}

Expand Down
Loading
Loading