Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
FilterImporter
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
4 / 4
18
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 encodeData
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 decodeData
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
4
 isValidImportData
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
11
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter;
4
5use FormatJson;
6use LogicException;
7use MediaWiki\Config\ServiceOptions;
8use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesRegistry;
9use MediaWiki\Extension\AbuseFilter\Filter\Filter;
10use MediaWiki\Extension\AbuseFilter\Filter\Flags;
11use MediaWiki\Extension\AbuseFilter\Filter\LastEditInfo;
12use MediaWiki\Extension\AbuseFilter\Filter\MutableFilter;
13use MediaWiki\Extension\AbuseFilter\Filter\Specs;
14
15/**
16 * This class allows encoding filters to (and decoding from) a string format that can be used
17 * to export them to another wiki.
18 *
19 * @internal
20 * @note Callers should NOT rely on the output format, as it may vary
21 */
22class FilterImporter {
23    public const SERVICE_NAME = 'AbuseFilterFilterImporter';
24
25    public const CONSTRUCTOR_OPTIONS = [
26        'AbuseFilterValidGroups',
27        'AbuseFilterIsCentral',
28    ];
29
30    private const TEMPLATE_KEYS = [
31        'rules',
32        'name',
33        'comments',
34        'group',
35        'actions',
36        'enabled',
37        'deleted',
38        'hidden',
39        'global'
40    ];
41
42    /** @var ServiceOptions */
43    private $options;
44
45    /** @var ConsequencesRegistry */
46    private $consequencesRegistry;
47
48    /**
49     * @param ServiceOptions $options
50     * @param ConsequencesRegistry $consequencesRegistry
51     */
52    public function __construct( ServiceOptions $options, ConsequencesRegistry $consequencesRegistry ) {
53        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
54        $this->options = $options;
55        $this->consequencesRegistry = $consequencesRegistry;
56    }
57
58    /**
59     * @param Filter $filter
60     * @param array $actions
61     * @return string
62     */
63    public function encodeData( Filter $filter, array $actions ): string {
64        $data = [
65            'rules' => $filter->getRules(),
66            'name' => $filter->getName(),
67            'comments' => $filter->getComments(),
68            'group' => $filter->getGroup(),
69            'actions' => $filter->getActions(),
70            'enabled' => $filter->isEnabled(),
71            'deleted' => $filter->isDeleted(),
72            'hidden' => $filter->isHidden(),
73            'global' => $filter->isGlobal()
74        ];
75        // @codeCoverageIgnoreStart
76        if ( array_keys( $data ) !== self::TEMPLATE_KEYS ) {
77            // Sanity
78            throw new LogicException( 'Bad keys' );
79        }
80        // @codeCoverageIgnoreEnd
81        return FormatJson::encode( [ 'data' => $data, 'actions' => $actions ] );
82    }
83
84    /**
85     * @param string $rawData
86     * @return Filter
87     * @throws InvalidImportDataException
88     */
89    public function decodeData( string $rawData ): Filter {
90        $validGroups = $this->options->get( 'AbuseFilterValidGroups' );
91        $globalFiltersEnabled = $this->options->get( 'AbuseFilterIsCentral' );
92
93        $data = FormatJson::decode( $rawData );
94        if ( !$this->isValidImportData( $data ) ) {
95            throw new InvalidImportDataException( $rawData );
96        }
97        [ 'data' => $filterData, 'actions' => $actions ] = wfObjectToArray( $data );
98
99        return new MutableFilter(
100            new Specs(
101                $filterData['rules'],
102                $filterData['comments'],
103                $filterData['name'],
104                array_keys( $actions ),
105                // Keep the group only if it exists on this wiki
106                in_array( $filterData['group'], $validGroups, true ) ? $filterData['group'] : 'default'
107            ),
108            new Flags(
109                (bool)$filterData['enabled'],
110                (bool)$filterData['deleted'],
111                (bool)$filterData['hidden'],
112                // And also make it global only if global filters are enabled here
113                $filterData['global'] && $globalFiltersEnabled
114            ),
115            $actions,
116            new LastEditInfo(
117                0,
118                '',
119                ''
120            )
121        );
122    }
123
124    /**
125     * Note: this doesn't check if parameters are valid etc., but only if the shape of the object is right.
126     *
127     * @param mixed $data Already decoded
128     * @return bool
129     */
130    private function isValidImportData( $data ): bool {
131        if ( !is_object( $data ) ) {
132            return false;
133        }
134
135        $arr = get_object_vars( $data );
136
137        $expectedKeys = [ 'data' => true, 'actions' => true ];
138        if ( count( $arr ) !== count( $expectedKeys ) || array_diff_key( $arr, $expectedKeys ) ) {
139            return false;
140        }
141
142        if ( !is_object( $arr['data'] ) || !( is_object( $arr['actions'] ) || $arr['actions'] === [] ) ) {
143            return false;
144        }
145
146        if ( array_keys( get_object_vars( $arr['data'] ) ) !== self::TEMPLATE_KEYS ) {
147            return false;
148        }
149
150        $allActions = $this->consequencesRegistry->getAllActionNames();
151        foreach ( $arr['actions'] as $action => $params ) {
152            if ( !in_array( $action, $allActions, true ) || !is_array( $params ) ) {
153                return false;
154            }
155        }
156
157        return true;
158    }
159}