Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
MediaModerationFileProcessor
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
6 / 6
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 fileHasAllowedMediaType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fileHasAllowedMimeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fileCanBeRendered
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 canScanFile
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 insertFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\Extension\MediaModeration\Services;
4
5use ArchivedFile;
6use Error;
7use File;
8use MediaHandlerFactory;
9use Psr\Log\LoggerInterface;
10
11class MediaModerationFileProcessor {
12    /** @var string[] An array of mime types that are supported by PhotoDNA. */
13    public const ALLOWED_MIME_TYPES = [
14        'image/gif',
15        'image/jpeg',
16        'image/png',
17        'image/bmp',
18        'image/tiff',
19    ];
20
21    /** @var array An array of mediatypes that can be properly converted to an accepted mime type for PhotoDNA. */
22    private const ALLOWED_MEDIA_TYPES = [
23        MEDIATYPE_BITMAP,
24        MEDIATYPE_DRAWING,
25    ];
26
27    private MediaModerationDatabaseManager $mediaModerationDatabaseManager;
28    private MediaHandlerFactory $mediaHandlerFactory;
29    private LoggerInterface $logger;
30
31    public function __construct(
32        MediaModerationDatabaseManager $mediaModerationDatabaseManager,
33        MediaHandlerFactory $mediaHandlerFactory,
34        LoggerInterface $logger
35    ) {
36        $this->mediaModerationDatabaseManager = $mediaModerationDatabaseManager;
37        $this->mediaHandlerFactory = $mediaHandlerFactory;
38        $this->logger = $logger;
39    }
40
41    /**
42     * Returns whether a file has an allowed media type.
43     * This check is needed because some files may be
44     * renderable but not in a supported format (T352234).
45     *
46     * @param File|ArchivedFile $file
47     * @return bool
48     */
49    private function fileHasAllowedMediaType( $file ): bool {
50        return in_array( $file->getMediaType(), self::ALLOWED_MEDIA_TYPES, true );
51    }
52
53    /**
54     * Returns whether a file has an allowed mime type
55     * and therefore could be sent directly to PhotoDNA
56     * without having to convert the file type.
57     *
58     * @param File|ArchivedFile $file
59     * @return bool
60     */
61    private function fileHasAllowedMimeType( $file ): bool {
62        return in_array( $file->getMimeType(), self::ALLOWED_MIME_TYPES, true );
63    }
64
65    /**
66     * Returns whether a file can be likely rendered,
67     * which is the result of File::canRender. The behaviour
68     * is similar for ArchivedFile objects.
69     *
70     * @param File|ArchivedFile $file
71     * @return bool
72     */
73    private function fileCanBeRendered( $file ): bool {
74        if ( $file instanceof File ) {
75            return $file->canRender();
76        }
77        $mediaHandler = $this->mediaHandlerFactory->getHandler( $file->getMimeType() );
78        if ( $mediaHandler ) {
79            try {
80                // This suppression and passing of ArchivedFile to a MediaHandler method
81                // which expects a File object is in the same way as ArchivedFile::pageCount.
82                // TODO: Fix me if ArchivedFile ever extends File.
83                // @phan-suppress-next-line PhanTypeMismatchArgument
84                $fileCanBeRendered = $mediaHandler->canRender( $file );
85            } catch ( Error $e ) {
86                // All errors need to be caught, because a method not existing
87                // will raise the generic in-built Error exception.
88                // If the MediaHandler raises an exception for any reason the
89                // result of this method will be false, and no further actions
90                // would be taken for this file.
91                $this->logger->error(
92                    'Call to MediaHandler::canRender with an ArchivedFile did not work ' .
93                    'for handler {handlerclass}',
94                    [
95                        'handlerclass' => get_class( $mediaHandler ),
96                        'exception' => $e
97                    ]
98                );
99                return false;
100            }
101            // The ArchivedFile::exists check is done to make this similar to File::canRender.
102            return $fileCanBeRendered && $file->exists();
103        }
104        return false;
105    }
106
107    /**
108     * Returns whether a file can be scanned by PhotoDNA.
109     *
110     * This currently is limited to whether the file has
111     * a MIME type that is supported or can be rendered
112     * into a thumbnail of a supported MIME type.
113     *
114     * This is to be used to determine whether an image
115     * should be added to the scan table and should be
116     * used before attempting to scan the file.
117     *
118     * @param File|ArchivedFile $file
119     * @return bool
120     */
121    public function canScanFile( $file ): bool {
122        $canScanFile = $this->fileHasAllowedMediaType( $file ) &&
123            (
124                $this->fileHasAllowedMimeType( $file ) ||
125                $this->fileCanBeRendered( $file )
126            );
127        if ( !$canScanFile ) {
128            $this->logger->debug(
129                'File with SHA-1 {sha1} cannot be scanned by PhotoDNA',
130                [ 'sha1' => $file->getSha1() ]
131            );
132        }
133        return $canScanFile;
134    }
135
136    /**
137     * Should be called when a file has been created in the
138     * 'image' table, or when backfilling entries from the
139     * image, oldimage, and filearchive tables.
140     *
141     * @param File|ArchivedFile $file
142     * @return void
143     */
144    public function insertFile( $file ): void {
145        if ( $this->canScanFile( $file ) ) {
146            $this->mediaModerationDatabaseManager->insertFileToScanTable( $file );
147        }
148    }
149}