Seb35 has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/328347 )
Change subject: [WIP] Activate Composer extensions in the same way than other extensions are activated ...................................................................... [WIP] Activate Composer extensions in the same way than other extensions are activated This proof-of-concept is not meant to be merged, I prepare a better-architectured version. Basically the strategy is to create /vendor/composerKEY with KEY representing the content of the autoloaders (typically a specific hash of its content). All these directories are created beforehand (2^n directories with n = number of Composer-managed extensions and skins) and the right directory – depending on the wiki – is loaded by the indirection-like /vendor/autoload.php. I didn’t observed side effects apart the minor detail that MediaWiki only display, in Special:Version, Composer libraries it reads in /vendor/composer/installed.json (only a matter of display there). With this architecture, extensions and skins are simply activated in the configuration by setting wgUseExtensionMyWonderfulExt to true. Obviously there could be higher level operations to do like update.php, create directories, setting extension-specific parameters, etc. A detail to be improved is to better manage dependencies between extensions: Composer is aware of dependencies, so some composerKEY directories are the same (e.g. SemanticFormsSelect (SFS) also autoloads SemanticMediaWiki (SMW) and PageForms (PF)); here the activation is independant from Composer and it would be great to synchronise the two mechanism in some way (e.g. activating SFS should not only select the right composerKEY but also activate SMW and PF, currently it selects the right composerKEY but SFS trigger a fatal error since PF 4.0 is not activated). Architecture: * the configuration compilation is moved in the bootstraping stage for the most part, but given we don’t know at this stage if the MediaWiki version is ExtensionRegistry-aware, this small part remains in the LS.php stage. * the composer key is written in a dedicated cache file: it cannot be in LS.php since this file is loaded after the composer key is needed, and it should not be in the existence file since it will change as soon as a config file change. Code: * added a CLI script mwcomposer.php * added MediaWikiFarmComposerScript containing the logic of mwcomposer.php * splitted the configuration part in two parts: * major part during the bootstraping stage * extension loading mechanism after MediaWiki is loaded Change-Id: I027251fabb32d6543d4bff7c610935316b639802 --- A bin/mwcomposer.php M extension.json M src/MediaWikiFarm.php A src/MediaWikiFarmComposerScript.php M src/main.php 5 files changed, 473 insertions(+), 33 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/MediaWikiFarm refs/changes/47/328347/1 diff --git a/bin/mwcomposer.php b/bin/mwcomposer.php new file mode 100644 index 0000000..97dbeca --- /dev/null +++ b/bin/mwcomposer.php @@ -0,0 +1,29 @@ +<?php +/** + * Wrapper around Composer to create as many autoloaders as MediaWiki extensions. + * + * @author Sébastien Beyou ~ Seb35 <se...@seb35.fr> + * @license GPL-3.0+ GNU General Public License v3.0 ou version ultérieure + * @license AGPL-3.0+ GNU Affero General Public License v3.0 ou version ultérieure + */ +// @codeCoverageIgnoreStart + +# Protect against web entry +if( PHP_SAPI != 'cli' && PHP_SAPI != 'phpdbg' ) { + exit; +} + +# Load classes +require_once dirname( dirname( __FILE__ ) ) . '/src/MediaWikiFarmComposerScript.php'; + +# Prepare environment +$wgMediaWikiFarmComposerScript = new MediaWikiFarmComposerScript( $argc, $argv ); + +$wgMediaWikiFarmComposerScript->load(); + +$wgMediaWikiFarmComposerScript->main(); + +if( $wgMediaWikiFarmComposerScript->status != 200 ) { + exit( $wgMediaWikiFarmComposerScript->status ); +} +// @codeCoverageIgnoreEnd diff --git a/extension.json b/extension.json index 8a2d26f..b49fe04 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "MediaWikiFarm", - "version": "0.2.0", + "version": "0.3.0-dev", "author": [ "Seb35" ], diff --git a/src/MediaWikiFarm.php b/src/MediaWikiFarm.php index 5f5c309..3da02c4 100644 --- a/src/MediaWikiFarm.php +++ b/src/MediaWikiFarm.php @@ -317,6 +317,26 @@ return true; } + function compileConfig() { + + $this->getMediaWikiConfig(); + + $composer = array(); + foreach( $this->configuration['extensions'] as $name => $loading ) { + if( $loading == 'composer' ) { + $composer[] = 'Extension' . $name; + } + } + foreach( $this->configuration['skins'] as $name => $loading ) { + if( $loading == 'composer' ) { + $composer[] = 'Skin' . $name; + } + } + $this->variables['$COMPOSER'] = self::composerKey( $composer ); + + return true; + } + /** * This function loads MediaWiki configuration. * @@ -387,7 +407,7 @@ $GLOBALS['wgExtensionCredits']['other'][] = array( 'path' => $this->farmDir . '/MediaWikiFarm.php', 'name' => 'MediaWikiFarm', - 'version' => '0.2.0', + 'version' => '0.3.0-dev', 'author' => 'Seb35', 'url' => 'https://www.mediawiki.org/wiki/Extension:MediaWikiFarm', 'descriptionmsg' => 'mediawikifarm-desc', @@ -434,10 +454,20 @@ function getConfigFile() { if( !$this->isLocalSettingsFresh() ) { + $this->compileConfig(); return $this->farmDir . '/src/main.php'; } return $this->cacheDir . '/LocalSettings/' . $this->variables['$SERVER'] . '.php'; + } + + function finaliseMediaWikiConfig() { + + $this->getMediaWikiConfig(); + + $this->finaliseExtractionSkinsAndExtensions(); + + $this->writeLocalSettings(); } @@ -532,9 +562,14 @@ unset( $result['$CONFIG'] ); unset( $result['$CORECONFIG'] ); $this->variables = $result; + $composerFile = $this->readFile( $host . '.php', $this->cacheDir . '/composer', false ); + if( is_array( $composerFile ) ) { + $this->variables['$COMPOSER'] = $composerFile[0]; + } return; } elseif( is_file( $this->cacheDir . '/LocalSettings/' . $host . '.php' ) ) { unlink( $this->cacheDir . '/LocalSettings/' . $host . '.php' ); + unlink( $this->cacheDir . '/composer/' . $host . '.php' ); } } @@ -906,6 +941,13 @@ # Extract from the general configuration skin and extension configuration $this->extractSkinsAndExtensions(); + } + + function writeLocalSettings( $force = false ) { + + if( !$force && $this->isLocalSettingsFresh() ) { + return; + } # Save this configuration in a PHP file if( is_string( $this->cacheDir ) && !count( $this->errors ) ) { @@ -917,6 +959,12 @@ $this->variables['$SERVER'] . '.php', $this->cacheDir . '/LocalSettings' ); + if( array_key_exists( '$COMPOSER', $this->variables ) ) { + self::cacheFile( array( $this->variables['$COMPOSER'] ), + $this->variables['$SERVER'] . '.php', + $this->cacheDir . '/composer' + ); + } } } @@ -1141,11 +1189,6 @@ $settings = &$this->configuration['settings']; - # Autodetect if ExtensionRegistry is here - if( is_null( $this->parameters['ExtensionRegistry'] ) ) { - $this->parameters['ExtensionRegistry'] = class_exists( 'ExtensionRegistry' ); - } - # Search for skin and extension activation $settings2 = $settings; # This line is about avoiding the behavious in PHP 5 where newly-added items are evaluated foreach( $settings2 as $setting => $value ) { @@ -1153,39 +1196,32 @@ $type = strtolower( $matches[1] ); $name = $matches[2]; - $loadingMechanism = $this->detectLoadingMechanism( $type, $name ); - if( $value !== true ) { - $loadingMechanism = $value; - } - if( is_null( $loadingMechanism ) ) { - $settings[$setting] = false; - } else { - $this->configuration[$type.'s'][$name] = $loadingMechanism; - $settings[preg_replace( '/[^a-zA-Z0-9_\x7f\xff]/', '', $setting )] = true; + $loadingMechanism = $value; + if( $value === true ) { + $loadingMechanism = $this->detectLoadingMechanismEarly( $type, $name ); } + $this->configuration[$type.'s'][$name] = $loadingMechanism; } elseif( preg_match( '/^wgUse(.+)$/', $setting, $matches ) && ( $value === true || $value == 'require_once' || $value == 'composer' ) ) { $name = $matches[1]; - $loadingMechanism = $this->detectLoadingMechanism( 'extension', $name ); - if( !is_null( $loadingMechanism ) ) { + $loadingMechanism = $this->detectLoadingMechanismEarly( 'extension', $name ); + if( $loadingMechanism ) { if( $value !== true ) { $loadingMechanism = $value; } $this->configuration['extensions'][$name] = $loadingMechanism; $settings['wgUseExtension'.preg_replace( '/[^a-zA-Z0-9_\x7f\xff]/', '', $name )] = true; - unset( $settings[$setting] ); } else { - $loadingMechanism = $this->detectLoadingMechanism( 'skin', $name ); - if( !is_null( $loadingMechanism ) ) { + $loadingMechanism = $this->detectLoadingMechanismEarly( 'skin', $name ); + if( $loadingMechanism ) { if( $value !== true ) { $loadingMechanism = $value; } $this->configuration['skins'][$name] = $loadingMechanism; $settings['wgUseSkin'.preg_replace( '/[^a-zA-Z0-9_\x7f\xff]/', '', $name )] = true; - unset( $settings[$setting] ); } } } @@ -1202,6 +1238,58 @@ } } + function finaliseExtractionSkinsAndExtensions() { + + # Autodetect if ExtensionRegistry is here + if( is_null( $this->parameters['ExtensionRegistry'] ) ) { + $this->parameters['ExtensionRegistry'] = class_exists( 'ExtensionRegistry' ); + } + + foreach( $this->configuration['extensions'] as $name => $loading ) { + + $loadingMechanism = $this->detectLoadingMechanism( 'extension', $name ); + if( $loadingMechanism ) { + $this->configuration['extensions'][$name] = $loadingMechanism; + $settings['wgUseExtension'.preg_replace( '/[^a-zA-Z0-9_\x7f\xff]/', '', $name )] = true; + } else { + unset( $this->configuration['extensions'][$name] ); + $settings['wgUseExtension'.preg_replace( '/[^a-zA-Z0-9_\x7f\xff]/', '', $name )] = false; + } + } + + foreach( $this->configuration['skins'] as $name => $loading ) { + + $loadingMechanism = $this->detectLoadingMechanism( 'skin', $name ); + if( $loadingMechanism ) { + $this->configuration['skins'][$name] = $loadingMechanism; + $settings['wgUseSkin'.preg_replace( '/[^a-zA-Z0-9_\x7f\xff]/', '', $name )] = true; + } else { + unset( $this->configuration['skins'][$name] ); + $settings['wgUseSkin'.preg_replace( '/[^a-zA-Z0-9_\x7f\xff]/', '', $name )] = false; + } + } + } + + /** + * Detect if extensions and skins load with Composer. + * + * @mediawikifarm-const + * + * @param string $type Type, in ['extension', 'skin']. + * @param string $name Name of the extension/skin. + * @return string 'composer' if successfully loaded with Composer or '' if it does not load with Composer. + */ + function detectLoadingMechanismEarly( $type, $name ) { + + # The Composer mechanism created a Composer autoloader -> this extension/skin is loaded with Composer + if( is_dir( $this->variables['$CODE'].'/'.$type.'s/'.$name ) + && is_dir( $this->variables['$CODE'].'/vendor/composer'.self::composerKey( ucfirst( $type ).$name ) ) ) { + return 'composer'; + } + + return ''; + } + /** * Detection of the loading mechanism of extensions and skins. * @@ -1213,23 +1301,19 @@ */ function detectLoadingMechanism( $type, $name ) { - if( !is_dir( $this->variables['$CODE'].'/'.$type.'s/'.$name ) ) { - return null; - } - # An extension.json/skin.json file is in the directory -> assume it is the loading mechanism if( $this->parameters['ExtensionRegistry'] && is_file( $this->variables['$CODE'].'/'.$type.'s/'.$name.'/'.$type.'.json' ) ) { return 'wfLoad' . ucfirst( $type ); } + # The Composer mechanism created a Composer autoloader -> this extension/skin is loaded with Composer + elseif( $this->detectLoadingMechanismEarly( $type, $name ) ) { + return 'composer'; + } + # A MyExtension.php file is in the directory -> assume it is the loading mechanism elseif( is_file( $this->variables['$CODE'].'/'.$type.'s/'.$name.'/'.$name.'.php' ) ) { return 'require_once'; - } - - # A composer.json file is in the directory -> assume it is the loading mechanism if previous mechanisms didn’t succeed - elseif( is_file( $this->variables['$CODE'].'/'.$type.'s/'.$name.'/composer.json' ) ) { - return 'composer'; } return null; @@ -1682,4 +1766,29 @@ return $result; } + + /** + * Composer key depending on the activated extensions and skins. + * + * Extension names must follow the form 'ExtensionMyWonderfulExtension'; + * Skin names must follow the form 'SkinMyWonderfulSkin'. + * + * @mediawikifarm-const + * @mediawikifarm-idempotent + * + * @param string|string[] $names Names of extensions and skins. + * @return string Composer key. + */ + static function composerKey( $names ) { + + if( is_string( $names ) ) { + $names = array( $names ); + } elseif( !count( $names ) ) { + return ''; + } else { + sort( $names ); + } + + return substr( md5( implode( '|', $names ) ), 0, 10 ); + } } diff --git a/src/MediaWikiFarmComposerScript.php b/src/MediaWikiFarmComposerScript.php new file mode 100644 index 0000000..0280fdf --- /dev/null +++ b/src/MediaWikiFarmComposerScript.php @@ -0,0 +1,302 @@ +<?php +/** + * Wrapper around Composer to create as many autoloaders as MediaWiki extensions. + * + * @author Sébastien Beyou ~ Seb35 <se...@seb35.fr> + * @license GPL-3.0+ GNU General Public License v3.0 ou version ultérieure + * @license AGPL-3.0+ GNU Affero General Public License v3.0 ou version ultérieure + */ + +// @codeCoverageIgnoreStart +require_once dirname( __FILE__ ) . '/AbstractMediaWikiFarmScript.php'; +// @codeCoverageIgnoreEnd + +/** + * This class contains the major part of the script utility, mainly in the main() method. + * Using a class instead of a raw script it better for testability purposes and to use + * less global variables (in fact none; the only global variable written are for + * compatibility purposes, e.g. PHPUnit expects $_SERVER['argv']). + */ +class MediaWikiFarmComposerScript extends AbstractMediaWikiFarmScript { + + /** @var string PHP code to be added in 'vendor/autoload.php'. */ + public $composerSelectorCode = <<<PHP +<?php + +// autoload.php @generated by Composer + +require_once __DIR__ . '/composer' . ( array_key_exists( 'wgMediaWikiFarm', \$GLOBALS ) ? \$GLOBALS['wgMediaWikiFarm']->getVariable('\$COMPOSER') : '' ) . '/autoload_real.php'; + + +PHP; + + /** + * Create the object with a copy of $argc and $argv. + * + * @param int $argc Number of input arguments. + * @param string[] $argv Input arguments. + * @return MediaWikiFarmScript + */ + function __construct( $argc, $argv ) { + + parent::__construct( $argc, $argv ); + + $this->shortUsage = <<<HELP + + Usage: php {$this->argv[0]} MediaWikiScript --wiki=hostname … + + Parameters: + + - MediaWikiScript: name of the script, e.g. "maintenance/runJobs.php" + - hostname: hostname of the wiki, e.g. "mywiki.example.org" + +HELP; + + $fullPath = realpath( $this->argv[0] ); + $this->longUsage = <<<HELP + | Note simple names as "runJobs" will be converted to "maintenance/runJobs.php". + | + | For easier use, you can alias it in your shell: + | + | alias mwscript='php $fullPath' + +HELP; + } + + /** + * Main program for the script. + * + * Although it returns void, the 'status' property says if there was an error or not. + * + * @return void. + */ + function main() { + + # Manage mandatory arguments. + $this->premain(); + if( $this->status ) { + return; + } + + # Get wiki + $this->host = $this->getParam( 'wiki' ); + if( is_null( $this->host ) ) { + $this->usage(); + $this->status = 400; + return; + } + $this->getParam( 0 ); + + # Initialise the requested version + $code = MediaWikiFarm::load( '', $this->host ); + if( $code != 200 ) { + $this->status = $code; + return; + } + + $wgMediaWikiFarm = $GLOBALS['wgMediaWikiFarm']; + + # Backup composer.json and copy MediaWiki directory in temporary dir to + # change its vendor directory and extensions without breaking current + # installation + $origComposerJson = file_get_contents( 'composer.json' ); + $tmpDir = $GLOBALS['wgMediaWikiFarmCacheDir']; + if( !$tmpDir ) { + $tmpDir = '/tmp'; + } + self::copyr( getcwd(), $tmpDir . '/mediawiki', true, array( '/extensions', '/skins', '/vendor', '/composer\.lock' ) ); + + $oldCwd = getcwd(); + chdir( $tmpDir . '/mediawiki' ); + + # Update complete dependencies from Composer + echo "1. Composer with complete extensions/skins set:\n"; + system( '/usr/local/bin/composer update ' . implode( ' ', $this->argv ) ); + + # Copy complete dependencies into 'read-only' directories + self::copyr( 'extensions', 'extensions-composer', true ); + self::copyr( 'skins', 'skins-composer', true ); + self::copyr( 'vendor', 'vendor-composer', true ); + if( is_file( 'composer.local.json' ) ) { + unlink( 'composer.local.json' ); + } + + # Get installed extensions + $installedJson = file_get_contents( 'vendor/composer/installed.json' ); + if( !$installedJson ) { + $this->status = 500; + return; + } + $installedJson = json_decode( $installedJson, true ); + $baseComposerJson = json_decode( $origComposerJson, true ); + + $installable = array(); + $counter = array(); + $transformation = array(); + foreach( $installedJson as $package ) { + if( $package['type'] == 'mediawiki-extension' || $package['type'] == 'mediawiki-skins' ) { + if( array_key_exists( 'require-dev', $baseComposerJson ) && array_key_exists( $package['name'], $baseComposerJson['require-dev'] ) ) { + $counter[$package['name']] = true; + $installable[$package['name']] = $package['version_normalized']; + $transformation[$package['name']] = self::composer2mediawiki( $package['name'], $package['type'] ); + unset( $baseComposerJson['require-dev'][$package['name']] ); + } else { + $counter[$package['name']] = true; + $installable[$package['name']] = $package['version_normalized']; + $transformation[$package['name']] = self::composer2mediawiki( $package['name'], $package['type'] ); + if( array_key_exists( 'require', $baseComposerJson ) && array_key_exists( $package['name'], $baseComposerJson['require'] ) ) { + unset( $baseComposerJson['require'][$package['name']] ); + } + } + } + } + ksort( $counter ); + echo "Complete extensions/skins set composed of:\n"; + foreach( $installable as $name => $version ) { + echo '* ' . preg_replace( '/^(Extension|Skin)/', '$1 ', $transformation[$name] ) . " ($version)\n"; + } + echo "\n"; + self::copyr( 'vendor/composer', 'vendor-composer/composer' . self::composerCounterKey( $counter, $transformation ), true, array(), array( '/autoload_.*\.php', '/ClassLoader\.php', '/installed\.json' ) ); + self::copyr( 'vendor/composer', 'vendor-composer/composer-init', true ); + + # Iterate over installable extensions/skins + $icounter = 2; + while( self::decrementBase2( $counter ) ) { + + $echo = ''; + $thisInstallation = $baseComposerJson; + foreach( $counter as $name => $value ) { + if( $value ) { + $echo .= ' * ' . preg_replace( '/^(Extension|Skin)/', '$1 ', $transformation[$name] ) . "\n"; + $thisInstallation['require'][$name] = $installable[$name]; + } + } + if( $echo ) { + echo "$icounter. Composer with extensions/skins set composed of:\n" . $echo; + } else { + echo "$icounter. Composer with empty extensions/skins set:\n"; + } + + file_put_contents( 'composer.json', str_replace( ' ', "\t", json_encode( $thisInstallation, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES ) ) . "\n" ); + system( '/usr/local/bin/composer update ' . implode( ' ', $this->argv ) ); + + self::copyr( 'vendor/composer', 'vendor-composer/composer' . self::composerCounterKey( $counter, $transformation ), true, array(), array( '/autoload_.*\.php', '/ClassLoader\.php', '/installed\.json' ) ); + $icounter++; + } + + # Merge things other than autoloader_*.php, e.g. subdirectory 'installers' + self::copyr( 'vendor/composer', 'vendor-composer/composer-init', false ); + self::copyr( 'vendor-composer/composer-init', 'vendor-composer/composer', true ); + self::rmdirr( 'vendor-composer/composer-init', true ); + + # Put autoloader indirection + $newAutoload = explode( "\n", file_get_contents( 'vendor-composer/autoload.php' ) ); + file_put_contents( 'vendor-composer/autoload.php', $this->composerSelectorCode . $newAutoload[6] . "\n" ); + + self::copyr( 'vendor-composer', $wgMediaWikiFarm->getVariable( '$CODE' ) . '/vendor', true ); + if( is_dir( 'extensions-composer' ) ) { + $files = array_diff( scandir( 'extensions-composer' ), array( '.', '..' ) ); + foreach( $files as $file ) { + self::copyr( 'extensions-composer/' . $file, $wgMediaWikiFarm->getVariable( '$CODE' ) . '/extensions/' . $file, true ); + } + } + if( is_dir( 'skins' ) ) { + $files = array_diff( scandir( 'skins-composer' ), array( '.', '..' ) ); + foreach( $files as $file ) { + self::copyr( 'skins-composer/' . $file, $wgMediaWikiFarm->getVariable( '$CODE' ) . '/skins/' . $file, true ); + } + } + if( is_file( $wgMediaWikiFarm->getVariable( '$CODE' ) . '/composer.lock' ) ) { + unlink( $wgMediaWikiFarm->getVariable( '$CODE' ) . '/composer.lock' ); + } + + chdir( $oldCwd ); + self::rmdirr( $tmpDir . '/mediawiki' ); + } + + /** + * Decrement a positive number in base 2. + * + * The number is an array with boolean values (false=0, true=1) with + * first index is the lowest value. This function returns false in + * case of newly-negative number. + * + * @param bool[] $number Number in base 2. + * @return bool The decremented number is greater or equal to zero. + */ + static function decrementBase2( &$number ) { + + $lastKey = null; + foreach( $number as $key => $value ) { + if( $value ) { + $lastKey = $key; + break; + } + } + if( is_null( $lastKey ) ) { + return false; + } + $number[$lastKey] = false; + foreach( $number as $key => $value ) { + if( $key == $lastKey ) { + break; + } + $number[$key] = true; + } + + return true; + } + + static function composerCounterKey( $counter, $transformation = array() ) { + + $names = array(); + foreach( $counter as $name => $activation ) { + if( $activation ) { + $names[] = $transformation[$name]; + } + } + + return self::composerKey( $names ); + } + + /** + * Composer key depending on the activated extensions and skins. + * + * Extension names must follow the form 'ExtensionMyWonderfulExtension'; + * Skin names must follow the form 'SkinMyWonderfulSkin'. + * + * @mediawikifarm-const + * @mediawikifarm-idempotent + * + * @param string|string[] $names Names of extensions and skins. + * @return string Composer key. + */ + static function composerKey( $names ) { + + if( is_string( $names ) ) { + $names = array( $names ); + } elseif( !count( $names ) ) { + return ''; + } else { + sort( $names ); + } + + return substr( md5( implode( '|', $names ) ), 0, 10 ); + } + + static function composer2mediawiki( $name, $type ) { + + $name = preg_replace( '/^mediawiki\//', '', $name ); + if( $type == 'mediawiki-extension' ) { + $name = preg_replace( '/-extension$/', '', $name ); + $name = str_replace( '-', ' ', $name ); + $name = str_replace( ' ', '', ucwords( $name ) ); + $name = 'Extension' . $name; + } elseif( $type == 'mediawiki-skin' ) { + $name = preg_replace( '/-skin$/', '', $name ); + $name = 'Skin' . $name; + } + + return $name; + } +} diff --git a/src/main.php b/src/main.php index e1a0eea..ae146f4 100644 --- a/src/main.php +++ b/src/main.php @@ -24,7 +24,7 @@ } # Compile MediaWiki configuration -$wgMediaWikiFarm->getMediaWikiConfig( true ); +$wgMediaWikiFarm->finaliseMediaWikiConfig(); # Load skins with the require_once mechanism foreach( $wgMediaWikiFarm->getConfiguration( 'skins' ) as $skin => $value ) { @@ -37,7 +37,7 @@ # Load extensions with the require_once mechanism foreach( $wgMediaWikiFarm->getConfiguration( 'extensions' ) as $extension => $value ) { - if( $value == 'require_once' ) { + if( $value == 'require_once' && $extension != 'MediaWikiFarm' ) { require_once "$IP/extensions/$extension/$extension.php"; } } -- To view, visit https://gerrit.wikimedia.org/r/328347 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I027251fabb32d6543d4bff7c610935316b639802 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/MediaWikiFarm Gerrit-Branch: master Gerrit-Owner: Seb35 <seb35wikipe...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits