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