60 private $titleFormatter;
64 private $loadBalancer;
69 private const MAX_SIZE = 10000;
72 private const ROW = 0;
74 private const FLAGS = 1;
88 $this->entries =
new MapCacheLRU( self::MAX_SIZE );
89 $this->wanCache = $cache;
90 $this->titleFormatter = $titleFormatter;
91 $this->nsInfo = $nsInfo;
92 $this->loadBalancer = $loadBalancer;
93 $this->logger =
new NullLogger();
96 public function setLogger( LoggerInterface $logger ): void {
97 $this->logger = $logger;
105 private function getCacheKey( $page, $passThrough =
false ) {
106 if ( is_string( $page ) ) {
107 if ( $passThrough ) {
110 throw new InvalidArgumentException(
'They key may not be given as a string here' );
114 if ( is_array( $page ) ) {
115 $namespace = $page[
'page_namespace'];
116 $dbkey = $page[
'page_title'];
117 return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ),
' ',
'_' );
123 'cross-wiki page reference',
125 'page-wiki' => $page->getWikiId(),
126 'page-reference' => $this->titleFormatter->getFullText( $page )
132 if ( $page instanceof PageIdentity && !$page->canExist() ) {
134 $this->logger->warning(
135 'non-proper page reference: {page-reference}',
136 [
'page-reference' => $this->titleFormatter->getFullText( $page ) ]
141 if ( $page instanceof LinkTarget
142 && ( $page->isExternal() || $page->getText() ===
'' || $page->getNamespace() < 0 )
146 $this->logger->warning(
147 'link to non-proper page: {page-link}',
148 [
'page-link' => $this->titleFormatter->getFullText( $page ) ]
153 return $this->titleFormatter->getPrefixedDBkey( $page );
166 $key = $this->getCacheKey( $page,
true );
167 if ( $key ===
null ) {
171 $entry = $this->entries->get( $key );
176 $row = $entry[self::ROW];
178 return $row ? (int)$row->page_id : 0;
194 $key = $this->getCacheKey( $page );
195 if ( $key ===
null ) {
199 $entry = $this->entries->get( $key );
204 $row = $entry[self::ROW];
211 return (
int)$row->page_id;
213 return (
int)$row->page_len;
215 return (
int)$row->page_is_redirect;
217 return (
int)$row->page_latest;
219 return !empty( $row->page_content_model )
220 ? (string)$row->page_content_model
223 return !empty( $row->page_lang )
224 ? (string)$row->page_lang
227 throw new InvalidArgumentException(
"Unknown field: $field" );
241 $key = $this->getCacheKey( $page,
true );
242 if ( $key ===
null ) {
246 $entry = $this->entries->get( $key );
248 return ( $entry && !$entry[self::ROW] );
270 int $queryFlags = IDBAccessObject::READ_NORMAL
272 $key = $this->getCacheKey( $page );
273 if ( $key ===
null ) {
277 foreach ( self::getSelectFields() as $field ) {
278 if ( !property_exists( $row, $field ) ) {
279 throw new InvalidArgumentException(
"Missing field: $field" );
283 $this->entries->set( $key, [ self::ROW => $row, self::FLAGS => $queryFlags ] );
300 public function addBadLinkObj( $page,
int $queryFlags = IDBAccessObject::READ_NORMAL ) {
301 $key = $this->getCacheKey( $page );
302 if ( $key ===
null ) {
306 $this->entries->set( $key, [ self::ROW =>
null, self::FLAGS => $queryFlags ] );
318 $key = $this->getCacheKey( $page,
true );
319 if ( $key ===
null ) {
323 $entry = $this->entries->get( $key );
324 if ( $entry && !$entry[self::ROW] ) {
325 $this->entries->clear( $key );
338 $key = $this->getCacheKey( $page );
339 if ( $key !==
null ) {
340 $this->entries->clear( $key );
354 $fields = array_merge(
358 'page_content_model',
362 if ( $pageLanguageUseDB ) {
363 $fields[] =
'page_lang';
383 public function addLinkObj( $page,
int $queryFlags = IDBAccessObject::READ_NORMAL ) {
384 $row = $this->getGoodLinkRow(
385 $page->getNamespace(),
387 $this->fetchPageRow( ... ),
391 return $row ? (int)$row->page_id : 0;
402 private function getGoodLinkRowInternal(
404 ?callable $fetchCallback =
null,
405 int $queryFlags = IDBAccessObject::READ_NORMAL
407 $callerShouldAddGoodLink = false;
409 $key = $this->getCacheKey( $link );
410 if ( $key ===
null ) {
411 return [ $callerShouldAddGoodLink, null ];
417 $entry = $this->entries->get( $key );
418 if ( $entry && $entry[self::FLAGS] >= $queryFlags ) {
419 return [ $callerShouldAddGoodLink, $entry[self::ROW] ?: null ];
422 if ( !$fetchCallback ) {
423 return [ $callerShouldAddGoodLink, null ];
426 $callerShouldAddGoodLink =
true;
428 $wanCacheKey = $this->getPersistentCacheKey( $link );
429 if ( $wanCacheKey !==
null && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) {
431 $row = $this->wanCache->getWithSetCallback(
433 WANObjectCache::TTL_DAY,
434 function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) {
435 $dbr = $this->loadBalancer->getConnection( ILoadBalancer::DB_REPLICA );
438 $row = $fetchCallback( $dbr, $ns, $dbkey, [] );
439 $mtime = $row ? (int)
wfTimestamp( TS::UNIX, $row->page_touched ) : false;
440 $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl );
447 if ( ( $queryFlags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) {
448 $dbr = $this->loadBalancer->getConnection(
DB_PRIMARY );
450 $dbr = $this->loadBalancer->getConnection(
DB_REPLICA );
453 if ( ( $queryFlags & IDBAccessObject::READ_EXCLUSIVE ) == IDBAccessObject::READ_EXCLUSIVE ) {
454 $options[] =
'FOR UPDATE';
455 } elseif ( ( $queryFlags & IDBAccessObject::READ_LOCKING ) == IDBAccessObject::READ_LOCKING ) {
456 $options[] =
'LOCK IN SHARE MODE';
458 $row = $fetchCallback( $dbr, $ns, $dbkey, $options );
461 return [ $callerShouldAddGoodLink, $row ?: null ];
481 ?callable $fetchCallback =
null,
482 int $queryFlags = IDBAccessObject::READ_NORMAL
485 if ( $link ===
null ) {
489 [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
496 if ( $shouldAddGoodLink ) {
498 $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
499 }
catch ( InvalidArgumentException ) {
501 $this->invalidateTitle( $link );
502 [ , $row ] = $this->getGoodLinkRowInternal(
507 $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
511 $this->addBadLinkObj( $link );
521 private function getPersistentCacheKey( $page ) {
523 if ( $this->
getCacheKey( $page ) ===
null || !$this->usePersistentCache( $page ) ) {
526 return $this->wanCache->makeKey(
'page', $page->getNamespace(), sha1( $page->getDBkey() ) );
533 private function usePersistentCache( $pageOrNamespace ) {
534 $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace();
537 ( !is_int( $pageOrNamespace ) &&
538 ( str_ends_with( $pageOrNamespace->getDBkey(),
'.css' ) ||
539 str_ends_with( $pageOrNamespace->getDBkey(),
'.js' ) ) ) ) {
543 if ( $this->nsInfo->isContent( $ns ) ) {
547 return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
557 private function fetchPageRow( IReadableDatabase $db,
int $ns,
string $dbkey, $options = [] ) {
558 $queryBuilder = $db->newSelectQueryBuilder()
559 ->select( self::getSelectFields() )
561 ->where( [
'page_namespace' => $ns,
'page_title' => $dbkey ] )
562 ->options( $options );
564 return $queryBuilder->caller( __METHOD__ )->fetchRow();
576 foreach ( $pages as $page ) {
577 $title = Title::newFromText( $page );
579 $cacheKey = $this->getPersistentCacheKey( $title );
580 $pageObject[$cacheKey] = $title;
584 $rows = $this->wanCache->getMulti( array_keys( $pageObject ) );
585 foreach ( $rows as $key => $row ) {
587 $title = TitleValue::tryNew( (
int)$row->page_namespace, $row->page_title );
588 $this->addGoodLinkObjFromRow( $title, $row );
590 $this->addBadLinkObj( $pageObject[$key] );
592 unset( $pageObject[$key] );
595 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
597 if ( count( $pageObject ) > 0 ) {
598 $linkBatch = $linkBatchFactory->newLinkBatch( array_values( $pageObject ) );
599 $linkBatch->setCaller( $fname );
600 $result = $linkBatch->doQuery();
601 $linkBatch->doGenderQuery();
604 foreach ( $result as $row ) {
605 $title = TitleValue::tryNew( (
int)$row->page_namespace, $row->page_title );
606 $cacheKey = $this->getPersistentCacheKey( $title );
607 $this->addGoodLinkObjFromRow( $title, $row );
608 $pageObject[$cacheKey] = $row;
611 foreach ( $pageObject as $key => $row ) {
612 if ( !$row instanceof
Title ) {
613 $this->wanCache->set( $key, $row, WANObjectCache::TTL_DAY );
615 $this->wanCache->set( $key,
null, WANObjectCache::TTL_DAY );
616 $this->addBadLinkObj( $row );
630 $wanCacheKey = $this->getPersistentCacheKey( $page );
631 if ( $wanCacheKey !==
null ) {
632 $this->wanCache->delete( $wanCacheKey );
635 $this->clearLink( $page );
642 $this->entries->clear();