SMWIssue4785


Wolfgang Fahl

Modified files for https://github.com/SemanticMediaWiki/SemanticMediaWiki/issues/4785

SetupCheck.php

<?php

namespace SMW;

use SMW\Utils\TemplateEngine;
use SMW\Utils\Logo;
use SMW\Localizer\LocalMessageProvider;
use SMW\Exception\FileNotReadableException;
use SMW\Exception\JSONFileParseException;
use RuntimeException;

/**
 * @private
 *
 * @license GNU GPL v2+
 * @since 3.1
 *
 * @author mwjames
 */
class SetupCheck {

	/**
	 * Semantic MediaWiki was loaded or accessed but not correctly enabled.
	 */
	const ERROR_EXTENSION_LOAD = 'ERROR_EXTENSION_LOAD';

	/**
	 * Semantic MediaWiki was loaded or accessed but not correctly enabled.
	 */
	const ERROR_EXTENSION_INVALID_ACCESS = 'ERROR_EXTENSION_INVALID_ACCESS';

	/**
	 * A user tried to use `wfLoadExtension( 'SemanticMediaWiki' )` and
	 * `enableSemantics` at the same causing the ExtensionRegistry to throw an
	 * "Uncaught Exception: It was attempted to load SemanticMediaWiki twice ..."
	 */
	const ERROR_EXTENSION_REGISTRY = 'ERROR_EXTENSION_REGISTRY';

	/**
	 * A dependency (extension, MediaWiki) causes an error
	 */
	const ERROR_EXTENSION_DEPENDENCY = 'ERROR_EXTENSION_DEPENDENCY';

	/**
	 * Multiple dependencies (extension, MediaWiki) caused an error
	 */
	const ERROR_EXTENSION_DEPENDENCY_MULTIPLE = 'ERROR_EXTENSION_DEPENDENCY_MULTIPLE';

	/**
	 * Extension doesn't match MediaWiki or the PHP requirement.
	 */
	const ERROR_EXTENSION_INCOMPATIBLE = 'ERROR_EXTENSION_INCOMPATIBLE';

	/**
	 * Extension doesn't match the DB requirement for Semantic MediaWiki.
	 */
	const ERROR_DB_REQUIREMENT_INCOMPATIBLE = 'ERROR_DB_REQUIREMENT_INCOMPATIBLE';

	/**
	 * The upgrade key has change causing the schema to be invalid
	 */
	const ERROR_SCHEMA_INVALID_KEY = 'ERROR_SCHEMA_INVALID_KEY';

	/**
	 * A selected default profile could not be loaded or is unknown.
	 */
	const ERROR_CONFIG_PROFILE_UNKNOWN = 'ERROR_CONFIG_PROFILE_UNKNOWN';

	/**
	 * The system is currently in a maintenance window
	 */
	const MAINTENANCE_MODE = 'MAINTENANCE_MODE';

	/**
	 * @var []
	 */
	private $options = [];

	/**
	 * @var SetupFile
	 */
	private $setupFile;

	/**
	 * @var TemplateEngine
	 */
	private $templateEngine;

	/**
	 * @var LocalMessageProvider
	 */
	private $localMessageProvider;

	/**
	 * @var []
	 */
	private $definitions = [];

	/**
	 * @var string
	 */
	private $languageCode = 'en';

	/**
	 * @var string
	 */
	private $fallbackLanguageCode = 'en';

	/**
	 * @var boolean
	 */
	private $sentHeader = true;

	/**
	 * @var string
	 */
	private $errorType = '';

	/**
	 * @var string
	 */
	private $errorMessage = '';

	/**
	 * @var string
	 */
	private $traceString = '';

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 * @param SetupFile|null $setupFile
	 */
	public function __construct( array $options, SetupFile $setupFile = null ) {
		$this->options = $options;
		$this->setupFile = $setupFile;
		$this->templateEngine = new TemplateEngine();
		$this->localMessageProvider = new LocalMessageProvider( '/local/setupcheck.i18n.json' );

		if ( $this->setupFile === null ) {
			$this->setupFile = new SetupFile();
		}
	}

	/**
	 * @since 3.2
	 *
	 * @param string $file
	 *
	 * @return array
	 * @throws RuntimeException
	 */
	public static function readFromFile( string $file ) : array {

		if ( !is_readable( $file ) ) {
			throw new FileNotReadableException( $file );
		}

		$contents = json_decode(
			file_get_contents( $file ),
			true
		);

		if ( json_last_error() === JSON_ERROR_NONE ) {
			return $contents;
		}

		throw new JSONFileParseException( $file );
	}

	/**
	 * @since 3.1
	 *
	 * @param SetupFile|null $setupFile
	 *
	 * @return SetupCheck
	 */
	public static function newFromDefaults( SetupFile $setupFile = null ) {

		if ( !defined( 'SMW_VERSION' ) ) {
			$version = self::readFromFile( $GLOBALS['smwgIP'] . 'extension.json' )['version'];
		} else {
			$version = SMW_VERSION;
		}

		$setupCheck = new SetupCheck(
			[
				'SMW_VERSION'    => $version,
				'MW_VERSION'     => $GLOBALS['wgVersion'], // MW_VERSION may not yet be defined!!
				'wgLanguageCode' => $GLOBALS['wgLanguageCode'],
				'smwgUpgradeKey' => $GLOBALS['smwgUpgradeKey']
			],
			$setupFile
		);

		return $setupCheck;
	}

	/**
	 * @since 3.2
	 */
	public function disableHeader() {
		$this->sentHeader = false;
	}

	/**
	 * @since 3.1
	 *
	 * @return boolean
	 */
	public function isCli() {
		return PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
	}

	/**
	 * @since 3.1
	 *
	 * @param string $traceString
	 */
	public function setTraceString( $traceString ) {
		$this->traceString = $traceString;
	}

	/**
	 * @since 3.2
	 *
	 * @param string $errorMessage
	 */
	public function setErrorMessage( string $errorMessage ) {
		$this->errorMessage = $errorMessage;
	}

	/**
	 * @since 3.2
	 *
	 * @param string $errorType
	 */
	public function setErrorType( string $errorType ) {
		$this->errorType = $errorType;
	}

	/**
	 * @since 3.2
	 *
	 * @return boolean
	 */
	public function isError( string $error ) : bool {
		return $this->errorType === $error;
	}

	/**
	 * @since 3.1
	 *
	 * @return boolean
	 */
	public function hasError() {

		$this->errorType = '';

		// When it is not a test run or run from the command line we expect that
		// the extension is registered using `enableSemantics`
		if ( !defined( 'SMW_EXTENSION_LOADED' ) && !$this->isCli() ) {
			$this->errorType = self::ERROR_EXTENSION_LOAD;
		} elseif ( $this->setupFile->inMaintenanceMode() ) {
			$this->errorType = self::MAINTENANCE_MODE;
		} elseif ( !$this->isCli() && !$this->setupFile->hasDatabaseMinRequirement() ) {
			$this->errorType = self::ERROR_DB_REQUIREMENT_INCOMPATIBLE;
		} elseif ( $this->setupFile->isGoodSchema() === false ) {
			$this->errorType = self::ERROR_SCHEMA_INVALID_KEY;
		}

		return $this->errorType !== '';
	}

