Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.62% covered (success)
90.62%
87 / 96
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiContinuationManager
91.58% covered (success)
91.58%
87 / 95
63.64% covered (warning)
63.64%
7 / 11
32.61
0.00% covered (danger)
0.00%
0 / 1
 __construct
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
7
 getSource
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isGeneratorDone
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRunModules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addContinueParam
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 addGeneratorNonContinueParam
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 addGeneratorContinueParam
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getRawContinuation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRawNonContinuation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContinuation
96.88% covered (success)
96.88%
31 / 32
0.00% covered (danger)
0.00%
0 / 1
9
 setContinuationIntoResult
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Api;
8
9use UnexpectedValueException;
10
11/**
12 * This manages continuation state.
13 * @since 1.25 this is no longer a subclass of ApiBase
14 * @ingroup API
15 */
16class ApiContinuationManager {
17    /** @var string */
18    private $source;
19
20    /** @var (ApiBase|false)[] */
21    private $allModules = [];
22    /** @var string[] */
23    private $generatedModules;
24
25    /** @var array[] */
26    private $continuationData = [];
27    /** @var array[] */
28    private $generatorContinuationData = [];
29    /** @var array[] */
30    private $generatorNonContinuationData = [];
31
32    /** @var array */
33    private $generatorParams = [];
34    /** @var bool */
35    private $generatorDone = false;
36
37    /**
38     * @param ApiBase $module Module starting the continuation
39     * @param ApiBase[] $allModules Contains ApiBase instances that will be executed
40     * @param string[] $generatedModules Names of modules that depend on the generator
41     * @throws ApiUsageException
42     */
43    public function __construct(
44        ApiBase $module, array $allModules = [], array $generatedModules = []
45    ) {
46        $this->source = get_class( $module );
47        $request = $module->getRequest();
48
49        $this->generatedModules = $generatedModules
50            ? array_combine( $generatedModules, $generatedModules )
51            : [];
52
53        $skip = [];
54        $continue = $request->getVal( 'continue', '' );
55        if ( $continue !== '' ) {
56            $continue = explode( '||', $continue );
57            if ( count( $continue ) !== 2 ) {
58                throw ApiUsageException::newWithMessage( $module->getMain(), 'apierror-badcontinue' );
59            }
60            $this->generatorDone = ( $continue[0] === '-' );
61            $skip = explode( '|', $continue[1] );
62            if ( !$this->generatorDone ) {
63                $params = explode( '|', $continue[0] );
64                $this->generatorParams = array_intersect_key(
65                    $request->getValues(),
66                    array_fill_keys( $params, true )
67                );
68            } else {
69                // When the generator is complete, don't run any modules that
70                // depend on it.
71                $skip += $this->generatedModules;
72            }
73        }
74
75        foreach ( $allModules as $module ) {
76            $name = $module->getModuleName();
77            if ( in_array( $name, $skip, true ) ) {
78                $this->allModules[$name] = false;
79                // Prevent spurious "unused parameter" warnings
80                $module->extractRequestParams();
81            } else {
82                $this->allModules[$name] = $module;
83            }
84        }
85    }
86
87    /**
88     * Get the class that created this manager
89     * @return string
90     */
91    public function getSource() {
92        return $this->source;
93    }
94
95    /**
96     * @return bool
97     */
98    public function isGeneratorDone() {
99        return $this->generatorDone;
100    }
101
102    /**
103     * Get the list of modules that should actually be run
104     * @return ApiBase[]
105     */
106    public function getRunModules() {
107        return array_values( array_filter( $this->allModules ) );
108    }
109
110    /**
111     * Set the continuation parameter for a module
112     * @param ApiBase $module
113     * @param string $paramName
114     * @param string|array $paramValue
115     * @throws UnexpectedValueException
116     */
117    public function addContinueParam( ApiBase $module, $paramName, $paramValue ) {
118        $name = $module->getModuleName();
119        if ( !isset( $this->allModules[$name] ) ) {
120            throw new UnexpectedValueException(
121                "Module '$name' called " . __METHOD__ .
122                    ' but was not passed to ' . __CLASS__ . '::__construct'
123            );
124        }
125        if ( !$this->allModules[$name] ) {
126            throw new UnexpectedValueException(
127                "Module '$name' was not supposed to have been executed, but " .
128                    'it was executed anyway'
129            );
130        }
131        $paramName = $module->encodeParamName( $paramName );
132        if ( is_array( $paramValue ) ) {
133            $paramValue = implode( '|', $paramValue );
134        }
135        $this->continuationData[$name][$paramName] = $paramValue;
136    }
137
138    /**
139     * Set the non-continuation parameter for the generator module
140     *
141     * In case the generator isn't going to be continued, this sets the fields
142     * to return.
143     *
144     * @since 1.28
145     * @param ApiBase $module
146     * @param string $paramName
147     * @param string|array $paramValue
148     */
149    public function addGeneratorNonContinueParam( ApiBase $module, $paramName, $paramValue ) {
150        $name = $module->getModuleName();
151        $paramName = $module->encodeParamName( $paramName );
152        if ( is_array( $paramValue ) ) {
153            $paramValue = implode( '|', $paramValue );
154        }
155        $this->generatorNonContinuationData[$name][$paramName] = $paramValue;
156    }
157
158    /**
159     * Set the continuation parameter for the generator module
160     * @param ApiBase $module
161     * @param string $paramName
162     * @param int|string|array $paramValue
163     */
164    public function addGeneratorContinueParam( ApiBase $module, $paramName, $paramValue ) {
165        $name = $module->getModuleName();
166        $paramName = $module->encodeParamName( $paramName );
167        if ( is_array( $paramValue ) ) {
168            $paramValue = implode( '|', $paramValue );
169        }
170        $this->generatorContinuationData[$name][$paramName] = $paramValue;
171    }
172
173    /**
174     * Fetch raw continuation data
175     * @return array[]
176     */
177    public function getRawContinuation() {
178        return array_merge_recursive( $this->continuationData, $this->generatorContinuationData );
179    }
180
181    /**
182     * Fetch raw non-continuation data
183     * @since 1.28
184     * @return array[]
185     */
186    public function getRawNonContinuation() {
187        return $this->generatorNonContinuationData;
188    }
189
190    /**
191     * Fetch continuation result data
192     * @return array [ (array)$data, (bool)$batchcomplete ]
193     */
194    public function getContinuation() {
195        $data = [];
196        $batchcomplete = false;
197
198        $finishedModules = array_diff(
199            array_keys( $this->allModules ),
200            array_keys( $this->continuationData )
201        );
202
203        // First, grab the non-generator-using continuation data
204        $continuationData = array_diff_key( $this->continuationData, $this->generatedModules );
205        foreach ( $continuationData as $kvp ) {
206            $data += $kvp;
207        }
208
209        // Next, handle the generator-using continuation data
210        $continuationData = array_intersect_key( $this->continuationData, $this->generatedModules );
211        if ( $continuationData ) {
212            // Some modules are unfinished: include those params, and copy
213            // the generator params.
214            foreach ( $continuationData as $kvp ) {
215                $data += $kvp;
216            }
217            $generatorParams = [];
218            foreach ( $this->generatorNonContinuationData as $kvp ) {
219                $generatorParams += $kvp;
220            }
221            $generatorParams += $this->generatorParams;
222            // @phan-suppress-next-line PhanTypeInvalidLeftOperand False positive in phan
223            $data += $generatorParams;
224            $generatorKeys = implode( '|', array_keys( $generatorParams ) );
225        } elseif ( $this->generatorContinuationData ) {
226            // All the generator-using modules are complete, but the
227            // generator isn't. Continue the generator and restart the
228            // generator-using modules
229            $generatorParams = [];
230            foreach ( $this->generatorContinuationData as $kvp ) {
231                $generatorParams += $kvp;
232            }
233            $data += $generatorParams;
234            $finishedModules = array_diff( $finishedModules, $this->generatedModules );
235            $generatorKeys = implode( '|', array_keys( $generatorParams ) );
236            $batchcomplete = true;
237        } else {
238            // Generator and prop modules are all done. Mark it so.
239            $generatorKeys = '-';
240            $batchcomplete = true;
241        }
242
243        // Set 'continue' if any continuation data is set or if the generator
244        // still needs to run
245        if ( $data || $generatorKeys !== '-' ) {
246            $data['continue'] = $generatorKeys . '||' . implode( '|', $finishedModules );
247        }
248
249        return [ $data, $batchcomplete ];
250    }
251
252    /**
253     * Store the continuation data into the result
254     */
255    public function setContinuationIntoResult( ApiResult $result ) {
256        [ $data, $batchcomplete ] = $this->getContinuation();
257        if ( $data ) {
258            $result->addValue( null, 'continue', $data,
259                ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
260        }
261        if ( $batchcomplete ) {
262            $result->addValue( null, 'batchcomplete', true,
263                ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
264        }
265    }
266}
267
268/** @deprecated class alias since 1.43 */
269class_alias( ApiContinuationManager::class, 'ApiContinuationManager' );