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