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