MediaWiki master
RedirectStore.php
Go to the documentation of this file.
1<?php
8namespace MediaWiki\Page;
9
15use Psr\Log\LoggerInterface;
18
25class RedirectStore implements RedirectLookup {
26 private IConnectionProvider $dbProvider;
27 private PageLookup $pageLookup;
28 private TitleParser $titleParser;
29 private RepoGroup $repoGroup;
30 private LoggerInterface $logger;
31 private MapCacheLRU $procCache;
32
33 public function __construct(
34 IConnectionProvider $dbProvider,
35 PageLookup $pageLookup,
36 TitleParser $titleParser,
37 RepoGroup $repoGroup,
38 LoggerInterface $logger
39 ) {
40 $this->dbProvider = $dbProvider;
41 $this->pageLookup = $pageLookup;
42 $this->titleParser = $titleParser;
43 $this->repoGroup = $repoGroup;
44 $this->logger = $logger;
45 // Must be 500+ for QueryPage and Pager uses to be effective
46 $this->procCache = new MapCacheLRU( 1_000 );
47 }
48
49 public function getRedirectTarget( PageIdentity $page ): ?LinkTarget {
50 $cacheKey = self::makeCacheKey( $page );
51 $cachedValue = $this->procCache->get( $cacheKey );
52 if ( $cachedValue !== null ) {
53 return $cachedValue ?: null;
54 }
55
56 // Handle redirects for files included from foreign image repositories.
57 if ( $page->getNamespace() === NS_FILE ) {
58 $file = $this->repoGroup->findFile( $page );
59 if ( $file && !$file->isLocal() ) {
60 $from = $file->getRedirected();
61 $to = $file->getName();
62 if ( $from === null || $from === $to ) {
63 $this->procCache->set( $cacheKey, false );
64 return null;
65 }
66
67 $target = new TitleValue( NS_FILE, $to );
68 $this->procCache->set( $cacheKey, $target );
69 return $target;
70 }
71 }
72
73 $page = $this->pageLookup->getPageByReference( $page );
74 if ( $page === null || !$page->isRedirect() ) {
75 $this->procCache->set( $cacheKey, false );
76 return null;
77 }
78
79 $dbr = $this->dbProvider->getReplicaDatabase();
80 $row = $dbr->newSelectQueryBuilder()
81 ->select( [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ] )
82 ->from( 'redirect' )
83 ->where( [ 'rd_from' => $page->getId() ] )
84 ->caller( __METHOD__ )
85 ->fetchRow();
86
87 if ( !$row ) {
88 $this->logger->info(
89 'Found inconsistent redirect status; probably the page was deleted after it was loaded'
90 );
91 $this->procCache->set( $cacheKey, false );
92 return null;
93 }
94
95 $target = $this->createRedirectTarget(
96 $row->rd_namespace,
97 $row->rd_title,
98 $row->rd_fragment,
99 $row->rd_interwiki
100 );
101
102 $this->procCache->set( $cacheKey, $target );
103 return $target;
104 }
105
116 public function updateRedirectTarget(
117 PageIdentity $page,
118 ?LinkTarget $target,
119 ?bool $lastRevWasRedirect = null
120 ) {
121 // Always update redirects (target link might have changed)
122 // Update/Insert if we don't know if the last revision was a redirect or not
123 // Delete if changing from redirect to non-redirect
124 $isRedirect = $target !== null;
125 $cacheKey = self::makeCacheKey( $page );
126
127 if ( !$isRedirect && $lastRevWasRedirect === false ) {
128 $this->procCache->set( $cacheKey, false );
129 return true;
130 }
131
132 if ( $isRedirect ) {
133 $rt = Title::newFromLinkTarget( $target );
134 if ( !$rt->isValidRedirectTarget() ) {
135 // Don't put a bad redirect into the database (T278367)
136 $this->procCache->set( $cacheKey, false );
137 return false;
138 }
139
140 $dbw = $this->dbProvider->getPrimaryDatabase();
141 $dbw->startAtomic( __METHOD__ );
142
143 $truncatedFragment = self::truncateFragment( $rt->getFragment() );
144 $dbw->newInsertQueryBuilder()
145 ->insertInto( 'redirect' )
146 ->row( [
147 'rd_from' => $page->getId(),
148 'rd_namespace' => $rt->getNamespace(),
149 'rd_title' => $rt->getDBkey(),
150 'rd_fragment' => $truncatedFragment,
151 'rd_interwiki' => $rt->getInterwiki(),
152 ] )
153 ->onDuplicateKeyUpdate()
154 ->uniqueIndexFields( [ 'rd_from' ] )
155 ->set( [
156 'rd_namespace' => $rt->getNamespace(),
157 'rd_title' => $rt->getDBkey(),
158 'rd_fragment' => $truncatedFragment,
159 'rd_interwiki' => $rt->getInterwiki(),
160 ] )
161 ->caller( __METHOD__ )
162 ->execute();
163
164 $dbw->endAtomic( __METHOD__ );
165
166 $this->procCache->set(
167 $cacheKey,
168 $this->createRedirectTarget(
169 $rt->getNamespace(),
170 $rt->getDBkey(),
171 $truncatedFragment,
172 $rt->getInterwiki()
173 )
174 );
175 } else {
176 $dbw = $this->dbProvider->getPrimaryDatabase();
177 // This is not a redirect, remove row from redirect table
178 $dbw->newDeleteQueryBuilder()
179 ->deleteFrom( 'redirect' )
180 ->where( [ 'rd_from' => $page->getId() ] )
181 ->caller( __METHOD__ )
182 ->execute();
183
184 $this->procCache->set( $cacheKey, false );
185 }
186
187 if ( $page->getNamespace() === NS_FILE ) {
188 $this->repoGroup->getLocalRepo()->invalidateImageRedirect( $page );
189 }
190
191 return true;
192 }
193
200 public function clearCache( $page ) {
201 $this->procCache->clear( self::makeCacheKey( $page ) );
202 }
203
209 private static function makeCacheKey( $page ) {
210 return "{$page->getNamespace()}:{$page->getDBkey()}";
211 }
212
223 private function createRedirectTarget( $namespace, $title, $fragment, $interwiki ): ?LinkTarget {
224 // (T203942) We can't redirect to Media namespace because it's virtual.
225 // We don't want to modify Title objects farther down the
226 // line. So, let's fix this here by changing to File namespace.
227 if ( $namespace == NS_MEDIA ) {
228 $namespace = NS_FILE;
229 }
230
231 // mimic behaviour of self::insertRedirectEntry for fragments that didn't
232 // come from the redirect table
233 $fragment = self::truncateFragment( $fragment );
234
235 // T261347: be defensive when fetching data from the redirect table.
236 // Use Title::makeTitleSafe(), and if that returns null, ignore the
237 // row. In an ideal world, the DB would be cleaned up after a
238 // namespace change, but nobody could be bothered to do that.
239 $target = $this->titleParser->makeTitleValueSafe( $namespace, $title, $fragment, $interwiki );
240 if ( $target !== null && Title::newFromLinkTarget( $target )->isValidRedirectTarget() ) {
241 return $target;
242 }
243
244 return null;
245 }
246
253 private static function truncateFragment( $fragment ) {
254 return mb_strcut( $fragment, 0, 255 );
255 }
256}
const NS_FILE
Definition Defines.php:57
const NS_MEDIA
Definition Defines.php:39
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
Prioritized list of file repositories.
Definition RepoGroup.php:29
Service for storing and retrieving page redirect information.
clearCache( $page)
Clear process-cached redirect information for a page.
updateRedirectTarget(PageIdentity $page, ?LinkTarget $target, ?bool $lastRevWasRedirect=null)
Update the redirect target for a page.
getRedirectTarget(PageIdentity $page)
Get the redirect destination.
__construct(IConnectionProvider $dbProvider, PageLookup $pageLookup, TitleParser $titleParser, RepoGroup $repoGroup, LoggerInterface $logger)
A title parser service for MediaWiki.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
Store key-value entries in a size-limited in-memory LRU cache.
Represents the target of a wiki link.
Interface for objects (potentially) representing an editable wiki page.
getId( $wikiId=self::LOCAL)
Returns the page ID.
Service for looking up information about wiki pages.
getNamespace()
Returns the page's namespace number.
Service for resolving a wiki page redirect.
Provide primary and replica IDatabase connections.