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