mediawiki-extensions-AbuseF.../includes/FilterImporter.php
Timo Tijhof d2fc2ff8bb maintenance,includes: Clean up file headers
Follows-up Iaa1b4683c5c856.

* Match $IP pattern verbatim from most other WMF extensions.

* Improve descriptions a bit, and move/merge any meaningful
  information from file docblock into class docblock. The file blocks
  are visually ignored and identical in each file, and often out of
  date or duplicated when given text separately from the class block.

  See also similar changes in core:
  https://gerrit.wikimedia.org/r/q/message:ingroup+owner:Krinkle

* Use `@internal` instead of `@private` as per Stable interface
  policy.

Change-Id: I8bed9a625af003446c7e25f6b794931164767b5a
2022-09-29 17:56:49 +01:00

160 lines
4.2 KiB
PHP

<?php
namespace MediaWiki\Extension\AbuseFilter;
use FormatJson;
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;
/**
* 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',
'hidden',
'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(),
'hidden' => $filter->isHidden(),
'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'],
(bool)$filterData['hidden'],
// 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;
}
}