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