'ExplodeIterator' => __DIR__ . '/includes/libs/ExplodeIterator.php',
'ExportProgressFilter' => __DIR__ . '/includes/export/ExportProgressFilter.php',
'ExportSites' => __DIR__ . '/maintenance/exportSites.php',
+ 'ExtensionDependencyError' => __DIR__ . '/includes/registration/ExtensionDependencyError.php',
'ExtensionJsonValidationError' => __DIR__ . '/includes/registration/ExtensionJsonValidationError.php',
'ExtensionJsonValidator' => __DIR__ . '/includes/registration/ExtensionJsonValidator.php',
'ExtensionLanguages' => __DIR__ . '/maintenance/language/languages.inc',
if ( !is_dir( "$extDir/$file" ) ) {
continue;
}
- if ( file_exists( "$extDir/$file/$jsonFile" ) || file_exists( "$extDir/$file/$file.php" ) ) {
+ $fullJsonFile = "$extDir/$file/$jsonFile";
+ $isJson = file_exists( $fullJsonFile );
+ $isPhp = false;
+ if ( !$isJson ) {
+ // Only fallback to PHP file if JSON doesn't exist
+ $fullPhpFile = "$extDir/$file/$file.php";
+ $isPhp = file_exists( $fullPhpFile );
+ }
+ if ( $isJson || $isPhp ) {
// Extension exists. Now see if there are screenshots
$exts[$file] = [];
if ( is_dir( "$extDir/$file/screenshots" ) ) {
}
}
+ if ( $isJson ) {
+ $info = $this->readExtension( $fullJsonFile );
+ if ( $info === false ) {
+ continue;
+ }
+ $exts[$file] += $info;
+ }
}
closedir( $dh );
uksort( $exts, 'strnatcasecmp' );
return $exts;
}
+ /**
+ * @param string $fullJsonFile
+ * @param array $extDeps
+ * @param array $skinDeps
+ *
+ * @return array|bool False if this extension can't be loaded
+ */
+ private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
+ $load = [
+ $fullJsonFile => 1
+ ];
+ if ( $extDeps ) {
+ $extDir = $this->getVar( 'IP' ) . '/extensions';
+ foreach ( $extDeps as $dep ) {
+ $fname = "$extDir/$dep/extension.json";
+ if ( !file_exists( $fname ) ) {
+ return false;
+ }
+ $load[$fname] = 1;
+ }
+ }
+ if ( $skinDeps ) {
+ $skinDir = $this->getVar( 'IP' ) . '/skins';
+ foreach ( $skinDeps as $dep ) {
+ $fname = "$skinDir/$dep/skin.json";
+ if ( !file_exists( $fname ) ) {
+ return false;
+ }
+ $load[$fname] = 1;
+ }
+ }
+ $registry = new ExtensionRegistry();
+ try {
+ $info = $registry->readFromQueue( $load );
+ } catch ( ExtensionDependencyError $e ) {
+ if ( $e->incompatibleCore || $e->incompatibleSkins
+ || $e->incompatibleExtensions
+ ) {
+ // If something is incompatible with a dependency, we have no real
+ // option besides skipping it
+ return false;
+ } elseif ( $e->missingExtensions || $e->missingSkins ) {
+ // There's an extension missing in the dependency tree,
+ // so add those to the dependency list and try again
+ return $this->readExtension(
+ $fullJsonFile,
+ array_merge( $extDeps, $e->missingExtensions ),
+ array_merge( $skinDeps, $e->missingSkins )
+ );
+ }
+ // Some other kind of dependency error?
+ return false;
+ }
+ $ret = [];
+ // The order of credits will be the order of $load,
+ // so the first extension is the one we want to load,
+ // everything else is a dependency
+ $i = 0;
+ foreach ( $info['credits'] as $name => $credit ) {
+ $i++;
+ if ( $i == 1 ) {
+ // Extension we want to load
+ continue;
+ }
+ $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
+ $ret['requires'][$type][] = $credit['name'];
+ }
+ $credits = array_values( $info['credits'] )[0];
+ if ( isset( $credits['url'] ) ) {
+ $ret['url'] = $credits['url'];
+ }
+ $ret['type'] = $credits['type'];
+
+ return $ret;
+ }
+
/**
* Returns a default value to be used for $wgDefaultSkin: normally the one set in DefaultSettings,
* but will fall back to another if the default skin is missing and some other one is present
* Parameters are:
* var: The variable to be configured (required)
* label: The message name for the label (required)
+ * labelAttribs:Additional attributes for the label element (optional)
* attribs: Additional attributes for the input element (optional)
* controlName: The name for the input element (optional)
* value: The current value of the variable (optional)
if ( !isset( $params['help'] ) ) {
$params['help'] = "";
}
+ if ( !isset( $params['labelAttribs'] ) ) {
+ $params['labelAttribs'] = [];
+ }
if ( isset( $params['rawtext'] ) ) {
$labelText = $params['rawtext'];
} else {
return "<div class=\"config-input-check\">\n" .
$params['help'] .
- "<label>\n" .
- Xml::check(
- $params['controlName'],
- $params['value'],
- $params['attribs'] + [
- 'id' => $params['controlName'],
- 'tabindex' => $this->nextTabIndex(),
- ]
- ) .
- $labelText . "\n" .
- "</label>\n" .
+ Html::rawElement(
+ 'label',
+ $params['labelAttribs'],
+ Xml::check(
+ $params['controlName'],
+ $params['value'],
+ $params['attribs'] + [
+ 'id' => $params['controlName'],
+ 'tabindex' => $this->nextTabIndex(),
+ ]
+ ) .
+ $labelText . "\n"
+ ) .
"</div>\n";
}
* @return string|null
*/
public function execute() {
+ global $wgLang;
+
if ( $this->getVar( '_SkipOptional' ) == 'skip' ) {
$this->submitSkins();
return 'skip';
$this->addHTML( $skinHtml );
$extensions = $this->parent->findExtensions();
+ $dependencyMap = [];
if ( $extensions ) {
$extHtml = $this->getFieldsetStart( 'config-extensions' );
+ $extByType = [];
+ $types = SpecialVersion::getExtensionTypes();
+ // Sort by type first
foreach ( $extensions as $ext => $info ) {
- $extHtml .= $this->parent->getCheckBox( [
- 'var' => "ext-$ext",
- 'rawtext' => $ext,
- ] );
+ if ( !isset( $info['type'] ) || !isset( $types[$info['type']] ) ) {
+ // We let extensions normally define custom types, but
+ // since we aren't loading extensions, we'll have to
+ // categorize them under other
+ $info['type'] = 'other';
+ }
+ $extByType[$info['type']][$ext] = $info;
+ }
+
+ foreach ( $types as $type => $message ) {
+ if ( !isset( $extByType[$type] ) ) {
+ continue;
+ }
+ $extHtml .= Html::element( 'h2', [], $message );
+ foreach ( $extByType[$type] as $ext => $info ) {
+ $urlText = '';
+ if ( isset( $info['url'] ) ) {
+ $urlText = ' ' . Html::element( 'a', [ 'href' => $info['url'] ], '(more information)' );
+ }
+ $attribs = [ 'data-name' => $ext ];
+ $labelAttribs = [];
+ $fullDepList = [];
+ if ( isset( $info['requires']['extensions'] ) ) {
+ $dependencyMap[$ext]['extensions'] = $info['requires']['extensions'];
+ $labelAttribs['class'] = 'mw-ext-with-dependencies';
+ }
+ if ( isset( $info['requires']['skins'] ) ) {
+ $dependencyMap[$ext]['skins'] = $info['requires']['skins'];
+ $labelAttribs['class'] = 'mw-ext-with-dependencies';
+ }
+ if ( isset( $dependencyMap[$ext] ) ) {
+ $links = [];
+ // For each dependency, link to the checkbox for each
+ // extension/skin that is required
+ if ( isset( $dependencyMap[$ext]['extensions'] ) ) {
+ foreach ( $dependencyMap[$ext]['extensions'] as $name ) {
+ $links[] = Html::element(
+ 'a',
+ [ 'href' => "#config_ext-$name" ],
+ $name
+ );
+ }
+ }
+ if ( isset( $dependencyMap[$ext]['skins'] ) ) {
+ foreach ( $dependencyMap[$ext]['skins'] as $name ) {
+ $links[] = Html::element(
+ 'a',
+ [ 'href' => "#config_skin-$name" ],
+ $name
+ );
+ }
+ }
+
+ $text = wfMessage( 'config-extensions-requires' )
+ ->rawParams( $ext, $wgLang->commaList( $links ) )
+ ->escaped();
+ } else {
+ $text = $ext;
+ }
+ $extHtml .= $this->parent->getCheckBox( [
+ 'var' => "ext-$ext",
+ 'rawtext' => $text,
+ 'attribs' => $attribs,
+ 'labelAttribs' => $labelAttribs,
+ ] );
+ }
}
$extHtml .= $this->parent->getHelpBox( 'config-extensions-help' ) .
$this->getFieldsetEnd();
$this->addHTML( $extHtml );
+ // Push the dependency map to the client side
+ $this->addHTML( Html::inlineScript(
+ 'var extDependencyMap = ' . Xml::encodeJsVar( $dependencyMap )
+ ) );
}
// Having / in paths in Windows looks funny :)
"config-extension-link": "Did you know that your wiki supports [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions]?\n\nYou can browse [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions by category] or the [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] to see the full list of extensions.",
"config-skins-screenshots": "$1 (screenshots: $2)",
"config-skins-screenshot": "$1 ($2)",
+ "config-extensions-requires": "$1 (requires $2)",
"config-screenshot": "screenshot",
"mainpagetext": "<strong>MediaWiki has been installed.</strong>",
"mainpagedocfooter": "Consult the [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide] for information on using the wiki software.\n\n== Getting started ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Learn how to combat spam on your wiki]"
"config-extension-link": "Shown on last page of installation to inform about possible extensions.\n{{Identical|Did you know}}",
"config-skins-screenshots": "Radio button text, $1 is the skin name, and $2 is a list of links to screenshots of that skin",
"config-skins-screenshot": "Radio button text, $1 is the skin name, and $2 is a link to a screenshot of that skin, where the link text is {{msg-mw|config-screenshot}}.",
+ "config-extensions-requires": "Radio button text, $1 is the extension name, and $2 are links to other extensions that this one requires.",
"config-screenshot": "Link text for the link in {{msg-mw|config-skins-screenshot}}\n{{Identical|Screenshot}}",
"mainpagetext": "Along with {{msg-mw|mainpagedocfooter}}, the text you will see on the Main Page when your wiki is installed.",
"mainpagedocfooter": "Along with {{msg-mw|mainpagetext}}, the text you will see on the Main Page when your wiki is installed.\nThis might be a good place to put information about <nowiki>{{GRAMMAR:}}</nowiki>. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/fi]] for an example. For languages having grammatical distinctions and not having an appropriate <nowiki>{{GRAMMAR:}}</nowiki> software available, a suggestion to check and possibly amend the messages having <nowiki>{{SITENAME}}</nowiki> may be valuable. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/ksh]] for an example."
--- /dev/null
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+/**
+ * @since 1.31
+ */
+class ExtensionDependencyError extends Exception {
+
+ /**
+ * @var string[]
+ */
+ public $missingExtensions = [];
+
+ /**
+ * @var string[]
+ */
+ public $missingSkins = [];
+
+ /**
+ * @var string[]
+ */
+ public $incompatibleExtensions = [];
+
+ /**
+ * @var string[]
+ */
+ public $incompatibleSkins = [];
+
+ /**
+ * @var bool
+ */
+ public $incompatibleCore = false;
+
+ /**
+ * @param array $errors Each error has a 'msg' and 'type' key at minimum
+ */
+ public function __construct( array $errors ) {
+ $msg = '';
+ foreach ( $errors as $info ) {
+ $msg .= $info['msg'] . "\n";
+ switch ( $info['type'] ) {
+ case 'incompatible-core':
+ $this->incompatibleCore = true;
+ break;
+ case 'missing-skins':
+ $this->missingSkins[] = $info['missing'];
+ break;
+ case 'missing-extensions':
+ $this->missingExtensions[] = $info['missing'];
+ break;
+ case 'incompatible-skins':
+ $this->incompatibleSkins[] = $info['incompatible'];
+ break;
+ case 'incompatible-extensions':
+ $this->incompatibleExtensions[] = $info['incompatible'];
+ break;
+ // default: continue
+ }
+ }
+
+ parent::__construct( $msg );
+ }
+
+}
* @param array $queue keys are filenames, values are ignored
* @return array extracted info
* @throws Exception
+ * @throws ExtensionDependencyError
*/
public function readFromQueue( array $queue ) {
global $wgVersion;
);
if ( $incompatible ) {
- if ( count( $incompatible ) === 1 ) {
- throw new Exception( $incompatible[0] );
- } else {
- throw new Exception( implode( "\n", $incompatible ) );
- }
+ throw new ExtensionDependencyError( $incompatible );
}
// Need to set this so we can += to it later
case ExtensionRegistry::MEDIAWIKI_CORE:
$mwError = $this->handleMediaWikiDependency( $values, $extension );
if ( $mwError !== false ) {
- $errors[] = $mwError;
+ $errors[] = [
+ 'msg' => $mwError,
+ 'type' => 'incompatible-core',
+ ];
}
break;
case 'extensions':
case 'skin':
foreach ( $values as $dependency => $constraint ) {
- $extError = $this->handleExtensionDependency( $dependency, $constraint, $extension );
+ $extError = $this->handleExtensionDependency(
+ $dependency, $constraint, $extension, $dependencyType
+ );
if ( $extError !== false ) {
$errors[] = $extError;
}
* @param string $dependencyName The name of the dependency
* @param string $constraint The required version constraint for this dependency
* @param string $checkedExt The Extension, which depends on this dependency
- * @return bool|string false for no errors, or a string message
+ * @param string $type Either 'extension' or 'skin'
+ * @return bool|array false for no errors, or an array of info
*/
- private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt ) {
+ private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt,
+ $type
+ ) {
// Check if the dependency is even installed
if ( !isset( $this->loaded[$dependencyName] ) ) {
- return "{$checkedExt} requires {$dependencyName} to be installed.";
+ return [
+ 'msg' => "{$checkedExt} requires {$dependencyName} to be installed.",
+ 'type' => "missing-$type",
+ 'missing' => $dependencyName,
+ ];
}
// Check if the dependency has specified a version
if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
return false;
} else {
// Otherwise, mark it as incompatible.
- return "{$dependencyName} does not expose its version, but {$checkedExt}"
+ $msg = "{$dependencyName} does not expose its version, but {$checkedExt}"
. " requires: {$constraint}.";
+ return [
+ 'msg' => $msg,
+ 'type' => "incompatible-$type",
+ 'incompatible' => $checkedExt,
+ ];
}
} else {
// Try to get a constraint for the dependency version
} catch ( UnexpectedValueException $e ) {
// Non-parsable version, output an error message that the version
// string is invalid
- return "$dependencyName does not have a valid version string.";
+ return [
+ 'msg' => "$dependencyName does not have a valid version string.",
+ 'type' => 'invalid-version',
+ ];
}
// Check if the constraint actually matches...
if (
!$this->versionParser->parseConstraints( $constraint )->matches( $installedVersion )
) {
- return "{$checkedExt} is not compatible with the current "
+ $msg = "{$checkedExt} is not compatible with the current "
. "installed version of {$dependencyName} "
. "({$this->loaded[$dependencyName]['version']}), "
. "it requires: " . $constraint . '.';
+ return [
+ 'msg' => $msg,
+ 'type' => "incompatible-$type",
+ 'incompatible' => $checkedExt,
+ ];
}
}
+/* global extDependencyMap */
( function ( $ ) {
$( function () {
var $label, labelText;
$memc.hide( 'slow' );
}
} );
+
+ function areReqsSatisfied( name ) {
+ var i, ext, skin, node;
+ if ( !extDependencyMap[ name ] ) {
+ return true;
+ }
+
+ if ( extDependencyMap[ name ].extensions ) {
+ for ( i in extDependencyMap[ name ].extensions ) {
+ ext = extDependencyMap[ name ].extensions[ i ];
+ node = document.getElementById( 'config_ext-' + ext );
+ if ( !node || !node.checked ) {
+ return false;
+ }
+ }
+ }
+ if ( extDependencyMap[ name ].skins ) {
+ for ( i in extDependencyMap[ name ].skins ) {
+ skin = extDependencyMap[ name ].skins[ i ];
+ node = document.getElementById( 'config_skin-' + skin );
+ if ( !node || !node.checked ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // Disable checkboxes if the extension has dependencies
+ $( '.mw-ext-with-dependencies input' ).prop( 'disabled', true );
+ $( 'input[data-name]' ).change( function () {
+ $( '.mw-ext-with-dependencies input' ).each( function () {
+ var $this = $( this ),
+ name = $this.data( 'name' );
+ if ( areReqsSatisfied( name ) ) {
+ // Un-disable it!
+ $this.prop( 'disabled', false );
+ } else {
+ // Disable the checkbox, and uncheck it if it is checked
+ $this.prop( 'disabled', true );
+ if ( $this.prop( 'checked' ) ) {
+ $this.prop( 'checked', false );
+ }
+ }
+ } );
+ } );
} );
}( jQuery ) );
'NoVersionGiven' => '1.0',
]
],
- [ 'NoVersionGiven does not expose its version, but FakeExtension requires: 1.0.' ],
+ [ [
+ 'incompatible' => 'FakeExtension',
+ 'type' => 'incompatible-extensions',
+ 'msg' => 'NoVersionGiven does not expose its version, but FakeExtension requires: 1.0.'
+ ] ],
],
[
[
'Missing' => '*',
]
],
- [ 'FakeExtension requires Missing to be installed.' ],
+ [ [
+ 'missing' => 'Missing',
+ 'type' => 'missing-extensions',
+ 'msg' => 'FakeExtension requires Missing to be installed.',
+ ] ],
],
[
[
'FakeDependency' => '2.0.0',
]
],
- // phpcs:ignore Generic.Files.LineLength.TooLong
- [ 'FakeExtension is not compatible with the current installed version of FakeDependency (1.0.0), it requires: 2.0.0.' ],
+ [ [
+ 'incompatible' => 'FakeExtension',
+ 'type' => 'incompatible-extensions',
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ 'msg' => 'FakeExtension is not compatible with the current installed version of FakeDependency (1.0.0), it requires: 2.0.0.'
+ ] ],
]
];
}
'version' => 'not really valid',
],
] );
- $this->assertEquals( [ "FakeDependency does not have a valid version string." ],
+ $this->assertEquals(
+ [ [
+ 'type' => 'invalid-version',
+ 'msg' => "FakeDependency does not have a valid version string."
+ ] ],
$checker->checkArray( [
'FakeExtension' => [
'extensions' => [