Hoo man has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/225474

Change subject: Introduce a RestrictedEntityLookup for client DataAccess
......................................................................

Introduce a RestrictedEntityLookup for client DataAccess

Which has the ability to throw an Exception in case a given
limit is reached.
That is not yet being made use of, but we already report how
many entities have been loaded in the parser limit report.

Bug: T93885
Change-Id: I6a2bb05954aba33cd4dad278e61c981425d207f0
---
M client/WikibaseClient.php
M client/i18n/en.json
M client/i18n/qqq.json
A client/includes/DataAccess/EntityAccessLimitException.php
A client/includes/DataAccess/RestrictedEntityLookup.php
M client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseEntityLibrary.php
M client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseLibrary.php
A client/includes/Hooks/ParserLimitHookHandlers.php
M client/includes/WikibaseClient.php
A client/tests/phpunit/includes/DataAccess/RestrictedEntityLookupTest.php
A client/tests/phpunit/includes/Hooks/ParserLimitHookHandlersTest.php
M client/tests/phpunit/includes/WikibaseClientTest.php
12 files changed, 452 insertions(+), 3 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Wikibase 
refs/changes/74/225474/1

diff --git a/client/WikibaseClient.php b/client/WikibaseClient.php
index 43e2d2b..dbfc2bf 100644
--- a/client/WikibaseClient.php
+++ b/client/WikibaseClient.php
@@ -114,6 +114,8 @@
        $wgHooks['GetBetaFeaturePreferences'][] = 
'\Wikibase\ClientHooks::onGetBetaFeaturePreferences';
        $wgHooks['ArticleDeleteComplete'][] = 
'\Wikibase\Client\Hooks\UpdateRepoHookHandlers::onArticleDeleteComplete';
        $wgHooks['ArticleDeleteAfterSuccess'][] = 
'\Wikibase\ClientHooks::onArticleDeleteAfterSuccess';
+       $wgHooks['ParserLimitReportFormat'][] = 
'\Wikibase\Client\Hooks\ParserLimitHookHandlers::onParserLimitReportFormat';
+       $wgHooks['ParserLimitReportPrepare'][] = 
'\Wikibase\Client\Hooks\ParserLimitHookHandlers::onParserLimitReportPrepare';
 
        // update hooks
        $wgHooks['LoadExtensionSchemaUpdates'][] = 
'\Wikibase\Client\Usage\Sql\SqlUsageTrackerSchemaUpdater::onSchemaUpdate';
diff --git a/client/i18n/en.json b/client/i18n/en.json
index 4daec07..68c261b 100644
--- a/client/i18n/en.json
+++ b/client/i18n/en.json
@@ -50,6 +50,7 @@
        "wikibase-linkitem-not-loggedin-title": "You need to be logged in",
        "wikibase-linkitem-not-loggedin": "You need to be logged in on this 
wiki and in the [$1 central data repository] to use this feature.",
        "wikibase-linkitem-success-link": "The pages have successfully been 
linked. You can find the item containing the links in our [$1 central data 
repository].",
+       "wikibase-limitreport-entities-accessed": "Number of Wikibase entities 
loaded",
        "wikibase-property-notfound": "$1 property not found.",
        "wikibase-rc-hide-wikidata": "$1 {{WBREPONAME}}",
        "wikibase-rc-hide-wikidata-hide": "Hide",
diff --git a/client/i18n/qqq.json b/client/i18n/qqq.json
index d590f37..558942c 100644
--- a/client/i18n/qqq.json
+++ b/client/i18n/qqq.json
@@ -61,6 +61,7 @@
        "wikibase-linkitem-not-loggedin-title": "Title of the dialog telling 
the user that he needs to login on both the repo and client to use this 
feature.",
        "wikibase-linkitem-not-loggedin": "This messages informs the user that 
he needs to be logged in on both this wiki and the repository to use this 
feature.\n\nParameters:\n* $1 - the URI to the login form of the repository",
        "wikibase-linkitem-success-link": "Success message after the page the 
