Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.00% covered (warning)
52.00%
52 / 100
10.00% covered (danger)
10.00%
1 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryReadingListEntries
52.00% covered (warning)
52.00%
52 / 100
10.00% covered (danger)
10.00%
1 / 10
145.25
0.00% covered (danger)
0.00%
0 / 1
 execute
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 executeGenerator
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 run
76.00% covered (warning)
76.00%
38 / 50
0.00% covered (danger)
0.00%
0 / 1
18.11
 getAllowedParams
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReverseInterwikiLookup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResultItem
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
6.80
 getResultTitle
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace MediaWiki\Extension\ReadingLists\Api;
4
5use ApiPageSet;
6use ApiQueryGeneratorBase;
7use LogicException;
8use MediaWiki\Extension\ReadingLists\Doc\ReadingListEntryRow;
9use MediaWiki\Extension\ReadingLists\ReadingListRepositoryException;
10use MediaWiki\Extension\ReadingLists\ReverseInterwikiLookup;
11use MediaWiki\Extension\ReadingLists\Utils;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Title\Title;
14use Wikimedia\ParamValidator\ParamValidator;
15
16/**
17 * API list module for getting list contents.
18 */
19class ApiQueryReadingListEntries extends ApiQueryGeneratorBase {
20
21    use ApiTrait;
22    use ApiQueryTrait;
23
24    /** @var string API module prefix */
25    private static $prefix = 'rle';
26
27    /**
28     * @inheritDoc
29     */
30    public function execute() {
31        try {
32            $this->run();
33        } catch ( ReadingListRepositoryException $e ) {
34            $this->dieWithException( $e );
35        }
36    }
37
38    /**
39     * @inheritDoc
40     */
41    public function executeGenerator( $resultPageSet ) {
42        try {
43            $this->run( $resultPageSet );
44        } catch ( ReadingListRepositoryException $e ) {
45            $this->dieWithException( $e );
46        }
47    }
48
49    /**
50     * Main API logic.
51     * @param ApiPageSet|null $resultPageSet
52     */
53    private function run( ApiPageSet $resultPageSet = null ) {
54        if ( !$this->getUser()->isNamed() ) {
55            $this->dieWithError( [ 'apierror-mustbeloggedin',
56                $this->msg( 'action-viewmyprivateinfo' ) ], 'notloggedin' );
57        }
58        $this->checkUserRightsAny( 'viewmyprivateinfo' );
59
60        $lists = $this->getParameter( 'lists' );
61        $changedSince = $this->getParameter( 'changedsince' );
62        $sort = $this->getParameter( 'sort' );
63        $dir = $this->getParameter( 'dir' );
64        $limit = $this->getParameter( 'limit' );
65        $continue = $this->getParameter( 'continue' );
66
67        $mode = $changedSince !== null ? self::$MODE_CHANGES : self::$MODE_ALL;
68        if ( $sort === null ) {
69            $sort = ( $mode === self::$MODE_CHANGES ) ? 'updated' : 'name';
70        }
71        if ( $mode === self::$MODE_CHANGES && $sort === 'name' ) {
72            // We don't have the right DB index for this. Wouldn't make much sense anyways.
73            $errorMessage = $this->msg( 'apierror-readinglists-invalidsort-notbyname', static::$prefix );
74            $this->dieWithError( $errorMessage, 'invalidparammix' );
75        }
76        $sort = self::$sortParamMap[$sort];
77        $dir = self::$sortParamMap[$dir];
78        $continue = $this->decodeContinuationParameter( $continue, $mode, $sort );
79
80        $this->requireOnlyOneParameter( $this->extractRequestParams(), 'lists', 'changedsince' );
81        if ( $mode === self::$MODE_CHANGES ) {
82            $expiry = Utils::getDeletedExpiry();
83            if ( $changedSince < $expiry ) {
84                $errorMessage = $this->msg( 'apierror-readinglists-too-old', static::$prefix,
85                    wfTimestamp( TS_ISO_8601, $expiry ) );
86                $this->dieWithError( $errorMessage );
87            }
88        }
89
90        $path = [ 'query', $this->getModuleName() ];
91        $result = $this->getResult();
92        $result->addIndexedTagName( $path, 'entry' );
93
94        $repository = $this->getReadingListRepository( $this->getUser() );
95        if ( $mode === self::$MODE_CHANGES ) {
96            $res = $repository->getListEntriesByDateUpdated( $changedSince, $dir, $limit + 1, $continue );
97        } else {
98            $res = $repository->getListEntries( $lists, $sort, $dir, $limit + 1, $continue );
99        }
100        '@phan-var stdClass[] $res';
101        $titles = [];
102        $fits = true;
103        foreach ( $res as $i => $row ) {
104            // @phan-suppress-next-line PhanTypeMismatchArgument
105            $item = $this->getResultItem( $row, $mode );
106            if ( $i >= $limit ) {
107                // we reached the extra row.
108                $this->setContinueEnumParameter( 'continue',
109                    $this->encodeContinuationParameter( $item, $mode, $sort ) );
110                break;
111            }
112            if ( $resultPageSet ) {
113                // @phan-suppress-next-line PhanTypeMismatchArgument
114                $titles[] = $this->getResultTitle( $row );
115            } else {
116                $fits = $result->addValue( $path, null, $item );
117            }
118            if ( !$fits ) {
119                $this->setContinueEnumParameter( 'continue',
120                    $this->encodeContinuationParameter( $item, $mode, $sort ) );
121                break;
122            }
123        }
124        if ( $resultPageSet ) {
125            $resultPageSet->populateFromTitles( $titles );
126        }
127    }
128
129    /**
130     * @inheritDoc
131     */
132    protected function getAllowedParams() {
133        return [
134            'lists' => [
135                ParamValidator::PARAM_TYPE => 'integer',
136                ParamValidator::PARAM_ISMULTI => true,
137            ],
138            'changedsince' => [
139                ParamValidator::PARAM_TYPE => 'timestamp',
140                self::PARAM_HELP_MSG => $this->msg( 'apihelp-query+readinglistentries-param-changedsince',
141                    wfTimestamp( TS_ISO_8601, Utils::getDeletedExpiry() ) ),
142            ],
143        ] + $this->getAllowedSortParams();
144    }
145
146    /**
147     * @inheritDoc
148     */
149    public function getHelpUrls() {
150        return [
151            'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
152        ];
153    }
154
155    /**
156     * @inheritDoc
157     */
158    protected function getExamplesMessages() {
159        $prefix = static::$prefix;
160        return [
161            "action=query&list=readinglistentries&{$prefix}lists=10|11|12"
162                => 'apihelp-query+readinglistentries-example-1',
163            "action=query&list=readinglistentries&{$prefix}changedsince=2013-01-01T00:00:00Z"
164                => 'apihelp-query+readinglistentries-example-2',
165        ];
166    }
167
168    /**
169     * @inheritDoc
170     */
171    public function isInternal() {
172        // ReadingLists API is still experimental
173        return true;
174    }
175
176    /**
177     * Initialize a reverse interwiki lookup helper.
178     * @return ReverseInterwikiLookup
179     */
180    private function getReverseInterwikiLookup() {
181        return MediaWikiServices::getInstance()->getService( 'ReverseInterwikiLookup' );
182    }
183
184    /**
185     * Transform a row into an API result item
186     * @param ReadingListEntryRow $row
187     * @param string $mode One of the MODE_* constants.
188     * @return array
189     */
190    private function getResultItem( $row, $mode ) {
191        if ( $row->rle_deleted && $mode !== self::$MODE_CHANGES ) {
192            $this->logger->error( 'Deleted row returned in non-changes mode', [
193                'rle_id' => $row->rle_id,
194                'rl_id' => $row->rle_rl_id,
195                'user_central_id' => $row->rle_user_id,
196            ] );
197            throw new LogicException( 'Deleted row returned in non-changes mode' );
198        }
199        return $this->getListEntryFromRow( $row );
200    }
201
202    /**
203     * Transform a row into an API result item
204     * @param ReadingListEntryRow $row
205     * @return Title|string
206     */
207    private function getResultTitle( $row ) {
208        $interwikiPrefix = $this->getReverseInterwikiLookup()->lookup( $row->rlp_project );
209        if ( is_string( $interwikiPrefix ) ) {
210            if ( $interwikiPrefix === '' ) {
211                $title = Title::newFromText( $row->rle_title );
212                if ( !$title ) {
213                    // Validation differences between wikis? Let's just return it as it is.
214                    $title = Title::makeTitle( NS_MAIN, $row->rle_title );
215                }
216            } else {
217                // We have no way of telling what the namespace is, but Title does not support
218                // foreign namespaces anyway. Let's just pretend it's in the main namespace so
219                // the prefixed title string works out as expected.
220                $title = Title::makeTitle( NS_MAIN, $row->rle_title, '', $interwikiPrefix );
221            }
222            return $title;
223        } elseif ( is_array( $interwikiPrefix ) ) {
224            $title = implode( ':', array_slice( $interwikiPrefix, 1 ) ) . ':' . $row->rle_title;
225            $prefix = $interwikiPrefix[0];
226            return Title::makeTitle( NS_MAIN, $title, '', $prefix );
227        }
228        // For lack of a better option let's create an invalid title.
229        // ApiPageSet::populateFromTitles() is not documented to accept strings
230        // but it will actually work.
231        return 'Invalid project|' . $row->rlp_project . '|' . $row->rle_title;
232    }
233
234}