1<?php 2/** 3 * Page existence cache. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Cache 22 */ 23 24use MediaWiki\Linker\LinkTarget; 25use MediaWiki\MediaWikiServices; 26use Wikimedia\Rdbms\Database; 27use Wikimedia\Rdbms\IDatabase; 28 29/** 30 * Cache for article titles (prefixed DB keys) and ids linked from one source 31 * 32 * @ingroup Cache 33 */ 34class LinkCache { 35 /** @var MapCacheLRU */ 36 private $goodLinks; 37 /** @var MapCacheLRU */ 38 private $badLinks; 39 /** @var WANObjectCache */ 40 private $wanCache; 41 42 /** @var bool */ 43 private $mForUpdate = false; 44 45 /** @var TitleFormatter */ 46 private $titleFormatter; 47 48 /** @var NamespaceInfo */ 49 private $nsInfo; 50 51 /** 52 * How many Titles to store. There are two caches, so the amount actually 53 * stored in memory can be up to twice this. 54 */ 55 private const MAX_SIZE = 10000; 56 57 public function __construct( 58 TitleFormatter $titleFormatter, 59 WANObjectCache $cache, 60 NamespaceInfo $nsInfo = null 61 ) { 62 if ( !$nsInfo ) { 63 wfDeprecated( __METHOD__ . ' with no NamespaceInfo argument', '1.34' ); 64 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); 65 } 66 $this->goodLinks = new MapCacheLRU( self::MAX_SIZE ); 67 $this->badLinks = new MapCacheLRU( self::MAX_SIZE ); 68 $this->wanCache = $cache; 69 $this->titleFormatter = $titleFormatter; 70 $this->nsInfo = $nsInfo; 71 } 72 73 /** 74 * Get an instance of this class. 75 * 76 * @return LinkCache 77 * @deprecated since 1.28, use MediaWikiServices instead 78 */ 79 public static function singleton() { 80 return MediaWikiServices::getInstance()->getLinkCache(); 81 } 82 83 /** 84 * General accessor to get/set whether the master DB should be used 85 * 86 * This used to also set the FOR UPDATE option (locking the rows read 87 * in order to avoid link table inconsistency), which was later removed 88 * for performance on wikis with a high edit rate. 89 * 90 * @param bool|null $update 91 * @return bool 92 * @deprecated Since 1.34 93 */ 94 public function forUpdate( $update = null ) { 95 return wfSetVar( $this->mForUpdate, $update ); 96 } 97 98 /** 99 * @param string $title Prefixed DB key 100 * @return int Page ID or zero 101 */ 102 public function getGoodLinkID( $title ) { 103 $info = $this->goodLinks->get( $title ); 104 if ( !$info ) { 105 return 0; 106 } 107 return $info['id']; 108 } 109 110 /** 111 * Get a field of a title object from cache. 112 * If this link is not a cached good title, it will return NULL. 113 * @param LinkTarget $target 114 * @param string $field ('length','redirect','revision','model') 115 * @return string|int|null 116 */ 117 public function getGoodLinkFieldObj( LinkTarget $target, $field ) { 118 $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); 119 $info = $this->goodLinks->get( $dbkey ); 120 if ( !$info ) { 121 return null; 122 } 123 return $info[$field]; 124 } 125 126 /** 127 * @param string $title Prefixed DB key 128 * @return bool 129 */ 130 public function isBadLink( $title ) { 131 // Use get() to ensure it records as used for LRU. 132 return $this->badLinks->has( $title ); 133 } 134 135 /** 136 * Add a link for the title to the link cache 137 * 138 * @param int $id Page's ID 139 * @param LinkTarget $target 140 * @param int $len Text's length 141 * @param int|null $redir Whether the page is a redirect 142 * @param int $revision Latest revision's ID 143 * @param string|null $model Latest revision's content model ID 144 * @param string|null $lang Language code of the page, if not the content language 145 */ 146 public function addGoodLinkObj( $id, LinkTarget $target, $len = -1, $redir = null, 147 $revision = 0, $model = null, $lang = null 148 ) { 149 $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); 150 $this->goodLinks->set( $dbkey, [ 151 'id' => (int)$id, 152 'length' => (int)$len, 153 'redirect' => (int)$redir, 154 'revision' => (int)$revision, 155 'model' => $model ? (string)$model : null, 156 'lang' => $lang ? (string)$lang : null, 157 'restrictions' => null 158 ] ); 159 } 160 161 /** 162 * Same as above with better interface. 163 * @since 1.19 164 * @param LinkTarget $target 165 * @param stdClass $row Object which has the fields page_id, page_is_redirect, 166 * page_latest and page_content_model 167 */ 168 public function addGoodLinkObjFromRow( LinkTarget $target, $row ) { 169 $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); 170 $this->goodLinks->set( $dbkey, [ 171 'id' => intval( $row->page_id ), 172 'length' => intval( $row->page_len ), 173 'redirect' => intval( $row->page_is_redirect ), 174 'revision' => intval( $row->page_latest ), 175 'model' => !empty( $row->page_content_model ) 176 ? strval( $row->page_content_model ) 177 : null, 178 'lang' => !empty( $row->page_lang ) 179 ? strval( $row->page_lang ) 180 : null, 181 'restrictions' => !empty( $row->page_restrictions ) 182 ? strval( $row->page_restrictions ) 183 : null 184 ] ); 185 } 186 187 /** 188 * @param LinkTarget $target 189 */ 190 public function addBadLinkObj( LinkTarget $target ) { 191 $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); 192 if ( !$this->isBadLink( $dbkey ) ) { 193 $this->badLinks->set( $dbkey, 1 ); 194 } 195 } 196 197 /** 198 * @param string $title Prefixed DB key 199 */ 200 public function clearBadLink( $title ) { 201 $this->badLinks->clear( $title ); 202 } 203 204 /** 205 * @param LinkTarget $target 206 */ 207 public function clearLink( LinkTarget $target ) { 208 $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); 209 $this->badLinks->clear( $dbkey ); 210 $this->goodLinks->clear( $dbkey ); 211 } 212 213 /** 214 * Fields that LinkCache needs to select 215 * 216 * @since 1.28 217 * @return array 218 */ 219 public static function getSelectFields() { 220 global $wgPageLanguageUseDB; 221 222 $fields = [ 223 'page_id', 224 'page_len', 225 'page_is_redirect', 226 'page_latest', 227 'page_restrictions', 228 'page_content_model', 229 ]; 230 231 if ( $wgPageLanguageUseDB ) { 232 $fields[] = 'page_lang'; 233 } 234 235 return $fields; 236 } 237 238 /** 239 * Add a title to the link cache, return the page_id or zero if non-existent 240 * 241 * @param LinkTarget $nt LinkTarget object to add 242 * @return int Page ID or zero 243 */ 244 public function addLinkObj( LinkTarget $nt ) { 245 $key = $this->titleFormatter->getPrefixedDBkey( $nt ); 246 if ( $this->isBadLink( $key ) || $nt->isExternal() || $nt->getNamespace() < 0 ) { 247 return 0; 248 } 249 $id = $this->getGoodLinkID( $key ); 250 if ( $id != 0 ) { 251 return $id; 252 } 253 254 if ( $key === '' ) { 255 return 0; 256 } 257 258 // Cache template/file pages as they are less often viewed but heavily used 259 if ( $this->mForUpdate ) { 260 $row = $this->fetchPageRow( wfGetDB( DB_MASTER ), $nt ); 261 } elseif ( $this->isCacheable( $nt ) ) { 262 // These pages are often transcluded heavily, so cache them 263 $cache = $this->wanCache; 264 $row = $cache->getWithSetCallback( 265 $cache->makeKey( 'page', $nt->getNamespace(), sha1( $nt->getDBkey() ) ), 266 $cache::TTL_DAY, 267 function ( $curValue, &$ttl, array &$setOpts ) use ( $cache, $nt ) { 268 $dbr = wfGetDB( DB_REPLICA ); 269 $setOpts += Database::getCacheSetOptions( $dbr ); 270 271 $row = $this->fetchPageRow( $dbr, $nt ); 272 $mtime = $row ? wfTimestamp( TS_UNIX, $row->page_touched ) : false; 273 $ttl = $cache->adaptiveTTL( $mtime, $ttl ); 274 275 return $row; 276 } 277 ); 278 } else { 279 $row = $this->fetchPageRow( wfGetDB( DB_REPLICA ), $nt ); 280 } 281 282 if ( $row ) { 283 $this->addGoodLinkObjFromRow( $nt, $row ); 284 $id = intval( $row->page_id ); 285 } else { 286 $this->addBadLinkObj( $nt ); 287 $id = 0; 288 } 289 290 return $id; 291 } 292 293 /** 294 * @param WANObjectCache $cache 295 * @param LinkTarget $t 296 * @return string[] 297 * @since 1.28 298 */ 299 public function getMutableCacheKeys( WANObjectCache $cache, LinkTarget $t ) { 300 if ( $this->isCacheable( $t ) ) { 301 return [ $cache->makeKey( 'page', $t->getNamespace(), sha1( $t->getDBkey() ) ) ]; 302 } 303 304 return []; 305 } 306 307 private function isCacheable( LinkTarget $title ) { 308 $ns = $title->getNamespace(); 309 if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) { 310 return true; 311 } 312 // Focus on transcluded pages more than the main content 313 if ( $this->nsInfo->isContent( $ns ) ) { 314 return false; 315 } 316 // Non-talk extension namespaces (e.g. NS_MODULE) 317 return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) ); 318 } 319 320 private function fetchPageRow( IDatabase $db, LinkTarget $nt ) { 321 $fields = self::getSelectFields(); 322 if ( $this->isCacheable( $nt ) ) { 323 $fields[] = 'page_touched'; 324 } 325 326 return $db->selectRow( 327 'page', 328 $fields, 329 [ 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ], 330 __METHOD__ 331 ); 332 } 333 334 /** 335 * Purge the link cache for a title 336 * 337 * @param LinkTarget $title 338 * @since 1.28 339 */ 340 public function invalidateTitle( LinkTarget $title ) { 341 if ( $this->isCacheable( $title ) ) { 342 $cache = $this->wanCache; 343 $cache->delete( 344 $cache->makeKey( 'page', $title->getNamespace(), sha1( $title->getDBkey() ) ) 345 ); 346 } 347 } 348 349 /** 350 * Clears cache 351 */ 352 public function clear() { 353 $this->goodLinks->clear(); 354 $this->badLinks->clear(); 355 } 356} 357