3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
19 * @author Derick Alangi
22 namespace MediaWiki\Page
;
25 use MediaWiki\Linker\LinkTarget
;
26 use MediaWiki\Title\Title
;
27 use MediaWiki\Title\TitleParser
;
28 use MediaWiki\Title\TitleValue
;
29 use Psr\Log\LoggerInterface
;
31 use Wikimedia\Rdbms\IConnectionProvider
;
34 * Service for storing and retrieving page redirect information.
39 class 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;
47 public function __construct(
48 IConnectionProvider
$dbProvider,
49 PageLookup
$pageLookup,
50 TitleParser
$titleParser,
52 LoggerInterface
$logger
54 $this->dbProvider
= $dbProvider;
55 $this->pageLookup
= $pageLookup;
56 $this->titleParser
= $titleParser;
57 $this->repoGroup
= $repoGroup;
58 $this->logger
= $logger;
59 $this->procCache
= new MapCacheLRU( 16 );
62 public function getRedirectTarget( PageIdentity
$page ): ?LinkTarget
{
63 $cacheKey = self
::makeCacheKey( $page );
64 $cachedValue = $this->procCache
->get( $cacheKey );
65 if ( $cachedValue !== null ) {
66 return $cachedValue ?
: null;
69 // Handle redirects for files included from foreign image repositories.
70 if ( $page->getNamespace() === NS_FILE
) {
71 $file = $this->repoGroup
->findFile( $page );
72 if ( $file && !$file->isLocal() ) {
73 $from = $file->getRedirected();
74 $to = $file->getName();
75 if ( $from === null ||
$from === $to ) {
76 $this->procCache
->set( $cacheKey, false );
80 $target = new TitleValue( NS_FILE
, $to );
81 $this->procCache
->set( $cacheKey, $target );
86 $page = $this->pageLookup
->getPageByReference( $page );
87 if ( $page === null ||
!$page->isRedirect() ) {
88 $this->procCache
->set( $cacheKey, false );
92 $dbr = $this->dbProvider
->getReplicaDatabase();
93 $row = $dbr->newSelectQueryBuilder()
94 ->select( [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ] )
96 ->where( [ 'rd_from' => $page->getId() ] )
97 ->caller( __METHOD__
)
102 'Found inconsistent redirect status; probably the page was deleted after it was loaded'
104 $this->procCache
->set( $cacheKey, false );
108 $target = $this->createRedirectTarget(
115 $this->procCache
->set( $cacheKey, $target );
120 * Update the redirect target for a page.
122 * @param PageIdentity $page The page to update the redirect target for.
123 * @param LinkTarget|null $target The new redirect target, or `null` if this is not a redirect.
124 * @param bool|null $lastRevWasRedirect Whether the last revision was a redirect, or `null`
125 * if not known. If set, this allows eliding writes to the redirect table.
127 * @return bool `true` on success, `false` on failure.
129 public function updateRedirectTarget(
132 ?
bool $lastRevWasRedirect = null
134 // Always update redirects (target link might have changed)
135 // Update/Insert if we don't know if the last revision was a redirect or not
136 // Delete if changing from redirect to non-redirect
137 $isRedirect = $target !== null;
138 $cacheKey = self
::makeCacheKey( $page );
140 if ( !$isRedirect && $lastRevWasRedirect === false ) {
141 $this->procCache
->set( $cacheKey, false );
146 $rt = Title
::newFromLinkTarget( $target );
147 if ( !$rt->isValidRedirectTarget() ) {
148 // Don't put a bad redirect into the database (T278367)
149 $this->procCache
->set( $cacheKey, false );
153 $dbw = $this->dbProvider
->getPrimaryDatabase();
154 $dbw->startAtomic( __METHOD__
);
156 $truncatedFragment = self
::truncateFragment( $rt->getFragment() );
157 $dbw->newInsertQueryBuilder()
158 ->insertInto( 'redirect' )
160 'rd_from' => $page->getId(),
161 'rd_namespace' => $rt->getNamespace(),
162 'rd_title' => $rt->getDBkey(),
163 'rd_fragment' => $truncatedFragment,
164 'rd_interwiki' => $rt->getInterwiki(),
166 ->onDuplicateKeyUpdate()
167 ->uniqueIndexFields( [ 'rd_from' ] )
169 'rd_namespace' => $rt->getNamespace(),
170 'rd_title' => $rt->getDBkey(),
171 'rd_fragment' => $truncatedFragment,
172 'rd_interwiki' => $rt->getInterwiki(),
174 ->caller( __METHOD__
)
177 $dbw->endAtomic( __METHOD__
);
179 $this->procCache
->set(
181 $this->createRedirectTarget(
189 $dbw = $this->dbProvider
->getPrimaryDatabase();
190 // This is not a redirect, remove row from redirect table
191 $dbw->newDeleteQueryBuilder()
192 ->deleteFrom( 'redirect' )
193 ->where( [ 'rd_from' => $page->getId() ] )
194 ->caller( __METHOD__
)
197 $this->procCache
->set( $cacheKey, false );
200 if ( $page->getNamespace() === NS_FILE
) {
201 $this->repoGroup
->getLocalRepo()->invalidateImageRedirect( $page );
208 * Clear process-cached redirect information for a page.
210 * @param LinkTarget|PageIdentity $page The page to clear the cache for.
213 public function clearCache( $page ) {
214 $this->procCache
->clear( self
::makeCacheKey( $page ) );
218 * Create a process cache key for the given page.
219 * @param LinkTarget|PageIdentity $page The page to create a cache key for.
220 * @return string Cache key.
222 private static function makeCacheKey( $page ) {
223 return "{$page->getNamespace()}:{$page->getDBkey()}";
227 * Create a LinkTarget appropriate for use as a redirect target.
229 * @param int $namespace The namespace of the article
230 * @param string $title Database key form
231 * @param string $fragment The link fragment (after the "#")
232 * @param string $interwiki Interwiki prefix
234 * @return LinkTarget|null `LinkTarget`, or `null` if this is not a valid redirect
236 private function createRedirectTarget( $namespace, $title, $fragment, $interwiki ): ?LinkTarget
{
237 // (T203942) We can't redirect to Media namespace because it's virtual.
238 // We don't want to modify Title objects farther down the
239 // line. So, let's fix this here by changing to File namespace.
240 if ( $namespace == NS_MEDIA
) {
241 $namespace = NS_FILE
;
244 // mimic behaviour of self::insertRedirectEntry for fragments that didn't
245 // come from the redirect table
246 $fragment = self
::truncateFragment( $fragment );
248 // T261347: be defensive when fetching data from the redirect table.
249 // Use Title::makeTitleSafe(), and if that returns null, ignore the
250 // row. In an ideal world, the DB would be cleaned up after a
251 // namespace change, but nobody could be bothered to do that.
252 $target = $this->titleParser
->makeTitleValueSafe( $namespace, $title, $fragment, $interwiki );
253 if ( $target !== null && Title
::newFromLinkTarget( $target )->isValidRedirectTarget() ) {
261 * Truncate link fragment to maximum storable value
263 * @param string $fragment The link fragment (after the "#")
266 private static function truncateFragment( $fragment ) {
267 return mb_strcut( $fragment, 0, 255 );