user currently is on has been linked with an item. $1 holds a URL pointing to 
the item.",
+       "wikibase-limitreport-entities-accessed": "Value shown in the Parser 
limit report telling the user how many Wikibase entities have been loaded 
during the parse of the current page.",
        "wikibase-property-notfound": "Message for property parser function 
when a property is not found. Parameters:\n* $1 - the name of the property",
        "wikibase-rc-hide-wikidata": "This refers to a toggle to hide or show 
edits (revisions) that come from Wikidata. If set to \"hide\", it hides edits 
made to the connected item in the Wikidata repository.\n\nParameters:\n* $1 - a 
link with the text {{msg-mw|wikibase-rc-hide-wikidata-show}} or 
{{msg-mw|wikibase-rc-hide-wikidata-hide}}\n* <nowiki>{{WBREPONAME}}</nowiki> - 
expanded to {{msg-mw|wikibase-repo-name}}",
        "wikibase-rc-hide-wikidata-hide": "{{doc-actionlink}}\nOption text in 
[[Special:RecentChanges]] in conjunction with 
{{msg-mw|wikibase-rc-hide-wikidata}}.\n\nSee also:\n* 
{{msg-mw|wikibase-rc-hide-wikidata-show}}\n{{Identical|Hide}}",
diff --git a/client/includes/DataAccess/EntityAccessLimitException.php 
b/client/includes/DataAccess/EntityAccessLimitException.php
new file mode 100644
index 0000000..8f371e8
--- /dev/null
+++ b/client/includes/DataAccess/EntityAccessLimitException.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Wikibase\Client\DataAccess;
+use Exception;
+
+/**
+ * @license GPL 2+
+ * @author Marius Hoch < [email protected] >
+ */
+class EntityAccessLimitException extends Exception {
+
+}
diff --git a/client/includes/DataAccess/RestrictedEntityLookup.php 
b/client/includes/DataAccess/RestrictedEntityLookup.php
new file mode 100644
index 0000000..03d67ab
--- /dev/null
+++ b/client/includes/DataAccess/RestrictedEntityLookup.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Wikibase\Client\DataAccess;
+
+use Wikimedia\Assert\Assert;
+use Wikibase\DataModel\Entity\EntityId;
+use Wikibase\Lib\Store\EntityLookup;
+
+/**
+ * EntityLookup that counts how many entities have been loaded through it and 
throws
+ * an exception once to many entities have been loaded.
+ *
+ * This is needed to limit the number of entities that can be loaded via some
+ * user controlled features, like entity access in Lua.
+ *
+ * @since 0.5
+ *
+ * @license GNU GPL v2+
+ * @author Marius Hoch < [email protected] >
+ */
+class RestrictedEntityLookup implements EntityLookup {
+
+       /**
+        * @var EntityLookup
+        */
+       private $entityLookup;
+
+       /**
+        * @var int
+        */
+       private $entityAccessLimit;
+
+       /**
+        * @var bool[] Entity id serialization => bool
+        */
+       private $entitiesAccessed = array();
+
+       /**
+        * @var int
+        */
+       private $entityAccessCount = 0;
+
+       /**
+        * @param EntityLookup $entityLookup
+        * @param int $entityAccessLimit
+        */
+       public function __construct( EntityLookup $entityLookup, 
$entityAccessLimit ) {
+               Assert::parameterType( 'integer', $entityAccessLimit, 
'$entityAccessLimit' );
+
+               $this->entityLookup = $entityLookup;
+               $this->entityAccessLimit = $entityAccessLimit;
+       }
+
+       /**
+        * @see EntityLookup::getEntity
+        *
+        * @param EntityId $entityId
+        *
+        * @throws StorageException|EntityAccessLimitException
+        * @return EntityDocument|null
+        */
+       public function getEntity( EntityId $entityId ) {
+               $entityIdSerialization = $entityId->getSerialization();
+
+               if ( !array_key_exists( $entityIdSerialization, 
$this->entitiesAccessed ) ) {
+                       $this->entityAccessCount++;
+                       $this->entitiesAccessed[$entityIdSerialization] = true;
+               }
+
+               if ( $this->entityAccessCount >= $this->entityAccessLimit ) {
+                       throw new EntityAccessLimitException(
+                               'To many entities loaded, must not load more 
than ' . $this->entityAccessLimit . ' entities.'
+                       );
+               }
+
+               return $this->entityLookup->getEntity( $entityId );
+       }
+
+       /**
+        * @see EntityLookup::hasEntity
+        *
+        * @since 0.4
+        *
+        * @param EntityId $entityId
+        *
+        * @throws StorageException
+        * @return bool
+        */
+       public function hasEntity( EntityId $entityId ) {
+               return $this->entityLookup->hasEntity( $entityId );
+       }
+
+       /**
+        * Returns the number of entities already loaded via this object.
+        *
+        * @return int
+        */
+       public function getEntityAccessCount() {
+               return $this->entityAccessCount;
+       }
+}
diff --git 
a/client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseEntityLibrary.php 
b/client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseEntityLibrary.php
index 443d9c8..e7be173 100644
--- 
a/client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseEntityLibrary.php
+++ 
b/client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseEntityLibrary.php
@@ -63,7 +63,7 @@
                        $languageFallbackChain->getFetchLanguageCodes()
                );
 
