install.php: Allow extensions and skins to be specified

Allow the extensions and skins installed by maintenance/install.php to
be customised using --skins= and --extensions=. If the argument is
am empty string then no extensions/skins are installed. For backwards
compatibility, the default is to install all skins, but to install all
extensions only if --with-extensions is given.

The new CLI options may be specified multiple times, but for
convenience, comma-separated lists can also be used.

Also:
* Rename $option to $options
* If an extension has a dependency error, propagate the very readable
  error message generated by ExtensionRegistry back to the user.
* Split getExtensionInfo() from the loop body of findExtensionsByType(),
  so that CliInstaller can use it to validate its parameters and get
  error messages.
* I didn't like the idea of removing the "s" from the directory name in
  order to construct the JSON file name, so I split
  findExtensionsByType() from findExtensions(), with the former not
  having this hack. In findExtensions(), make the previous assumption
  that the directory name is always "extensions" or "skins" explicit,
  throwing an exception if it is otherwise.

Change-Id: Id0fb63cd4e61a047ef3396ee1c38d6073dfc7fd1
This commit is contained in:
Tim Starling 2018-09-19 15:43:14 +10:00 committed by Legoktm
parent e94964989d
commit 5e9ada5882
5 changed files with 149 additions and 56 deletions

View file

@ -50,30 +50,30 @@ class CliInstaller extends Installer {
/**
* @param string $siteName
* @param string|null $admin
* @param array $option
* @param array $options
*/
function __construct( $siteName, $admin = null, array $option = [] ) {
function __construct( $siteName, $admin = null, array $options = [] ) {
global $wgContLang;
parent::__construct();
if ( isset( $option['scriptpath'] ) ) {
if ( isset( $options['scriptpath'] ) ) {
$this->specifiedScriptPath = true;
}
foreach ( $this->optionMap as $opt => $global ) {
if ( isset( $option[$opt] ) ) {
$GLOBALS[$global] = $option[$opt];
$this->setVar( $global, $option[$opt] );
if ( isset( $options[$opt] ) ) {
$GLOBALS[$global] = $options[$opt];
$this->setVar( $global, $options[$opt] );
}
}
if ( isset( $option['lang'] ) ) {
if ( isset( $options['lang'] ) ) {
global $wgLang, $wgLanguageCode;
$this->setVar( '_UserLang', $option['lang'] );
$wgLanguageCode = $option['lang'];
$this->setVar( '_UserLang', $options['lang'] );
$wgLanguageCode = $options['lang'];
$wgContLang = MediaWikiServices::getInstance()->getContentLanguage();
$wgLang = Language::factory( $option['lang'] );
$wgLang = Language::factory( $options['lang'] );
RequestContext::getMain()->setLanguage( $wgLang );
}
@ -89,32 +89,47 @@ class CliInstaller extends Installer {
$this->setVar( '_AdminName', $admin );
}
if ( !isset( $option['installdbuser'] ) ) {
if ( !isset( $options['installdbuser'] ) ) {
$this->setVar( '_InstallUser',
$this->getVar( 'wgDBuser' ) );
$this->setVar( '_InstallPassword',
$this->getVar( 'wgDBpassword' ) );
} else {
$this->setVar( '_InstallUser',
$option['installdbuser'] );
$options['installdbuser'] );
$this->setVar( '_InstallPassword',
$option['installdbpass'] ?? "" );
$options['installdbpass'] ?? "" );
// Assume that if we're given the installer user, we'll create the account.
$this->setVar( '_CreateDBAccount', true );
}
if ( isset( $option['pass'] ) ) {
$this->setVar( '_AdminPassword', $option['pass'] );
if ( isset( $options['pass'] ) ) {
$this->setVar( '_AdminPassword', $options['pass'] );
}
// Detect and inject any extension found
if ( isset( $option['with-extensions'] ) ) {
if ( isset( $options['extensions'] ) ) {
$status = $this->validateExtensions(
'extension', 'extensions', $options['extensions'] );
if ( !$status->isOK() ) {
$this->showStatusMessage( $status );
}
$this->setVar( '_Extensions', $status->value );
} elseif ( isset( $options['with-extensions'] ) ) {
$this->setVar( '_Extensions', array_keys( $this->findExtensions() ) );
}
// Set up the default skins
$skins = array_keys( $this->findExtensions( 'skins' ) );
if ( isset( $options['skins'] ) ) {
$status = $this->validateExtensions( 'skin', 'skins', $options['skins'] );
if ( !$status->isOK() ) {
$this->showStatusMessage( $status );
}
$skins = $status->value;
} else {
$skins = array_keys( $this->findExtensions( 'skins' ) );
}
$this->setVar( '_Skins', $skins );
if ( $skins ) {
@ -123,6 +138,28 @@ class CliInstaller extends Installer {
}
}
private function validateExtensions( $type, $directory, $nameLists ) {
$extensions = [];
$status = new Status;
foreach ( (array)$nameLists as $nameList ) {
foreach ( explode( ',', $nameList ) as $name ) {
$name = trim( $name );
if ( $name === '' ) {
continue;
}
$extStatus = $this->getExtensionInfo( $type, $directory, $name );
if ( $extStatus->isOK() ) {
$extensions[] = $name;
} else {
$status->merge( $extStatus );
}
}
}
$extensions = array_unique( $extensions );
$status->value = $extensions;
return $status;
}
/**
* Main entry point.
*/

View file

@ -1264,15 +1264,33 @@ abstract class Installer {
}
/**
* Finds extensions that follow the format /$directory/Name/Name.php,
* and returns an array containing the value for 'Name' for each found extension.
* Find extensions or skins in a subdirectory of $IP.
* Returns an array containing the value for 'Name' for each found extension.
*
* Reasonable values for $directory include 'extensions' (the default) and 'skins'.
*
* @param string $directory Directory to search in
* @param string $directory Directory to search in, relative to $IP, must be either "extensions"
* or "skins"
* @return array [ $extName => [ 'screenshots' => [ '...' ] ]
*/
public function findExtensions( $directory = 'extensions' ) {
switch ( $directory ) {
case 'extensions':
return $this->findExtensionsByType( 'extension', 'extensions' );
case 'skins':
return $this->findExtensionsByType( 'skin', 'skins' );
default:
throw new InvalidArgumentException( "Invalid extension type" );
}
}
/**
* Find extensions or skins, and return an array containing the value for 'Name' for each found
* extension.
*
* @param string $type Either "extension" or "skin"
* @param string $directory Directory to search in, relative to $IP
* @return array [ $extName => [ 'screenshots' => [ '...' ] ]
*/
protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
if ( $this->getVar( 'IP' ) === null ) {
return [];
}
@ -1282,40 +1300,15 @@ abstract class Installer {
return [];
}
// extensions -> extension.json, skins -> skin.json
$jsonFile = substr( $directory, 0, strlen( $directory ) - 1 ) . '.json';
$dh = opendir( $extDir );
$exts = [];
while ( ( $file = readdir( $dh ) ) !== false ) {
if ( !is_dir( "$extDir/$file" ) ) {
continue;
}
$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" ) ) {
$paths = glob( "$extDir/$file/screenshots/*.png" );
foreach ( $paths as $path ) {
$exts[$file]['screenshots'][] = str_replace( $extDir, "../$directory", $path );
}
}
}
if ( $isJson ) {
$info = $this->readExtension( $fullJsonFile );
if ( $info === false ) {
continue;
}
$exts[$file] += $info;
$status = $this->getExtensionInfo( $type, $directory, $file );
if ( $status->isOK() ) {
$exts[$file] = $status->value;
}
}
closedir( $dh );
@ -1324,12 +1317,65 @@ abstract class Installer {
return $exts;
}
/**
* @param string $type Either "extension" or "skin"
* @param string $parentRelPath The parent directory relative to $IP
* @param string $name The extension or skin name
* @return Status An object containing an error list. If there were no errors, an associative
* array of information about the extension can be found in $status->value.
*/
protected function getExtensionInfo( $type, $parentRelPath, $name ) {
if ( $this->getVar( 'IP' ) === null ) {
throw new Exception( 'Cannot find extensions since the IP variable is not yet set' );
}
if ( $type !== 'extension' && $type !== 'skin' ) {
throw new InvalidArgumentException( "Invalid extension type" );
}
$absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
$relDir = "../$parentRelPath/$name";
if ( !is_dir( $absDir ) ) {
return Status::newFatal( 'config-extension-not-found', $name );
}
$jsonFile = $type . '.json';
$fullJsonFile = "$absDir/$jsonFile";
$isJson = file_exists( $fullJsonFile );
$isPhp = false;
if ( !$isJson ) {
// Only fallback to PHP file if JSON doesn't exist
$fullPhpFile = "$absDir/$name.php";
$isPhp = file_exists( $fullPhpFile );
}
if ( !$isJson && !$isPhp ) {
return Status::newFatal( 'config-extension-not-found', $name );
}
// Extension exists. Now see if there are screenshots
$info = [];
if ( is_dir( "$absDir/screenshots" ) ) {
$paths = glob( "$absDir/screenshots/*.png" );
foreach ( $paths as $path ) {
$info['screenshots'][] = str_replace( $absDir, $relDir, $path );
}
}
if ( $isJson ) {
$jsonStatus = $this->readExtension( $fullJsonFile );
if ( !$jsonStatus->isOK() ) {
return $jsonStatus;
}
$info += $jsonStatus->value;
}
return Status::newGood( $info );
}
/**
* @param string $fullJsonFile
* @param array $extDeps
* @param array $skinDeps
*
* @return array|bool False if this extension can't be loaded
* @return Status On success, an array of extension information is in $status->value. On
* failure, the Status object will have an error list.
*/
private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
$load = [
@ -1340,7 +1386,7 @@ abstract class Installer {
foreach ( $extDeps as $dep ) {
$fname = "$extDir/$dep/extension.json";
if ( !file_exists( $fname ) ) {
return false;
return Status::newFatal( 'config-extension-not-found', $dep );
}
$load[$fname] = 1;
}
@ -1350,7 +1396,7 @@ abstract class Installer {
foreach ( $skinDeps as $dep ) {
$fname = "$skinDir/$dep/skin.json";
if ( !file_exists( $fname ) ) {
return false;
return Status::newFatal( 'config-extension-not-found', $dep );
}
$load[$fname] = 1;
}
@ -1364,7 +1410,8 @@ abstract class Installer {
) {
// If something is incompatible with a dependency, we have no real
// option besides skipping it
return false;
return Status::newFatal( 'config-extension-dependency',
basename( dirname( $fullJsonFile ) ), $e->getMessage() );
} elseif ( $e->missingExtensions || $e->missingSkins ) {
// There's an extension missing in the dependency tree,
// so add those to the dependency list and try again
@ -1375,7 +1422,8 @@ abstract class Installer {
);
}
// Some other kind of dependency error?
return false;
return Status::newFatal( 'config-extension-dependency',
basename( dirname( $fullJsonFile ) ), $e->getMessage() );
}
$ret = [];
// The order of credits will be the order of $load,
@ -1397,7 +1445,7 @@ abstract class Installer {
}
$ret['type'] = $credits['type'];
return $ret;
return Status::newGood( $ret );
}
/**

View file

@ -309,6 +309,8 @@
"config-skins-screenshot": "$1 ($2)",
"config-extensions-requires": "$1 (requires $2)",
"config-screenshot": "screenshot",
"config-extension-not-found": "Could not find the registration file for the extension \"$1\"",
"config-extension-dependency": "A dependency error was encountered while installing the extension \"$1\": $2",
"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]"
}

View file

@ -330,6 +330,8 @@
"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.\n{{Identical|Require}}",
"config-screenshot": "Link text for the link in {{msg-mw|config-skins-screenshot}}\n{{Identical|Screenshot}}",
"config-extension-not-found": "An error shown when an extension or skin named by the user could not be found.\n* $1 is the extension name",
"config-extension-dependency": "An error shown if an extension could not be loaded due to it depending on the wrong version of MediaWiki or an uninstallable extension.\n* $1 is the extension name\n* $2 is a more detailed explanation, in English",
"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."
}

View file

@ -90,6 +90,10 @@ class CommandLineInstaller extends Maintenance {
$this->addOption( 'env-checks', "Run environment checks only, don't change anything" );
$this->addOption( 'with-extensions', "Detect and include extensions" );
$this->addOption( 'extensions', 'Comma-separated list of extensions to install',
false, true, false, true );
$this->addOption( 'skins', 'Comma-separated list of skins to install (default: all)',
false, true, false, true );
}
public function getDbType() {