Page MenuHomePhabricator

0001-SECURITY-Add-EntityDataPurger.patch

Authored By
Lucas_Werkmeister_WMDE
Sep 28 2020, 12:07 PM
Size
8 KB
Referenced Files
None
Subscribers
None

0001-SECURITY-Add-EntityDataPurger.patch

From 01f9643e6cbebf2b4408571db5c28e8a2a994c50 Mon Sep 17 00:00:00 2001
From: Lucas Werkmeister <lucas.werkmeister@wikimedia.de>
Date: Tue, 25 Aug 2020 15:29:33 +0200
Subject: [PATCH] SECURITY: Add EntityDataPurger
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This subscribes to the ArticleRevisionVisibilitySet hook and purges
affected Special:EntityData URLs from the web cache. For simplicity, it
does this regardless of whether the visibility change affected the
cached data (currently, the cached data does not include the user name
or comment associated with the revision, but who knows, maybe that’ll
change in the future).
This is implemented as a second hook handler, independent of the already
existing ArticleRevisionVisibilitySetHookHandler, because the two do
very different things and would share almost no code even if they were
merged into one class.
Bug: T260349
Change-Id: Ief8aba38205187fe969e8d87471919b2b258c615
Co-Authored-By: Itamar Givon <itamar.givon@wikimedia.de>
---
extension-repo.json | 12 ++-
repo/includes/Hooks/EntityDataPurger.php | 71 ++++++++++++++
.../includes/Hooks/EntityDataPurgerTest.php | 98 +++++++++++++++++++
3 files changed, 180 insertions(+), 1 deletion(-)
create mode 100644 repo/includes/Hooks/EntityDataPurger.php
create mode 100644 repo/tests/phpunit/includes/Hooks/EntityDataPurgerTest.php
diff --git a/extension-repo.json b/extension-repo.json
index 7f2102c0a3..061cd1f3be 100644
--- a/extension-repo.json
+++ b/extension-repo.json
@@ -448,6 +448,13 @@
"class": "\\Wikibase\\Repo\\Hooks\\DeleteDispatcher",
"factory": "\\Wikibase\\Repo\\Hooks\\DeleteDispatcher::factory"
},
+ "EntityDataPurger": {
+ "class": "\\Wikibase\\Repo\\Hooks\\EntityDataPurger",
+ "factory": "\\Wikibase\\Repo\\Hooks\\EntityDataPurger::factory",
+ "services": [
+ "HtmlCacheUpdater"
+ ]
+ },
"FederatedPropertiesSpecialPage": {
"class": "\\Wikibase\\Repo\\Hooks\\FederatedPropertiesSpecialPageHookHandler",
"factory": "\\Wikibase\\Repo\\Hooks\\FederatedPropertiesSpecialPageHookHandler::factory"
@@ -498,7 +505,10 @@
"DeleteDispatcher",
"\\Wikibase\\Repo\\RepoHooks::onArticleDeleteComplete"
],
- "ArticleRevisionVisibilitySet": "ArticleRevisionVisibilitySet",
+ "ArticleRevisionVisibilitySet": [
+ "ArticleRevisionVisibilitySet",
+ "EntityDataPurger"
+ ],
"ArticleUndelete": "\\Wikibase\\Repo\\RepoHooks::onArticleUndelete",
"BeforeDisplayNoArticleText": "\\Wikibase\\Repo\\Actions\\ViewEntityAction::onBeforeDisplayNoArticleText",
"BeforePageDisplay": "\\Wikibase\\Repo\\RepoHooks::onBeforePageDisplay",
diff --git a/repo/includes/Hooks/EntityDataPurger.php b/repo/includes/Hooks/EntityDataPurger.php
new file mode 100644
index 0000000000..aaa63ed94a
--- /dev/null
+++ b/repo/includes/Hooks/EntityDataPurger.php
@@ -0,0 +1,71 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Wikibase\Repo\Hooks;
+
+use HtmlCacheUpdater;
+use MediaWiki\Hook\ArticleRevisionVisibilitySetHook;
+use Title;
+use Wikibase\Lib\Store\EntityIdLookup;
+use Wikibase\Repo\LinkedData\EntityDataUriManager;
+use Wikibase\Repo\WikibaseRepo;
+
+/**
+ * @license GPL-2.0-or-later
+ */
+class EntityDataPurger implements ArticleRevisionVisibilitySetHook {
+
+ /** @var EntityIdLookup */
+ private $entityIdLookup;
+
+ /** @var EntityDataUriManager */
+ private $entityDataUriManager;
+
+ /** @var HtmlCacheUpdater */
+ private $htmlCacheUpdater;
+
+ public function __construct(
+ EntityIdLookup $entityIdLookup,
+ EntityDataUriManager $entityDataUriManager,
+ HtmlCacheUpdater $htmlCacheUpdater
+ ) {
+ $this->entityIdLookup = $entityIdLookup;
+ $this->entityDataUriManager = $entityDataUriManager;
+ $this->htmlCacheUpdater = $htmlCacheUpdater;
+ }
+
+ public static function factory( HtmlCacheUpdater $htmlCacheUpdater ): self {
+ $wikibaseRepo = WikibaseRepo::getDefaultInstance();
+ return new self(
+ $wikibaseRepo->getEntityIdLookup(),
+ $wikibaseRepo->getEntityDataUriManager(),
+ $htmlCacheUpdater
+ );
+ }
+
+ /**
+ * @param Title $title
+ * @param int[] $ids
+ * @param int[][] $visibilityChangeMap
+ */
+ public function onArticleRevisionVisibilitySet( $title, $ids, $visibilityChangeMap ): void {
+ $entityId = $this->entityIdLookup->getEntityIdForTitle( $title );
+ if ( !$entityId ) {
+ return;
+ }
+
+ $urls = [];
+ foreach ( $ids as $revisionId ) {
+ $urls = array_merge( $urls, $this->entityDataUriManager->getPotentiallyCachedUrls(
+ $entityId,
+ // $ids should be int[] but MediaWiki may call with a string[], so cast to int
+ (int)$revisionId
+ ) );
+ }
+ if ( $urls !== [] ) {
+ $this->htmlCacheUpdater->purgeUrls( $urls );
+ }
+ }
+
+}
diff --git a/repo/tests/phpunit/includes/Hooks/EntityDataPurgerTest.php b/repo/tests/phpunit/includes/Hooks/EntityDataPurgerTest.php
new file mode 100644
index 0000000000..a58766c619
--- /dev/null
+++ b/repo/tests/phpunit/includes/Hooks/EntityDataPurgerTest.php
@@ -0,0 +1,98 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Wikibase\Repo\Tests\Hooks;
+
+use HtmlCacheUpdater;
+use PHPUnit\Framework\TestCase;
+use Title;
+use Wikibase\DataModel\Entity\ItemId;
+use Wikibase\Lib\Store\EntityIdLookup;
+use Wikibase\Repo\Hooks\EntityDataPurger;
+use Wikibase\Repo\LinkedData\EntityDataRequestHandler;
+use Wikibase\Repo\LinkedData\EntityDataUriManager;
+
+/**
+ * @covers \Wikibase\Repo\Hooks\EntityDataPurger
+ *
+ * @group Wikibase
+ *
+ * @license GPL-2.0-or-later
+ */
+class EntityDataPurgerTest extends TestCase {
+
+ public function testGivenEntityIdLookupReturnsNull_handlerDoesNothing() {
+ $title = Title::newFromText( 'Project:About' );
+ $entityIdLookup = $this->createMock( EntityIdLookup::class );
+ $entityIdLookup->expects( $this->once() )
+ ->method( 'getEntityIdForTitle' )
+ ->with( $title )
+ ->willReturn( null );
+ $entityDataUriManager = $this->createMock( EntityDataUriManager::class );
+ $entityDataUriManager->expects( $this->never() )
+ ->method( 'getPotentiallyCachedUrls' );
+ $htmlCacheUpdater = $this->createMock( HtmlCacheUpdater::class );
+ $htmlCacheUpdater->expects( $this->never() )
+ ->method( 'purgeUrls' );
+ $purger = new EntityDataPurger( $entityIdLookup, $entityDataUriManager, $htmlCacheUpdater );
+
+ $purger->onArticleRevisionVisibilitySet( $title, [ 1, 2, 3 ], [] );
+ }
+
+ public function testGivenEntityIdLookupReturnsId_handlerPurgesCache() {
+ $title = Title::newFromText( 'Item:Q1' );
+ $entityId = new ItemId( 'Q1' );
+ $entityIdLookup = $this->createMock( EntityIdLookup::class );
+ $entityIdLookup->expects( $this->once() )
+ ->method( 'getEntityIdForTitle' )
+ ->with( $title )
+ ->willReturn( $entityId );
+ $entityDataUriManager = $this->createMock( EntityDataUriManager::class );
+ $entityDataUriManager->expects( $this->once() )
+ ->method( 'getPotentiallyCachedUrls' )
+ ->with( $entityId, 1 )
+ ->willReturn( [ 'urlA/Q1/1', 'urlB/Q1/1' ] );
+ $htmlCacheUpdater = $this->createMock( HtmlCacheUpdater::class );
+ $htmlCacheUpdater->expects( $this->once() )
+ ->method( 'purgeUrls' )
+ ->with( [ 'urlA/Q1/1', 'urlB/Q1/1' ] );
+ $purger = new EntityDataPurger( $entityIdLookup, $entityDataUriManager, $htmlCacheUpdater );
+
+ $purger->onArticleRevisionVisibilitySet( $title, [ 1 ], [] );
+ }
+
+ public function testGivenMultipleRevisions_handlerPurgesCacheOnce() {
+ $title = Title::newFromText( 'Item:Q1' );
+ $entityId = new ItemId( 'Q1' );
+ $entityIdLookup = $this->createMock( EntityIdLookup::class );
+ $entityIdLookup->expects( $this->once() )
+ ->method( 'getEntityIdForTitle' )
+ ->with( $title )
+ ->willReturn( $entityId );
+ $entityDataUriManager = $this->createMock( EntityDataUriManager::class );
+ $entityDataUriManager
+ ->method( 'getPotentiallyCachedUrls' )
+ ->withConsecutive(
+ [ $entityId, 1 ],
+ [ $entityId, 2 ],
+ [ $entityId, 3 ]
+ )
+ ->willReturnOnConsecutiveCalls(
+ [ 'urlA/Q1/1', 'urlB/Q1/1' ],
+ [ 'urlA/Q1/2', 'urlB/Q1/2' ],
+ [ 'urlA/Q1/3', 'urlB/Q1/3' ]
+ );
+ $htmlCacheUpdater = $this->createMock( HtmlCacheUpdater::class );
+ $htmlCacheUpdater->expects( $this->once() )
+ ->method( 'purgeUrls' )
+ ->with( [
+ 'urlA/Q1/1', 'urlB/Q1/1',
+ 'urlA/Q1/2', 'urlB/Q1/2',
+ 'urlA/Q1/3', 'urlB/Q1/3',
+ ] );
+ $purger = new EntityDataPurger( $entityIdLookup, $entityDataUriManager, $htmlCacheUpdater );
+
+ $purger->onArticleRevisionVisibilitySet( $title, [ 1, 2, 3 ], [] );
+ }
+}
--
2.25.1

File Metadata

Mime Type
text/x-diff
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
8577707
Default Alt Text
0001-SECURITY-Add-EntityDataPurger.patch (8 KB)

Event Timeline