Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
UserAgentClientHintsLookup
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
4 / 4
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
 getClientHintsByReferenceIds
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
6
 prepareFirstResultsArray
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 generateUniqueClientHintsIdCombinations
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3namespace MediaWiki\CheckUser\Services;
4
5use MediaWiki\CheckUser\ClientHints\ClientHintsData;
6use MediaWiki\CheckUser\ClientHints\ClientHintsLookupResults;
7use MediaWiki\CheckUser\ClientHints\ClientHintsReferenceIds;
8use Wikimedia\Rdbms\IReadableDatabase;
9
10/**
11 * A service that gets ClientHintsData objects from the database given
12 * a ClientHintsReferenceIds object of reference IDs.
13 */
14class UserAgentClientHintsLookup {
15    private IReadableDatabase $dbr;
16
17    /**
18     * @param IReadableDatabase $dbr
19     */
20    public function __construct( IReadableDatabase $dbr ) {
21        $this->dbr = $dbr;
22    }
23
24    /**
25     * Gets and returns ClientHintsData objects for given
26     * reference IDs.
27     *
28     * @param ClientHintsReferenceIds $referenceIds The reference IDs
29     * @return ClientHintsLookupResults The ClientHintsData objects in a helper object.
30     */
31    public function getClientHintsByReferenceIds( ClientHintsReferenceIds $referenceIds ): ClientHintsLookupResults {
32        $referenceIdsToClientHintIds = $this->prepareFirstResultsArray( $referenceIds );
33
34        // If there are no conditions, then the reference IDs list was empty.
35        // Therefore, in this case, return with no data.
36        if ( !count( $referenceIdsToClientHintIds ) ) {
37            return new ClientHintsLookupResults( [], [] );
38        }
39
40        $res = $this->dbr->newSelectQueryBuilder()
41            ->field( '*' )
42            ->table( 'cu_useragent_clienthints_map' )
43            // The results list can be used to generate the WHERE condition.
44            ->where( $this->dbr->makeWhereFrom2d(
45                $referenceIdsToClientHintIds, 'uachm_reference_type', 'uachm_reference_id'
46            ) )
47            ->caller( __METHOD__ )
48            ->fetchResultSet();
49
50        // If there are no map rows, then there is no Client Hints data
51        // so return early.
52        if ( !$res->count() ) {
53            return new ClientHintsLookupResults( [], [] );
54        }
55
56        // Fill out the results list with the associated uach_id values
57        // for each reference ID provided in the first parameter to this method.
58        $clientHintIds = [];
59        foreach ( $res as $row ) {
60            $clientHintIds[] = $row->uachm_uach_id;
61            $referenceIdsToClientHintIds[$row->uachm_reference_type][$row->uachm_reference_id][] = $row->uachm_uach_id;
62        }
63
64        // De-duplicate the $clientHintsIds array to reduce the size of the WHERE condition
65        // of the SQL query.
66        $clientHintIds = array_unique( $clientHintIds );
67
68        $uniqueClientHintsDataCombinations = $this->generateUniqueClientHintsIdCombinations(
69            $referenceIdsToClientHintIds
70        );
71
72        // Get all the data for the uach_id values that were collected in the first query.
73        $clientHintsRows = $this->dbr->newSelectQueryBuilder()
74            ->field( '*' )
75            ->table( 'cu_useragent_clienthints' )
76            ->where( [ 'uach_id' => $clientHintIds ] )
77            ->caller( __METHOD__ )
78            ->fetchResultSet();
79
80        // Make an array of Client Hints data database rows for each uach_id.
81        $clientHintsRowsAsArray = [];
82        foreach ( $clientHintsRows as $row ) {
83            $clientHintsRowsAsArray[$row->uach_id] = [
84                'uach_name' => $row->uach_name, 'uach_value' => $row->uach_value
85            ];
86        }
87
88        foreach ( $uniqueClientHintsDataCombinations as &$clientHintsData ) {
89            $clientHintRowIdsForReferenceId = array_intersect_key(
90                $clientHintsRowsAsArray,
91                array_flip( $clientHintsData )
92            );
93            $clientHintsData = ClientHintsData::newFromDatabaseRows( $clientHintRowIdsForReferenceId );
94        }
95
96        // Return the results in a result wrapper to make the results easier to use by the callers.
97        return new ClientHintsLookupResults( $referenceIdsToClientHintIds, $uniqueClientHintsDataCombinations );
98    }
99
100    /**
101     * Generates the first results array for use in
102     * ::getClientHintsByReferenceIds.
103     *
104     * @param ClientHintsReferenceIds $referenceIds The reference IDs passed to ::getClientHintsByReferenceIds
105     * @return array The first results array which is a two-dimensional map where the first
106     *   dimension keys is the reference type, the second dimension keys is the reference ID,
107     *   and the values is initially an empty array.
108     */
109    private function prepareFirstResultsArray( ClientHintsReferenceIds $referenceIds ): array {
110        $referenceIdsToClientHintIds = [];
111        foreach ( $referenceIds->getReferenceIds() as $mappingId => $referenceIdsForMappingId ) {
112            if ( !count( $referenceIdsForMappingId ) ) {
113                // If there are no reference IDs for this reference type, then just skip to the
114                // next reference type.
115                continue;
116            }
117            // Use an empty array at first as the results have not been generated.
118            $referenceIdsToClientHintIds[$mappingId] = array_fill_keys(
119                $referenceIdsForMappingId,
120                []
121            );
122        }
123        return $referenceIdsToClientHintIds;
124    }
125
126    /**
127     * Generates an array of unique combinations of uach_ids
128     * and updates the first results array provided as the
129     * first argument to reference the key associated with
130     * the array in the newly generated second results array.
131     *
132     * @param array &$referenceIdsToClientHintIds The first results list currently under construction
133     * @return array The unique combinations of uach_ids as values with integer keys.
134     */
135    private function generateUniqueClientHintsIdCombinations( array &$referenceIdsToClientHintIds ): array {
136        // Generate an two-dimensional array of all the uach_ids
137        // grouped by their reference ID.
138        $clientHintIdCombinations = [];
139        foreach ( $referenceIdsToClientHintIds as &$referenceIdsForMapId ) {
140            foreach ( $referenceIdsForMapId as &$clientHintsIds ) {
141                // Sort the array as the ordering of the uach_id values
142                // does not affect what ClientHintsData object is produced.
143                // Therefore sorting the IDs helps to eliminate duplicates.
144                sort( $clientHintsIds, SORT_NUMERIC );
145                // Add this array of uach_ids to the combinations array
146                $clientHintIdCombinations[] = $clientHintsIds;
147            }
148        }
149
150        // Make the combinations unique (SORT_REGULAR is used as this is performed on arrays)
151        // and get rid of the previous integer keys.
152        $clientHintIdCombinations = array_values( array_unique( $clientHintIdCombinations, SORT_REGULAR ) );
153
154        foreach ( $referenceIdsToClientHintIds as &$referenceIdsForMapId ) {
155            foreach ( $referenceIdsForMapId as &$clientHintsIds ) {
156                // Update the $referenceIdsToClientHintIds array to now reference the integer key
157                // for this array of uach_ids in $uniqueClientHintsDataCombinations.
158                $clientHintsIds = array_search( $clientHintsIds, $clientHintIdCombinations );
159            }
160        }
161
162        return $clientHintIdCombinations;
163    }
164}