58 private $titleFormatter;
62 private $loadBalancer;
67 private const MAX_SIZE = 10000;
70 private const ROW = 0;
72 private const FLAGS = 1;
87 $this->wanCache = $cache;
88 $this->titleFormatter = $titleFormatter;
89 $this->nsInfo = $nsInfo;
90 $this->loadBalancer = $loadBalancer;
91 $this->logger =
new NullLogger();
94 public function setLogger( LoggerInterface $logger ) {
95 $this->logger = $logger;
103 private function getCacheKey( $page, $passThrough =
false ) {
104 if ( is_string( $page ) ) {
105 if ( $passThrough ) {
108 throw new InvalidArgumentException(
'They key may not be given as a string here' );
112 if ( is_array( $page ) ) {
113 $namespace = $page[
'page_namespace'];
114 $dbkey = $page[
'page_title'];
115 return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ),
' ',
'_' );
118 if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) {
121 'cross-wiki page reference',
123 'page-wiki' => $page->getWikiId(),
124 'page-reference' => $this->titleFormatter->getFullText( $page )
130 if ( $page instanceof PageIdentity && !$page->canExist() ) {
132 $this->logger->warning(
133 'non-proper page reference: {page-reference}',
134 [
'page-reference' => $this->titleFormatter->getFullText( $page ) ]
139 if ( $page instanceof LinkTarget
140 && ( $page->isExternal() || $page->getText() ===
'' || $page->getNamespace() < 0 )
144 $this->logger->warning(
145 'link to non-proper page: {page-link}',
146 [
'page-link' => $this->titleFormatter->getFullText( $page ) ]
151 return $this->titleFormatter->getPrefixedDBkey( $page );
164 $key = $this->getCacheKey( $page,
true );
165 if ( $key ===
null ) {
169 $entry = $this->entries->get( $key );
174 $row = $entry[self::ROW];
176 return $row ? (int)$row->page_id : 0;
192 $key = $this->getCacheKey( $page );
193 if ( $key ===
null ) {
197 $entry = $this->entries->get( $key );
202 $row = $entry[self::ROW];
209 return (
int)$row->page_id;
211 return (
int)$row->page_len;
213 return (
int)$row->page_is_redirect;
215 return (
int)$row->page_latest;
217 return !empty( $row->page_content_model )
218 ? (string)$row->page_content_model
221 return !empty( $row->page_lang )
222 ? (string)$row->page_lang
225 throw new InvalidArgumentException(
"Unknown field: $field" );
239 $key = $this->getCacheKey( $page,
true );
240 if ( $key ===
null ) {
244 $entry = $this->entries->get( $key );
246 return ( $entry && !$entry[self::ROW] );
268 int $queryFlags = IDBAccessObject::READ_NORMAL
270 $key = $this->getCacheKey( $page );
271 if ( $key ===
null ) {
275 foreach ( self::getSelectFields() as $field ) {
276 if ( !property_exists( $row, $field ) ) {
277 throw new InvalidArgumentException(
"Missing field: $field" );
281 $this->entries->set( $key, [ self::ROW => $row, self::FLAGS => $queryFlags ] );
298 public function addBadLinkObj( $page,
int $queryFlags = IDBAccessObject::READ_NORMAL ) {
299 $key = $this->getCacheKey( $page );
300 if ( $key ===
null ) {
304 $this->entries->set( $key, [ self::ROW =>
null, self::FLAGS => $queryFlags ] );
316 $key = $this->getCacheKey( $page,
true );
317 if ( $key ===
null ) {
321 $entry = $this->entries->get( $key );
322 if ( $entry && !$entry[self::ROW] ) {
323 $this->entries->clear( $key );
336 $key = $this->getCacheKey( $page );
337 if ( $key !==
null ) {
338 $this->entries->clear( $key );
352 $fields = array_merge(
356 'page_content_model',
360 if ( $pageLanguageUseDB ) {
361 $fields[] =
'page_lang';
381 public function addLinkObj( $page,
int $queryFlags = IDBAccessObject::READ_NORMAL ) {
383 $page->getNamespace(),
385 [ $this,
'fetchPageRow' ],
389 return $row ? (int)$row->page_id : 0;
400 private function getGoodLinkRowInternal(
402 ?callable $fetchCallback =
null,
403 int $queryFlags = IDBAccessObject::READ_NORMAL
405 $callerShouldAddGoodLink = false;
407 $key = $this->getCacheKey( $link );
408 if ( $key ===
null ) {
409 return [ $callerShouldAddGoodLink, null ];
415 $entry = $this->entries->get( $key );
416 if ( $entry && $entry[self::FLAGS] >= $queryFlags ) {
417 return [ $callerShouldAddGoodLink, $entry[self::ROW] ?: null ];
420 if ( !$fetchCallback ) {
421 return [ $callerShouldAddGoodLink, null ];
424 $callerShouldAddGoodLink =
true;
426 $wanCacheKey = $this->getPersistentCacheKey( $link );
427 if ( $wanCacheKey !==
null && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) {
429 $row = $this->wanCache->getWithSetCallback(
431 WANObjectCache::TTL_DAY,
432 function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) {
433 $dbr = $this->loadBalancer->getConnection( ILoadBalancer::DB_REPLICA );
436 $row = $fetchCallback( $dbr, $ns, $dbkey, [] );
437 $mtime = $row ? (int)
wfTimestamp( TS_UNIX, $row->page_touched ) : false;
438 $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl );
445 if ( ( $queryFlags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) {
446 $dbr = $this->loadBalancer->getConnection(
DB_PRIMARY );
448 $dbr = $this->loadBalancer->getConnection(
DB_REPLICA );
451 if ( ( $queryFlags & IDBAccessObject::READ_EXCLUSIVE ) == IDBAccessObject::READ_EXCLUSIVE ) {
452 $options[] =
'FOR UPDATE';
453 } elseif ( ( $queryFlags & IDBAccessObject::READ_LOCKING ) == IDBAccessObject::READ_LOCKING ) {
454 $options[] =
'LOCK IN SHARE MODE';
456 $row = $fetchCallback( $dbr, $ns, $dbkey, $options );
459 return [ $callerShouldAddGoodLink, $row ?: null ];
479 ?callable $fetchCallback =
null,
480 int $queryFlags = IDBAccessObject::READ_NORMAL
483 if ( $link ===
null ) {
487 [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
494 if ( $shouldAddGoodLink ) {
496 $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
497 }
catch ( InvalidArgumentException $e ) {
499 $this->invalidateTitle( $link );
500 [ , $row ] = $this->getGoodLinkRowInternal(
505 $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
509 $this->addBadLinkObj( $link );
519 private function getPersistentCacheKey( $page ) {
521 if ( $this->
getCacheKey( $page ) ===
null || !$this->usePersistentCache( $page ) ) {
525 return $this->wanCache->makeKey(
527 $page->getNamespace(),
528 sha1( $page->getDBkey() )
536 private function usePersistentCache( $pageOrNamespace ) {
537 $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace();
542 if ( $this->nsInfo->isContent( $ns ) ) {
546 return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
556 private function fetchPageRow( IReadableDatabase $db,
int $ns,
string $dbkey, $options = [] ) {
557 $queryBuilder = $db->newSelectQueryBuilder()
558 ->select( self::getSelectFields() )
560 ->where( [
'page_namespace' => $ns,
'page_title' => $dbkey ] )
561 ->options( $options );
563 return $queryBuilder->caller( __METHOD__ )->fetchRow();
574 $wanCacheKey = $this->getPersistentCacheKey( $page );
575 if ( $wanCacheKey !==
null ) {
576 $this->wanCache->delete( $wanCacheKey );
579 $this->clearLink( $page );
586 $this->entries->clear();