Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialRunJobs
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 5
240
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
90
 doRun
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getQuerySignature
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Deferred\DeferredUpdates;
10use MediaWiki\Deferred\TransactionRoundDefiningUpdate;
11use MediaWiki\JobQueue\JobRunner;
12use MediaWiki\Json\FormatJson;
13use MediaWiki\MainConfigNames;
14use MediaWiki\Request\ContentSecurityPolicy;
15use MediaWiki\SpecialPage\UnlistedSpecialPage;
16use Wikimedia\Http\HttpStatus;
17use Wikimedia\Rdbms\ReadOnlyMode;
18
19/**
20 * Special page designed for running background tasks (internal use only)
21 *
22 * @internal
23 * @ingroup SpecialPage
24 * @ingroup JobQueue
25 */
26class SpecialRunJobs extends UnlistedSpecialPage {
27
28    public function __construct(
29        private readonly JobRunner $jobRunner,
30        private readonly ReadOnlyMode $readOnlyMode
31    ) {
32        parent::__construct( 'RunJobs' );
33    }
34
35    /** @inheritDoc */
36    public function doesWrites() {
37        return true;
38    }
39
40    /** @inheritDoc */
41    public function execute( $par ) {
42        $this->getOutput()->disable();
43        ContentSecurityPolicy::sendRestrictiveHeader();
44
45        if ( $this->readOnlyMode->isReadOnly() ) {
46            wfHttpError( 423, 'Locked', 'Wiki is in read-only mode.' );
47            return;
48        }
49
50        // Validate request method
51        if ( !$this->getRequest()->wasPosted() ) {
52            wfHttpError( 400, 'Bad Request', 'Request must be POSTed.' );
53            return;
54        }
55
56        // Validate request parameters
57        $optional = [
58            'maxjobs' => 0,
59            'maxtime' => 30,
60            'type' => false,
61            'async' => true,
62            'stats' => false,
63            // Ignored, only for signature calculation
64            'tasks' => 'jobs'
65        ];
66        $required = array_fill_keys( [ 'title', 'signature', 'sigexpiry' ], true );
67        $params = array_intersect_key( $this->getRequest()->getValues(), $required + $optional );
68        $missing = array_diff_key( $required, $params );
69        if ( count( $missing ) ) {
70            wfHttpError( 400, 'Bad Request',
71                'Missing parameters: ' . implode( ', ', array_keys( $missing ) )
72            );
73            return;
74        }
75
76        // Validate request signature
77        $squery = $params;
78        unset( $squery['signature'] );
79        $correctSignature = self::getQuerySignature( $squery,
80            $this->getConfig()->get( MainConfigNames::SecretKey ) );
81        $providedSignature = $params['signature'];
82        $verified = is_string( $providedSignature )
83            && hash_equals( $correctSignature, $providedSignature );
84        if ( !$verified || $params['sigexpiry'] < time() ) {
85            wfHttpError( 400, 'Bad Request', 'Invalid or stale signature provided.' );
86            return;
87        }
88
89        // Apply any default parameter values
90        $params += $optional;
91
92        if ( $params['async'] ) {
93            // HTTP 202 Accepted
94            HttpStatus::header( 202 );
95            // Clients are meant to disconnect without waiting for the full response.
96            // Let the page output happen before the jobs start, so that clients know it's
97            // safe to disconnect. MediaWiki::preOutputCommit() calls ignore_user_abort()
98            // or similar to make sure we stay alive to run the deferred update.
99            DeferredUpdates::addUpdate(
100                new TransactionRoundDefiningUpdate(
101                    function () use ( $params ) {
102                        $this->doRun( $params );
103                    },
104                    __METHOD__
105                ),
106                DeferredUpdates::POSTSEND
107            );
108        } else {
109            $stats = $this->doRun( $params );
110
111            if ( $params['stats'] ) {
112                $this->getRequest()->response()->header( 'Content-Type: application/json' );
113                print FormatJson::encode( $stats );
114            } else {
115                print "Done\n";
116            }
117        }
118    }
119
120    protected function doRun( array $params ): array {
121        return $this->jobRunner->run( [
122            'type'     => $params['type'],
123            'maxJobs'  => $params['maxjobs'] ?: 1,
124            'maxTime'  => $params['maxtime'] ?: 30
125        ] );
126    }
127
128    /**
129     * @param array $query
130     * @param string $secretKey
131     * @return string
132     */
133    public static function getQuerySignature( array $query, $secretKey ) {
134        ksort( $query ); // stable order
135        return hash_hmac( 'sha1', wfArrayToCgi( $query ), $secretKey );
136    }
137}
138
139/**
140 * Retain the old class name for backwards compatibility.
141 * @deprecated since 1.41
142 */
143class_alias( SpecialRunJobs::class, 'SpecialRunJobs' );