	/**
	 * @note Adding a new error type requires to:
	 *
	 * - Define a constant to clearly identify the type of error
	 * - Extend the `setupcheck.json` to add a definition for the new type and
	 *   specify which information should be displayed
	 * - In case the existing HTML elements aren't sufficient, create a new
	 *   zxy.ms file and define the HTML code
	 *
	 * The `TemplateEngine` will replace arguments defined in the HTML hereby
	 * absolving this class from any direct HTML manipulation.
	 *
	 * @since 3.1
	 *
	 * @param boolean $isCli
	 *
	 * @return string
	 */
	public function getError( $isCli = false ) {

		$error = [
			'title' => '',
			'content' => ''
		];

		$this->languageCode = $_GET['uselang'] ?? $this->options['wgLanguageCode'] ?? 'en';

		// Output forms for different error types are registered with a JSON file.
		$this->definitions = $this->readFromFile(
			$GLOBALS['smwgDir'] . '/data/template/setupcheck/setupcheck.json'
		);

		// Error messages are specified in a special i18n JSON file to avoid relying
		// on the MW message system especially when SMW isn't fully registered
		// and we are unable to access any `smw-...` message keys from the standard
		// i18n files.
		$this->localMessageProvider->setLanguageCode(
			$this->languageCode
		);

		$this->localMessageProvider->loadMessages();

		// HTML specific formatting is contained in the following files where
		// a defined group of targets correspond to types used in the JSON
		$this->templateEngine->bulkLoad(
			[
				'/setupcheck/setupcheck.ms' => 'setupcheck-html',
				'/setupcheck/setupcheck.progress.ms' => 'setupcheck-progress',

				// Target specific elements
				'/setupcheck/setupcheck.section.ms'   => 'section',
				'/setupcheck/setupcheck.version.ms'   => 'version',
				'/setupcheck/setupcheck.paragraph.ms' => 'paragraph',
				'/setupcheck/setupcheck.errorbox.ms'  => 'errorbox',
				'/setupcheck/setupcheck.db.requirement.ms' => 'db-requirement',
			]
		);

		if ( !isset( $this->definitions['error_types'][$this->errorType] ) ) {
			throw new RuntimeException( "The `{$this->errorType}` type is not defined in the `setupcheck.json`!" );
		}

		$error = $this->createErrorContent( $this->errorType );

		if ( $isCli === false ) {
			$content = $this->buildHTML( $error );
			$this->header( 'Content-Type: text/html; charset=UTF-8' );
			$this->header( 'Content-Length: ' . strlen( $content ) );
			$this->header( 'Cache-control: none' );
			$this->header( 'Pragma: no-cache' );
		} else {
			$content = $error['title'] . "\n\n" . $error['content'];
			$content = str_replace(
				[ '<!-- ROW -->', '</h3>', '</h4>', '</p>', '&nbsp;' ],
				[ "\n", "\n\n", "\n\n", "\n\n", ' ' ],
				$content
			);
			$content = "\n" . wordwrap( strip_tags( trim( $content ) ), 73 );
		}

		return $content;
	}

	/**
	 * @since 3.1
	 *
	 * @param boolean $isCli
	 */
	public function showErrorAndAbort( $isCli = false ) {

		echo $this->getError( $isCli );

		if ( ob_get_level() ) {
			ob_flush();
			flush();
			ob_end_clean();
		}

		die();
	}

	private function header( $text ) {
		if ( $this->sentHeader ) {
			header( $text );
		}
  }

  private function schemaError() {
 	  // get trace
		// https://stackoverflow.com/a/7039409/1497139
		//
		$e = new \Exception;
		$content ='<pre>'.$e->getTraceAsString().'</pre>';
		$content .='PHP_SAPI: '.PHP_SAPI."<br>";
		$isCli=$this->isCli();
		$schemaState=SetupFile::getSchemaState($isCli);
		$content.='Schema state: '.$schemaState."<br>";
		$upgradeKeyBase=SetupFile::makeKey($GLOBALS);
    $content.='upgrade key base: '.$upgradeKeyBase;
    return $content;
  }

	private function createErrorContent( $type ) {

		$indicator_title = 'Error';
		$template = $this->definitions['error_types'][$type];
    $content = '';

    if ($type==self::ERROR_SCHEMA_INVALID_KEY) {
      $content.=$this->schemaError($this->isCli());
    }

		/**
		 * Actual output form
		 */
		foreach ( $template['output_form'] as $value ) {
			$content .= $this->createContent( $value, $type );
		}

		/**
		 * Special handling for the progress output
		 */
		if ( isset( $template['progress'] ) ) {
			foreach ( $template['progress'] as $value ) {
				$text = $this->createCopy( $value['text'] );

				if ( isset( $value['progress_keys'] ) ) {
					$content .= $this->createProgressIndicator( $value );
				}

				$args = [
					'text' => $text,
					'template' => $value['type']
				];

				$this->templateEngine->compile(
					$value['type'],
					$args
				);

				$content .= $this->templateEngine->publish( $value['type'] );
			}
		}

		/**
		 * Special handling for the stack trace output
		 */
		if ( isset( $template['stack_trace'] ) && $this->traceString !== '' ) {
			foreach ( $template['stack_trace'] as $value ) {
				$content .= $this->createContent( $value, $type );
			}
		}

		if ( isset( $template['indicator_title'] ) ) {
			$indicator_title = $this->createCopy( $template['indicator_title'] );
		}

		$error = [
			'title' => 'Semantic MediaWiki',
			'indicator_title' => $indicator_title,
			'content' => $content,
			'borderColor' => $template['indicator_color']
		];

		return $error;
	}

	private function createContent( $value, $type ) {

		if ( $value['text'] === 'ERROR_TEXT' ) {
			$text = str_replace( "\n", '<br>', $this->errorMessage );
		} elseif ( $value['text'] === 'ERROR_TEXT_MULTIPLE' ) {
			$errors = explode( "\n", $this->errorMessage );
			$text = '<ul><li>' . implode( '</li><li>', array_filter( $errors ) ) . '</li></ul>';
		} elseif ( $value['text'] === 'TRACE_STRING' ) {
			$text = $this->traceString;
		} else {
			$text = $this->createCopy( $value['text'] );
		}

		$args = [
			'text' => $text,
			'template' => $value['type']
		];

		if ( $value['type'] === 'version' ) {
			$args['version-title'] = $text;
			$args['smw-title'] = 'Semantic MediaWiki';
			$args['smw-version'] = $this->options['SMW_VERSION'] ?? 'n/a';
			$args['smw-upgradekey'] = $this->options['smwgUpgradeKey'] ?? 'n/a';
			$args['mw-title'] = 'MediaWiki';
			$args['mw-version'] = $this->options['MW_VERSION'] ?? 'n/a';
			$args['code-title'] = $this->createCopy( 'smw-setupcheck-code' );
			$args['code-type'] = $type;
		}

		if ( $value['type'] === 'db-requirement' ) {
			$requirements = $this->setupFile->get( SetupFile::DB_REQUIREMENTS );
			$args['version-title'] = $text;
			$args['db-title'] = $this->createCopy( 'smw-setupcheck-db-title' );
			$args['db-type'] = $requirements['type'] ?? 'N/A';
			$args['db-current-title'] = $this->createCopy( 'smw-setupcheck-db-current-title' );
			$args['db-minimum-title'] = $this->createCopy( 'smw-setupcheck-db-minimum-title' );
			$args['db-current-version'] = $requirements['latest_version'] ?? 'N/A';
			$args['db-minimum-version'] = $requirements['minimum_version'] ?? 'N/A';
		}

		// The type is expected to match a defined target and in an event
		// that those don't match an exception will be raised.
		$this->templateEngine->compile(
			$value['type'],
			$args
		);

		return $this->templateEngine->publish( $value['type'] );
	}

