mediawiki-extensions-AbuseF.../includes/FilterImporter.php
STran ca23e9f06b Convert af_hidden into a bitmask
Protected variables will cause the filter using them to become
protected as well. `af_hidden` can be used to track this flag,
as it is a TINYINT and can be converted into a bitmask with no
schema changes.

This is not a backwards-compatible change, as now all checks must
check the `hidden` flag specifically or otherwise will be cast to
true if any flag is set.

To support this change:
- "hidden" is considered a flag set in the `af_hidden`. This is a
  change in concept with no need for updates to the column values,
  as there is currently only one flag in the bitmask.
- `Flag`s store the bitmask as well as the state of single flags
  and can return either.
- Any checks against the `af_hidden` value no longer check a
  boolean value and instead now check the `hidden` flag value.

Bug: T363906
Change-Id: I358205cb1119cf1e4004892c37e36e0c0a864f37
2024-05-28 00:59:08 -07: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',
'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;
}
}