Файловый менеджер - Редактировать - /var/www/html/mediawiki-1.43.1/extensions/CheckUser/src/Services/UserAgentClientHintsManager.php
Ðазад
<?php namespace MediaWiki\CheckUser\Services; use DatabaseLogEntry; use LogicException; use MediaWiki\CheckUser\ClientHints\ClientHintsData; use MediaWiki\CheckUser\ClientHints\ClientHintsReferenceIds; use MediaWiki\Config\ServiceOptions; use MediaWiki\Revision\RevisionLookup; use Psr\Log\LoggerInterface; use StatusValue; use Wikimedia\Rdbms\IConnectionProvider; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Timestamp\ConvertibleTimestamp; /** * Service to insert and delete user-agent client hint values and their associations with rows in cu_changes, * cu_log_event and cu_private_event. */ class UserAgentClientHintsManager { public const CONSTRUCTOR_OPTIONS = [ 'CUDMaxAge', ]; public const SUPPORTED_TYPES = [ 'revision', ]; /** * TINYINT references for use in cu_useragent_clienthints_map.uachm_reference_type */ // Identifier for the cu_changes table public const IDENTIFIER_CU_CHANGES = 0; // Identifier for the cu_log_event table public const IDENTIFIER_CU_LOG_EVENT = 1; // Identifier for the cu_private_event table public const IDENTIFIER_CU_PRIVATE_EVENT = 2; public const IDENTIFIER_TO_TABLE_NAME_MAP = [ self::IDENTIFIER_CU_CHANGES => 'cu_changes', self::IDENTIFIER_CU_LOG_EVENT => 'cu_log_event', self::IDENTIFIER_CU_PRIVATE_EVENT => 'cu_private_event', ]; public const IDENTIFIER_TO_COLUMN_NAME_MAP = [ self::IDENTIFIER_CU_CHANGES => 'cuc_this_oldid', self::IDENTIFIER_CU_LOG_EVENT => 'cule_log_id', self::IDENTIFIER_CU_PRIVATE_EVENT => 'cupe_id', ]; private IDatabase $dbw; private IReadableDatabase $dbr; private RevisionLookup $revisionLookup; private ServiceOptions $options; private LoggerInterface $logger; /** * @param IConnectionProvider $connectionProvider * @param RevisionLookup $revisionLookup * @param ServiceOptions $options * @param LoggerInterface $logger */ public function __construct( IConnectionProvider $connectionProvider, RevisionLookup $revisionLookup, ServiceOptions $options, LoggerInterface $logger ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; $this->dbw = $connectionProvider->getPrimaryDatabase(); $this->dbr = $connectionProvider->getReplicaDatabase(); $this->revisionLookup = $revisionLookup; $this->logger = $logger; } /** * Given an array of client hint data, a reference ID, and an identifier type, record the data to the * cu_useragent_clienthints and cu_useragent_clienthints_map tables. * * @param ClientHintsData $clientHintsData * @param int $referenceId An ID to use in `uachm_reference_id` column in the * cu_useragent_clienthints_map table * @param string $type The type of event this data is associated with. Valid values are: * - revision * @param bool $usePrimary If true, use the primary DB for SELECT queries. * @return StatusValue */ public function insertClientHintValues( ClientHintsData $clientHintsData, int $referenceId, string $type, bool $usePrimary = false ): StatusValue { // Check if there are rows to insert to the map table. $rows = $clientHintsData->toDatabaseRows(); if ( !count( $rows ) ) { // Nothing to insert, so return early. // Having nothing to insert isn't considered "bad", so return a new good // For example, a browser could choose to provide no Client Hints data but // still send an empty API request. return StatusValue::newGood(); } // Check for existing entry. $existingRecord = $this->dbr->newSelectQueryBuilder() ->table( 'cu_useragent_clienthints_map' ) ->where( [ 'uachm_reference_type' => $this->getMapIdByType( $type ), 'uachm_reference_id' => $referenceId ] ) ->caller( __METHOD__ ) ->fetchRowCount(); if ( $existingRecord ) { return StatusValue::newFatal( 'checkuser-api-useragent-clienthints-mappings-exist', [ $type, $referenceId ] ); } $rows = $this->excludeExistingClientHintData( $rows, $usePrimary ); if ( count( $rows ) ) { $this->dbw->newInsertQueryBuilder() ->insertInto( 'cu_useragent_clienthints' ) ->ignore() ->rows( $rows ) ->caller( __METHOD__ ) ->execute(); // We just inserted rows to cu_useragent_clienthints, so // use the primary DB for subsequent SELECT queries. $usePrimary = true; } return $this->insertMappingRows( $clientHintsData, $referenceId, $type, $usePrimary ); } /** * Given an identifier for the type of event (e.g. 'revision'), return the relevant TINYINT * for the table that the database entry for cu_useragent_clienthints_map refers to * * @param string $type * @return int One of self::IDENTIFIER_* constants */ private function getMapIdByType( string $type ): int { switch ( $type ) { case 'revision': return self::IDENTIFIER_CU_CHANGES; case 'log': return self::IDENTIFIER_CU_LOG_EVENT; case 'privatelog': return self::IDENTIFIER_CU_PRIVATE_EVENT; default: throw new LogicException( "Invalid type $type" ); } } /** * Insert rows into the cu_useragent_clienthints_map table. * * This links a foreign ID (e.g. "revision 1234") with client hint data values stored in cu_useragent_clienthints. * * @param ClientHintsData $clientHintsData * @param int $foreignId * @param string $type * @param bool $usePrimary If true, use the primary DB for SELECT queries. * @return StatusValue * @see insertClientHintValues, which invokes this method. * */ private function insertMappingRows( ClientHintsData $clientHintsData, int $foreignId, string $type, bool $usePrimary = false ): StatusValue { $rows = $clientHintsData->toDatabaseRows(); // We might need primary DB if the call is happening in the context of a server-side hook, $db = $usePrimary ? $this->dbw : $this->dbr; // TINYINT reference to cu_changes, cu_log_event or cu_private_event. $idType = $this->getMapIdByType( $type ); $mapRows = []; foreach ( $rows as $row ) { $result = $db->newSelectQueryBuilder() ->table( 'cu_useragent_clienthints' ) ->field( 'uach_id' ) ->where( $row ) ->caller( __METHOD__ ) ->fetchField(); if ( $result !== false ) { $mapRows[] = [ 'uachm_uach_id' => (int)$result, 'uachm_reference_type' => $idType, 'uachm_reference_id' => $foreignId, ]; } else { $this->logger->warning( "Lookup failed for cu_useragent_clienthints row with name {name} and value {value}.", [ $row['uach_name'], $row['uach_value'] ] ); } } if ( count( $mapRows ) ) { $this->dbw->newInsertQueryBuilder() ->insertInto( 'cu_useragent_clienthints_map' ) ->ignore() ->rows( $mapRows ) ->caller( __METHOD__ ) ->execute(); } return StatusValue::newGood(); } /** * Given reference IDs this method finds and deletes * the mapping entries for these reference IDs. * * @param ClientHintsReferenceIds $clientHintsReferenceIds * @return int The number of mapping rows deleted. */ public function deleteMappingRows( ClientHintsReferenceIds $clientHintsReferenceIds ): int { // Keep a track of the number of mapping rows that are deleted. $mappingRowsDeleted = 0; foreach ( $clientHintsReferenceIds->getReferenceIds() as $mapId => $referenceIds ) { if ( !count( $referenceIds ) ) { continue; } // Delete the rows in cu_useragent_clienthints_map associated with these reference IDs $this->dbw->newDeleteQueryBuilder() ->table( 'cu_useragent_clienthints_map' ) ->where( [ 'uachm_reference_id' => $referenceIds, 'uachm_reference_type' => $mapId ] ) ->caller( __METHOD__ ) ->execute(); $mappingRowsDeleted += $this->dbw->affectedRows(); } if ( !$mappingRowsDeleted ) { $this->logger->info( "No mapping rows deleted." ); } else { $this->logger->debug( "Deleted {mapping_rows_deleted} mapping rows.", [ 'mapping_rows_deleted' => $mappingRowsDeleted ] ); } return $mappingRowsDeleted; } /** * Checks 100 rows with the smallest uachm_reference_id * for each uachm_reference_type value to see whether their * associated entry referenced by the uachm_reference_id * value has been already purged. * * If it reaches an entry that is not orphaned, the checks are * stopped as items with a larger reference ID are unlikely to * be orphaned. * * This catches rows that have been left without deletion * due to unforeseen circumstances, as described in T350681. * * @return int The number of orphaned map rows deleted. */ public function deleteOrphanedMapRows(): int { // Keep a track of the number of mapping rows that are deleted. $mappingRowsDeleted = 0; foreach ( self::IDENTIFIER_TO_TABLE_NAME_MAP as $mappingId => $table ) { // Get 100 rows with the given mapping ID $resultSet = $this->dbr->newSelectQueryBuilder() ->select( 'uachm_reference_id' ) ->table( 'cu_useragent_clienthints_map' ) ->where( [ 'uachm_reference_type' => $mappingId ] ) ->orderBy( 'uachm_reference_id' ) ->groupBy( 'uachm_reference_id' ) ->limit( 100 ) ->caller( __METHOD__ ) ->fetchResultSet(); foreach ( $resultSet as $row ) { // For each row, check if the ::isMapRowOrphaned method // indicates that the row is orphaned. $referenceId = $row->uachm_reference_id; $mapRowIsOrphaned = $this->isMapRowOrphaned( $referenceId, $mappingId ); if ( $mapRowIsOrphaned ) { // If the map row is orphaned, then perform the deletion // and add the affected rows count to the return count. $this->dbw->newDeleteQueryBuilder() ->deleteFrom( 'cu_useragent_clienthints_map' ) ->where( [ 'uachm_reference_id' => $referenceId, 'uachm_reference_type' => $mappingId, ] ) ->caller( __METHOD__ ) ->execute(); $mappingRowsDeleted += $this->dbw->affectedRows(); } else { // If the map row is probably not orphaned, then just stop processing // the rows in this table. break; } } } if ( $mappingRowsDeleted ) { $this->logger->info( "Deleted {mapping_rows_deleted} orphaned mapping rows.", [ 'mapping_rows_deleted' => $mappingRowsDeleted ] ); } return $mappingRowsDeleted; } /** * Returns whether rows with the given $referenceId and $mappingId * in cu_useragent_clienthints_map are likely orphaned. * * @param int $referenceId * @param int $mappingId * @return bool */ private function isMapRowOrphaned( int $referenceId, int $mappingId ): bool { if ( !array_key_exists( $mappingId, self::IDENTIFIER_TO_TABLE_NAME_MAP ) ) { throw new LogicException( "Unrecognised map ID '$mappingId'" ); } if ( !in_array( $mappingId, [ self::IDENTIFIER_CU_LOG_EVENT, self::IDENTIFIER_CU_CHANGES ] ) ) { // If the mapping ID is not for cu_changes or cu_log_event, // query the table directly to check if the associated reference ID // exists in the table. return !$this->dbr->newSelectQueryBuilder() ->field( '1' ) ->table( self::IDENTIFIER_TO_TABLE_NAME_MAP[$mappingId] ) ->where( [ self::IDENTIFIER_TO_COLUMN_NAME_MAP[$mappingId] => $referenceId ] ) ->caller( __METHOD__ ) ->fetchField(); } // If the mapping ID is for cu_changes or cu_log_event, // then query the revision table or logging table respectively // for the associated timestamp to determine if the map // row should have already been deleted. $associatedTimestamp = false; if ( $mappingId === self::IDENTIFIER_CU_CHANGES ) { // Get the timestamp from the revision lookup service $revisionRecord = $this->revisionLookup->getRevisionById( $referenceId ); if ( $revisionRecord ) { $associatedTimestamp = $revisionRecord->getTimestamp(); } } elseif ( $mappingId === self::IDENTIFIER_CU_LOG_EVENT ) { // Get the timestamp from using DatabaseLogEntry::newFromId $logObject = DatabaseLogEntry::newFromId( $referenceId, $this->dbr ); if ( $logObject ) { $associatedTimestamp = $logObject->getTimestamp(); } } // The map rows are considered orphaned if of the following any apply: // * There is no timestamp for the revision or log event (should be generally impossible for this // to be the case). // * No such reference ID exists (i.e. no such revision ID or log ID) // * The timestamp associated with the revision or log event is before the // wgCUDMaxAge + 100 seconds ago to the current time. // // The 100 seconds are added to wgCUDMaxAge to prevent attempting to delete map rows // that would have been normally deleted. This code is intended to catch map rows that // were not deleted normally. return !$associatedTimestamp || $associatedTimestamp < ConvertibleTimestamp::convert( TS_MW, ConvertibleTimestamp::time() - ( $this->options->get( 'CUDMaxAge' ) + 100 ) ); } /** * Helper method to avoid duplicate INSERT for existing client hint values. * * E.g. if "architecture: arm" already exists as a name/value pair, exclude this from the set of rows to insert. * * @param array[] $rows An array of arrays, where each array contains a key/value pair: * uach_name => "some name", * uach_value => "some value" * @param bool $usePrimary If true, use the primary DB for SELECT queries. * @return array[] An array of arrays to insert to the cu_useragent_clienthints table, see the @param $rows * documentation for the format. */ private function excludeExistingClientHintData( array $rows, bool $usePrimary = false ): array { $rowsToInsert = []; $db = $usePrimary ? $this->dbw : $this->dbr; foreach ( $rows as $row ) { $result = $db->newSelectQueryBuilder() ->table( 'cu_useragent_clienthints' ) ->where( $row ) ->caller( __METHOD__ ) ->fetchRowCount(); if ( $result === 0 ) { $rowsToInsert[] = $row; } } return $rowsToInsert; } }
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0 |
proxy
|
phpinfo
|
ÐаÑтройка