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();
97 public function setLogger( LoggerInterface $logger ) {
98 $this->logger = $logger;
106 private function getCacheKey( $page, $passThrough =
false ) {
107 if ( is_string( $page ) ) {
108 if ( $passThrough ) {
111 throw new InvalidArgumentException(
'They key may not be given as a string here' );
115 if ( is_array( $page ) ) {
116 $namespace = $page[
'page_namespace'];
117 $dbkey = $page[
'page_title'];
118 return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ),
' ',
'_' );
121 if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) {
124 'cross-wiki page reference',
126 'page-wiki' => $page->getWikiId(),
127 'page-reference' => $this->titleFormatter->getFullText( $page )
133 if ( $page instanceof PageIdentity && !$page->canExist() ) {
135 $this->logger->warning(
136 'non-proper page reference: {page-reference}',
137 [
'page-reference' => $this->titleFormatter->getFullText( $page ) ]
142 if ( $page instanceof LinkTarget
143 && ( $page->isExternal() || $page->getText() ===
'' || $page->getNamespace() < 0 )
147 $this->logger->warning(
148 'link to non-proper page: {page-link}',
149 [
'page-link' => $this->titleFormatter->getFullText( $page ) ]
154 return $this->titleFormatter->getPrefixedDBkey( $page );
168 if ( $key ===
null ) {
172 $entry = $this->entries->get( $key );
177 $row = $entry[self::ROW];
179 return $row ? (int)$row->page_id : 0;
196 if ( $key ===
null ) {
200 $entry = $this->entries->get( $key );
205 $row = $entry[self::ROW];
212 return (
int)$row->page_id;
214 return (
int)$row->page_len;
216 return (
int)$row->page_is_redirect;
218 return (
int)$row->page_latest;
220 return !empty( $row->page_content_model )
221 ? (string)$row->page_content_model
224 return !empty( $row->page_lang )
225 ? (string)$row->page_lang
228 throw new InvalidArgumentException(
"Unknown field: $field" );
243 if ( $key ===
null ) {
247 $entry = $this->entries->get( $key );
249 return ( $entry && !$entry[self::ROW] );
271 int $queryFlags = IDBAccessObject::READ_NORMAL
274 if ( $key ===
null ) {
278 foreach ( self::getSelectFields() as $field ) {
279 if ( !property_exists( $row, $field ) ) {
280 throw new InvalidArgumentException(
"Missing field: $field" );
284 $this->entries->set( $key, [ self::ROW => $row, self::FLAGS => $queryFlags ] );
301 public function addBadLinkObj( $page,
int $queryFlags = IDBAccessObject::READ_NORMAL ) {
303 if ( $key ===
null ) {
307 $this->entries->set( $key, [ self::ROW =>
null, self::FLAGS => $queryFlags ] );
320 if ( $key ===
null ) {
324 $entry = $this->entries->get( $key );
325 if ( $entry && !$entry[self::ROW] ) {
326 $this->entries->clear( $key );
340 if ( $key !==
null ) {
341 $this->entries->clear( $key );
355 $fields = array_merge(
359 'page_content_model',
363 if ( $pageLanguageUseDB ) {
364 $fields[] =
'page_lang';
384 public function addLinkObj( $page,
int $queryFlags = IDBAccessObject::READ_NORMAL ) {
386 $page->getNamespace(),
388 [ $this,
'fetchPageRow' ],
392 return $row ? (int)$row->page_id : 0;
403 private function getGoodLinkRowInternal(
405 callable $fetchCallback =
null,
406 int $queryFlags = IDBAccessObject::READ_NORMAL
408 $callerShouldAddGoodLink = false;
411 if ( $key ===
null ) {
412 return [ $callerShouldAddGoodLink, null ];
418 $entry = $this->entries->get( $key );
419 if ( $entry && $entry[self::FLAGS] >= $queryFlags ) {
420 return [ $callerShouldAddGoodLink, $entry[self::ROW] ?: null ];
423 if ( !$fetchCallback ) {
424 return [ $callerShouldAddGoodLink, null ];
427 $callerShouldAddGoodLink =
true;
429 $wanCacheKey = $this->getPersistentCacheKey( $link );
430 if ( $wanCacheKey !==
null && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) {
432 $row = $this->wanCache->getWithSetCallback(
434 WANObjectCache::TTL_DAY,
435 function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) {
436 $dbr = $this->loadBalancer->getConnection( ILoadBalancer::DB_REPLICA );
439 $row = $fetchCallback( $dbr, $ns, $dbkey, [] );
440 $mtime = $row ? (int)
wfTimestamp( TS_UNIX, $row->page_touched ) : false;
441 $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl );
449 $dbr = $this->loadBalancer->getConnection(
DB_PRIMARY );
451 $dbr = $this->loadBalancer->getConnection(
DB_REPLICA );
455 $options[] =
'FOR UPDATE';
456 } elseif ( ( $queryFlags & IDBAccessObject::READ_LOCKING ) == IDBAccessObject::READ_LOCKING ) {
457 $options[] =
'LOCK IN SHARE MODE';
459 $row = $fetchCallback( $dbr, $ns, $dbkey, $options );
462 return [ $callerShouldAddGoodLink, $row ?: null ];
482 callable $fetchCallback =
null,
483 int $queryFlags = IDBAccessObject::READ_NORMAL
486 if ( $link ===
null ) {
490 [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
497 if ( $shouldAddGoodLink ) {
499 $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
500 }
catch ( InvalidArgumentException $e ) {
502 $this->invalidateTitle( $link );
503 [ , $row ] = $this->getGoodLinkRowInternal(
508 $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
512 $this->addBadLinkObj( $link );
522 private function getPersistentCacheKey( $page ) {
524 if ( $this->
getCacheKey( $page ) ===
null || !$this->usePersistentCache( $page ) ) {
528 return $this->wanCache->makeKey(
530 $page->getNamespace(),
531 sha1( $page->getDBkey()
539 private function usePersistentCache( $pageOrNamespace ) {
540 $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace();
545 if ( $this->nsInfo->isContent( $ns ) ) {
549 return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
559 private function fetchPageRow( IReadableDatabase $db,
int $ns,
string $dbkey, $options = [] ) {
560 $queryBuilder = $db->newSelectQueryBuilder()
561 ->select( self::getSelectFields() )
563 ->where( [
'page_namespace' => $ns,
'page_title' => $dbkey ] )
564 ->options( $options );
566 return $queryBuilder->caller( __METHOD__ )->fetchRow();
577 $wanCacheKey = $this->getPersistentCacheKey( $page );
578 if ( $wanCacheKey !==
null ) {
579 $this->wanCache->delete( $wanCacheKey );
582 $this->clearLink( $page );
589 $this->entries->clear();