Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.51% |
83 / 86 |
|
62.50% |
5 / 8 |
CRAP | |
0.00% |
0 / 1 |
ApiTrait | |
96.51% |
83 / 86 |
|
62.50% |
5 / 8 |
25 | |
0.00% |
0 / 1 |
factory | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getParent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
injectDatabaseDependencies | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getReadingListRepository | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
getBatchOps | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
11 | |||
requireAtLeastOneBatchParameter | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
3 | |||
getListFromRow | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
3.00 | |||
getListEntryFromRow | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
3.00 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\ReadingLists\Api; |
4 | |
5 | use MediaWiki\Api\ApiBase; |
6 | use MediaWiki\Api\ApiUsageException; |
7 | use MediaWiki\Extension\ReadingLists\Doc\ReadingListEntryRow; |
8 | use MediaWiki\Extension\ReadingLists\Doc\ReadingListEntryRowWithMergeFlag; |
9 | use MediaWiki\Extension\ReadingLists\Doc\ReadingListRow; |
10 | use MediaWiki\Extension\ReadingLists\Doc\ReadingListRowWithMergeFlag; |
11 | use MediaWiki\Extension\ReadingLists\ReadingListRepository; |
12 | use MediaWiki\Logger\LoggerFactory; |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\Message\Message; |
15 | use MediaWiki\User\CentralId\CentralIdLookup; |
16 | use MediaWiki\User\UserIdentity; |
17 | use Psr\Log\LoggerInterface; |
18 | use Wikimedia\Rdbms\LBFactory; |
19 | |
20 | /** |
21 | * Shared initialization and helper methods for the APIs. |
22 | * Classes using it must have a static $prefix property (the API module prefix). |
23 | * |
24 | * Issue with phan and traits - https://github.com/phan/phan/issues/1067 |
25 | */ |
26 | trait ApiTrait { |
27 | |
28 | /** @var LoggerInterface */ |
29 | private $logger; |
30 | |
31 | /** @var ReadingListRepository */ |
32 | private $repository; |
33 | |
34 | /** @var LBFactory */ |
35 | private $loadBalancerFactory; |
36 | |
37 | /** @var ApiBase */ |
38 | private $parent; |
39 | |
40 | /** |
41 | * Static entry point for initializing the module |
42 | * @param ApiBase $parent Parent module |
43 | * @param string $name Module name |
44 | * @return static |
45 | * @suppress PhanUndeclaredStaticProperty, PhanUndeclaredMethod |
46 | */ |
47 | public static function factory( ApiBase $parent, $name ) { |
48 | if ( static::$prefix ) { |
49 | // We are in one of the read modules, $parent is ApiQuery. |
50 | // This is an ApiQueryBase subclass so we need to pass ApiQuery. |
51 | $module = new static( $parent, $name, static::$prefix ); |
52 | } else { |
53 | // We are in one of the write submodules, $parent is ApiReadingLists. |
54 | // This is an ApiBase subclass so we need to pass ApiMain. |
55 | $module = new static( $parent->getMain(), $name, static::$prefix ); |
56 | } |
57 | $module->parent = $parent; |
58 | |
59 | $services = MediaWikiServices::getInstance(); |
60 | $loadBalancerFactory = $services->getDBLoadBalancerFactory(); |
61 | $module->injectDatabaseDependencies( $loadBalancerFactory ); |
62 | |
63 | $module->logger = LoggerFactory::getInstance( 'readinglists' ); |
64 | |
65 | return $module; |
66 | } |
67 | |
68 | /** |
69 | * Get the parent module. |
70 | * @return ApiBase |
71 | */ |
72 | public function getParent() { |
73 | return $this->parent; |
74 | } |
75 | |
76 | /** |
77 | * Set database-related dependencies. Required when initializing a module that uses this trait. |
78 | * @param LBFactory $loadBalancerFactory |
79 | */ |
80 | protected function injectDatabaseDependencies( LBFactory $loadBalancerFactory ) { |
81 | $this->loadBalancerFactory = $loadBalancerFactory; |
82 | } |
83 | |
84 | /** |
85 | * Get the repository for the given user. |
86 | * @param UserIdentity|null $user |
87 | * @return ReadingListRepository |
88 | * @suppress PhanUndeclaredMethod |
89 | */ |
90 | protected function getReadingListRepository( ?UserIdentity $user = null ) { |
91 | $config = $this->getConfig(); |
92 | $centralId = MediaWikiServices::getInstance() |
93 | ->getCentralIdLookupFactory() |
94 | ->getLookup() |
95 | ->centralIdFromLocalUser( $user, CentralIdLookup::AUDIENCE_RAW ); |
96 | $repository = new ReadingListRepository( $centralId, $this->loadBalancerFactory ); |
97 | $repository->setLimits( $config->get( 'ReadingListsMaxListsPerUser' ), |
98 | $config->get( 'ReadingListsMaxEntriesPerList' ) ); |
99 | $repository->setLogger( $this->logger ); |
100 | return $repository; |
101 | } |
102 | |
103 | /** |
104 | * Decode, validate and normalize the 'batch' parameter of write APIs. |
105 | * @param string $rawBatch The raw value of the 'batch' parameter. |
106 | * @return array[] One operation, typically a flat associative array. |
107 | * @throws ApiUsageException |
108 | * @suppress PhanUndeclaredMethod |
109 | */ |
110 | protected function getBatchOps( $rawBatch ) { |
111 | $batch = json_decode( $rawBatch, true ); |
112 | |
113 | // Must be a real array, and not empty. |
114 | if ( !is_array( $batch ) || $batch !== array_values( $batch ) || !$batch ) { |
115 | if ( json_last_error() ) { |
116 | $jsonError = json_last_error_msg(); |
117 | $this->dieWithError( wfMessage( 'apierror-readinglists-batch-invalid-json', |
118 | wfEscapeWikiText( $jsonError ) ) ); |
119 | } |
120 | $this->dieWithError( 'apierror-readinglists-batch-invalid-structure' ); |
121 | } |
122 | |
123 | $i = 0; |
124 | $request = $this->getContext()->getRequest(); |
125 | foreach ( $batch as &$op ) { |
126 | if ( ++$i > ApiBase::LIMIT_BIG1 ) { |
127 | $msg = wfMessage( 'apierror-readinglists-batch-toomanyvalues', ApiBase::LIMIT_BIG1 ); |
128 | $this->dieWithError( $msg, 'toomanyvalues' ); |
129 | } |
130 | // Each batch operation must be an associative array with scalar fields. |
131 | if ( |
132 | !is_array( $op ) |
133 | || array_values( $op ) === $op |
134 | || array_filter( $op, 'is_scalar' ) !== $op |
135 | ) { |
136 | $this->dieWithError( 'apierror-readinglists-batch-invalid-structure' ); |
137 | } |
138 | // JSON-escaped characters might have skipped WebRequest's normalization, repeat it. |
139 | array_walk_recursive( $op, static function ( &$value ) use ( $request ) { |
140 | if ( is_string( $value ) ) { |
141 | $value = $request->normalizeUnicode( $value ); |
142 | } |
143 | } ); |
144 | } |
145 | return $batch; |
146 | } |
147 | |
148 | /** |
149 | * Validate a single operation in the 'batch' parameter of write APIs. Works the same way as |
150 | * requireAtLeastOneParameter. |
151 | * @param array $op |
152 | * @param string ...$param |
153 | * @throws ApiUsageException |
154 | */ |
155 | protected function requireAtLeastOneBatchParameter( array $op, ...$param ) { |
156 | $intersection = array_intersect( |
157 | array_keys( array_filter( $op, static function ( $val ) { |
158 | return $val !== null && $val !== false; |
159 | } ) ), |
160 | $param |
161 | ); |
162 | |
163 | if ( count( $intersection ) == 0 ) { |
164 | // @phan-suppress-next-line PhanUndeclaredMethod |
165 | $this->dieWithError( [ |
166 | 'apierror-readinglists-batch-missingparam-at-least-one-of', |
167 | Message::listParam( array_map( |
168 | function ( $p ) { |
169 | // @phan-suppress-next-line PhanUndeclaredMethod |
170 | return '<var>' . $this->encodeParamName( $p ) . '</var>'; |
171 | }, |
172 | $param |
173 | ) ), |
174 | count( $param ), |
175 | ], 'missingparam' ); |
176 | } |
177 | } |
178 | |
179 | /** |
180 | * Convert a list record from ReadingListRepository into an array suitable for adding to |
181 | * the API result. |
182 | * @param ReadingListRow|ReadingListRowWithMergeFlag $row |
183 | * @return array |
184 | */ |
185 | protected function getListFromRow( $row ) { |
186 | $item = [ |
187 | 'id' => (int)$row->rl_id, |
188 | 'name' => $row->rl_name, |
189 | 'default' => (bool)$row->rl_is_default, |
190 | 'description' => $row->rl_description, |
191 | 'created' => wfTimestamp( TS_ISO_8601, $row->rl_date_created ), |
192 | 'updated' => wfTimestamp( TS_ISO_8601, $row->rl_date_updated ), |
193 | ]; |
194 | // @phan-suppress-next-line MediaWikiNoIssetIfDefined |
195 | if ( isset( $row->merged ) ) { |
196 | $item['duplicate'] = (bool)$row->merged; |
197 | } |
198 | if ( $row->rl_deleted ) { |
199 | $item['deleted'] = (bool)$row->rl_deleted; |
200 | } |
201 | return $item; |
202 | } |
203 | |
204 | /** |
205 | * Convert a list entry record from ReadingListRepository into an array suitable for adding to |
206 | * the API result. |
207 | * @param ReadingListEntryRow|ReadingListEntryRowWithMergeFlag $row |
208 | * @return array |
209 | */ |
210 | protected function getListEntryFromRow( $row ) { |
211 | $item = [ |
212 | 'id' => (int)$row->rle_id, |
213 | 'listId' => (int)$row->rle_rl_id, |
214 | 'project' => $row->rlp_project, |
215 | 'title' => $row->rle_title, |
216 | 'created' => wfTimestamp( TS_ISO_8601, $row->rle_date_created ), |
217 | 'updated' => wfTimestamp( TS_ISO_8601, $row->rle_date_updated ), |
218 | ]; |
219 | // @phan-suppress-next-line MediaWikiNoIssetIfDefined |
220 | if ( isset( $row->merged ) ) { |
221 | $item['duplicate'] = (bool)$row->merged; |
222 | } |
223 | if ( $row->rle_deleted ) { |
224 | $item['deleted'] = true; |
225 | } |
226 | return $item; |
227 | } |
228 | |
229 | } |