Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 135
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialDisambiguationPageLinks
0.00% covered (danger)
0.00%
0 / 135
0.00% covered (danger)
0.00%
0 / 12
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 isExpensive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isSyndicated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
2
 getOrderFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sortDescending
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatResult
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 recache
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
90
 execute
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 fetchFromCache
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 preprocessResults
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * DisambiguationPageLinks SpecialPage for Disambiguator extension
4 * This page lists all pages that link to disambiguation pages.
5 *
6 * @file
7 * @ingroup Extensions
8 */
9
10namespace MediaWiki\Extension\Disambiguator\Specials;
11
12use Exception;
13use MediaWiki\Cache\LinkBatchFactory;
14use MediaWiki\Content\IContentHandlerFactory;
15use MediaWiki\Linker\LinksMigration;
16use MediaWiki\SpecialPage\QueryPage;
17use MediaWiki\Title\NamespaceInfo;
18use MediaWiki\Title\Title;
19use Wikimedia\Rdbms\DBError;
20use Wikimedia\Rdbms\IConnectionProvider;
21use Wikimedia\Rdbms\IDatabase;
22use Wikimedia\Rdbms\IResultWrapper;
23use Wikimedia\Rdbms\SelectQueryBuilder;
24
25class SpecialDisambiguationPageLinks extends QueryPage {
26
27    /** @var NamespaceInfo */
28    private $namespaceInfo;
29
30    /** @var LinkBatchFactory */
31    private $linkBatchFactory;
32
33    /** @var IContentHandlerFactory */
34    private $contentHandlerFactory;
35    private IConnectionProvider $dbProvider;
36    private LinksMigration $linksMigration;
37
38    /**
39     * @param NamespaceInfo $namespaceInfo
40     * @param LinkBatchFactory $linkBatchFactory
41     * @param IContentHandlerFactory $contentHandlerFactory
42     * @param IConnectionProvider $dbProvider
43     * @param LinksMigration $linksMigration
44     */
45    public function __construct(
46        NamespaceInfo $namespaceInfo,
47        LinkBatchFactory $linkBatchFactory,
48        IContentHandlerFactory $contentHandlerFactory,
49        IConnectionProvider $dbProvider,
50        LinksMigration $linksMigration
51    ) {
52        parent::__construct( 'DisambiguationPageLinks' );
53        $this->namespaceInfo = $namespaceInfo;
54        $this->linkBatchFactory = $linkBatchFactory;
55        $this->contentHandlerFactory = $contentHandlerFactory;
56        $this->dbProvider = $dbProvider;
57        $this->linksMigration = $linksMigration;
58    }
59
60    public function isExpensive() {
61        return true;
62    }
63
64    public function isSyndicated() {
65        return false;
66    }
67
68    public function getQueryInfo() {
69        [ $blNamespace, $blTitle ] = $this->linksMigration->getTitleFields( 'pagelinks' );
70        $queryInfo = $this->linksMigration->getQueryInfo( 'pagelinks', 'pagelinks' );
71        return [
72            'tables' => array_merge( $queryInfo['tables'], [
73                'p1' => 'page',
74                'p2' => 'page',
75                'page_props'
76            ] ),
77            // The fields we are selecting correspond with fields in the
78            // querycachetwo table so that the results are cachable.
79            'fields' => [
80                'value' => 'pl_from',
81                'namespace' => 'p2.page_namespace',
82                'title' => 'p2.page_title',
83                'to_namespace' => 'p1.page_namespace',
84                'to_title' => 'p1.page_title',
85            ],
86            'conds' => [
87                'pp_propname' => 'disambiguation',
88                'p2.page_namespace' => $this->namespaceInfo->getContentNamespaces(),
89                'p2.page_is_redirect != 1'
90            ],
91            'join_conds' => array_merge( $queryInfo['joins'], [
92                'p1' => [ 'JOIN', [ "$blNamespace = p1.page_namespace", "$blTitle = p1.page_title" ] ],
93                'page_props' => [ 'JOIN', [ 'p1.page_id = pp_page' ] ],
94                'p2' => [ 'JOIN', [ 'p2.page_id = pl_from' ] ]
95            ] )
96        ];
97    }
98
99    /**
100     * Order the results by ID of the linking page (value), namespace of the
101     * linked page, and title of the linked page. This function only affects
102     * ordering when not using $wgMiserMode.
103     *
104     * @return array
105     */
106    public function getOrderFields() {
107        return [ 'value', 'to_namespace', 'to_title' ];
108    }
109
110    public function sortDescending() {
111        return false;
112    }
113
114    public function formatResult( $skin, $result ) {
115        $fromTitle = Title::makeTitle( $result->namespace, $result->title );
116        $toTitle = Title::makeTitle( $result->to_namespace, $result->to_title );
117
118        $linkRenderer = $this->getLinkRenderer();
119        $from = $linkRenderer->makeKnownLink( $fromTitle );
120        $arr = $this->getLanguage()->getArrow();
121        $to = $linkRenderer->makeKnownLink( $toTitle );
122
123        // Check if user is allowed to edit
124        if (
125            $this->getAuthority()->isAllowed( 'edit' ) &&
126            $this->contentHandlerFactory->getContentHandler( $fromTitle->getContentModel() )->supportsDirectEditing()
127        ) {
128            $edit = $linkRenderer->makeLink(
129                $fromTitle,
130                $this->msg( 'parentheses', $this->msg( 'editlink' )->text() )->text(),
131                [],
132                [ 'redirect' => 'no', 'action' => 'edit' ]
133            );
134            return "$from $edit $arr $to";
135        }
136
137        return "$from $arr $to";
138    }
139
140    protected function getGroupName() {
141        return 'pages';
142    }
143
144    /**
145     * Clear the cache and save new results
146     *
147     * @param int|bool $limit Limit for SQL statement
148     * @param bool $ignoreErrors Whether to ignore database errors
149     * @return bool|int
150     * @throws DBError|Exception
151     */
152    public function recache( $limit, $ignoreErrors = true ) {
153        if ( !$this->isCacheable() ) {
154            return 0;
155        }
156
157        $fname = get_class( $this ) . '::recache';
158        $dbw = $this->dbProvider->getPrimaryDatabase();
159
160        try {
161            // Do query
162            $res = $this->reallyDoQuery( $limit, false );
163            $num = false;
164            if ( $res ) {
165                $num = $res->numRows();
166                // Fetch results
167                $vals = [];
168                foreach ( $res as $row ) {
169                    if ( isset( $row->value ) ) {
170                        if ( $this->usesTimestamps() ) {
171                            $value = wfTimestamp( TS_UNIX, $row->value );
172                        } else {
173                            $value = intval( $row->value );
174                        }
175                    } else {
176                        $value = 0;
177                    }
178
179                    $vals[] = [
180                        'qcc_type' => $this->getName(),
181                        'qcc_value' => $value,
182                        'qcc_namespace' => $row->namespace,
183                        'qcc_title' => $row->title,
184                        'qcc_namespacetwo' => $row->to_namespace,
185                        'qcc_titletwo' => $row->to_title,
186                    ];
187                }
188
189                $dbw->startAtomic( __METHOD__ );
190                // Clear out any old cached data
191                $dbw->newDeleteQueryBuilder()
192                    ->deleteFrom( 'querycachetwo' )
193                    ->where( [ 'qcc_type' => $this->getName() ] )
194                    ->caller( $fname )
195                    ->execute();
196                // Save results into the querycachetwo table on the master
197                if ( count( $vals ) ) {
198                    $dbw->newInsertQueryBuilder()
199                        ->insertInto( 'querycachetwo' )
200                        ->rows( $vals )
201                        ->caller( $fname )
202                        ->execute();
203                }
204                // Update the querycache_info record for the page
205                $dbw->newDeleteQueryBuilder()
206                    ->deleteFrom( 'querycache_info' )
207                    ->where( [ 'qci_type' => $this->getName() ] )
208                    ->caller( $fname )
209                    ->execute();
210                $dbw->newInsertQueryBuilder()
211                    ->insertInto( 'querycache_info' )
212                    ->row( [ 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ] )
213                    ->caller( $fname )
214                    ->execute();
215                $dbw->endAtomic( __METHOD__ );
216            }
217        } catch ( DBError $e ) {
218            if ( !$ignoreErrors ) {
219                // Report query error
220                throw $e;
221            }
222            // Set result to false to indicate error
223            $num = false;
224        }
225
226        return $num;
227    }
228
229    public function execute( $par ) {
230        $this->addHelpLink( 'Extension:Disambiguator' );
231        parent::execute( $par );
232    }
233
234    /**
235     * Fetch the query results from the query cache
236     *
237     * @param int|bool $limit Numerical limit or false for no limit
238     * @param int|bool $offset Numerical offset or false for no offset
239     * @return IResultWrapper
240     */
241    public function fetchFromCache( $limit, $offset = false ) {
242        $dbr = $this->dbProvider->getReplicaDatabase();
243
244        $queryBuilder = $dbr->newSelectQueryBuilder()
245            ->select( [
246                'value' => 'qcc_value',
247                'namespace' => 'qcc_namespace',
248                'title' => 'qcc_title',
249                'to_namespace' => 'qcc_namespacetwo',
250                'to_title' => 'qcc_titletwo',
251            ] )
252            ->from( 'querycachetwo' )
253            ->where( [ 'qcc_type' => $this->getName() ] )
254            ->caller( __METHOD__ );
255
256        if ( $limit !== false ) {
257            $queryBuilder->limit( intval( $limit ) );
258        }
259        if ( $offset !== false ) {
260            $queryBuilder->offset( intval( $offset ) );
261        }
262        // Set sort order. This should match the ordering in getOrderFields().
263        $queryBuilder->orderBy( [ 'qcc_value', 'qcc_namespacetwo' ] );
264        if ( $this->sortDescending() ) {
265            $queryBuilder->orderBy( 'qcc_titletwo', SelectQueryBuilder::SORT_DESC );
266        } else {
267            $queryBuilder->orderBy( 'qcc_titletwo', SelectQueryBuilder::SORT_ASC );
268        }
269
270        return $queryBuilder->fetchResultSet();
271    }
272
273    /**
274     * Cache page content model for performance
275     *
276     * @param IDatabase $db
277     * @param IResultWrapper $res
278     */
279    public function preprocessResults( $db, $res ) {
280        if ( !$res->numRows() ) {
281            return;
282        }
283
284        $batch = $this->linkBatchFactory->newLinkBatch();
285        foreach ( $res as $row ) {
286            $batch->add( $row->namespace, $row->title );
287            $batch->add( $row->to_namespace, $row->to_title );
288        }
289        $batch->execute();
290
291        $res->seek( 0 );
292    }
293}