Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
70.37% |
76 / 108 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryReadingLists | |
70.37% |
76 / 108 |
|
16.67% |
1 / 6 |
48.39 | |
0.00% |
0 / 1 |
execute | |
80.88% |
55 / 68 |
|
0.00% |
0 / 1 |
24.08 | |||
getAllowedParams | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 | |||
getHelpUrls | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getResultItem | |
28.57% |
2 / 7 |
|
0.00% |
0 / 1 |
6.28 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\ReadingLists\Api; |
4 | |
5 | use ApiQueryBase; |
6 | use DateTime; |
7 | use DateTimeZone; |
8 | use LogicException; |
9 | use MediaWiki\Extension\ReadingLists\Doc\ReadingListRow; |
10 | use MediaWiki\Extension\ReadingLists\ReadingListRepositoryException; |
11 | use MediaWiki\Extension\ReadingLists\Utils; |
12 | use MediaWiki\Utils\MWTimestamp; |
13 | use Wikimedia\ParamValidator\ParamValidator; |
14 | |
15 | /** |
16 | * API meta module for getting list metadata. |
17 | */ |
18 | class ApiQueryReadingLists extends ApiQueryBase { |
19 | |
20 | use ApiTrait; |
21 | use ApiQueryTrait; |
22 | |
23 | /** @var string API module prefix */ |
24 | private static $prefix = 'rl'; |
25 | |
26 | /** |
27 | * @inheritDoc |
28 | */ |
29 | public function execute() { |
30 | try { |
31 | if ( !$this->getUser()->isNamed() ) { |
32 | $this->dieWithError( [ 'apierror-mustbeloggedin', |
33 | $this->msg( 'action-viewmyprivateinfo' ) ], 'notloggedin' ); |
34 | } |
35 | $this->checkUserRightsAny( 'viewmyprivateinfo' ); |
36 | |
37 | $listId = $this->getParameter( 'list' ); |
38 | $changedSince = $this->getParameter( 'changedsince' ); |
39 | $project = $this->getParameter( 'project' ); |
40 | $title = $this->getParameter( 'title' ); |
41 | $sort = $this->getParameter( 'sort' ); |
42 | $dir = $this->getParameter( 'dir' ); |
43 | $limit = $this->getParameter( 'limit' ); |
44 | $continue = $this->getParameter( 'continue' ); |
45 | |
46 | $path = [ 'query', $this->getModuleName() ]; |
47 | $result = $this->getResult(); |
48 | $result->addIndexedTagName( $path, 'list' ); |
49 | $repository = $this->getReadingListRepository( $this->getUser() ); |
50 | |
51 | $mode = null; |
52 | $this->requireMaxOneParameter( $this->extractRequestParams(), 'list', 'title', 'changedsince' ); |
53 | if ( $project !== null && $title !== null ) { |
54 | $mode = self::$MODE_PAGE; |
55 | } elseif ( $project !== null || $title !== null ) { |
56 | $errorMessage = $this->msg( 'apierror-readinglists-project-title-param', static::$prefix ); |
57 | $this->dieWithError( $errorMessage, 'missingparam' ); |
58 | } elseif ( $changedSince !== null ) { |
59 | $expiry = Utils::getDeletedExpiry(); |
60 | if ( $changedSince < $expiry ) { |
61 | $errorMessage = $this->msg( 'apierror-readinglists-too-old', static::$prefix, |
62 | wfTimestamp( TS_ISO_8601, $expiry ) ); |
63 | $this->dieWithError( $errorMessage ); |
64 | } |
65 | $mode = self::$MODE_CHANGES; |
66 | } elseif ( $listId !== null ) { |
67 | // FIXME 'dir' and 'limit' aren't compatible either but requireMaxOneParameter |
68 | // does not work with parameters which have a default value |
69 | $params = [ 'sort', 'continue' ]; |
70 | foreach ( $params as $name ) { |
71 | $this->requireMaxOneParameter( $this->extractRequestParams(), 'list', $name ); |
72 | } |
73 | $mode = self::$MODE_ID; |
74 | } else { |
75 | $mode = self::$MODE_ALL; |
76 | } |
77 | |
78 | if ( $sort === null ) { |
79 | $sort = ( $mode === self::$MODE_CHANGES ) ? 'updated' : 'name'; |
80 | } |
81 | $sort = self::$sortParamMap[$sort]; |
82 | $dir = self::$sortParamMap[$dir]; |
83 | $continue = $this->decodeContinuationParameter( $continue, $mode, $sort ); |
84 | // timestamp from before querying the DB |
85 | $timestamp = new DateTime( 'now', new DateTimeZone( 'GMT' ) ); |
86 | |
87 | if ( $mode === self::$MODE_PAGE ) { |
88 | $res = $repository->getListsByPage( $project, $title, $limit + 1, $continue ); |
89 | } elseif ( $mode === self::$MODE_CHANGES ) { |
90 | $res = $repository->getListsByDateUpdated( $changedSince, $sort, $dir, $limit + 1, $continue ); |
91 | } elseif ( $mode === self::$MODE_ID ) { |
92 | $res = [ $repository->selectValidList( $listId ) ]; |
93 | } else { |
94 | $res = $repository->getAllLists( $sort, $dir, $limit + 1, $continue ); |
95 | } |
96 | foreach ( $res as $i => $row ) { |
97 | $item = $this->getResultItem( $row, $mode ); |
98 | if ( $i >= $limit ) { |
99 | // we reached the extra row. |
100 | $this->setContinueEnumParameter( 'continue', |
101 | $this->encodeContinuationParameter( $item, $mode, $sort ) ); |
102 | break; |
103 | } |
104 | $fits = $result->addValue( $path, null, $item ); |
105 | if ( !$fits ) { |
106 | $this->setContinueEnumParameter( 'continue', |
107 | $this->encodeContinuationParameter( $item, $mode, $sort ) ); |
108 | break; |
109 | } |
110 | } |
111 | |
112 | // Add a timestamp that, when used in the changedsince parameter in the readinglists |
113 | // and readinglistentries modules, guarantees that no change will be skipped (at the |
114 | // cost of possibly repeating some changes in the current query). See T182706 for details. |
115 | if ( !$continue ) { |
116 | // Ignore continuations (the client should just use the timestamp received in the |
117 | // first step). Otherwise, backdate more than the max transaction duration, to |
118 | // prevent the situation where a DB write happens before the current request |
119 | // but is only committed after it. (If there is no max transaction duration, the |
120 | // client is on its own.) |
121 | $maxUserDBWriteDuration = $this->getConfig()->get( 'MaxUserDBWriteDuration' ); |
122 | if ( $maxUserDBWriteDuration ) { |
123 | $timestamp->modify( '-' . ( $maxUserDBWriteDuration + 1 ) . ' seconds' ); |
124 | } |
125 | $syncTimestamp = ( new MWTimestamp( $timestamp ) )->getTimestamp( TS_ISO_8601 ); |
126 | $result->addValue( 'query', 'readinglists-synctimestamp', $syncTimestamp ); |
127 | } |
128 | } catch ( ReadingListRepositoryException $e ) { |
129 | $this->dieWithException( $e ); |
130 | } |
131 | } |
132 | |
133 | /** |
134 | * @inheritDoc |
135 | */ |
136 | protected function getAllowedParams() { |
137 | return [ |
138 | 'list' => [ |
139 | ParamValidator::PARAM_TYPE => 'integer', |
140 | ParamValidator::PARAM_REQUIRED => false, |
141 | self::PARAM_MIN => 1, |
142 | ParamValidator::PARAM_DEFAULT => null, |
143 | ], |
144 | 'project' => [ |
145 | ParamValidator::PARAM_TYPE => 'string', |
146 | ], |
147 | 'title' => [ |
148 | ParamValidator::PARAM_TYPE => 'string', |
149 | ], |
150 | 'changedsince' => [ |
151 | ParamValidator::PARAM_TYPE => 'timestamp', |
152 | self::PARAM_HELP_MSG => $this->msg( 'apihelp-query+readinglists-param-changedsince', |
153 | wfTimestamp( TS_ISO_8601, Utils::getDeletedExpiry() ) ), |
154 | ], |
155 | ] + $this->getAllowedSortParams(); |
156 | } |
157 | |
158 | /** |
159 | * @inheritDoc |
160 | */ |
161 | public function getHelpUrls() { |
162 | return [ |
163 | 'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API', |
164 | ]; |
165 | } |
166 | |
167 | /** |
168 | * @inheritDoc |
169 | */ |
170 | protected function getExamplesMessages() { |
171 | $prefix = static::$prefix; |
172 | return [ |
173 | 'action=query&meta=readinglists' |
174 | => 'apihelp-query+readinglists-example-1', |
175 | "action=query&meta=readinglists&{$prefix}changedsince=2013-01-01T00:00:00Z" |
176 | => 'apihelp-query+readinglists-example-2', |
177 | "action=query&meta=readinglists&{$prefix}project=https%3A%2F%2Fen.wikipedia.org" |
178 | . "&{$prefix}title=Dog" |
179 | => 'apihelp-query+readinglists-example-3', |
180 | ]; |
181 | } |
182 | |
183 | /** |
184 | * @inheritDoc |
185 | */ |
186 | public function isInternal() { |
187 | // ReadingLists API is still experimental |
188 | return true; |
189 | } |
190 | |
191 | /** |
192 | * Transform a row into an API result item |
193 | * @param ReadingListRow $row List row, with additions from addExtraData(). |
194 | * @param string $mode One of the MODE_* constants. |
195 | * @return array |
196 | */ |
197 | private function getResultItem( $row, $mode ) { |
198 | if ( $row->rl_deleted && $mode !== self::$MODE_CHANGES ) { |
199 | $this->logger->error( 'Deleted row returned in non-changes mode', [ |
200 | 'rl_id' => $row->rl_id, |
201 | 'user_central_id' => $row->rl_user_id, |
202 | ] ); |
203 | throw new LogicException( 'Deleted row returned in non-changes mode' ); |
204 | } |
205 | return $this->getListFromRow( $row ); |
206 | } |
207 | |
208 | } |