Difference between revisions of "SMWIssue4785"

From BITPlan Wiki
Jump to navigation Jump to search
Line 588: Line 588:
 
</source>
 
</source>
 
= SetupFile.php =
 
= SetupFile.php =
<source lang='php' line>
+
<source lang='php' line highlight='181'>
 
<?php
 
<?php
  

Revision as of 08:27, 24 May 2021

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

SetupCheck.php

  1<?php
  2
  3namespace SMW;
  4
  5use SMW\Utils\TemplateEngine;
  6use SMW\Utils\Logo;
  7use SMW\Localizer\LocalMessageProvider;
  8use SMW\Exception\FileNotReadableException;
  9use SMW\Exception\JSONFileParseException;
 10use RuntimeException;
 11
 12/**
 13 * @private
 14 *
 15 * @license GNU GPL v2+
 16 * @since 3.1
 17 *
 18 * @author mwjames
 19 */
 20class SetupCheck {
 21
 22	/**
 23	 * Semantic MediaWiki was loaded or accessed but not correctly enabled.
 24	 */
 25	const ERROR_EXTENSION_LOAD = 'ERROR_EXTENSION_LOAD';
 26
 27	/**
 28	 * Semantic MediaWiki was loaded or accessed but not correctly enabled.
 29	 */
 30	const ERROR_EXTENSION_INVALID_ACCESS = 'ERROR_EXTENSION_INVALID_ACCESS';
 31
 32	/**
 33	 * A user tried to use `wfLoadExtension( 'SemanticMediaWiki' )` and
 34	 * `enableSemantics` at the same causing the ExtensionRegistry to throw an
 35	 * "Uncaught Exception: It was attempted to load SemanticMediaWiki twice ..."
 36	 */
 37	const ERROR_EXTENSION_REGISTRY = 'ERROR_EXTENSION_REGISTRY';
 38
 39	/**
 40	 * A dependency (extension, MediaWiki) causes an error
 41	 */
 42	const ERROR_EXTENSION_DEPENDENCY = 'ERROR_EXTENSION_DEPENDENCY';
 43
 44	/**
 45	 * Multiple dependencies (extension, MediaWiki) caused an error
 46	 */
 47	const ERROR_EXTENSION_DEPENDENCY_MULTIPLE = 'ERROR_EXTENSION_DEPENDENCY_MULTIPLE';
 48
 49	/**
 50	 * Extension doesn't match MediaWiki or the PHP requirement.
 51	 */
 52	const ERROR_EXTENSION_INCOMPATIBLE = 'ERROR_EXTENSION_INCOMPATIBLE';
 53
 54	/**
 55	 * Extension doesn't match the DB requirement for Semantic MediaWiki.
 56	 */
 57	const ERROR_DB_REQUIREMENT_INCOMPATIBLE = 'ERROR_DB_REQUIREMENT_INCOMPATIBLE';
 58
 59	/**
 60	 * The upgrade key has change causing the schema to be invalid
 61	 */
 62	const ERROR_SCHEMA_INVALID_KEY = 'ERROR_SCHEMA_INVALID_KEY';
 63
 64	/**
 65	 * A selected default profile could not be loaded or is unknown.
 66	 */
 67	const ERROR_CONFIG_PROFILE_UNKNOWN = 'ERROR_CONFIG_PROFILE_UNKNOWN';
 68
 69	/**
 70	 * The system is currently in a maintenance window
 71	 */
 72	const MAINTENANCE_MODE = 'MAINTENANCE_MODE';
 73
 74	/**
 75	 * @var []
 76	 */
 77	private $options = [];
 78
 79	/**
 80	 * @var SetupFile
 81	 */
 82	private $setupFile;
 83
 84	/**
 85	 * @var TemplateEngine
 86	 */
 87	private $templateEngine;
 88
 89	/**
 90	 * @var LocalMessageProvider
 91	 */
 92	private $localMessageProvider;
 93
 94	/**
 95	 * @var []
 96	 */
 97	private $definitions = [];
 98
 99	/**
100	 * @var string
101	 */
102	private $languageCode = 'en';
103
104	/**
105	 * @var string
106	 */
107	private $fallbackLanguageCode = 'en';
108
109	/**
110	 * @var boolean
111	 */
112	private $sentHeader = true;
113
114	/**
115	 * @var string
116	 */
117	private $errorType = '';
118
119	/**
120	 * @var string
121	 */
122	private $errorMessage = '';
123
124	/**
125	 * @var string
126	 */
127	private $traceString = '';
128
129	/**
130	 * @since 3.1
131	 *
132	 * @param array $vars
133	 * @param SetupFile|null $setupFile
134	 */
135	public function __construct( array $options, SetupFile $setupFile = null ) {
136		$this->options = $options;
137		$this->setupFile = $setupFile;
138		$this->templateEngine = new TemplateEngine();
139		$this->localMessageProvider = new LocalMessageProvider( '/local/setupcheck.i18n.json' );
140
141		if ( $this->setupFile === null ) {
142			$this->setupFile = new SetupFile();
143		}
144	}
145
146	/**
147	 * @since 3.2
148	 *
149	 * @param string $file
150	 *
151	 * @return array
152	 * @throws RuntimeException
153	 */
154	public static function readFromFile( string $file ) : array {
155
156		if ( !is_readable( $file ) ) {
157			throw new FileNotReadableException( $file );
158		}
159
160		$contents = json_decode(
161			file_get_contents( $file ),
162			true
163		);
164
165		if ( json_last_error() === JSON_ERROR_NONE ) {
166			return $contents;
167		}
168
169		throw new JSONFileParseException( $file );
170	}
171
172	/**
173	 * @since 3.1
174	 *
175	 * @param SetupFile|null $setupFile
176	 *
177	 * @return SetupCheck
178	 */
179	public static function newFromDefaults( SetupFile $setupFile = null ) {
180
181		if ( !defined( 'SMW_VERSION' ) ) {
182			$version = self::readFromFile( $GLOBALS['smwgIP'] . 'extension.json' )['version'];
183		} else {
184			$version = SMW_VERSION;
185		}
186
187		$setupCheck = new SetupCheck(
188			[
189				'SMW_VERSION'    => $version,
190				'MW_VERSION'     => $GLOBALS['wgVersion'], // MW_VERSION may not yet be defined!!
191				'wgLanguageCode' => $GLOBALS['wgLanguageCode'],
192				'smwgUpgradeKey' => $GLOBALS['smwgUpgradeKey']
193			],
194			$setupFile
195		);
196
197		return $setupCheck;
198	}
199
200	/**
201	 * @since 3.2
202	 */
203	public function disableHeader() {
204		$this->sentHeader = false;
205	}
206
207	/**
208	 * @since 3.1
209	 *
210	 * @return boolean
211	 */
212	public function isCli() {
213		return PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
214	}
215
216	/**
217	 * @since 3.1
218	 *
219	 * @param string $traceString
220	 */
221	public function setTraceString( $traceString ) {
222		$this->traceString = $traceString;
223	}
224
225	/**
226	 * @since 3.2
227	 *
228	 * @param string $errorMessage
229	 */
230	public function setErrorMessage( string $errorMessage ) {
231		$this->errorMessage = $errorMessage;
232	}
233
234	/**
235	 * @since 3.2
236	 *
237	 * @param string $errorType
238	 */
239	public function setErrorType( string $errorType ) {
240		$this->errorType = $errorType;
241	}
242
243	/**
244	 * @since 3.2
245	 *
246	 * @return boolean
247	 */
248	public function isError( string $error ) : bool {
249		return $this->errorType === $error;
250	}
251
252	/**
253	 * @since 3.1
254	 *
255	 * @return boolean
256	 */
257	public function hasError() {
258
259		$this->errorType = '';
260
261		// When it is not a test run or run from the command line we expect that
262		// the extension is registered using `enableSemantics`
263		if ( !defined( 'SMW_EXTENSION_LOADED' ) && !$this->isCli() ) {
264			$this->errorType = self::ERROR_EXTENSION_LOAD;
265		} elseif ( $this->setupFile->inMaintenanceMode() ) {
266			$this->errorType = self::MAINTENANCE_MODE;
267		} elseif ( !$this->isCli() && !$this->setupFile->hasDatabaseMinRequirement() ) {
268			$this->errorType = self::ERROR_DB_REQUIREMENT_INCOMPATIBLE;
269		} elseif ( $this->setupFile->isGoodSchema() === false ) {
270			$this->errorType = self::ERROR_SCHEMA_INVALID_KEY;
271		}
272
273		return $this->errorType !== '';
274	}
275
276	/**
277	 * @note Adding a new error type requires to:
278	 *
279	 * - Define a constant to clearly identify the type of error
280	 * - Extend the `setupcheck.json` to add a definition for the new type and
281	 *   specify which information should be displayed
282	 * - In case the existing HTML elements aren't sufficient, create a new
283	 *   zxy.ms file and define the HTML code
284	 *
285	 * The `TemplateEngine` will replace arguments defined in the HTML hereby
286	 * absolving this class from any direct HTML manipulation.
287	 *
288	 * @since 3.1
289	 *
290	 * @param boolean $isCli
291	 *
292	 * @return string
293	 */
294	public function getError( $isCli = false ) {
295
296		$error = [
297			'title' => '',
298			'content' => ''
299		];
300
301		$this->languageCode = $_GET['uselang'] ?? $this->options['wgLanguageCode'] ?? 'en';
302
303		// Output forms for different error types are registered with a JSON file.
304		$this->definitions = $this->readFromFile(
305			$GLOBALS['smwgDir'] . '/data/template/setupcheck/setupcheck.json'
306		);
307
308		// Error messages are specified in a special i18n JSON file to avoid relying
309		// on the MW message system especially when SMW isn't fully registered
310		// and we are unable to access any `smw-...` message keys from the standard
311		// i18n files.
312		$this->localMessageProvider->setLanguageCode(
313			$this->languageCode
314		);
315
316		$this->localMessageProvider->loadMessages();
317
318		// HTML specific formatting is contained in the following files where
319		// a defined group of targets correspond to types used in the JSON
320		$this->templateEngine->bulkLoad(
321			[
322				'/setupcheck/setupcheck.ms' => 'setupcheck-html',
323				'/setupcheck/setupcheck.progress.ms' => 'setupcheck-progress',
324
325				// Target specific elements
326				'/setupcheck/setupcheck.section.ms'   => 'section',
327				'/setupcheck/setupcheck.version.ms'   => 'version',
328				'/setupcheck/setupcheck.paragraph.ms' => 'paragraph',
329				'/setupcheck/setupcheck.errorbox.ms'  => 'errorbox',
330				'/setupcheck/setupcheck.db.requirement.ms' => 'db-requirement',
331			]
332		);
333
334		if ( !isset( $this->definitions['error_types'][$this->errorType] ) ) {
335			throw new RuntimeException( "The `{$this->errorType}` type is not defined in the `setupcheck.json`!" );
336		}
337
338		$error = $this->createErrorContent( $this->errorType );
339
340		if ( $isCli === false ) {
341			$content = $this->buildHTML( $error );
342			$this->header( 'Content-Type: text/html; charset=UTF-8' );
343			$this->header( 'Content-Length: ' . strlen( $content ) );
344			$this->header( 'Cache-control: none' );
345			$this->header( 'Pragma: no-cache' );
346		} else {
347			$content = $error['title'] . "\n\n" . $error['content'];
348			$content = str_replace(
349				[ '<!-- ROW -->', '</h3>', '</h4>', '</p>', '&nbsp;' ],
350				[ "\n", "\n\n", "\n\n", "\n\n", ' ' ],
351				$content
352			);
353			$content = "\n" . wordwrap( strip_tags( trim( $content ) ), 73 );
354		}
355
356		return $content;
357	}
358
359	/**
360	 * @since 3.1
361	 *
362	 * @param boolean $isCli
363	 */
364	public function showErrorAndAbort( $isCli = false ) {
365
366		echo $this->getError( $isCli );
367
368		if ( ob_get_level() ) {
369			ob_flush();
370			flush();
371			ob_end_clean();
372		}
373
374		die();
375	}
376
377	private function header( $text ) {
378		if ( $this->sentHeader ) {
379			header( $text );
380		}
381  }
382
383  private function schemaError() {
384 	  // get trace
385		// https://stackoverflow.com/a/7039409/1497139
386		//
387		$e = new \Exception;
388		$content ='<pre>'.$e->getTraceAsString().'</pre>';
389		$content .='PHP_SAPI: '.PHP_SAPI."<br>";
390		$isCli=$this->isCli();
391		$schemaState=SetupFile::getSchemaState($isCli);
392		$content.='Schema state: '.$schemaState."<br>";
393		$upgradeKeyBase=SetupFile::makeKey($GLOBALS);
394    $content.='upgrade key base: '.$upgradeKeyBase;
395    return $content;
396  }
397
398	private function createErrorContent( $type ) {
399
400		$indicator_title = 'Error';
401		$template = $this->definitions['error_types'][$type];
402    $content = '';
403
404    if ($type==self::ERROR_SCHEMA_INVALID_KEY) {
405      $content.=$this->schemaError($this->isCli());
406    }
407
408		/**
409		 * Actual output form
410		 */
411		foreach ( $template['output_form'] as $value ) {
412			$content .= $this->createContent( $value, $type );
413		}
414
415		/**
416		 * Special handling for the progress output
417		 */
418		if ( isset( $template['progress'] ) ) {
419			foreach ( $template['progress'] as $value ) {
420				$text = $this->createCopy( $value['text'] );
421
422				if ( isset( $value['progress_keys'] ) ) {
423					$content .= $this->createProgressIndicator( $value );
424				}
425
426				$args = [
427					'text' => $text,
428					'template' => $value['type']
429				];
430
431				$this->templateEngine->compile(
432					$value['type'],
433					$args
434				);
435
436				$content .= $this->templateEngine->publish( $value['type'] );
437			}
438		}
439
440		/**
441		 * Special handling for the stack trace output
442		 */
443		if ( isset( $template['stack_trace'] ) && $this->traceString !== '' ) {
444			foreach ( $template['stack_trace'] as $value ) {
445				$content .= $this->createContent( $value, $type );
446			}
447		}
448
449		if ( isset( $template['indicator_title'] ) ) {
450			$indicator_title = $this->createCopy( $template['indicator_title'] );
451		}
452
453		$error = [
454			'title' => 'Semantic MediaWiki',
455			'indicator_title' => $indicator_title,
456			'content' => $content,
457			'borderColor' => $template['indicator_color']
458		];
459
460		return $error;
461	}
462
463	private function createContent( $value, $type ) {
464
465		if ( $value['text'] === 'ERROR_TEXT' ) {
466			$text = str_replace( "\n", '<br>', $this->errorMessage );
467		} elseif ( $value['text'] === 'ERROR_TEXT_MULTIPLE' ) {
468			$errors = explode( "\n", $this->errorMessage );
469			$text = '<ul><li>' . implode( '</li><li>', array_filter( $errors ) ) . '</li></ul>';
470		} elseif ( $value['text'] === 'TRACE_STRING' ) {
471			$text = $this->traceString;
472		} else {
473			$text = $this->createCopy( $value['text'] );
474		}
475
476		$args = [
477			'text' => $text,
478			'template' => $value['type']
479		];
480
481		if ( $value['type'] === 'version' ) {
482			$args['version-title'] = $text;
483			$args['smw-title'] = 'Semantic MediaWiki';
484			$args['smw-version'] = $this->options['SMW_VERSION'] ?? 'n/a';
485			$args['smw-upgradekey'] = $this->options['smwgUpgradeKey'] ?? 'n/a';
486			$args['mw-title'] = 'MediaWiki';
487			$args['mw-version'] = $this->options['MW_VERSION'] ?? 'n/a';
488			$args['code-title'] = $this->createCopy( 'smw-setupcheck-code' );
489			$args['code-type'] = $type;
490		}
491
492		if ( $value['type'] === 'db-requirement' ) {
493			$requirements = $this->setupFile->get( SetupFile::DB_REQUIREMENTS );
494			$args['version-title'] = $text;
495			$args['db-title'] = $this->createCopy( 'smw-setupcheck-db-title' );
496			$args['db-type'] = $requirements['type'] ?? 'N/A';
497			$args['db-current-title'] = $this->createCopy( 'smw-setupcheck-db-current-title' );
498			$args['db-minimum-title'] = $this->createCopy( 'smw-setupcheck-db-minimum-title' );
499			$args['db-current-version'] = $requirements['latest_version'] ?? 'N/A';
500			$args['db-minimum-version'] = $requirements['minimum_version'] ?? 'N/A';
501		}
502
503		// The type is expected to match a defined target and in an event
504		// that those don't match an exception will be raised.
505		$this->templateEngine->compile(
506			$value['type'],
507			$args
508		);
509
510		return $this->templateEngine->publish( $value['type'] );
511	}
512
513	private function createProgressIndicator( $value ) {
514
515		$maintenanceMode = (array)$this->setupFile->getMaintenanceMode();
516		$content = '';
517
518		foreach ( $maintenanceMode as $key => $v ) {
519
520			$args = [
521				'label' => $key,
522				'value' => $v
523			];
524
525			if ( isset( $value['progress_keys'][$key] ) ) {
526				$args['label'] = $this->createCopy( $value['progress_keys'][$key] );
527			}
528
529			$this->templateEngine->compile(
530				'setupcheck-progress',
531				$args
532			);
533
534			$content .= $this->templateEngine->publish( 'setupcheck-progress' );
535		}
536
537		return $content;
538	}
539
540	private function createCopy( $value, $default = 'n/a' ) {
541
542		if ( is_string( $value ) && $this->localMessageProvider->has( $value ) ) {
543			return $this->localMessageProvider->msg( $value );
544		}
545
546		return $default;
547	}
548
549	private function buildHTML( array $error ) {
550
551		$args = [
552			'logo' => Logo::get( 'small' ),
553			'title' => $error['title'] ?? '',
554			'indicator' => $error['indicator_title'] ?? '',
555			'content' => $error['content'] ?? '',
556			'borderColor' => $error['borderColor'] ?? '#fff',
557			'refresh' => $error['refresh'] ?? '30',
558		];
559
560		$this->templateEngine->compile(
561			'setupcheck-html',
562			$args
563		);
564
565		$html = $this->templateEngine->publish( 'setupcheck-html' );
566
567		// Minify CSS rules, we keep them readable in the template to allow for
568		// better adaption
569		// @see http://manas.tungare.name/software/css-compression-in-php/
570		$html = preg_replace_callback( "/<style\\b[^>]*>(.*?)<\\/style>/s", function( $matches ) {
571				// Remove space after colons
572				$style = str_replace( ': ', ':', $matches[0] );
573
574				// Remove whitespace
575				return str_replace( [ "\r\n", "\r", "\n", "\t", '  ', '    ', '    '], '', $style );
576			},
577			$html
578		);
579
580		return $html;
581	}
582
583}

SetupFile.php

  1<?php
  2
  3namespace SMW;
  4
  5use SMW\Exception\FileNotWritableException;
  6use SMW\Utils\File;
  7use SMW\SQLStore\Installer;
  8
  9/**
 10 * @private
 11 *
 12 * @license GNU GPL v2+
 13 * @since 3.1
 14 *
 15 * @author mwjames
 16 */
 17class SetupFile {
 18
 19	/**
 20	 * Describes the maintenance mode
 21	 */
 22	const MAINTENANCE_MODE = 'maintenance_mode';
 23
 24	/**
 25	 * Describes the upgrade key
 26	 */
 27	const UPGRADE_KEY = 'upgrade_key';
 28
 29	/**
 30	 * Describes the database requirements
 31	 */
 32	const DB_REQUIREMENTS = 'db_requirements';
 33
 34	/**
 35	 * Describes the entity collection setting
 36	 */
 37	const ENTITY_COLLATION = 'entity_collation';
 38
 39	/**
 40	 * Key that describes the date of the last table optimization run.
 41	 */
 42	const LAST_OPTIMIZATION_RUN = 'last_optimization_run';
 43
 44	/**
 45	 * Describes the file name
 46	 */
 47	const FILE_NAME = '.smw.json';
 48
 49	/**
 50	 * Describes incomplete tasks
 51	 */
 52	const INCOMPLETE_TASKS = 'incomplete_tasks';
 53
 54	/**
 55	 * Versions
 56	 */
 57	const LATEST_VERSION = 'latest_version';
 58	const PREVIOUS_VERSION = 'previous_version';
 59
 60	/**
 61	 * @var File
 62	 */
 63	private $file;
 64
 65	/**
 66	 * @since 3.1
 67	 *
 68	 * @param File|null $file
 69	 */
 70	public function __construct( File $file = null ) {
 71		$this->file = $file;
 72
 73		if ( $this->file === null ) {
 74			$this->file = new File();
 75		}
 76	}
 77
 78	/**
 79	 * @since 3.1
 80	 *
 81	 * @param array $vars
 82	 */
 83	public function loadSchema( &$vars = [] ) {
 84
 85		if ( $vars === [] ) {
 86			$vars = $GLOBALS;
 87		}
 88
 89		if ( isset( $vars['smw.json'] ) ) {
 90			return;
 91		}
 92
 93		// @see #3506
 94		$file = File::dir( $vars['smwgConfigFileDir'] . '/' . self::FILE_NAME );
 95
 96		// Doesn't exist? The `Setup::init` will take care of it by trying to create
 97		// a new file and if it fails or unable to do so wail raise an exception
 98		// as we expect to have access to it.
 99		if ( is_readable( $file ) ) {
100			$vars['smw.json'] = json_decode( file_get_contents( $file ), true );
101		}
102	}
103
104	/**
105	 * @since 3.1
106	 *
107	 * @param boolean $isCli
108	 *
109	 * @return boolean
110	 */
111	public static function isGoodSchema( $isCli = false ) {
112		// get the schema State as a  human readable description
113		$schemaState=SetupFile::getSchemaState($isCli);
114		// check that it starts with "ok:" and not "error:"
115		$result=SetupFile::strStartsWith($schemaState,"ok:");
116		return $result;
117	}
118
119	/**
120	 * @since 3.1.7
121	 *
122	 * see https://stackoverflow.com/a/6513929/1497139
123	 *
124	 * @param string $haystack
125	 * @param string $needle
126	 *
127	 * return boolean
128	 */
129	public static function strStartsWith($haystack, $needle) {
130           return (strpos($haystack, $needle) === 0);
131  }
132
133	/**
134	 * @since 3.1.7
135	 *
136	 * @param boolean $isCli
137	 *
138	 * @return string 
139	 */
140	public static function getSchemaState( $isCli = false ) {
141          
142		if ( $isCli && defined( 'MW_PHPUNIT_TEST' ) ) {
143			return "ok: CLI with PHP Unit Test active";
144		}
145
146		if ( $isCli === false && ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) ) {
147			return "ok: isCli is true and PHP_SAPI cli/phpdbg=".PHP_SAPI;
148		}
149
150		// #3563, Use the specific wiki-id as identifier for the instance in use
151		$id = Site::id();
152
153    if ( !isset( $GLOBALS['smw.json'][$id]['upgrade_key'] ) ) {
154      global $smwgConfigFileDir;
155			return "error: smw.json for ".$id." upgrade key missing - you might want to check \$smwgConfigFileDir:".$smwgConfigFileDir;
156		}
157
158		$upgradeKey = self::makeUpgradeKey( $GLOBALS );
159		$expected   =$GLOBALS['smw.json'][$id]['upgrade_key'];
160		if ( $upgradeKey === $expected ) 
161			$schemaState= "ok: found upgradeKey.".$upgradeKey;
162		else
163			$schemaState= "error: expected upgradeKey ".$expected." for ".$id." but found ".$upgradeKey;
164
165		if (
166			isset( $GLOBALS['smw.json'][$id][self::MAINTENANCE_MODE] ) &&
167			$GLOBALS['smw.json'][$id][self::MAINTENANCE_MODE] !== false ) {
168			$schemaState= "error: upgradeKey ".$upgradeKey." is ok but maintainance is active";
169		}
170
171		return $schemaState;
172	}
173
174	/**
175	 * @since 3.1
176	 *
177	 * @param array $vars
178	 *
179	 * @return string
180	 */
181	public static function makeUpgradeKey( $vars ) {
182		return sha1( self::makeKey( $vars ) );
183	}
184
185	/**
186	 * @since 3.1
187	 *
188	 * @param array $vars
189	 *
190	 * @return boolean
191	 */
192	public function inMaintenanceMode( $vars = [] ) {
193
194		if ( !defined( 'MW_PHPUNIT_TEST' ) && ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) ) {
195			return false;
196		}
197
198		if ( $vars === [] ) {
199			$vars = $GLOBALS;
200		}
201
202		$id = Site::id();
203
204		if ( !isset( $vars['smw.json'][$id][self::MAINTENANCE_MODE] ) ) {
205			return false;
206		}
207
208		return $vars['smw.json'][$id][self::MAINTENANCE_MODE] !== false;
209	}
210
211	/**
212	 * @since 3.1
213	 *
214	 * @param array $vars
215	 *
216	 * @return []
217	 */
218	public function getMaintenanceMode( $vars = [] ) {
219
220		if ( $vars === [] ) {
221			$vars = $GLOBALS;
222		}
223
224		$id = Site::id();
225
226		if ( !isset( $vars['smw.json'][$id][self::MAINTENANCE_MODE] ) ) {
227			return [];
228		}
229
230		return $vars['smw.json'][$id][self::MAINTENANCE_MODE];
231	}
232
233	/**
234	 * Tracking the latest and previous version, which allows us to decide whether
235	 * current activties relate to an install (new) or upgrade.
236	 *
237	 * @since 3.2
238	 *
239	 * @param int $version
240	 */
241	public function setLatestVersion( $version ) {
242
243		$latest = $this->get( SetupFile::LATEST_VERSION );
244		$previous = $this->get( SetupFile::PREVIOUS_VERSION );
245
246		if ( $latest === null && $previous === null ) {
247			$this->set(
248				[
249					SetupFile::LATEST_VERSION => $version
250				]
251			);
252		} elseif ( $latest !== $version ) {
253			$this->set(
254				[
255					SetupFile::LATEST_VERSION => $version,
256					SetupFile::PREVIOUS_VERSION => $latest
257				]
258			);
259		}
260	}
261
262	/**
263	 * @since 3.2
264	 *
265	 * @param string $key
266	 * @param array $args
267	 */
268	public function addIncompleteTask( string $key, array $args = [] ) {
269
270		$incomplete_tasks = $this->get( self::INCOMPLETE_TASKS );
271
272		if ( $incomplete_tasks === null ) {
273			$incomplete_tasks = [];
274		}
275
276		$incomplete_tasks[$key] = $args === [] ? true : $args;
277
278		$this->set( [ self::INCOMPLETE_TASKS => $incomplete_tasks ] );
279	}
280
281	/**
282	 * @since 3.2
283	 *
284	 * @param string $key
285	 */
286	public function removeIncompleteTask( string $key ) {
287
288		$incomplete_tasks = $this->get( self::INCOMPLETE_TASKS );
289
290		if ( $incomplete_tasks === null ) {
291			$incomplete_tasks = [];
292		}
293
294		unset( $incomplete_tasks[$key] );
295
296		$this->set( [ self::INCOMPLETE_TASKS => $incomplete_tasks ] );
297	}
298
299	/**
300	 * @since 3.2
301	 *
302	 * @param array $vars
303	 *
304	 * @return boolean
305	 */
306	public function hasDatabaseMinRequirement( array $vars = [] ) : bool {
307
308		if ( $vars === [] ) {
309			$vars = $GLOBALS;
310		}
311
312		$id = Site::id();
313
314		// No record means, no issues!
315		if ( !isset( $vars['smw.json'][$id][self::DB_REQUIREMENTS] ) ) {
316			return true;
317		}
318
319		$requirements = $vars['smw.json'][$id][self::DB_REQUIREMENTS];
320
321		return version_compare( $requirements['latest_version'], $requirements['minimum_version'], 'ge' );
322	}
323
324	/**
325	 * @since 3.1
326	 *
327	 * @param array $vars
328	 *
329	 * @return []
330	 */
331	public function findIncompleteTasks( $vars = [] ) {
332
333		if ( $vars === [] ) {
334			$vars = $GLOBALS;
335		}
336
337		$id = Site::id();
338		$tasks = [];
339
340		// Key field => [ value that constitutes the `INCOMPLETE` state, error msg ]
341		$checks = [
342			\SMW\SQLStore\Installer::POPULATE_HASH_FIELD_COMPLETE => [ false, 'smw-install-incomplete-populate-hash-field' ],
343			\SMW\Elastic\ElasticStore::REBUILD_INDEX_RUN_COMPLETE => [ false, 'smw-install-incomplete-elasticstore-indexrebuild' ]
344		];
345
346		foreach ( $checks as $key => $value ) {
347
348			if ( !isset( $vars['smw.json'][$id][$key] ) ) {
349				continue;
350			}
351
352			if ( $vars['smw.json'][$id][$key] === $value[0] ) {
353				$tasks[] = $value[1];
354			}
355		}
356
357		if ( isset( $vars['smw.json'][$id][self::INCOMPLETE_TASKS] ) ) {
358			foreach ( $vars['smw.json'][$id][self::INCOMPLETE_TASKS] as $key => $args ) {
359				if ( $args === true ) {
360					$tasks[] = $key;
361				} else {
362					$tasks[] = [ $key, $args ];
363				}
364			}
365		}
366
367		return $tasks;
368	}
369
370	/**
371	 * @since 3.1
372	 *
373	 * @param mixed $maintenanceMode
374	 */
375	public function setMaintenanceMode( $maintenanceMode, $vars = [] ) {
376
377		if ( $vars === [] ) {
378			$vars = $GLOBALS;
379		}
380
381		$this->write(
382			[
383				self::UPGRADE_KEY => self::makeUpgradeKey( $vars ),
384				self::MAINTENANCE_MODE => $maintenanceMode
385			],
386			$vars
387		);
388	}
389
390	/**
391	 * @since 3.1
392	 *
393	 * @param array $vars
394	 */
395	public function finalize( $vars = [] ) {
396
397		if ( $vars === [] ) {
398			$vars = $GLOBALS;
399		}
400
401		// #3563, Use the specific wiki-id as identifier for the instance in use
402		$key = self::makeUpgradeKey( $vars );
403		$id = Site::id();
404
405		if (
406			isset( $vars['smw.json'][$id][self::UPGRADE_KEY] ) &&
407			$key === $vars['smw.json'][$id][self::UPGRADE_KEY] &&
408			$vars['smw.json'][$id][self::MAINTENANCE_MODE] === false ) {
409			return false;
410		}
411
412		$this->write(
413			[
414				self::UPGRADE_KEY => $key,
415				self::MAINTENANCE_MODE => false
416			],
417			$vars
418		);
419	}
420
421	/**
422	 * @since 3.1
423	 *
424	 * @param array $vars
425	 */
426	public function reset( $vars = [] ) {
427
428		if ( $vars === [] ) {
429			$vars = $GLOBALS;
430		}
431
432		$id = Site::id();
433		$args = [];
434
435		if ( !isset( $vars['smw.json'][$id] ) ) {
436			return;
437		}
438
439		$vars['smw.json'][$id] = [];
440
441		$this->write( [], $vars );
442	}
443
444	/**
445	 * @since 3.1
446	 *
447	 * @param array $args
448	 */
449	public function set( array $args, $vars = [] ) {
450
451		if ( $vars === [] ) {
452			$vars = $GLOBALS;
453		}
454
455		$this->write( $args, $vars );
456	}
457
458	/**
459	 * @since 3.1
460	 *
461	 * @param array $args
462	 */
463	public function get( $key, $vars = [] ) {
464
465		if ( $vars === [] ) {
466			$vars = $GLOBALS;
467		}
468
469		$id = Site::id();
470
471		if ( isset( $vars['smw.json'][$id][$key] ) ) {
472			return $vars['smw.json'][$id][$key];
473		}
474
475		return null;
476	}
477
478	/**
479	 * @since 3.1
480	 *
481	 * @param string $key
482	 */
483	public function remove( $key, $vars = [] ) {
484
485		if ( $vars === [] ) {
486			$vars = $GLOBALS;
487		}
488
489		$this->write( [ $key => null ], $vars );
490	}
491
492	/**
493	 * @since 3.1
494	 *
495	 * @param array $vars
496	 * @param array $args
497	 */
498	public function write( array $args, array $vars ) {
499
500		$configFile = File::dir( $vars['smwgConfigFileDir'] . '/' . self::FILE_NAME );
501		$id = Site::id();
502
503		if ( !isset( $vars['smw.json'] ) ) {
504			$vars['smw.json'] = [];
505		}
506
507		foreach ( $args as $key => $value ) {
508			// NULL means that the key key is removed
509			if ( $value === null ) {
510				unset( $vars['smw.json'][$id][$key] );
511			} else {
512				$vars['smw.json'][$id][$key] = $value;
513			}
514		}
515
516		// Log the base elements used for computing the key
517		$vars['smw.json'][$id]['upgrade_key_base'] = self::makeKey(
518			$vars
519		);
520
521		// Remove legacy
522		if ( isset( $vars['smw.json']['upgradeKey'] ) ) {
523			unset( $vars['smw.json']['upgradeKey'] );
524		}
525		if ( isset( $vars['smw.json'][$id]['in.maintenance_mode'] ) ) {
526			unset( $vars['smw.json'][$id]['in.maintenance_mode'] );
527		}
528
529		try {
530			$this->file->write(
531				$configFile,
532				json_encode( $vars['smw.json'], JSON_PRETTY_PRINT )
533			);
534		} catch( FileNotWritableException $e ) {
535			// Users may not have `wgShowExceptionDetails` enabled and would
536			// therefore not see the exception error message hence we fail hard
537			// and die
538			die(
539				"\n\nERROR: " . $e->getMessage() . "\n" .
540				"\n       The \"smwgConfigFileDir\" setting should point to a" .
541				"\n       directory that is persistent and writable!\n"
542			);
543		}
544	}
545
546	/**
547	 * Listed keys will have a "global" impact of how data are stored, formatted,
548	 * or represented in Semantic MediaWiki. In most cases it will require an action
549	 * from an adminstrator when one of those keys are altered.
550	 */
551	public static function makeKey( $vars ) {
552
553		// Only recognize those properties that require a fixed table
554		$pageSpecialProperties = array_intersect(
555			// Special properties enabled?
556			$vars['smwgPageSpecialProperties'],
557
558			// Any custom fixed properties require their own table?
559			TypesRegistry::getFixedProperties( 'custom_fixed' )
560		);
561
562		$pageSpecialProperties = array_unique( $pageSpecialProperties );
563
564		// Sort to ensure the key contains the same order
565		sort( $vars['smwgFixedProperties'] );
566		sort( $pageSpecialProperties );
567
568		// The following settings influence the "shape" of the tables required
569		// therefore use the content to compute a key that reflects any
570		// changes to them
571		$components = [
572			$vars['smwgUpgradeKey'],
573			$vars['smwgDefaultStore'],
574			$vars['smwgFixedProperties'],
575			$vars['smwgEnabledFulltextSearch'],
576			$pageSpecialProperties
577		];
578
579		// Only add the key when it is different from the default setting
580		if ( $vars['smwgEntityCollation'] !== 'identity' ) {
581			$components += [ 'smwgEntityCollation' => $vars['smwgEntityCollation'] ];
582		}
583
584		if ( $vars['smwgFieldTypeFeatures'] !== false ) {
585			$components += [ 'smwgFieldTypeFeatures' => $vars['smwgFieldTypeFeatures'] ];
586		}
587
588		// Recognize when the version requirements change and force
589		// an update to be able to check the requirements
590		$components += Setup::MINIMUM_DB_VERSION;
591
592		return json_encode( $components );
593	}
594
595}