Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
VariablesBlobStore
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
3 / 3
15
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 storeVarDump
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
6
 loadVarDump
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\Variables;
4
5use InvalidArgumentException;
6use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
7use MediaWiki\Json\FormatJson;
8use MediaWiki\Storage\BlobAccessException;
9use MediaWiki\Storage\BlobStore;
10use MediaWiki\Storage\BlobStoreFactory;
11use stdClass;
12use Wikimedia\IPUtils;
13
14/**
15 * This service is used to generate the value of afl_var_dump for an abuse_filter_log row and
16 * parse afl_var_dump from an abuse_filter_log row into a {@link VariableHolder}
17 */
18class VariablesBlobStore {
19    public const SERVICE_NAME = 'AbuseFilterVariablesBlobStore';
20
21    public function __construct(
22        private readonly VariablesManager $varManager,
23        private readonly AbuseFilterPermissionManager $permissionManager,
24        private readonly BlobStoreFactory $blobStoreFactory,
25        private readonly BlobStore $blobStore,
26        private readonly ?string $centralDB
27    ) {
28    }
29
30    /**
31     * Store a var dump to a BlobStore.
32     *
33     * @param VariableHolder $varsHolder
34     * @param bool $global
35     *
36     * @return string Blob store address or JSON if the var dump included protected variables.
37     */
38    public function storeVarDump( VariableHolder $varsHolder, $global = false ) {
39        // Get all variables yet set and compute old and new wikitext if not yet done
40        // as those are needed for the diff view on top of the abuse log pages
41        $varsForBlobStore = $this->varManager->dumpAllVars( $varsHolder, [ 'old_wikitext', 'new_wikitext' ] );
42        $varsForDB = [];
43
44        // Get a list of the protected variables present in the VariableHolder. This excludes user_unnamed_ip always
45        // as it is handled separately later in this method.
46        $usedProtectedVariables = $this->permissionManager->getUsedProtectedVariables(
47            array_keys( $varsForBlobStore )
48        );
49        unset( $usedProtectedVariables['user_unnamed_ip'] );
50
51        // Store the values of protected variables in the DB instead of the append-only external storage.
52        // Leave a reference to these variables so that they display nothing when the data is purged.
53        foreach ( $usedProtectedVariables as $protectedVariable ) {
54            $varsForDB[$protectedVariable] = $varsForBlobStore[$protectedVariable];
55            $varsForBlobStore[$protectedVariable] = true;
56        }
57
58        // Set the value to something safe here, as by now it's been used in the filter and if
59        // logs later need it, it can be reconstructed from afl_ip_hex.
60        if ( isset( $varsForBlobStore[ 'user_unnamed_ip' ] ) && $varsForBlobStore[ 'user_unnamed_ip' ] ) {
61            $varsForBlobStore[ 'user_unnamed_ip' ] = true;
62        }
63
64        // Vars is an array with native PHP data types (non-objects) now
65        $text = FormatJson::encode( $varsForBlobStore );
66
67        $dbDomain = $global ? $this->centralDB : false;
68        $blobStore = $this->blobStoreFactory->newBlobStore( $dbDomain );
69
70        $hints = [
71            BlobStore::DESIGNATION_HINT => 'AbuseFilter',
72            BlobStore::MODEL_HINT => 'AbuseFilter',
73        ];
74        $blobStoreAddress = $blobStore->storeBlob( $text, $hints );
75
76        if ( !count( $varsForDB ) ) {
77            return $blobStoreAddress;
78        }
79
80        return FormatJson::encode( array_merge( $varsForDB, [ '_blob' => $blobStoreAddress ] ) );
81    }
82
83    /**
84     * Retrieve a var dump from a BlobStore.
85     *
86     * The entire $row is passed through but only the following columns are actually required:
87     * - afl_var_dump: the main variable store to load
88     * - afl_ip_hex: the IP value to use if necessary
89     *
90     * @param stdClass $row
91     *
92     * @return VariableHolder
93     */
94    public function loadVarDump( stdClass $row ): VariableHolder {
95        if ( !isset( $row->afl_var_dump ) || !isset( $row->afl_ip_hex ) ) {
96            throw new InvalidArgumentException( 'Both afl_var_dump and afl_ip_hex must be set' );
97        }
98        $variablesFromDb = [];
99
100        $varDumpJsonParseStatus = FormatJson::parse( $row->afl_var_dump, FormatJson::FORCE_ASSOC );
101        if ( $varDumpJsonParseStatus->isGood() ) {
102            $varDumpAsJson = $varDumpJsonParseStatus->getValue();
103            $blobStoreAddress = $varDumpAsJson['_blob'];
104            unset( $varDumpAsJson['_blob'] );
105            $variablesFromDb = $varDumpAsJson;
106        } else {
107            $blobStoreAddress = $row->afl_var_dump;
108        }
109
110        try {
111            $blob = $this->blobStore->getBlob( $blobStoreAddress );
112        } catch ( BlobAccessException ) {
113            return new VariableHolder;
114        }
115
116        $vars = FormatJson::decode( $blob, true );
117        $obj = VariableHolder::newFromArray( $vars );
118
119        // If user_unnamed_ip was set when afl_var_dump was saved, it was saved as a visibility boolean
120        // and needs to be translated back into an IP
121        // user_unnamed_ip uses afl_ip_hex instead of saving the value because afl_ip_hex gets purged and the blob
122        // that contains user_unnamed_ip can't be modified
123        if (
124            $this->varManager->getVar( $obj, 'user_unnamed_ip', VariablesManager::GET_LAX )->toNative()
125        ) {
126            $formattedIP = $row->afl_ip_hex ? IPUtils::formatHex( $row->afl_ip_hex ) : '';
127            $obj->setVar( 'user_unnamed_ip', $formattedIP );
128        }
129
130        // Add variables from the DB into the returned VariableHolder.
131        foreach ( $variablesFromDb as $variable => $value ) {
132            $obj->setVar( $variable, $value );
133        }
134
135        $this->varManager->translateDeprecatedVars( $obj );
136        return $obj;
137    }
138}