Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventBodyValidator
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 4
420
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 validateBody
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
240
 getJobFromParams
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 throwJobErrors
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\EventBus\Rest;
4
5use Exception;
6use Job;
7use MediaWiki\Extension\EventBus\EventBus;
8use MediaWiki\Extension\EventBus\EventFactory;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Rest\HttpException;
11use MediaWiki\Rest\RequestInterface;
12use MediaWiki\Rest\Validator\BodyValidator;
13use Psr\Log\LoggerInterface;
14
15/**
16 * Validates the body
17 */
18class EventBodyValidator implements BodyValidator {
19
20    /**
21     * @var string
22     */
23    private $secretKey;
24
25    /**
26     * @var LoggerInterface
27     */
28    private $logger;
29
30    public function __construct( $secretKey, LoggerInterface $logger ) {
31        $this->secretKey = $secretKey;
32        $this->logger = $logger;
33    }
34
35    /**
36     * @param RequestInterface $request
37     * @return Job|mixed|void
38     * @throws HttpException
39     */
40    public function validateBody( RequestInterface $request ) {
41        // get the info contained in the body
42        $event = null;
43        try {
44            $event = json_decode( $request->getBody()->getContents(), true );
45        } catch ( Exception $e ) {
46            throw new HttpException( "Could not decode the event", 500, [
47                'error' => $e->getMessage(),
48            ] );
49        }
50
51        // check that we have the needed components of the event
52        if ( !isset( $event['database'] ) ||
53            !isset( $event['type'] ) ||
54            !isset( $event['params'] )
55        ) {
56            $missingParams = [];
57            if ( !isset( $event['database'] ) ) {
58                $missingParams[] = 'database';
59            }
60            if ( !isset( $event['type'] ) ) {
61                $missingParams[] = 'type';
62            }
63            if ( !isset( $event['params'] ) ) {
64                $missingParams[] = 'params';
65            }
66            throw new HttpException( 'Invalid event received', 400, [ 'missing_params' => $missingParams ] );
67        }
68
69        if ( !isset( $event['mediawiki_signature'] ) ) {
70            throw new HttpException( 'Missing mediawiki signature', 403 );
71        }
72
73        $signature = $event['mediawiki_signature'];
74        unset( $event['mediawiki_signature'] );
75
76        $serialized_event = EventBus::serializeEvents( $event );
77        $expected_signature = EventFactory::getEventSignature(
78            $serialized_event,
79            $this->secretKey
80        );
81
82        $verified = is_string( $signature )
83            && hash_equals( $expected_signature, $signature );
84
85        if ( !$verified ) {
86            throw new HttpException( 'Invalid mediawiki signature', 403 );
87        }
88
89        // check if there are any base64-encoded parameters and if so decode them
90        foreach ( $event['params'] as $key => &$value ) {
91            if ( !is_string( $value ) ) {
92                continue;
93            }
94            if ( preg_match( '/^data:application\/octet-stream;base64,([\s\S]+)$/', $value, $match ) ) {
95                $value = base64_decode( $match[1], true );
96                if ( $value === false ) {
97
98                    throw new HttpException(
99                        'Parameter base64_decode() failed',
100                        500,
101                        [
102                            'param_name'  => $key,
103                            'param_value' => $match[1]
104                        ]
105                    );
106                }
107            }
108        }
109        unset( $value );
110
111        return $this->getJobFromParams( $event );
112    }
113
114    /**
115     * @param array $jobEvent containing the job EventBus event
116     * @return Job|void
117     * @throws HttpException
118     */
119    private function getJobFromParams( array $jobEvent ) {
120        try {
121            $jobFactory = MediaWikiServices::getInstance()->getJobFactory();
122            $job = $jobFactory->newJob( $jobEvent['type'], $jobEvent['params'] );
123        } catch ( Exception $e ) {
124            $this->throwJobErrors( [
125                'status'  => false,
126                'error' => $e->getMessage(),
127                'type' => $jobEvent['type']
128            ] );
129        }
130
131        if ( $job === null ) {
132            $this->throwJobErrors( [
133                'status'  => false,
134                'error' => 'Could not create a job from event',
135                'type' => $jobEvent['type']
136            ] );
137        }
138
139        return $job;
140    }
141
142    /**
143     * @param array $jobResults
144     * @throws HttpException
145     * @return never
146     */
147    private function throwJobErrors( $jobResults ) {
148        $this->logger->error( 'Failed creating job from description', [
149            'job_type' => $jobResults['type'],
150            'error' => $jobResults['error']
151        ] );
152
153        throw new HttpException( "Failed creating job from description",
154            400,
155            [ 'error' => $jobResults['error'] ]
156        );
157    }
158}