mediawiki-extensions-Gadgets/includes/MediaWikiGadgetsJsonRepo.php
Umherirrender f6b16d34de Use namespaced classes
Changes to the use statements done automatically via script
Addition of missing use statement done manually

Change-Id: Ib342f7315b331c18b3f7d9e3c22ff3e4d5d8c7f2
2024-10-20 01:15:50 +02:00

223 lines
5.7 KiB
PHP

<?php
namespace MediaWiki\Extension\Gadgets;
use InvalidArgumentException;
use MediaWiki\Extension\Gadgets\Content\GadgetDefinitionContent;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Title\Title;
use Wikimedia\ObjectCache\WANObjectCache;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IConnectionProvider;
use Wikimedia\Rdbms\IExpression;
use Wikimedia\Rdbms\LikeValue;
/**
* Gadgets repo powered by `MediaWiki:Gadgets/<id>.json` pages.
*
* Each gadget has its own gadget definition page, using GadgetDefinitionContent.
*/
class MediaWikiGadgetsJsonRepo extends GadgetRepo {
/**
* How long in seconds the list of gadget ids and
* individual gadgets should be cached for (1 day)
*/
private const CACHE_TTL = 86400;
public const DEF_PREFIX = 'Gadgets/';
public const DEF_SUFFIX = '.json';
private IConnectionProvider $dbProvider;
private WANObjectCache $wanCache;
private RevisionLookup $revLookup;
public function __construct(
IConnectionProvider $dbProvider,
WANObjectCache $wanCache,
RevisionLookup $revLookup
) {
$this->dbProvider = $dbProvider;
$this->wanCache = $wanCache;
$this->revLookup = $revLookup;
}
/**
* Get a list of gadget ids from cache/database
*
* @return string[]
*/
public function getGadgetIds(): array {
$key = $this->getGadgetIdsKey();
$fname = __METHOD__;
$dbr = $this->dbProvider->getReplicaDatabase();
$titles = $this->wanCache->getWithSetCallback(
$key,
self::CACHE_TTL,
static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname, $dbr ) {
$setOpts += Database::getCacheSetOptions( $dbr );
return $dbr->newSelectQueryBuilder()
->select( 'page_title' )
->from( 'page' )
->where( [
'page_namespace' => NS_MEDIAWIKI,
'page_content_model' => 'GadgetDefinition',
$dbr->expr(
'page_title',
IExpression::LIKE,
new LikeValue( self::DEF_PREFIX, $dbr->anyString(), self::DEF_SUFFIX )
)
] )
->caller( $fname )
->fetchFieldValues();
},
[
'checkKeys' => [ $key ],
'pcTTL' => WANObjectCache::TTL_PROC_SHORT,
// Bump when changing the database query.
'version' => 2,
'lockTSE' => 30
]
);
$ids = [];
foreach ( $titles as $title ) {
$id = self::getGadgetId( $title );
if ( $id !== '' ) {
$ids[] = $id;
}
}
return $ids;
}
/**
* @inheritDoc
*/
public function handlePageUpdate( LinkTarget $target ): void {
if ( $this->isGadgetDefinitionTitle( $target ) ) {
$this->purgeGadgetIdsList();
$this->purgeGadgetEntry( self::getGadgetId( $target->getText() ) );
}
}
/**
* Purge the list of gadget ids when a page is deleted or if a new page is created
*/
public function purgeGadgetIdsList(): void {
$this->wanCache->touchCheckKey( $this->getGadgetIdsKey() );
}
/**
* @param string $title Gadget definition page title
* @return string Gadget ID
*/
private static function getGadgetId( string $title ): string {
if ( !str_starts_with( $title, self::DEF_PREFIX ) || !str_ends_with( $title, self::DEF_SUFFIX ) ) {
throw new InvalidArgumentException( 'Invalid definition page title' );
}
return substr( $title, strlen( self::DEF_PREFIX ), -strlen( self::DEF_SUFFIX ) );
}
/**
* @param LinkTarget $target
* @return bool
*/
public static function isGadgetDefinitionTitle( LinkTarget $target ): bool {
if ( !$target->inNamespace( NS_MEDIAWIKI ) ) {
return false;
}
$title = $target->getText();
try {
self::getGadgetId( $title );
return true;
} catch ( InvalidArgumentException $e ) {
return false;
}
}
/**
* @inheritDoc
*/
public function getGadgetDefinitionTitle( string $id ): ?Title {
return Title::makeTitleSafe( NS_MEDIAWIKI, self::DEF_PREFIX . $id . self::DEF_SUFFIX );
}
/**
* @param string $id
* @throws InvalidArgumentException
* @return Gadget
*/
public function getGadget( string $id ): Gadget {
$key = $this->getGadgetCacheKey( $id );
$gadget = $this->wanCache->getWithSetCallback(
$key,
self::CACHE_TTL,
function ( $old, &$ttl, array &$setOpts ) use ( $id ) {
$setOpts += Database::getCacheSetOptions( $this->dbProvider->getReplicaDatabase() );
$title = $this->getGadgetDefinitionTitle( $id );
if ( !$title ) {
$ttl = WANObjectCache::TTL_UNCACHEABLE;
return null;
}
$revRecord = $this->revLookup->getRevisionByTitle( $title );
if ( !$revRecord ) {
$ttl = WANObjectCache::TTL_UNCACHEABLE;
return null;
}
$content = $revRecord->getContent( SlotRecord::MAIN );
if ( !$content instanceof GadgetDefinitionContent ) {
// Uhm...
$ttl = WANObjectCache::TTL_UNCACHEABLE;
return null;
}
$handler = $content->getContentHandler();
'@phan-var \MediaWiki\Extension\Gadgets\Content\GadgetDefinitionContentHandler $handler';
$data = wfArrayPlus2d( $content->getAssocArray(), $handler->getDefaultMetadata() );
return Gadget::serializeDefinition( $id, $data );
},
[
'checkKeys' => [ $key ],
'pcTTL' => WANObjectCache::TTL_PROC_SHORT,
'lockTSE' => 30,
'version' => 2,
]
);
if ( $gadget === null ) {
throw new InvalidArgumentException( "Unknown gadget $id" );
}
return new Gadget( $gadget );
}
/**
* Update the cache for a specific Gadget whenever it is updated
*
* @param string $id
*/
public function purgeGadgetEntry( $id ) {
$this->wanCache->touchCheckKey( $this->getGadgetCacheKey( $id ) );
}
/**
* @return string
*/
private function getGadgetIdsKey() {
return $this->wanCache->makeKey( 'gadgets-jsonrepo-ids' );
}
/**
* @param string $id
* @return string
*/
private function getGadgetCacheKey( $id ) {
return $this->wanCache->makeKey( 'gadgets-object', $id, Gadget::GADGET_CLASS_VERSION );
}
}