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