File: FilterImporter.php

package info (click to toggle)
mediawiki 1%3A1.43.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 417,464 kB
  • sloc: php: 1,062,949; javascript: 664,290; sql: 9,714; python: 5,458; xml: 3,489; sh: 1,131; makefile: 64
file content (159 lines) | stat: -rw-r--r-- 4,359 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
<?php

namespace MediaWiki\Extension\AbuseFilter;

use LogicException;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesRegistry;
use MediaWiki\Extension\AbuseFilter\Filter\Filter;
use MediaWiki\Extension\AbuseFilter\Filter\Flags;
use MediaWiki\Extension\AbuseFilter\Filter\LastEditInfo;
use MediaWiki\Extension\AbuseFilter\Filter\MutableFilter;
use MediaWiki\Extension\AbuseFilter\Filter\Specs;
use MediaWiki\Json\FormatJson;

/**
 * This class allows encoding filters to (and decoding from) a string format that can be used
 * to export them to another wiki.
 *
 * @internal
 * @note Callers should NOT rely on the output format, as it may vary
 */
class FilterImporter {
	public const SERVICE_NAME = 'AbuseFilterFilterImporter';

	public const CONSTRUCTOR_OPTIONS = [
		'AbuseFilterValidGroups',
		'AbuseFilterIsCentral',
	];

	private const TEMPLATE_KEYS = [
		'rules',
		'name',
		'comments',
		'group',
		'actions',
		'enabled',
		'deleted',
		'privacylevel',
		'global'
	];

	/** @var ServiceOptions */
	private $options;

	/** @var ConsequencesRegistry */
	private $consequencesRegistry;

	/**
	 * @param ServiceOptions $options
	 * @param ConsequencesRegistry $consequencesRegistry
	 */
	public function __construct( ServiceOptions $options, ConsequencesRegistry $consequencesRegistry ) {
		$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
		$this->options = $options;
		$this->consequencesRegistry = $consequencesRegistry;
	}

	/**
	 * @param Filter $filter
	 * @param array $actions
	 * @return string
	 */
	public function encodeData( Filter $filter, array $actions ): string {
		$data = [
			'rules' => $filter->getRules(),
			'name' => $filter->getName(),
			'comments' => $filter->getComments(),
			'group' => $filter->getGroup(),
			'actions' => $filter->getActions(),
			'enabled' => $filter->isEnabled(),
			'deleted' => $filter->isDeleted(),
			'privacylevel' => $filter->getPrivacyLevel(),
			'global' => $filter->isGlobal()
		];
		// @codeCoverageIgnoreStart
		if ( array_keys( $data ) !== self::TEMPLATE_KEYS ) {
			// Sanity
			throw new LogicException( 'Bad keys' );
		}
		// @codeCoverageIgnoreEnd
		return FormatJson::encode( [ 'data' => $data, 'actions' => $actions ] );
	}

	/**
	 * @param string $rawData
	 * @return Filter
	 * @throws InvalidImportDataException
	 */
	public function decodeData( string $rawData ): Filter {
		$validGroups = $this->options->get( 'AbuseFilterValidGroups' );
		$globalFiltersEnabled = $this->options->get( 'AbuseFilterIsCentral' );

		$data = FormatJson::decode( $rawData );
		if ( !$this->isValidImportData( $data ) ) {
			throw new InvalidImportDataException( $rawData );
		}
		[ 'data' => $filterData, 'actions' => $actions ] = wfObjectToArray( $data );

		return new MutableFilter(
			new Specs(
				$filterData['rules'],
				$filterData['comments'],
				$filterData['name'],
				array_keys( $actions ),
				// Keep the group only if it exists on this wiki
				in_array( $filterData['group'], $validGroups, true ) ? $filterData['group'] : 'default'
			),
			new Flags(
				(bool)$filterData['enabled'],
				(bool)$filterData['deleted'],
				(int)$filterData['privacylevel'],
				// And also make it global only if global filters are enabled here
				$filterData['global'] && $globalFiltersEnabled
			),
			$actions,
			new LastEditInfo(
				0,
				'',
				''
			)
		);
	}

	/**
	 * Note: this doesn't check if parameters are valid etc., but only if the shape of the object is right.
	 *
	 * @param mixed $data Already decoded
	 * @return bool
	 */
	private function isValidImportData( $data ): bool {
		if ( !is_object( $data ) ) {
			return false;
		}

		$arr = get_object_vars( $data );

		$expectedKeys = [ 'data' => true, 'actions' => true ];
		if ( count( $arr ) !== count( $expectedKeys ) || array_diff_key( $arr, $expectedKeys ) ) {
			return false;
		}

		if ( !is_object( $arr['data'] ) || !( is_object( $arr['actions'] ) || $arr['actions'] === [] ) ) {
			return false;
		}

		if ( array_keys( get_object_vars( $arr['data'] ) ) !== self::TEMPLATE_KEYS ) {
			return false;
		}

		$allActions = $this->consequencesRegistry->getAllActionNames();
		foreach ( $arr['actions'] as $action => $params ) {
			if ( !in_array( $action, $allActions, true ) || !is_array( $params ) ) {
				return false;
			}
		}

		return true;
	}
}