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