Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
12.90% covered (danger)
12.90%
8 / 62
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiTrait
12.90% covered (danger)
12.90%
8 / 62
40.00% covered (danger)
40.00%
2 / 5
437.94
0.00% covered (danger)
0.00%
0 / 1
 getCirrusConnection
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getSearchConfig
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 loadDocuments
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 determineCirrusDocId
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
72
 hasRedirect
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 getUser
n/a
0 / 0
n/a
0 / 0
0
1<?php
2
3namespace CirrusSearch\Api;
4
5use CirrusSearch\Connection;
6use CirrusSearch\SearchConfig;
7use CirrusSearch\Searcher;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Revision\RevisionRecord;
10use MediaWiki\Revision\SlotRecord;
11use MediaWiki\Title\Title;
12use MediaWiki\User\User;
13
14trait ApiTrait {
15    /** @var Connection */
16    private $connection;
17    /** @var SearchConfig */
18    private $searchConfig;
19
20    /**
21     * @return Connection
22     */
23    public function getCirrusConnection() {
24        if ( $this->connection === null ) {
25            $this->connection = new Connection( $this->getSearchConfig() );
26        }
27        return $this->connection;
28    }
29
30    /**
31     * @return SearchConfig
32     */
33    protected function getSearchConfig() {
34        if ( $this->searchConfig === null ) {
35            $this->searchConfig = MediaWikiServices::getInstance()
36                ->getConfigFactory()
37                ->makeConfig( 'CirrusSearch' );
38        }
39        return $this->searchConfig;
40    }
41
42    /**
43     * @param Title $title
44     * @param string[]|bool $sourceFiltering source filtering to apply
45     * @return array
46     */
47    public function loadDocuments( Title $title, $sourceFiltering = true ) {
48        [ $docId, $hasRedirects ] = $this->determineCirrusDocId( $title );
49        if ( $docId === null ) {
50            return [];
51        }
52        // could be optimized by implementing multi-get but not
53        // expecting much usage except debugging/tests.
54        $searcher = new Searcher( $this->getCirrusConnection(), 0, 0, $this->getSearchConfig(), [], $this->getUser() );
55        $esSources = $searcher->get( [ $docId ], $sourceFiltering );
56        $result = [];
57        if ( $esSources->isOK() ) {
58            foreach ( $esSources->getValue() as $esSource ) {
59                // If we have followed redirects only report the
60                // article dump if the redirect has been indexed. If it
61                // hasn't been indexed this document does not represent
62                // the original title.
63                if ( $hasRedirects &&
64                     !$this->hasRedirect( $esSource->getData(), $title )
65                ) {
66                    continue;
67                }
68
69                // If this was not a redirect and the title doesn't match that
70                // means a page was moved, but elasticsearch has not yet been
71                // updated. Don't return the document that doesn't actually
72                // represent the page (yet).
73                if ( !$hasRedirects && $esSource->getData()['title'] != $title->getText() ) {
74                    continue;
75                }
76
77                $result[] = [
78                    'index' => $esSource->getIndex(),
79                    'type' => $esSource->getType(),
80                    'id' => $esSource->getId(),
81                    'version' => $esSource->getVersion(),
82                    'source' => $esSource->getData(),
83                ];
84            }
85        }
86        return $result;
87    }
88
89    /**
90     * Trace redirects to find the page id the title should be indexed to in
91     * cirrussearch. Differs from Updater::traceRedirects in that this also
92     * supports archived pages. Archive support is important for integration
93     * tests that need to know when a page that was deleted from SQL was
94     * finally removed from elasticsearch.
95     *
96     * This still fails to find the correct page id if something was moved, as
97     * that page is renamed rather than being moved to the archive. We could
98     * further complicate things by looking into move logs but not sure that
99     * is worth the complication.
100     *
101     * @param Title $title
102     * @return array Two element array containing first the cirrus doc id
103     *  the title should have been indexed into elasticsearch and second a
104     *  boolean indicating if redirects were followed. If the page would
105     *  not be indexed (for example a redirect loop, or redirect to
106     *  invalid page) the first array element will be null.
107     */
108    private function determineCirrusDocId( Title $title ) {
109        $hasRedirects = false;
110        $seen = [];
111        $now = wfTimestamp( TS_MW );
112        $services = MediaWikiServices::getInstance();
113        $contentHandlerFactory = $services->getContentHandlerFactory();
114        $archivedRevisionLookup = $services->getArchivedRevisionLookup();
115        while ( true ) {
116            if ( isset( $seen[$title->getPrefixedText()] ) || count( $seen ) > 10 ) {
117                return [ null, $hasRedirects ];
118            }
119            $seen[$title->getPrefixedText()] = true;
120
121            // To help the integration tests figure out when a deleted page has
122            // been removed from the elasticsearch index we lookup the page in
123            // the archive to get it's page id. getPreviousRevisionRecord will
124            // check both the archive and live content to return the most recent.
125            $revRecord = $archivedRevisionLookup->getPreviousRevisionRecord( $title, $now );
126            if ( !$revRecord ) {
127                return [ null, $hasRedirects ];
128            }
129
130            $pageId = $revRecord->getPageId();
131            $mainSlot = $revRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
132            $handler = $contentHandlerFactory->getContentHandler( $mainSlot->getModel() );
133            if ( !$handler->supportsRedirects() ) {
134                return [ $pageId, $hasRedirects ];
135            }
136            $content = $mainSlot->getContent();
137            // getUltimateRedirectTarget() would be prefered, but it wont find
138            // archive pages...
139            if ( !$content->isRedirect() ) {
140                return [ $this->getSearchConfig()->makeId( $pageId ), $hasRedirects ];
141            }
142            $redirect = $content->getRedirectTarget();
143            if ( !$redirect ) {
144                // TODO: Can this happen?
145                return [ $pageId, $hasRedirects ];
146            }
147
148            $hasRedirects = true;
149            $title = $redirect;
150        }
151    }
152
153    /**
154     * @param array $source _source document from elasticsearch
155     * @param Title $title Title to check for redirect
156     * @return bool True when $title is stored as a redirect in $source
157     */
158    private function hasRedirect( array $source, Title $title ) {
159        if ( !isset( $source['redirect'] ) ) {
160            return false;
161        }
162        foreach ( $source['redirect'] as $redirect ) {
163            if ( $redirect['namespace'] === $title->getNamespace()
164                && $redirect['title'] === $title->getText()
165            ) {
166                return true;
167            }
168        }
169        return false;
170    }
171
172    /**
173     * @return User
174     */
175    abstract public function getUser();
176
177}