Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
MediaModerationFileScanner
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
2 / 2
11
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 scanSha1
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2
3namespace MediaWiki\Extension\MediaModeration\Services;
4
5use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
6use MediaWiki\Extension\MediaModeration\PhotoDNA\IMediaModerationPhotoDNAServiceProvider;
7use MediaWiki\Extension\MediaModeration\PhotoDNA\Response;
8use MediaWiki\Language\RawMessage;
9use MediaWiki\Status\StatusFormatter;
10use Psr\Log\LoggerInterface;
11use StatusValue;
12
13/**
14 * Scans SHA-1 values in batches to reduce the effect that slow requests to
15 * PhotoDNA have on the overall speed of scanning.
16 */
17class MediaModerationFileScanner {
18
19    private MediaModerationDatabaseManager $mediaModerationDatabaseManager;
20    private MediaModerationDatabaseLookup $mediaModerationDatabaseLookup;
21    private MediaModerationFileLookup $mediaModerationFileLookup;
22    private MediaModerationFileProcessor $mediaModerationFileProcessor;
23    private IMediaModerationPhotoDNAServiceProvider $mediaModerationPhotoDNAServiceProvider;
24    private MediaModerationEmailer $mediaModerationEmailer;
25    private StatsdDataFactoryInterface $perDbNameStatsdDataFactory;
26    private StatusFormatter $statusFormatter;
27    private LoggerInterface $logger;
28
29    public function __construct(
30        MediaModerationDatabaseLookup $mediaModerationDatabaseLookup,
31        MediaModerationDatabaseManager $mediaModerationDatabaseManager,
32        MediaModerationFileLookup $mediaModerationFileLookup,
33        MediaModerationFileProcessor $mediaModerationFileProcessor,
34        IMediaModerationPhotoDNAServiceProvider $mediaModerationPhotoDNAServiceProvider,
35        MediaModerationEmailer $mediaModerationEmailer,
36        StatusFormatter $statusFormatter,
37        StatsdDataFactoryInterface $perDbNameStatsdDataFactory,
38        LoggerInterface $logger
39    ) {
40        $this->mediaModerationDatabaseLookup = $mediaModerationDatabaseLookup;
41        $this->mediaModerationDatabaseManager = $mediaModerationDatabaseManager;
42        $this->mediaModerationFileLookup = $mediaModerationFileLookup;
43        $this->mediaModerationFileProcessor = $mediaModerationFileProcessor;
44        $this->mediaModerationPhotoDNAServiceProvider = $mediaModerationPhotoDNAServiceProvider;
45        $this->mediaModerationEmailer = $mediaModerationEmailer;
46        $this->statusFormatter = $statusFormatter;
47        $this->perDbNameStatsdDataFactory = $perDbNameStatsdDataFactory;
48        $this->logger = $logger;
49    }
50
51    /**
52     * Scans the files that have the given SHA-1
53     *
54     * @param string $sha1
55     * @return StatusValue
56     */
57    public function scanSha1( string $sha1 ): StatusValue {
58        $returnStatus = new StatusValue();
59        // Until a match is got from PhotoDNA, the return status should be not okay as the operation has not completed.
60        $returnStatus->setOK( false );
61        // Get the current scan status from the DB, so that we keep the current value if
62        // nothing matches but still update the last checked value.
63        $oldMatchStatus = $this->mediaModerationDatabaseLookup->getMatchStatusForSha1( $sha1 );
64        $newMatchStatus = null;
65        foreach ( $this->mediaModerationFileLookup->getFileObjectsForSha1( $sha1 ) as $file ) {
66            if ( !$this->mediaModerationFileProcessor->canScanFile( $file ) ) {
67                // If this $file cannot be scanned, then try the next file with this SHA-1
68                // and if in verbose mode output to the console about this.
69                $this->perDbNameStatsdDataFactory->increment( 'MediaModeration.FileScanner.CanScanFileReturnedFalse' );
70                $returnStatus->fatal( new RawMessage(
71                    'The file $1 cannot be scanned.',
72                    [ $file->getName() ]
73                ) );
74                continue;
75            }
76            // Run the check using the PhotoDNA API.
77            $checkResult = $this->mediaModerationPhotoDNAServiceProvider->check( $file );
78            /** @var Response|null $response */
79            $response = $checkResult->getValue();
80            if ( $response === null || $response->getStatusCode() !== Response::STATUS_OK ) {
81                // Assume something is wrong with the thumbnail or source file if the request fails,
82                // and just try a new $file with this SHA-1. Add the information about the
83                // failure to the return status for tracking and logging.
84                $returnStatus->merge( $checkResult );
85                continue;
86            }
87            $newMatchStatus = $response->isMatch();
88            // Stop processing this SHA-1 as we have a result.
89            break;
90        }
91        // Update the match status, even if none of the $file objects could be scanned.
92        // If no scanning was successful, then the status will remain
93        $this->mediaModerationDatabaseManager->updateMatchStatusForSha1( $sha1, $newMatchStatus ?? $oldMatchStatus );
94        if ( $newMatchStatus && $newMatchStatus !== $oldMatchStatus ) {
95            // Send an email for this SHA-1 if the match status has changed to positive.
96            // If the match status was already positive, then an email has already been sent.
97            $this->mediaModerationEmailer->sendEmailForSha1( $sha1 );
98        }
99        if ( $newMatchStatus !== null ) {
100            $returnStatus->setResult( true, $newMatchStatus );
101        }
102        if ( !$returnStatus->isOK() ) {
103            // Create a info if the SHA-1 could not be scanned.
104            $this->logger->info(
105                'Unable to scan SHA-1 {sha1}. MediaModerationFileScanner::scanSha1 returned this: {return-message}',
106                [
107                    'sha1' => $sha1,
108                    'return-message' => $this->statusFormatter->getMessage( $returnStatus, [ 'lang' => 'en' ] ),
109                ]
110            );
111        } elseif ( !$returnStatus->isGood() ) {
112            // Create a debug if the SHA-1 scanning succeeded with warnings.
113            $this->logger->debug(
114                'Scan of SHA-1 {sha1} succeeded with warnings. MediaModerationFileScanner::scanSha1 ' .
115                'returned this: {return-message}',
116                [
117                    'sha1' => $sha1,
118                    'return-message' => $this->statusFormatter->getMessage( $returnStatus, [ 'lang' => 'en' ] ),
119                ]
120            );
121        }
122        return $returnStatus;
123    }
124}