Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.51% covered (success)
96.51%
83 / 86
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiTrait
96.51% covered (success)
96.51%
83 / 86
62.50% covered (warning)
62.50%
5 / 8
25
0.00% covered (danger)
0.00%
0 / 1
 factory
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getParent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 injectDatabaseDependencies
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReadingListRepository
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 getBatchOps
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
11
 requireAtLeastOneBatchParameter
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 getListFromRow
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 getListEntryFromRow
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
1<?php
2
3namespace MediaWiki\Extension\ReadingLists\Api;
4
5use ApiBase;
6use ApiUsageException;
7use MediaWiki\Extension\ReadingLists\Doc\ReadingListEntryRow;
8use MediaWiki\Extension\ReadingLists\Doc\ReadingListEntryRowWithMergeFlag;
9use MediaWiki\Extension\ReadingLists\Doc\ReadingListRow;
10use MediaWiki\Extension\ReadingLists\Doc\ReadingListRowWithMergeFlag;
11use MediaWiki\Extension\ReadingLists\ReadingListRepository;
12use MediaWiki\Logger\LoggerFactory;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\User\CentralId\CentralIdLookup;
15use MediaWiki\User\UserIdentity;
16use Message;
17use Psr\Log\LoggerInterface;
18use 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 */
26trait 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        if ( isset( $row->merged ) ) {
195            $item['duplicate'] = (bool)$row->merged;
196        }
197        if ( $row->rl_deleted ) {
198            $item['deleted'] = (bool)$row->rl_deleted;
199        }
200        return $item;
201    }
202
203    /**
204     * Convert a list entry record from ReadingListRepository into an array suitable for adding to
205     * the API result.
206     * @param ReadingListEntryRow|ReadingListEntryRowWithMergeFlag $row
207     * @return array
208     */
209    protected function getListEntryFromRow( $row ) {
210        $item = [
211            'id' => (int)$row->rle_id,
212            'listId' => (int)$row->rle_rl_id,
213            'project' => $row->rlp_project,
214            'title' => $row->rle_title,
215            'created' => wfTimestamp( TS_ISO_8601, $row->rle_date_created ),
216            'updated' => wfTimestamp( TS_ISO_8601, $row->rle_date_updated ),
217        ];
218        if ( isset( $row->merged ) ) {
219            $item['duplicate'] = (bool)$row->merged;
220        }
221        if ( $row->rle_deleted ) {
222            $item['deleted'] = true;
223        }
224        return $item;
225    }
226
227}