-               $entityLookup = $wikibaseClient->getStore()->getEntityLookup();
+               $entityLookup = $wikibaseClient->getRestrictedEntityLookup();
 
                $propertyIdResolver = new PropertyIdResolver(
                        $entityLookup,
diff --git 
a/client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseLibrary.php 
b/client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseLibrary.php
index 73b768e..2559b9a 100644
--- a/client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseLibrary.php
+++ b/client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseLibrary.php
@@ -153,7 +153,7 @@
 
                return new EntityAccessor(
                        $wikibaseClient->getEntityIdParser(),
-                       $wikibaseClient->getStore()->getEntityLookup(),
+                       $wikibaseClient->getRestrictedEntityLookup(),
                        $this->getUsageAccumulator(),
                        $wikibaseClient->getPropertyDataTypeLookup(),
                        $this->getLanguageFallbackChain(),
diff --git a/client/includes/Hooks/ParserLimitHookHandlers.php 
b/client/includes/Hooks/ParserLimitHookHandlers.php
new file mode 100644
index 0000000..34110a8
--- /dev/null
+++ b/client/includes/Hooks/ParserLimitHookHandlers.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Wikibase\Client\Hooks;
+
+use Html;
+use Language;
+use Parser;
+use ParserOutput;
+use Wikibase\Client\WikibaseClient;
+use Wikibase\Client\DataAccess\RestrictedEntityLookup;
+
+/**
+ * @since 0.5.
+ *
+ * @license GPL 2+
+ * @author Marius Hoch < [email protected] >
+ */
+class ParserLimitHookHandlers {
+
+       /**
+        * @var RestrictedEntityLookup
+        */
+       private $restrictedEntityLookup;
+
+       /**
+        * @var Language
+        */
+       private $language;
+
+       /**
+        * @param RestrictedEntityLookup $restrictedEntityLookup
+        * @param Language $language
+        */
+       public function __construct( RestrictedEntityLookup 
$restrictedEntityLookup, Language $language ) {
+               $this->restrictedEntityLookup = $restrictedEntityLookup;
+               $this->language = $language;
+       }
+
+       /**
+        * @return ParserLimitHookHandlers
+        */
+       public static function newFromGlobalState() {
+               global $wgLang;
+
+               $wikibaseClient = WikibaseClient::getDefaultInstance();
+
+               return new self(
+                       $wikibaseClient->getRestrictedEntityLookup(),
+                       $wgLang
+               );
+       }
+
+       /**
+        * @param Parser $parser
+        * @param ParserOutput $output
+        *
+        * @return bool
+        */
+       public static function onParserLimitReportPrepare( Parser $parser, 
ParserOutput $output ) {
+               $handler = self::newFromGlobalState();
+
+               return $handler->doParserLimitReportPrepare( $parser, $output );
+       }
+
+       /**
+        * @param string $key
+        * @param string &$value
+        * @param string &$report
+        * @param bool $isHTML
+        * @param bool $localize
+        *
+        * @return bool
+        */
+       public static function onParserLimitReportFormat( $key, &$value, 
&$report, $isHTML, $localize ) {
+               $handler = self::newFromGlobalState();
+
+               return $handler->doParserLimitReportFormat( $key, $value, 
$report, $isHTML, $localize );
+       }
+
+       /**
+        * @param string $key
+        * @param string &$value
+        * @param string &$report
+        * @param bool $isHTML
+        * @param bool $localize
+        *
+        * @return bool
+        */
+       public function doParserLimitReportFormat( $key, &$value, &$report, 
$isHTML, $localize ) {
+               if ( $key !== 'EntityAccessCount' ) {
+                       return true;
+               }
+
+               $language = $localize ? $this->language : Language::factory( 
'en' );
+               $label = wfMessage( 'wikibase-limitreport-entities-accessed' 
)->inLanguage( $language )->text();
+
+               if ( $isHTML ) {
+                       $report .= Html::openElement( 'tr' ) .
+                       Html::element( 'th', array(), $label ) .
+                       Html::element( 'td', array(), $value ) .
+                       Html::closeElement( 'tr' );
+               } else {
+                       $report .= $label . wfMessage( 'colon-separator' 
)->inLanguage( $language )->text() . $value;
+               }
+
+               return true;
+       }
+
+       /**
+        * @param Parser $parser
+        * @param ParserOutput $output
+        *
+        * @return bool
+        */
+       public function doParserLimitReportPrepare( Parser $parser, 
ParserOutput $output ) {
+               $output->setLimitReportData(
+                       'EntityAccessCount',
+                       $this->restrictedEntityLookup->getEntityAccessCount()
+               );
+
+               return true;
+       }
+}
diff --git a/client/includes/WikibaseClient.php 
b/client/includes/WikibaseClient.php
index 1f3359e..9a6987e 100644
--- a/client/includes/WikibaseClient.php
+++ b/client/includes/WikibaseClient.php
@@ -18,6 +18,7 @@
 use Wikibase\Client\Changes\ChangeHandler;
 use Wikibase\Client\Changes\ChangeRunCoalescer;
 use Wikibase\Client\Changes\WikiPageUpdater;
+use Wikibase\Client\DataAccess\RestrictedEntityLookup;
 use Wikibase\Client\Hooks\LanguageLinkBadgeDisplay;
 use Wikibase\Client\Hooks\OtherProjectsSidebarGeneratorFactory;
 use Wikibase\Client\Hooks\ParserFunctionRegistrant;
@@ -151,6 +152,11 @@
         * @var NamespaceChecker|null
         */
        private $namespaceChecker = null;
+
+       /**
+        * @var RestrictedEntityLookup|null
+        */
+       private $restrictedEntityLookup = null;
 
        /**
         * @since 0.4
@@ -729,7 +735,7 @@
         * @return PropertyClaimsRendererFactory
         */
        private function getPropertyClaimsRendererFactory() {
-               $entityLookup = $this->getEntityLookup();
+               $entityLookup = $this->getRestrictedEntityLookup();
 
                $propertyIdResolver = new PropertyIdResolver(
                        $entityLookup,
@@ -827,4 +833,17 @@
                );
        }
 
+       /**
+        * @return RestrictedEntityLookup
+        */
+       public function getRestrictedEntityLookup() {
+               if ( $this->restrictedEntityLookup === null ) {
+                       $this->restrictedEntityLookup = new 
RestrictedEntityLookup(
+                               $this->getEntityLookup(),
+                               PHP_INT_MAX // Don't throw any exceptions, yet
+                       );
+               }
+
+               return $this->restrictedEntityLookup;
+       }
 }
diff --git 
a/client/tests/phpunit/includes/DataAccess/RestrictedEntityLookupTest.php 
b/client/tests/phpunit/includes/DataAccess/RestrictedEntityLookupTest.php
new file mode 100644
index 0000000..6c6a120
--- /dev/null
+++ b/client/tests/phpunit/includes/DataAccess/RestrictedEntityLookupTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Wikibase\Client\Tests\DataAccess;
+
+use Wikibase\Client\DataAccess\RestrictedEntityLookup;
+use Wikibase\DataModel\Entity\EntityId;
+use Wikibase\DataModel\Entity\ItemId;
+
+/**
+ * @covers Wikibase\Client\DataAccess\RestrictedEntityLookup
+ *
+ * @group Wikibase
+ * @group WikibaseClient
+ * @group WikibaseDataAccess
+ *
+ * @licence GNU GPL v2+
+ * @author Marius Hoch
+ */
+class RestrictedEntityLookupTest extends \PHPUnit_Framework_TestCase {
+
+       private function getEntityLookup() {
+               $entityLookup = $this->getMock( 
'Wikibase\Lib\Store\EntityLookup' );
+
+               $entityLookup->expects( $this->any() )
+                       ->method( 'hasEntity' )
+                       ->will( $this->returnValue( true ) );
+
+               $entityLookup->expects( $this->any() )
+                       ->method( 'getEntity' )
+                       ->will( $this->returnCallback( function( EntityId 
$entityId ) {
+                               return $entityId->getSerialization();
+                       } ) );
+
+               return $entityLookup;
+       }
+
+       public function testHasEntity() {
+               $lookup = new RestrictedEntityLookup( $this->getEntityLookup(), 
200 );
+
+               $this->assertTrue( $lookup->hasEntity( new ItemId( 'Q22' ) ) );
+       }
+
+       public function testGetEntityAccessCount() {
+               $lookup = new RestrictedEntityLookup( $this->getEntityLookup(), 
200 );
+
+               for ( $i = 1; $i < 6; $i++ ) {
+                       $lookup->getEntity( new ItemId( 'Q' . $i ) );
+               }
+               $lookup->getEntity( new ItemId( 'Q3' ) ); // Q3 has already 
been loaded, thus doesn't count
+
+               $this->assertSame( 5, $lookup->getEntityAccessCount() );
+       }
+
+       public function testGetEntity() {
+               $lookup = new RestrictedEntityLookup( $this->getEntityLookup(), 
200 );
+
+               for ( $i = 1; $i < 6; $i++ ) {
+                       $this->assertSame(
+                               'Q' . $i,
+                               $lookup->getEntity( new ItemId( 'Q' . $i ) )
+                       );
+               }
+       }
+
+       /**
+        * @expectedException 
Wikibase\Client\DataAccess\EntityAccessLimitException
+        */
+       public function testGetEntity_exception() {
+               $lookup = new RestrictedEntityLookup( $this->getEntityLookup(), 
3 );
+
+               for ( $i = 1; $i < 6; $i++ ) {
+                       $lookup->getEntity( new ItemId( 'Q' . $i ) );
+               }
+       }
+
+}
diff --git 
a/client/tests/phpunit/includes/Hooks/ParserLimitHookHandlersTest.php 
b/client/tests/phpunit/includes/Hooks/ParserLimitHookHandlersTest.php
new file mode 100644
index 0000000..232a5dc
--- /dev/null
+++ b/client/tests/phpunit/includes/Hooks/ParserLimitHookHandlersTest.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace Wikibase\Client\Tests\Hooks;
+
+use Language;
+use ParserOutput;
+use Wikibase\Client\Hooks\ParserLimitHookHandlers;
+
+/**
+ * @covers Wikibase\Client\Hooks\ParserLimitHookHandlersTest
+ *
+ * @group WikibaseClient
+ * @group Wikibase
+ * @group WikibaseHooks
+ *
+ * @license GNU GPL v2+
+ * @author Marius Hoch
+ */
+class ParserLimitHookHandlersTest extends \PHPUnit_Framework_TestCase {
+
+       public function testDoParserLimitReportPrepare() {
+               $restrictedEntityLookup = $this->getMockBuilder( 
'Wikibase\Client\DataAccess\RestrictedEntityLookup' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $restrictedEntityLookup->expects( $this->once() )
+                       ->method( 'getEntityAccessCount' )
+                       ->will( $this->returnValue( 42 ) );
+
+               $handler = new ParserLimitHookHandlers(
+                       $restrictedEntityLookup,
+                       Language::factory( 'en' )
+               );
+
+               $parserOutput = new ParserOutput();
+
+               $handler->doParserLimitReportPrepare(
+                       $this->getMock( 'Parser' ),
+                       $parserOutput
+               );
+
+               $limitReportData = $parserOutput->getLimitReportData();
+
+               $this->assertSame( 42, $limitReportData['EntityAccessCount'] );
+       }
+
+       /**
+        * @dataProvider doParserLimitReportFormatProvider
+        */
+       public function testDoParserLimitReportFormat( $expected, Language 
$language, $isHTML, $localize ) {
+               $restrictedEntityLookup = $this->getMockBuilder( 
'Wikibase\Client\DataAccess\RestrictedEntityLookup' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $handler = new ParserLimitHookHandlers(
+                       $restrictedEntityLookup,
+                       $language
+               );
+
+               $value = 123;
+               $result = '';
+
+               $handler->doParserLimitReportFormat(
+                       'EntityAccessCount',
+                       $value,
+                       $result,
+                       $isHTML,
+                       $localize
+               );
+
+               $this->assertSame( $expected, $result );
+       }
+
+       public function doParserLimitReportFormatProvider() {
+               $languageRu = Language::factory( 'ru' );
+               $languageEn = Language::factory( 'en' );
+               $labelRu = wfMessage( 'wikibase-limitreport-entities-accessed' 
)->inLanguage( $languageRu )->text();
+               $labelEn = wfMessage( 'wikibase-limitreport-entities-accessed' 
)->inLanguage( $languageEn )->text();
+               $colonSeparatorRu = wfMessage( 'colon-separator' )->inLanguage( 
$languageRu )->text();
+               $colonSeparatorEn = wfMessage( 'colon-separator' )->inLanguage( 
$languageEn )->text();
+
+               return array(
+                       'Russian, html' => array(
+                               '<tr><th>' . $labelRu . 
'</th><td>123</td></tr>',
+                               $languageRu,
+                               true,
+                               true
+                       ),
+                       'Non-localized (English), html' => array(
+                               '<tr><th>' . $labelEn . 
'</th><td>123</td></tr>',
+                               $languageRu,
+                               true,
+                               false
+                       ),
+                       'Russian, non-html' => array(
+                               $labelRu . $colonSeparatorRu . 123,
+                               $languageRu,
+                               false,
+                               true
+                       ),
+                       'Non-localized (English), non-html' => array(
+                               $labelRu . $colonSeparatorEn . 123,
+                               $languageRu,
+                               false,
+                               false
+                       )
+               );
+       }
+}
diff --git a/client/tests/phpunit/includes/WikibaseClientTest.php 
b/client/tests/phpunit/includes/WikibaseClientTest.php
index 5ce07be..7e68ece 100644
--- a/client/tests/phpunit/includes/WikibaseClientTest.php
+++ b/client/tests/phpunit/includes/WikibaseClientTest.php
@@ -240,6 +240,11 @@
                $this->assertInstanceOf( 'Wikibase\Lib\ContentLanguages', 
$langs );
        }
 
+       public function testGetRestrictedEntityLookup() {
+               $restrictedEntityLookup = 
$this->getWikibaseClient()->getRestrictedEntityLookup();
+               $this->assertInstanceOf( 
'Wikibase\Client\DataAccess\RestrictedEntityLookup', $restrictedEntityLookup );
+       }
+
        /**
         * @return WikibaseClient
         */

-- 
To view, visit https://gerrit.wikimedia.org/r/225474
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I6a2bb05954aba33cd4dad278e61c981425d207f0
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Hoo man <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to