Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
52.00% |
52 / 100 |
|
10.00% |
1 / 10 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryReadingListEntries | |
52.00% |
52 / 100 |
|
10.00% |
1 / 10 |
145.25 | |
0.00% |
0 / 1 |
execute | |
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
3.19 | |||
executeGenerator | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
run | |
76.00% |
38 / 50 |
|
0.00% |
0 / 1 |
18.11 | |||
getAllowedParams | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
getHelpUrls | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getReverseInterwikiLookup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getResultItem | |
25.00% |
2 / 8 |
|
0.00% |
0 / 1 |
6.80 | |||
getResultTitle | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\ReadingLists\Api; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Api\ApiPageSet; |
7 | use MediaWiki\Api\ApiQueryGeneratorBase; |
8 | use MediaWiki\Extension\ReadingLists\Doc\ReadingListEntryRow; |
9 | use MediaWiki\Extension\ReadingLists\ReadingListRepositoryException; |
10 | use MediaWiki\Extension\ReadingLists\ReverseInterwikiLookup; |
11 | use MediaWiki\Extension\ReadingLists\Utils; |
12 | use MediaWiki\MediaWikiServices; |
13 | use MediaWiki\Title\Title; |
14 | use Wikimedia\ParamValidator\ParamValidator; |
15 | |
16 | /** |
17 | * API list module for getting list contents. |
18 | */ |
19 | class 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 | } |