diff --git a/config/Farm/CentralAuthUserManagement.php b/config/Farm/CentralAuthUserManagement.php new file mode 100644 index 0000000..6458ea8 --- /dev/null +++ b/config/Farm/CentralAuthUserManagement.php @@ -0,0 +1,80 @@ +CentralAuth( + $farm->getCentralWiki() + ); + $mwc->conf( 'wgCentralAuthLoginWiki', $farm->getCentralWiki() ); + $this->enableSUL3( $farm, $mwc ); + } + + private function enableSUL3( MWCFarm $farm, MediaWikiConfig $mwc ) { + $mwc + ->conf( 'wgCentralAuthSharedDomainCallback', fn ( $dbname ) => $this->getAuthDomain( $mwc, $dbname ) ) + ->conf( 'wgCentralAuthEnableSul3', true ) + ->conf( 'wgServer', WebRequest::detectServer( true ) ) + ->cloneConf( 'wgCanonicalServer', 'wgServer' ); + + if ( $mwc->getConf( 'wgServer' ) === $this->getAuthDomain( $mwc, null ) ) { + $dbName = $mwc->getConf( 'wgDBname' ); + $mwc + ->conf( 'wgEnableSidebarCache', false ) + ->conf( 'wgCentralAuthCookieDomain', '' ) + ->conf( 'wgCookiePrefix', 'auth' ) + ->conf( 'wgSessionName', 'authSession' ) + ->conf( 'wgScriptPath', "/$dbName/w" ) + ->conf( 'wgLoadScript', $mwc->getConf( 'wgScriptPath' ) . '/load.php' ) + ->conf( 'wgCanonicalServer', $this->getAuthDomain( $mwc, null ) ) + ->conf( 'wgScript', $mwc->getConf( 'wgScriptPath' ) . '/index.php' ) + ->conf( 'wgResourceBasePath', "/$dbName/w" ) + ->conf( 'wgExtensionAssetsPath', $mwc->getConf( 'wgResourceBasePath' ) . '/extensions' ) + ->conf( 'wgStylePath', $mwc->getConf( 'wgResourceBasePath' ) . '/skins' ) + ->cloneConf( 'wgLocalStylePath', 'wgStylePath' ) + ->conf( 'wgArticlePath', "/$dbName/wiki/\$1" ) + ->conf( 'wgServer', $this->getAuthDomain( $mwc, null ) ); + } + } + + /** @inheritDoc */ + public function overrideWikiExists( MWCFarm $farm, MediaWikiConfig $mwc, string $subdomain ): ?string { + if ( $subdomain !== 'auth' ) { + return null; + } + // Taken from https://github.com/miraheze/mw-config/blob/main/initialise/MirahezeFunctions.php#L287-L298 + $requestUri = $_SERVER['REQUEST_URI']; + $pathBits = explode( '/', $requestUri, 3 ); + if ( count( $pathBits ) < 3 ) { + trigger_error( + "Invalid request URI (requestUri=$requestUri), can't determine wiki.\n", + E_USER_ERROR + ); + } + return $pathBits[1]; + } + + private function getAuthDomain( MediaWikiConfig $mwc, ?string $dbName ): string { + $port = $mwc->env( 'MW_DOCKER_PORT' ); + return "http://auth.localhost:$port" . ( $dbName ? "/$dbName" : '' ); + } + +} diff --git a/config/Farm/IFarmUserManagement.php b/config/Farm/IFarmUserManagement.php new file mode 100644 index 0000000..e15b280 --- /dev/null +++ b/config/Farm/IFarmUserManagement.php @@ -0,0 +1,20 @@ + $wikis * @param array $settings - * @param string $defaultWiki The wiki that will be used for maintenance scripts by default + * @param string $centralWiki The central wiki (will be used for maintenance scripts by default) + * @param int $userManagementType One of the USER_MANAGEMENT_ constants */ public function __construct( private readonly array $wikis, private array $settings, - private readonly string $defaultWiki, + private readonly string $centralWiki, + private readonly int $userManagementType, ) { + require_once 'IFarmUserManagement.php'; + switch ( $this->userManagementType ) { + case self::USER_MANAGEMENT_STANDALONE: + require_once 'StandaloneUserManagement.php'; + $this->userManagement = new StandaloneUserManagement(); + break; + case self::USER_MANAGEMENT_CENTRAL_AUTH: + require_once 'CentralAuthUserManagement.php'; + $this->userManagement = new CentralAuthUserManagement(); + break; + } } public function apply( MediaWikiConfig $mwc ): void { @@ -26,17 +45,23 @@ public function apply( MediaWikiConfig $mwc ): void { $serverVals[$dbname] = "http://$subdomain.localhost:$port"; } $this->settings['wgServer'] = $serverVals; + $this->settings['wgArticlePath'] = [ + 'default' => $mwc->getConf( 'wgArticlePath' ), + ]; if ( defined( 'MW_DB' ) ) { $wikiId = MW_DB; } elseif ( MW_ENTRY_POINT === 'cli' ) { - $wikiId = $this->defaultWiki; + $wikiId = $this->centralWiki; } else { $subdomain = explode( '.', $_SERVER['SERVER_NAME'] )[0]; - if ( !array_key_exists( $subdomain, $this->wikis ) ) { - $this->showWikiMap(); - } else { - $wikiId = $this->wikis[$subdomain]; + $wikiId = $this->userManagement->overrideWikiExists( $this, $mwc, $subdomain ); + if ( $wikiId === null ) { + if ( !array_key_exists( $subdomain, $this->wikis ) ) { + $this->showWikiMap(); + } else { + $wikiId = $this->wikis[$subdomain]; + } } } @@ -53,6 +78,9 @@ public function apply( MediaWikiConfig $mwc ): void { } $mwc->conf( 'wgConf', $siteConfiguration ); + + // Setup user management after config + $this->userManagement->setup( $this, $mwc ); } private function showWikiMap(): never { @@ -67,4 +95,12 @@ public function getWikis(): array { return $this->wikis; } + public function getUserManagement(): IFarmUserManagement { + return $this->userManagement; + } + + public function getCentralWiki(): string { + return $this->centralWiki; + } + } diff --git a/config/Farm/StandaloneUserManagement.php b/config/Farm/StandaloneUserManagement.php new file mode 100644 index 0000000..74b9cb6 --- /dev/null +++ b/config/Farm/StandaloneUserManagement.php @@ -0,0 +1,22 @@ +ext( 'Analytics' ); } + public function AntiSpoof(): self { + return $this->ext( 'AntiSpoof' ); + } + public function ApprovedRevs(): self { return $this->ext( 'ApprovedRevs' ); } @@ -73,6 +77,10 @@ public function AutoCreatePage(): self { return $this->ext( 'AutoCreatePage' ); } + public function BetaFeatures(): self { + return $this->ext( 'BetaFeatures' ); + } + public function Bootstrap(): self { return $this->ext( 'Bootstrap' ); } @@ -100,6 +108,19 @@ public function Cargo(): self { return $this->ext( 'Cargo' ); } + public function CentralAuth( + string $centralWiki, + ): self { + global $wgMwcFarm; + if ( !$wgMwcFarm ) { + throw new Exception( 'Please call setupFarm() before using ' . __METHOD__ . '!' ); + } + // Call wfLoadExtension *before* setting the virtual domain mapping and similar settings + $this->ext( 'CentralAuth' ); + return $this + ->virtualDomainMapping( 'virtual-centralauth', $centralWiki ); + } + public function CentralNotice(): self { return $this ->ext( 'CentralNotice' ) @@ -153,6 +174,10 @@ public function Cite(): self { return $this->ext( 'Cite' ); } + public function CodeEditor(): self { + return $this->ext( 'CodeEditor' ); + } + public function CodeMirror( CodeMirrorVersion $version = CodeMirrorVersion::V6 ): self { if ( $version === CodeMirrorVersion::V6 ) { $this->conf( 'wgCodeMirrorV6', true ); @@ -342,6 +367,10 @@ public function Gadgets(): self { } public function GlobalUserPage( string $apiUrl ): self { + $farm = $this->getFarm(); + if ( $farm ) { + $this->conf( 'wgGlobalUserPageDBname', $farm->getCentralWiki() ); + } return $this ->ext( 'GlobalUserPage' ) ->conf( 'wgGlobalUserPageAPIUrl', $apiUrl ); @@ -351,6 +380,10 @@ public function GlobalUserrights(): self { return $this->ext( 'GlobalUserrights' ); } + public function GlobalWatchlist(): self { + return $this->ext( 'GlobalWatchlist' ); + } + public function GrowthExperiments(): self { // https://www.mediawiki.org/wiki/Extension:GrowthExperiments/developer_setup return $this @@ -443,6 +476,10 @@ public function Lockdown(): self { return $this->ext( 'Lockdown' ); } + public function Loops(): self { + return $this->ext( 'Loops' ); + } + public function Maps(): self { return $this->ext( 'Maps' ); } @@ -453,6 +490,11 @@ public function MassEditRegex(): self { ->grantPermission( 'sysop', 'masseditregex' ); } + public function MediaSearch(): self { + // TODO are there any dependencies or settings? + return $this->ext( 'MediaSearch' ); + } + public function MediaUploader(): self { return $this->ext( 'MediaUploader' ); } @@ -611,6 +653,30 @@ public function ProtectSite(): self { return $this->ext( 'ProtectSite' ); } + public function QuickInstantCommons( ?string $apiUrl = null, ?string $name = null ): self { + if ( $apiUrl !== null ) { + // See https://www.mediawiki.org/wiki/Extension:QuickInstantCommons#Advanced_Configuration + $this->appendToIndexedConfArray( 'wgForeignFileRepos', [ + 'class' => '\MediaWiki\Extension\QuickInstantCommons\Repo', + 'name' => $name ?? 'externalrepowiki', + 'directory' => $this->getConf( 'wgUploadDirectory' ), + 'apibase' => $apiUrl, + 'hashLevels' => 2, + 'thumbUrl' => false, + 'fetchDescription' => true, + 'descriptionCacheExpiry' => 43200, + 'transformVia404' => false, + 'abbrvThreshold' => 160, + 'apiMetadataExpiry' => 60 * 60 * 24, + 'disabledMediaHandlers' => [], + ] ); + } + return $this + ->ext( 'QuickInstantCommons' ) + ->conf( 'wgUseInstantCommons', false ) + ->conf( 'wgUseQuickInstantCommons', $apiUrl === null ); + } + public function QuizGame(): self { return $this ->SocialProfile() @@ -621,6 +687,10 @@ public function RandomImageByCategory(): self { return $this->ext( 'RandomImageByCategory' ); } + public function RandomSelection(): self { + return $this->ext( 'RandomSelection' ); + } + public function RatePage(): self { return $this->ext( 'RatePage' ); } @@ -888,6 +958,17 @@ public function UserVerification(): self { return $this->ext( 'UserVerification' ); } + public function Variables(): self { + return $this->ext( 'Variables' ); + } + + public function VariablesLua(): self { + return $this + ->Scribunto() + ->Variables() + ->ext( 'VariablesLua' ); + } + public function Video(): self { return $this->ext( 'Video' ); } @@ -953,7 +1034,7 @@ public function WikiCategoryTagCloud(): self { public function WikiEditor(): self { return $this ->ext( 'WikiEditor' ) - ->modConf( 'wgHiddenPrefs', static fn ( &$c ) => $c[] = 'usebetatoolbar' ) + ->appendToIndexedConfArray( 'wgHiddenPrefs', 'usebetatoolbar' ) ->defaultUserOption( 'usebetatoolbar', 1 ); } diff --git a/config/MWCFunctions.php b/config/MWCFunctions.php index 71846dd..0d397e9 100644 --- a/config/MWCFunctions.php +++ b/config/MWCFunctions.php @@ -85,6 +85,10 @@ public function modConf( string $name, callable $modify, mixed $defaultIfNotSet return $this->conf( $name, $val ); } + public function appendToIndexedConfArray( string $name, mixed $value ): self { + return $this->modConf( $name, static fn ( &$c ) => $c[] = $value ); + } + /** * @param string $name * // phpcs:ignore MediaWiki.Commenting.FunctionComment.ObjectTypeHintParam @@ -139,6 +143,14 @@ public function defaultUserOption( string $option, mixed $value ): self { } ); } + public function virtualDomainMapping( string $virtual, string $db, ?string $cluster = null ): self { + $mapping = [ 'db' => $db ]; + if ( $cluster !== null ) { + $mapping['cluster'] = $cluster; + } + return $this->modConf( 'wgVirtualDomainsMapping', static fn ( &$c ) => $c[$virtual] = $mapping ); + } + public function env( string $key, string $default = '' ): string { global $wgMwcEnv; // integration test fix @@ -161,4 +173,16 @@ public function onBeforePageDisplay( $out, $skin ): void { } ); } + public function customProtectionLevel( string $name, bool $cascading, ?string $grantTo = null ): self { + if ( $cascading ) { + $this->appendToIndexedConfArray( 'wgCascadingRestrictionLevels', $name ); + } + if ( $grantTo !== null ) { + $this->grantPermission( $grantTo, $name ); + } + return $this + ->appendToIndexedConfArray( 'wgRestrictionLevels', $name ) + ->appendToIndexedConfArray( 'wgAvailableRights', $name ); + } + } diff --git a/docker-compose.yml b/docker-compose.yml index f70c9ba..0282785 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: mediawiki: - image: docker-registry.wikimedia.org/dev/bookworm-php83-fpm:1.0.0 + image: docker-registry.wikimedia.org/dev/bookworm-php83-fpm:1.0.0-s1 user: "${MW_DOCKER_UID}:${MW_DOCKER_GID}" extra_hosts: - "host.docker.internal:host-gateway" @@ -28,7 +28,7 @@ services: env_file: config/.env networks: [ mw ] mediawiki-jobrunner: - image: docker-registry.wikimedia.org/dev/bookworm-php83-jobrunner:1.0.0 + image: docker-registry.wikimedia.org/dev/bookworm-php83-jobrunner:1.0.0-s1 user: "${MW_DOCKER_UID}:${MW_DOCKER_GID}" volumes: - "./core:${MW_INSTALL_PATH}:cached" diff --git a/features/dockerfiles/apache.Dockerfile b/features/dockerfiles/apache.Dockerfile new file mode 100644 index 0000000..df6e699 --- /dev/null +++ b/features/dockerfiles/apache.Dockerfile @@ -0,0 +1,10 @@ +# Important: Make sure the version here matches the latest version of the mediawiki-web image in docker-compose.yml +FROM docker-registry.wikimedia.org/dev/bookworm-apache2:1.0.1 + +# Append SUL3 load.php rewrite first, then generic wiki index rewrite, after "RewriteEngine On" +RUN grep -q "wiki rewrite rules" /etc/apache2/sites-available/000-default.conf || \ + sed -i '/RewriteEngine On/a \ + # wiki rewrite rules\n\ + RewriteRule ^/[^/]+wiki/w/load\.php$ /w/load.php [L]\n\ + RewriteRule ^/[^/]+wiki(/.*)?$ %{DOCUMENT_ROOT}/w/index.php [L]' \ + /etc/apache2/sites-available/000-default.conf diff --git a/features/luasandbox/luasandbox.Dockerfile b/features/dockerfiles/fpm.Dockerfile similarity index 55% rename from features/luasandbox/luasandbox.Dockerfile rename to features/dockerfiles/fpm.Dockerfile index e651996..7354891 100644 --- a/features/luasandbox/luasandbox.Dockerfile +++ b/features/dockerfiles/fpm.Dockerfile @@ -1,11 +1,18 @@ -FROM docker-registry.wikimedia.org/dev/bookworm-php83-fpm:1.0.0 AS build +FROM docker-registry.wikimedia.org/dev/bookworm-php83-fpm:1.0.0-s1 AS build +# Compile LuaSandbox from source WORKDIR /src RUN git clone https://gerrit.wikimedia.org/r/mediawiki/php/luasandbox RUN apt update && apt install php8.3-dev liblua5.1-0-dev -y RUN cd luasandbox && phpize && ./configure && make -FROM docker-registry.wikimedia.org/dev/bookworm-php83-fpm:1.0.0 +FROM docker-registry.wikimedia.org/dev/bookworm-php83-fpm:1.0.0-s1 +# Install LuaSandbox +RUN apt update && apt install liblua5.1-0 -y COPY --from=build /src/luasandbox/modules/luasandbox.so /usr/lib/php/20230831/luasandbox.so RUN echo 'extension=luasandbox.so' > /etc/php/8.3/mods-available/luasandbox.ini && phpenmod luasandbox + +# Install Excimer +RUN apt-get update && \ + apt-get install php8.3-excimer diff --git a/features/dockerfiles/jobrunner.Dockerfile b/features/dockerfiles/jobrunner.Dockerfile new file mode 100644 index 0000000..4b6e809 --- /dev/null +++ b/features/dockerfiles/jobrunner.Dockerfile @@ -0,0 +1,18 @@ +FROM docker-registry.wikimedia.org/dev/bookworm-php83-jobrunner:1.0.0-s1 as build + +# Compile LuaSandbox from source +WORKDIR /src +RUN git clone https://gerrit.wikimedia.org/r/mediawiki/php/luasandbox +RUN apt update && apt install php8.3-dev liblua5.1-0-dev -y +RUN cd luasandbox && phpize && ./configure && make + +FROM docker-registry.wikimedia.org/dev/bookworm-php83-jobrunner:1.0.0-s1 + +# Install LuaSandbox +RUN apt update && apt install liblua5.1-0 -y +COPY --from=build /src/luasandbox/modules/luasandbox.so /usr/lib/php/20230831/luasandbox.so +RUN echo 'extension=luasandbox.so' > /etc/php/8.3/mods-available/luasandbox.ini && phpenmod luasandbox + +# Install Excimer +RUN apt-get update && \ + apt-get install php8.3-excimer diff --git a/features/luasandbox/luasandbox.md b/features/luasandbox/luasandbox.md deleted file mode 100644 index 961ee12..0000000 --- a/features/luasandbox/luasandbox.md +++ /dev/null @@ -1,23 +0,0 @@ -# LuaSandbox - -docker-compose.override.yml: - -```yml -services: - mediawiki: - build: - dockerfile: features/luasandbox/luasandbox.Dockerfile - context: . -``` - -LocalSettings.php: -```php -$c->Scribunto( $c::SCRIBUNTO_ENGINE_LUASANDBOX ); -``` - -Then run the following commands: - -```shell -docker compose --env-file config/.env -p main build mediawiki -mwutil recreate -``` diff --git a/features/profiling/profiling.Dockerfile b/features/profiling/profiling.Dockerfile deleted file mode 100644 index a232501..0000000 --- a/features/profiling/profiling.Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM docker-registry.wikimedia.org/dev/bookworm-php83-fpm:1.0.0 - -RUN apt-get update && \ - apt-get install php8.3-excimer diff --git a/features/profiling/profiling.md b/features/profiling/profiling.md deleted file mode 100644 index b6e7ad6..0000000 --- a/features/profiling/profiling.md +++ /dev/null @@ -1,24 +0,0 @@ -# Profiling - -docker-compose.override.yml: - -```yml -services: - mediawiki: - build: - dockerfile: features/profiling/profiling.Dockerfile - context: . -``` - -LocalSettings.php: -```php -MediaWikiConfig::getInstance()->enableTraceLogging(); -``` - -Then run the following commands: - -```shell -docker compose --env-file config/.env -p main build mediawiki -mwutil recreate -mwutil profiling watch -```