	private function createProgressIndicator( $value ) {

		$maintenanceMode = (array)$this->setupFile->getMaintenanceMode();
		$content = '';

		foreach ( $maintenanceMode as $key => $v ) {

			$args = [
				'label' => $key,
				'value' => $v
			];

			if ( isset( $value['progress_keys'][$key] ) ) {
				$args['label'] = $this->createCopy( $value['progress_keys'][$key] );
			}

			$this->templateEngine->compile(
				'setupcheck-progress',
				$args
			);

			$content .= $this->templateEngine->publish( 'setupcheck-progress' );
		}

		return $content;
	}

	private function createCopy( $value, $default = 'n/a' ) {

		if ( is_string( $value ) && $this->localMessageProvider->has( $value ) ) {
			return $this->localMessageProvider->msg( $value );
		}

		return $default;
	}

	private function buildHTML( array $error ) {

		$args = [
			'logo' => Logo::get( 'small' ),
			'title' => $error['title'] ?? '',
			'indicator' => $error['indicator_title'] ?? '',
			'content' => $error['content'] ?? '',
			'borderColor' => $error['borderColor'] ?? '#fff',
			'refresh' => $error['refresh'] ?? '30',
		];

		$this->templateEngine->compile(
			'setupcheck-html',
			$args
		);

		$html = $this->templateEngine->publish( 'setupcheck-html' );

		// Minify CSS rules, we keep them readable in the template to allow for
		// better adaption
		// @see http://manas.tungare.name/software/css-compression-in-php/
		$html = preg_replace_callback( "/<style\\b[^>]*>(.*?)<\\/style>/s", function( $matches ) {
				// Remove space after colons
				$style = str_replace( ': ', ':', $matches[0] );

				// Remove whitespace
				return str_replace( [ "\r\n", "\r", "\n", "\t", '  ', '    ', '    '], '', $style );
			},
			$html
		);

		return $html;
	}

}

SetupFile.php

<?php

namespace SMW;

use SMW\Exception\FileNotWritableException;
use SMW\Utils\File;
use SMW\SQLStore\Installer;

/**
 * @private
 *
 * @license GNU GPL v2+
 * @since 3.1
 *
 * @author mwjames
 */
class SetupFile {

	/**
	 * Describes the maintenance mode
	 */
	const MAINTENANCE_MODE = 'maintenance_mode';

	/**
	 * Describes the upgrade key
	 */
	const UPGRADE_KEY = 'upgrade_key';

	/**
	 * Describes the database requirements
	 */
	const DB_REQUIREMENTS = 'db_requirements';

	/**
	 * Describes the entity collection setting
	 */
	const ENTITY_COLLATION = 'entity_collation';

	/**
	 * Key that describes the date of the last table optimization run.
	 */
	const LAST_OPTIMIZATION_RUN = 'last_optimization_run';

	/**
	 * Describes the file name
	 */
	const FILE_NAME = '.smw.json';

	/**
	 * Describes incomplete tasks
	 */
	const INCOMPLETE_TASKS = 'incomplete_tasks';

	/**
	 * Versions
	 */
	const LATEST_VERSION = 'latest_version';
	const PREVIOUS_VERSION = 'previous_version';

	/**
	 * @var File
	 */
	private $file;

	/**
	 * @since 3.1
	 *
	 * @param File|null $file
	 */
	public function __construct( File $file = null ) {
		$this->file = $file;

		if ( $this->file === null ) {
			$this->file = new File();
		}
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 */
	public function loadSchema( &$vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		if ( isset( $vars['smw.json'] ) ) {
			return;
		}

		// @see #3506
		$file = File::dir( $vars['smwgConfigFileDir'] . '/' . self::FILE_NAME );

		// Doesn't exist? The `Setup::init` will take care of it by trying to create
		// a new file and if it fails or unable to do so wail raise an exception
		// as we expect to have access to it.
		if ( is_readable( $file ) ) {
			$vars['smw.json'] = json_decode( file_get_contents( $file ), true );
		}
	}

	/**
	 * @since 3.1
	 *
	 * @param boolean $isCli
	 *
	 * @return boolean
	 */
	public static function isGoodSchema( $isCli = false ) {
		// get the schema State as a  human readable description
		$schemaState=SetupFile::getSchemaState($isCli);
		// check that it starts with "ok:" and not "error:"
		$result=SetupFile::strStartsWith($schemaState,"ok:");
		return $result;
	}

	/**
	 * @since 3.1.7
	 *
	 * see https://stackoverflow.com/a/6513929/1497139
	 *
	 * @param string $haystack
	 * @param string $needle
	 *
	 * return boolean
	 */
	public static function strStartsWith($haystack, $needle) {
           return (strpos($haystack, $needle) === 0);
  }

	/**
	 * @since 3.1.7
	 *
	 * @param boolean $isCli
	 *
	 * @return string 
	 */
	public static function getSchemaState( $isCli = false ) {
          
		if ( $isCli && defined( 'MW_PHPUNIT_TEST' ) ) {
			return "ok: CLI with PHP Unit Test active";
		}

		if ( $isCli === false && ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) ) {
			return "ok: isCli is true and PHP_SAPI cli/phpdbg=".PHP_SAPI;
		}

		// #3563, Use the specific wiki-id as identifier for the instance in use
		$id = Site::id();

    if ( !isset( $GLOBALS['smw.json'][$id]['upgrade_key'] ) ) {
      global $smwgConfigFileDir;
			return "error: smw.json for ".$id." upgrade key missing - you might want to check \$smwgConfigFileDir:".$smwgConfigFileDir;
		}

		$upgradeKey = self::makeUpgradeKey( $GLOBALS );
		$expected   =$GLOBALS['smw.json'][$id]['upgrade_key'];
		if ( $upgradeKey === $expected ) 
			$schemaState= "ok: found upgradeKey.".$upgradeKey;
		else
			$schemaState= "error: expected upgradeKey ".$expected." for ".$id." but found ".$upgradeKey;

		if (
			isset( $GLOBALS['smw.json'][$id][self::MAINTENANCE_MODE] ) &&
			$GLOBALS['smw.json'][$id][self::MAINTENANCE_MODE] !== false ) {
			$schemaState= "error: upgradeKey ".$upgradeKey." is ok but maintainance is active";
		}

		return $schemaState;
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 *
	 * @return string
	 */
	public static function makeUpgradeKey( $vars ) {
		return sha1( self::makeKey( $vars ) );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 *
	 * @return boolean
	 */
	public function inMaintenanceMode( $vars = [] ) {

		if ( !defined( 'MW_PHPUNIT_TEST' ) && ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) ) {
			return false;
		}

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();

		if ( !isset( $vars['smw.json'][$id][self::MAINTENANCE_MODE] ) ) {
			return false;
		}

		return $vars['smw.json'][$id][self::MAINTENANCE_MODE] !== false;
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 *
	 * @return []
	 */
	public function getMaintenanceMode( $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();

		if ( !isset( $vars['smw.json'][$id][self::MAINTENANCE_MODE] ) ) {
			return [];
		}

		return $vars['smw.json'][$id][self::MAINTENANCE_MODE];
	}

	/**
	 * Tracking the latest and previous version, which allows us to decide whether
	 * current activties relate to an install (new) or upgrade.
	 *
	 * @since 3.2
	 *
	 * @param int $version
	 */
	public function setLatestVersion( $version ) {

		$latest = $this->get( SetupFile::LATEST_VERSION );
		$previous = $this->get( SetupFile::PREVIOUS_VERSION );

		if ( $latest === null && $previous === null ) {
			$this->set(
				[
					SetupFile::LATEST_VERSION => $version
				]
			);
		} elseif ( $latest !== $version ) {
			$this->set(
				[
					SetupFile::LATEST_VERSION => $version,
					SetupFile::PREVIOUS_VERSION => $latest
				]
			);
		}
	}

	/**
	 * @since 3.2
	 *
	 * @param string $key
	 * @param array $args
	 */
	public function addIncompleteTask( string $key, array $args = [] ) {

		$incomplete_tasks = $this->get( self::INCOMPLETE_TASKS );

		if ( $incomplete_tasks === null ) {
			$incomplete_tasks = [];
		}

		$incomplete_tasks[$key] = $args === [] ? true : $args;

		$this->set( [ self::INCOMPLETE_TASKS => $incomplete_tasks ] );
	}

	/**
	 * @since 3.2
	 *
	 * @param string $key
	 */
	public function removeIncompleteTask( string $key ) {

		$incomplete_tasks = $this->get( self::INCOMPLETE_TASKS );

		if ( $incomplete_tasks === null ) {
			$incomplete_tasks = [];
		}

		unset( $incomplete_tasks[$key] );

		$this->set( [ self::INCOMPLETE_TASKS => $incomplete_tasks ] );
	}

	/**
	 * @since 3.2
	 *
	 * @param array $vars
	 *
	 * @return boolean
	 */
	public function hasDatabaseMinRequirement( array $vars = [] ) : bool {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();

		// No record means, no issues!
		if ( !isset( $vars['smw.json'][$id][self::DB_REQUIREMENTS] ) ) {
			return true;
		}

		$requirements = $vars['smw.json'][$id][self::DB_REQUIREMENTS];

		return version_compare( $requirements['latest_version'], $requirements['minimum_version'], 'ge' );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 *
	 * @return []
	 */
	public function findIncompleteTasks( $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();
		$tasks = [];

		// Key field => [ value that constitutes the `INCOMPLETE` state, error msg ]
		$checks = [
			\SMW\SQLStore\Installer::POPULATE_HASH_FIELD_COMPLETE => [ false, 'smw-install-incomplete-populate-hash-field' ],
			\SMW\Elastic\ElasticStore::REBUILD_INDEX_RUN_COMPLETE => [ false, 'smw-install-incomplete-elasticstore-indexrebuild' ]
		];

		foreach ( $checks as $key => $value ) {

			if ( !isset( $vars['smw.json'][$id][$key] ) ) {
				continue;
			}

			if ( $vars['smw.json'][$id][$key] === $value[0] ) {
				$tasks[] = $value[1];
			}
		}

		if ( isset( $vars['smw.json'][$id][self::INCOMPLETE_TASKS] ) ) {
			foreach ( $vars['smw.json'][$id][self::INCOMPLETE_TASKS] as $key => $args ) {
				if ( $args === true ) {
					$tasks[] = $key;
				} else {
					$tasks[] = [ $key, $args ];
				}
			}
		}

		return $tasks;
	}

	/**
	 * @since 3.1
	 *
	 * @param mixed $maintenanceMode
	 */
	public function setMaintenanceMode( $maintenanceMode, $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$this->write(
			[
				self::UPGRADE_KEY => self::makeUpgradeKey( $vars ),
				self::MAINTENANCE_MODE => $maintenanceMode
			],
			$vars
		);
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 */
	public function finalize( $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		// #3563, Use the specific wiki-id as identifier for the instance in use
		$key = self::makeUpgradeKey( $vars );
		$id = Site::id();

		if (
			isset( $vars['smw.json'][$id][self::UPGRADE_KEY] ) &&
			$key === $vars['smw.json'][$id][self::UPGRADE_KEY] &&
			$vars['smw.json'][$id][self::MAINTENANCE_MODE] === false ) {
			return false;
		}

		$this->write(
			[
				self::UPGRADE_KEY => $key,
				self::MAINTENANCE_MODE => false
			],
			$vars
		);
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 */
	public function reset( $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();
		$args = [];

		if ( !isset( $vars['smw.json'][$id] ) ) {
			return;
		}

		$vars['smw.json'][$id] = [];

		$this->write( [], $vars );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $args
	 */
	public function set( array $args, $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$this->write( $args, $vars );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $args
	 */
	public function get( $key, $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();

		if ( isset( $vars['smw.json'][$id][$key] ) ) {
			return $vars['smw.json'][$id][$key];
		}

		return null;
	}

	/**
	 * @since 3.1
	 *
	 * @param string $key
	 */
	public function remove( $key, $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$this->write( [ $key => null ], $vars );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 * @param array $args
	 */
	public function write( array $args, array $vars ) {

		$configFile = File::dir( $vars['smwgConfigFileDir'] . '/' . self::FILE_NAME );
		$id = Site::id();

		if ( !isset( $vars['smw.json'] ) ) {
			$vars['smw.json'] = [];
		}

		foreach ( $args as $key => $value ) {
			// NULL means that the key key is removed
			if ( $value === null ) {
				unset( $vars['smw.json'][$id][$key] );
			} else {
				$vars['smw.json'][$id][$key] = $value;
			}
		}

		// Log the base elements used for computing the key
		$vars['smw.json'][$id]['upgrade_key_base'] = self::makeKey(
			$vars
		);

		// Remove legacy
		if ( isset( $vars['smw.json']['upgradeKey'] ) ) {
			unset( $vars['smw.json']['upgradeKey'] );
		}
		if ( isset( $vars['smw.json'][$id]['in.maintenance_mode'] ) ) {
			unset( $vars['smw.json'][$id]['in.maintenance_mode'] );
		}

		try {
			$this->file->write(
				$configFile,
				json_encode( $vars['smw.json'], JSON_PRETTY_PRINT )
			);
		} catch( FileNotWritableException $e ) {
			// Users may not have `wgShowExceptionDetails` enabled and would
			// therefore not see the exception error message hence we fail hard
			// and die
			die(
				"\n\nERROR: " . $e->getMessage() . "\n" .
				"\n       The \"smwgConfigFileDir\" setting should point to a" .
				"\n       directory that is persistent and writable!\n"
			);
		}
	}

	/**
	 * Listed keys will have a "global" impact of how data are stored, formatted,
	 * or represented in Semantic MediaWiki. In most cases it will require an action
	 * from an adminstrator when one of those keys are altered.
	 */
	public static function makeKey( $vars ) {

		// Only recognize those properties that require a fixed table
		$pageSpecialProperties = array_intersect(
			// Special properties enabled?
			$vars['smwgPageSpecialProperties'],

			// Any custom fixed properties require their own table?
			TypesRegistry::getFixedProperties( 'custom_fixed' )
		);

		$pageSpecialProperties = array_unique( $pageSpecialProperties );

		// Sort to ensure the key contains the same order
		sort( $vars['smwgFixedProperties'] );
		sort( $pageSpecialProperties );

		// The following settings influence the "shape" of the tables required
		// therefore use the content to compute a key that reflects any
		// changes to them
		$components = [
			$vars['smwgUpgradeKey'],
			$vars['smwgDefaultStore'],
			$vars['smwgFixedProperties'],
			$vars['smwgEnabledFulltextSearch'],
			$pageSpecialProperties
		];

		// Only add the key when it is different from the default setting
		if ( $vars['smwgEntityCollation'] !== 'identity' ) {
			$components += [ 'smwgEntityCollation' => $vars['smwgEntityCollation'] ];
		}

		if ( $vars['smwgFieldTypeFeatures'] !== false ) {
			$components += [ 'smwgFieldTypeFeatures' => $vars['smwgFieldTypeFeatures'] ];
		}

		// Recognize when the version requirements change and force
		// an update to be able to check the requirements
		$components += Setup::MINIMUM_DB_VERSION;

		return json_encode( $components );
	}

}

SetupCheck.php[edit]

<?php

namespace SMW;

use SMW\Utils\TemplateEngine;
use SMW\Utils\Logo;
use SMW\Localizer\LocalMessageProvider;
use SMW\Exception\FileNotReadableException;
use SMW\Exception\JSONFileParseException;
use RuntimeException;

/**
 * @private
 *
 * @license GNU GPL v2+
 * @since 3.1
 *
 * @author mwjames
 */
class SetupCheck {

	/**
	 * Semantic MediaWiki was loaded or accessed but not correctly enabled.
	 */
	const ERROR_EXTENSION_LOAD = 'ERROR_EXTENSION_LOAD';

	/**
	 * Semantic MediaWiki was loaded or accessed but not correctly enabled.
	 */
	const ERROR_EXTENSION_INVALID_ACCESS = 'ERROR_EXTENSION_INVALID_ACCESS';

	/**
	 * A user tried to use `wfLoadExtension( 'SemanticMediaWiki' )` and
	 * `enableSemantics` at the same causing the ExtensionRegistry to throw an
	 * "Uncaught Exception: It was attempted to load SemanticMediaWiki twice ..."
	 */
	const ERROR_EXTENSION_REGISTRY = 'ERROR_EXTENSION_REGISTRY';

	/**
	 * A dependency (extension, MediaWiki) causes an error
	 */
	const ERROR_EXTENSION_DEPENDENCY = 'ERROR_EXTENSION_DEPENDENCY';

	/**
	 * Multiple dependencies (extension, MediaWiki) caused an error
	 */
	const ERROR_EXTENSION_DEPENDENCY_MULTIPLE = 'ERROR_EXTENSION_DEPENDENCY_MULTIPLE';

	/**
	 * Extension doesn't match MediaWiki or the PHP requirement.
	 */
	const ERROR_EXTENSION_INCOMPATIBLE = 'ERROR_EXTENSION_INCOMPATIBLE';

	/**
	 * Extension doesn't match the DB requirement for Semantic MediaWiki.
	 */
	const ERROR_DB_REQUIREMENT_INCOMPATIBLE = 'ERROR_DB_REQUIREMENT_INCOMPATIBLE';

	/**
	 * The upgrade key has change causing the schema to be invalid
	 */
	const ERROR_SCHEMA_INVALID_KEY = 'ERROR_SCHEMA_INVALID_KEY';

	/**
	 * A selected default profile could not be loaded or is unknown.
	 */
	const ERROR_CONFIG_PROFILE_UNKNOWN = 'ERROR_CONFIG_PROFILE_UNKNOWN';

	/**
	 * The system is currently in a maintenance window
	 */
	const MAINTENANCE_MODE = 'MAINTENANCE_MODE';

	/**
	 * @var []
	 */
	private $options = [];

	/**
	 * @var SetupFile
	 */
	private $setupFile;

	/**
	 * @var TemplateEngine
	 */
	private $templateEngine;

	/**
	 * @var LocalMessageProvider
	 */
	private $localMessageProvider;

	/**
	 * @var []
	 */
	private $definitions = [];

	/**
	 * @var string
	 */
	private $languageCode = 'en';

	/**
	 * @var string
	 */
	private $fallbackLanguageCode = 'en';

	/**
	 * @var boolean
	 */
	private $sentHeader = true;

	/**
	 * @var string
	 */
	private $errorType = '';

	/**
	 * @var string
	 */
	private $errorMessage = '';

	/**
	 * @var string
	 */
	private $traceString = '';

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 * @param SetupFile|null $setupFile
	 */
	public function __construct( array $options, SetupFile $setupFile = null ) {
		$this->options = $options;
		$this->setupFile = $setupFile;
		$this->templateEngine = new TemplateEngine();
		$this->localMessageProvider = new LocalMessageProvider( '/local/setupcheck.i18n.json' );

		if ( $this->setupFile === null ) {
			$this->setupFile = new SetupFile();
		}
	}

	/**
	 * @since 3.2
	 *
	 * @param string $file
	 *
	 * @return array
	 * @throws RuntimeException
	 */
	public static function readFromFile( string $file ) : array {

		if ( !is_readable( $file ) ) {
			throw new FileNotReadableException( $file );
		}

		$contents = json_decode(
			file_get_contents( $file ),
			true
		);

		if ( json_last_error() === JSON_ERROR_NONE ) {
			return $contents;
		}

		throw new JSONFileParseException( $file );
	}

	/**
	 * @since 3.1
	 *
	 * @param SetupFile|null $setupFile
	 *
	 * @return SetupCheck
	 */
	public static function newFromDefaults( SetupFile $setupFile = null ) {

		if ( !defined( 'SMW_VERSION' ) ) {
			$version = self::readFromFile( $GLOBALS['smwgIP'] . 'extension.json' )['version'];
		} else {
			$version = SMW_VERSION;
		}

		$setupCheck = new SetupCheck(
			[
				'SMW_VERSION'    => $version,
				'MW_VERSION'     => $GLOBALS['wgVersion'], // MW_VERSION may not yet be defined!!
				'wgLanguageCode' => $GLOBALS['wgLanguageCode'],
				'smwgUpgradeKey' => $GLOBALS['smwgUpgradeKey']
			],
			$setupFile
		);

		return $setupCheck;
	}

	/**
	 * @since 3.2
	 */
	public function disableHeader() {
		$this->sentHeader = false;
	}

	/**
	 * @since 3.1
	 *
	 * @return boolean
	 */
	public function isCli() {
		return PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
	}

	/**
	 * @since 3.1
	 *
	 * @param string $traceString
	 */
	public function setTraceString( $traceString ) {
		$this->traceString = $traceString;
	}

	/**
	 * @since 3.2
	 *
	 * @param string $errorMessage
	 */
	public function setErrorMessage( string $errorMessage ) {
		$this->errorMessage = $errorMessage;
	}

	/**
	 * @since 3.2
	 *
	 * @param string $errorType
	 */
	public function setErrorType( string $errorType ) {
		$this->errorType = $errorType;
	}

	/**
	 * @since 3.2
	 *
	 * @return boolean
	 */
	public function isError( string $error ) : bool {
		return $this->errorType === $error;
	}

	/**
	 * @since 3.1
	 *
	 * @return boolean
	 */
	public function hasError() {

		$this->errorType = '';

		// When it is not a test run or run from the command line we expect that
		// the extension is registered using `enableSemantics`
		if ( !defined( 'SMW_EXTENSION_LOADED' ) && !$this->isCli() ) {
			$this->errorType = self::ERROR_EXTENSION_LOAD;
		} elseif ( $this->setupFile->inMaintenanceMode() ) {
			$this->errorType = self::MAINTENANCE_MODE;
		} elseif ( !$this->isCli() && !$this->setupFile->hasDatabaseMinRequirement() ) {
			$this->errorType = self::ERROR_DB_REQUIREMENT_INCOMPATIBLE;
		} elseif ( $this->setupFile->isGoodSchema() === false ) {
			$this->errorType = self::ERROR_SCHEMA_INVALID_KEY;
		}

		return $this->errorType !== '';
	}

	/**
	 * @note Adding a new error type requires to:
	 *
	 * - Define a constant to clearly identify the type of error
	 * - Extend the `setupcheck.json` to add a definition for the new type and
	 *   specify which information should be displayed
	 * - In case the existing HTML elements aren't sufficient, create a new
	 *   zxy.ms file and define the HTML code
	 *
	 * The `TemplateEngine` will replace arguments defined in the HTML hereby
	 * absolving this class from any direct HTML manipulation.
	 *
	 * @since 3.1
	 *
	 * @param boolean $isCli
	 *
	 * @return string
	 */
	public function getError( $isCli = false ) {

		$error = [
			'title' => '',
			'content' => ''
		];

		$this->languageCode = $_GET['uselang'] ?? $this->options['wgLanguageCode'] ?? 'en';

		// Output forms for different error types are registered with a JSON file.
		$this->definitions = $this->readFromFile(
			$GLOBALS['smwgDir'] . '/data/template/setupcheck/setupcheck.json'
		);

		// Error messages are specified in a special i18n JSON file to avoid relying
		// on the MW message system especially when SMW isn't fully registered
		// and we are unable to access any `smw-...` message keys from the standard
		// i18n files.
		$this->localMessageProvider->setLanguageCode(
			$this->languageCode
		);

		$this->localMessageProvider->loadMessages();

		// HTML specific formatting is contained in the following files where
		// a defined group of targets correspond to types used in the JSON
		$this->templateEngine->bulkLoad(
			[
				'/setupcheck/setupcheck.ms' => 'setupcheck-html',
				'/setupcheck/setupcheck.progress.ms' => 'setupcheck-progress',

				// Target specific elements
				'/setupcheck/setupcheck.section.ms'   => 'section',
				'/setupcheck/setupcheck.version.ms'   => 'version',
				'/setupcheck/setupcheck.paragraph.ms' => 'paragraph',
				'/setupcheck/setupcheck.errorbox.ms'  => 'errorbox',
				'/setupcheck/setupcheck.db.requirement.ms' => 'db-requirement',
			]
		);

		if ( !isset( $this->definitions['error_types'][$this->errorType] ) ) {
			throw new RuntimeException( "The `{$this->errorType}` type is not defined in the `setupcheck.json`!" );
		}

		$error = $this->createErrorContent( $this->errorType );

		if ( $isCli === false ) {
			$content = $this->buildHTML( $error );
			$this->header( 'Content-Type: text/html; charset=UTF-8' );
			$this->header( 'Content-Length: ' . strlen( $content ) );
			$this->header( 'Cache-control: none' );
			$this->header( 'Pragma: no-cache' );
		} else {
			$content = $error['title'] . "\n\n" . $error['content'];
			$content = str_replace(
				[ '<!-- ROW -->', '</h3>', '</h4>', '</p>', '&nbsp;' ],
				[ "\n", "\n\n", "\n\n", "\n\n", ' ' ],
				$content
			);
			$content = "\n" . wordwrap( strip_tags( trim( $content ) ), 73 );
		}

		return $content;
	}

	/**
	 * @since 3.1
	 *
	 * @param boolean $isCli
	 */
	public function showErrorAndAbort( $isCli = false ) {

		echo $this->getError( $isCli );

		if ( ob_get_level() ) {
			ob_flush();
			flush();
			ob_end_clean();
		}

		die();
	}

	private function header( $text ) {
		if ( $this->sentHeader ) {
			header( $text );
		}
  }

  private function schemaError() {
 	  // get trace
		// https://stackoverflow.com/a/7039409/1497139
		//
		$e = new \Exception;
		$content ='<pre>'.$e->getTraceAsString().'</pre>';
		$content .='PHP_SAPI: '.PHP_SAPI."<br>";
		$isCli=$this->isCli();
		$schemaState=SetupFile::getSchemaState($isCli);
		$content.='Schema state: '.$schemaState."<br>";
		$upgradeKeyBase=SetupFile::makeKey($GLOBALS);
    $content.='upgrade key base: '.$upgradeKeyBase;
    return $content;
  }

	private function createErrorContent( $type ) {

		$indicator_title = 'Error';
		$template = $this->definitions['error_types'][$type];
    $content = '';

    if ($type==self::ERROR_SCHEMA_INVALID_KEY) {
      $content.=$this->schemaError($this->isCli());
    }

		/**
		 * Actual output form
		 */
		foreach ( $template['output_form'] as $value ) {
			$content .= $this->createContent( $value, $type );
		}

		/**
		 * Special handling for the progress output
		 */
		if ( isset( $template['progress'] ) ) {
			foreach ( $template['progress'] as $value ) {
				$text = $this->createCopy( $value['text'] );

				if ( isset( $value['progress_keys'] ) ) {
					$content .= $this->createProgressIndicator( $value );
				}

				$args = [
					'text' => $text,
					'template' => $value['type']
				];

				$this->templateEngine->compile(
					$value['type'],
					$args
				);

				$content .= $this->templateEngine->publish( $value['type'] );
			}
		}

		/**
		 * Special handling for the stack trace output
		 */
		if ( isset( $template['stack_trace'] ) && $this->traceString !== '' ) {
			foreach ( $template['stack_trace'] as $value ) {
				$content .= $this->createContent( $value, $type );
			}
		}

		if ( isset( $template['indicator_title'] ) ) {
			$indicator_title = $this->createCopy( $template['indicator_title'] );
		}

		$error = [
			'title' => 'Semantic MediaWiki',
			'indicator_title' => $indicator_title,
			'content' => $content,
			'borderColor' => $template['indicator_color']
		];

		return $error;
	}

	private function createContent( $value, $type ) {

		if ( $value['text'] === 'ERROR_TEXT' ) {
			$text = str_replace( "\n", '<br>', $this->errorMessage );
		} elseif ( $value['text'] === 'ERROR_TEXT_MULTIPLE' ) {
			$errors = explode( "\n", $this->errorMessage );
			$text = '<ul><li>' . implode( '</li><li>', array_filter( $errors ) ) . '</li></ul>';
		} elseif ( $value['text'] === 'TRACE_STRING' ) {
			$text = $this->traceString;
		} else {
			$text = $this->createCopy( $value['text'] );
		}

		$args = [
			'text' => $text,
			'template' => $value['type']
		];

		if ( $value['type'] === 'version' ) {
			$args['version-title'] = $text;
			$args['smw-title'] = 'Semantic MediaWiki';
			$args['smw-version'] = $this->options['SMW_VERSION'] ?? 'n/a';
			$args['smw-upgradekey'] = $this->options['smwgUpgradeKey'] ?? 'n/a';
			$args['mw-title'] = 'MediaWiki';
			$args['mw-version'] = $this->options['MW_VERSION'] ?? 'n/a';
			$args['code-title'] = $this->createCopy( 'smw-setupcheck-code' );
			$args['code-type'] = $type;
		}

		if ( $value['type'] === 'db-requirement' ) {
			$requirements = $this->setupFile->get( SetupFile::DB_REQUIREMENTS );
			$args['version-title'] = $text;
			$args['db-title'] = $this->createCopy( 'smw-setupcheck-db-title' );
			$args['db-type'] = $requirements['type'] ?? 'N/A';
			$args['db-current-title'] = $this->createCopy( 'smw-setupcheck-db-current-title' );
			$args['db-minimum-title'] = $this->createCopy( 'smw-setupcheck-db-minimum-title' );
			$args['db-current-version'] = $requirements['latest_version'] ?? 'N/A';
			$args['db-minimum-version'] = $requirements['minimum_version'] ?? 'N/A';
		}

		// The type is expected to match a defined target and in an event
		// that those don't match an exception will be raised.
		$this->templateEngine->compile(
			$value['type'],
			$args
		);

		return $this->templateEngine->publish( $value['type'] );
	}

	private function createProgressIndicator( $value ) {

		$maintenanceMode = (array)$this->setupFile->getMaintenanceMode();
		$content = '';

		foreach ( $maintenanceMode as $key => $v ) {

			$args = [
				'label' => $key,
				'value' => $v
			];

			if ( isset( $value['progress_keys'][$key] ) ) {
				$args['label'] = $this->createCopy( $value['progress_keys'][$key] );
			}

			$this->templateEngine->compile(
				'setupcheck-progress',
				$args
			);

			$content .= $this->templateEngine->publish( 'setupcheck-progress' );
		}

		return $content;
	}

	private function createCopy( $value, $default = 'n/a' ) {

		if ( is_string( $value ) && $this->localMessageProvider->has( $value ) ) {
			return $this->localMessageProvider->msg( $value );
		}

		return $default;
	}

	private function buildHTML( array $error ) {

		$args = [
			'logo' => Logo::get( 'small' ),
			'title' => $error['title'] ?? '',
			'indicator' => $error['indicator_title'] ?? '',
			'content' => $error['content'] ?? '',
			'borderColor' => $error['borderColor'] ?? '#fff',
			'refresh' => $error['refresh'] ?? '30',
		];

		$this->templateEngine->compile(
			'setupcheck-html',
			$args
		);

		$html = $this->templateEngine->publish( 'setupcheck-html' );

		// Minify CSS rules, we keep them readable in the template to allow for
		// better adaption
		// @see http://manas.tungare.name/software/css-compression-in-php/
		$html = preg_replace_callback( "/<style\\b[^>]*>(.*?)<\\/style>/s", function( $matches ) {
				// Remove space after colons
				$style = str_replace( ': ', ':', $matches[0] );

				// Remove whitespace
				return str_replace( [ "\r\n", "\r", "\n", "\t", '  ', '    ', '    '], '', $style );
			},
			$html
		);

		return $html;
	}

}

SetupFile.php[edit]

<?php

namespace SMW;

use SMW\Exception\FileNotWritableException;
use SMW\Utils\File;
use SMW\SQLStore\Installer;

/**
 * @private
 *
 * @license GNU GPL v2+
 * @since 3.1
 *
 * @author mwjames
 */
class SetupFile {

	/**
	 * Describes the maintenance mode
	 */
	const MAINTENANCE_MODE = 'maintenance_mode';

	/**
	 * Describes the upgrade key
	 */
	const UPGRADE_KEY = 'upgrade_key';

	/**
	 * Describes the database requirements
	 */
	const DB_REQUIREMENTS = 'db_requirements';

	/**
	 * Describes the entity collection setting
	 */
	const ENTITY_COLLATION = 'entity_collation';

	/**
	 * Key that describes the date of the last table optimization run.
	 */
	const LAST_OPTIMIZATION_RUN = 'last_optimization_run';

	/**
	 * Describes the file name
	 */
	const FILE_NAME = '.smw.json';

	/**
	 * Describes incomplete tasks
	 */
	const INCOMPLETE_TASKS = 'incomplete_tasks';

	/**
	 * Versions
	 */
	const LATEST_VERSION = 'latest_version';
	const PREVIOUS_VERSION = 'previous_version';

	/**
	 * @var File
	 */
	private $file;

	/**
	 * @since 3.1
	 *
	 * @param File|null $file
	 */
	public function __construct( File $file = null ) {
		$this->file = $file;

		if ( $this->file === null ) {
			$this->file = new File();
		}
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 */
	public function loadSchema( &$vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		if ( isset( $vars['smw.json'] ) ) {
			return;
		}

		// @see #3506
		$file = File::dir( $vars['smwgConfigFileDir'] . '/' . self::FILE_NAME );

		// Doesn't exist? The `Setup::init` will take care of it by trying to create
		// a new file and if it fails or unable to do so wail raise an exception
		// as we expect to have access to it.
		if ( is_readable( $file ) ) {
			$vars['smw.json'] = json_decode( file_get_contents( $file ), true );
		}
	}

	/**
	 * @since 3.1
	 *
	 * @param boolean $isCli
	 *
	 * @return boolean
	 */
	public static function isGoodSchema( $isCli = false ) {
		// get the schema State as a  human readable description
		$schemaState=SetupFile::getSchemaState($isCli);
		// check that it starts with "ok:" and not "error:"
		$result=SetupFile::strStartsWith($schemaState,"ok:");
		return $result;
	}

	/**
	 * @since 3.1.7
	 *
	 * see https://stackoverflow.com/a/6513929/1497139
	 *
	 * @param string $haystack
	 * @param string $needle
	 *
	 * return boolean
	 */
	public static function strStartsWith($haystack, $needle) {
           return (strpos($haystack, $needle) === 0);
  }

	/**
	 * @since 3.1.7
	 *
	 * @param boolean $isCli
	 *
	 * @return string 
	 */
	public static function getSchemaState( $isCli = false ) {
          
		if ( $isCli && defined( 'MW_PHPUNIT_TEST' ) ) {
			return "ok: CLI with PHP Unit Test active";
		}

		if ( $isCli === false && ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) ) {
			return "ok: isCli is true and PHP_SAPI cli/phpdbg=".PHP_SAPI;
		}

		// #3563, Use the specific wiki-id as identifier for the instance in use
		$id = Site::id();

    if ( !isset( $GLOBALS['smw.json'][$id]['upgrade_key'] ) ) {
      global $smwgConfigFileDir;
			return "error: smw.json for ".$id." upgrade key missing - you might want to check \$smwgConfigFileDir:".$smwgConfigFileDir;
		}

		$upgradeKey = self::makeUpgradeKey( $GLOBALS );
		$expected   =$GLOBALS['smw.json'][$id]['upgrade_key'];
		if ( $upgradeKey === $expected ) 
			$schemaState= "ok: found upgradeKey.".$upgradeKey;
		else
			$schemaState= "error: expected upgradeKey ".$expected." for ".$id." but found ".$upgradeKey;

		if (
			isset( $GLOBALS['smw.json'][$id][self::MAINTENANCE_MODE] ) &&
			$GLOBALS['smw.json'][$id][self::MAINTENANCE_MODE] !== false ) {
			$schemaState= "error: upgradeKey ".$upgradeKey." is ok but maintainance is active";
		}

		return $schemaState;
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 *
	 * @return string
	 */
	public static function makeUpgradeKey( $vars ) {
		return sha1( self::makeKey( $vars ) );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 *
	 * @return boolean
	 */
	public function inMaintenanceMode( $vars = [] ) {

		if ( !defined( 'MW_PHPUNIT_TEST' ) && ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) ) {
			return false;
		}

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();

		if ( !isset( $vars['smw.json'][$id][self::MAINTENANCE_MODE] ) ) {
			return false;
		}

		return $vars['smw.json'][$id][self::MAINTENANCE_MODE] !== false;
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 *
	 * @return []
	 */
	public function getMaintenanceMode( $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();

		if ( !isset( $vars['smw.json'][$id][self::MAINTENANCE_MODE] ) ) {
			return [];
		}

		return $vars['smw.json'][$id][self::MAINTENANCE_MODE];
	}

	/**
	 * Tracking the latest and previous version, which allows us to decide whether
	 * current activties relate to an install (new) or upgrade.
	 *
	 * @since 3.2
	 *
	 * @param int $version
	 */
	public function setLatestVersion( $version ) {

		$latest = $this->get( SetupFile::LATEST_VERSION );
		$previous = $this->get( SetupFile::PREVIOUS_VERSION );

		if ( $latest === null && $previous === null ) {
			$this->set(
				[
					SetupFile::LATEST_VERSION => $version
				]
			);
		} elseif ( $latest !== $version ) {
			$this->set(
				[
					SetupFile::LATEST_VERSION => $version,
					SetupFile::PREVIOUS_VERSION => $latest
				]
			);
		}
	}

	/**
	 * @since 3.2
	 *
	 * @param string $key
	 * @param array $args
	 */
	public function addIncompleteTask( string $key, array $args = [] ) {

		$incomplete_tasks = $this->get( self::INCOMPLETE_TASKS );

		if ( $incomplete_tasks === null ) {
			$incomplete_tasks = [];
		}

		$incomplete_tasks[$key] = $args === [] ? true : $args;

		$this->set( [ self::INCOMPLETE_TASKS => $incomplete_tasks ] );
	}

	/**
	 * @since 3.2
	 *
	 * @param string $key
	 */
	public function removeIncompleteTask( string $key ) {

		$incomplete_tasks = $this->get( self::INCOMPLETE_TASKS );

		if ( $incomplete_tasks === null ) {
			$incomplete_tasks = [];
		}

		unset( $incomplete_tasks[$key] );

		$this->set( [ self::INCOMPLETE_TASKS => $incomplete_tasks ] );
	}

	/**
	 * @since 3.2
	 *
	 * @param array $vars
	 *
	 * @return boolean
	 */
	public function hasDatabaseMinRequirement( array $vars = [] ) : bool {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();

		// No record means, no issues!
		if ( !isset( $vars['smw.json'][$id][self::DB_REQUIREMENTS] ) ) {
			return true;
		}

		$requirements = $vars['smw.json'][$id][self::DB_REQUIREMENTS];

		return version_compare( $requirements['latest_version'], $requirements['minimum_version'], 'ge' );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 *
	 * @return []
	 */
	public function findIncompleteTasks( $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();
		$tasks = [];

		// Key field => [ value that constitutes the `INCOMPLETE` state, error msg ]
		$checks = [
			\SMW\SQLStore\Installer::POPULATE_HASH_FIELD_COMPLETE => [ false, 'smw-install-incomplete-populate-hash-field' ],
			\SMW\Elastic\ElasticStore::REBUILD_INDEX_RUN_COMPLETE => [ false, 'smw-install-incomplete-elasticstore-indexrebuild' ]
		];

		foreach ( $checks as $key => $value ) {

			if ( !isset( $vars['smw.json'][$id][$key] ) ) {
				continue;
			}

			if ( $vars['smw.json'][$id][$key] === $value[0] ) {
				$tasks[] = $value[1];
			}
		}

		if ( isset( $vars['smw.json'][$id][self::INCOMPLETE_TASKS] ) ) {
			foreach ( $vars['smw.json'][$id][self::INCOMPLETE_TASKS] as $key => $args ) {
				if ( $args === true ) {
					$tasks[] = $key;
				} else {
					$tasks[] = [ $key, $args ];
				}
			}
		}

		return $tasks;
	}

	/**
	 * @since 3.1
	 *
	 * @param mixed $maintenanceMode
	 */
	public function setMaintenanceMode( $maintenanceMode, $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$this->write(
			[
				self::UPGRADE_KEY => self::makeUpgradeKey( $vars ),
				self::MAINTENANCE_MODE => $maintenanceMode
			],
			$vars
		);
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 */
	public function finalize( $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		// #3563, Use the specific wiki-id as identifier for the instance in use
		$key = self::makeUpgradeKey( $vars );
		$id = Site::id();

		if (
			isset( $vars['smw.json'][$id][self::UPGRADE_KEY] ) &&
			$key === $vars['smw.json'][$id][self::UPGRADE_KEY] &&
			$vars['smw.json'][$id][self::MAINTENANCE_MODE] === false ) {
			return false;
		}

		$this->write(
			[
				self::UPGRADE_KEY => $key,
				self::MAINTENANCE_MODE => false
			],
			$vars
		);
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 */
	public function reset( $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();
		$args = [];

		if ( !isset( $vars['smw.json'][$id] ) ) {
			return;
		}

		$vars['smw.json'][$id] = [];

		$this->write( [], $vars );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $args
	 */
	public function set( array $args, $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$this->write( $args, $vars );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $args
	 */
	public function get( $key, $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$id = Site::id();

		if ( isset( $vars['smw.json'][$id][$key] ) ) {
			return $vars['smw.json'][$id][$key];
		}

		return null;
	}

	/**
	 * @since 3.1
	 *
	 * @param string $key
	 */
	public function remove( $key, $vars = [] ) {

		if ( $vars === [] ) {
			$vars = $GLOBALS;
		}

		$this->write( [ $key => null ], $vars );
	}

	/**
	 * @since 3.1
	 *
	 * @param array $vars
	 * @param array $args
	 */
	public function write( array $args, array $vars ) {

		$configFile = File::dir( $vars['smwgConfigFileDir'] . '/' . self::FILE_NAME );
		$id = Site::id();

		if ( !isset( $vars['smw.json'] ) ) {
			$vars['smw.json'] = [];
		}

		foreach ( $args as $key => $value ) {
			// NULL means that the key key is removed
			if ( $value === null ) {
				unset( $vars['smw.json'][$id][$key] );
			} else {
				$vars['smw.json'][$id][$key] = $value;
			}
		}

		// Log the base elements used for computing the key
		$vars['smw.json'][$id]['upgrade_key_base'] = self::makeKey(
			$vars
		);

		// Remove legacy
		if ( isset( $vars['smw.json']['upgradeKey'] ) ) {
			unset( $vars['smw.json']['upgradeKey'] );
		}
		if ( isset( $vars['smw.json'][$id]['in.maintenance_mode'] ) ) {
			unset( $vars['smw.json'][$id]['in.maintenance_mode'] );
		}

		try {
			$this->file->write(
				$configFile,
				json_encode( $vars['smw.json'], JSON_PRETTY_PRINT )
			);
		} catch( FileNotWritableException $e ) {
			// Users may not have `wgShowExceptionDetails` enabled and would
			// therefore not see the exception error message hence we fail hard
			// and die
			die(
				"\n\nERROR: " . $e->getMessage() . "\n" .
				"\n       The \"smwgConfigFileDir\" setting should point to a" .
				"\n       directory that is persistent and writable!\n"
			);
		}
	}

	/**
	 * Listed keys will have a "global" impact of how data are stored, formatted,
	 * or represented in Semantic MediaWiki. In most cases it will require an action
	 * from an adminstrator when one of those keys are altered.
	 */
	public static function makeKey( $vars ) {

		// Only recognize those properties that require a fixed table
		$pageSpecialProperties = array_intersect(
			// Special properties enabled?
			$vars['smwgPageSpecialProperties'],

			// Any custom fixed properties require their own table?
			TypesRegistry::getFixedProperties( 'custom_fixed' )
		);

		$pageSpecialProperties = array_unique( $pageSpecialProperties );

		// Sort to ensure the key contains the same order
		sort( $vars['smwgFixedProperties'] );
		sort( $pageSpecialProperties );

		// The following settings influence the "shape" of the tables required
		// therefore use the content to compute a key that reflects any
		// changes to them
		$components = [
			$vars['smwgUpgradeKey'],
			$vars['smwgDefaultStore'],
			$vars['smwgFixedProperties'],
			$vars['smwgEnabledFulltextSearch'],
			$pageSpecialProperties
		];

		// Only add the key when it is different from the default setting
		if ( $vars['smwgEntityCollation'] !== 'identity' ) {
			$components += [ 'smwgEntityCollation' => $vars['smwgEntityCollation'] ];
		}

		if ( $vars['smwgFieldTypeFeatures'] !== false ) {
			$components += [ 'smwgFieldTypeFeatures' => $vars['smwgFieldTypeFeatures'] ];
		}

		// Recognize when the version requirements change and force
		// an update to be able to check the requirements
		$components += Setup::MINIMUM_DB_VERSION;

		return json_encode( $components );
	}

}
🖨 🚪