Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
66 / 66 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
1 / 1 |
MediaModerationPhotoDNAServiceProvider | |
100.00% |
66 / 66 |
|
100.00% |
3 / 3 |
12 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
check | |
100.00% |
42 / 42 |
|
100.00% |
1 / 1 |
8 | |||
getRequest | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\MediaModeration\Services; |
4 | |
5 | use ArchivedFile; |
6 | use File; |
7 | use FormatJson; |
8 | use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; |
9 | use MediaWiki\Config\ServiceOptions; |
10 | use MediaWiki\Extension\MediaModeration\PhotoDNA\IMediaModerationPhotoDNAServiceProvider; |
11 | use MediaWiki\Extension\MediaModeration\PhotoDNA\MediaModerationPhotoDNAResponseHandler; |
12 | use MediaWiki\Extension\MediaModeration\PhotoDNA\Response; |
13 | use MediaWiki\Http\HttpRequestFactory; |
14 | use MediaWiki\Language\RawMessage; |
15 | use MediaWiki\Status\StatusFormatter; |
16 | use MWHttpRequest; |
17 | use StatusValue; |
18 | |
19 | /** |
20 | * Service for interacting with Microsoft PhotoDNA API. |
21 | * |
22 | * @see https://developer.microsoftmoderator.com/docs/services/57c7426e2703740ec4c9f4c3/operations/57c7426f27037407c8cc69e6 |
23 | */ |
24 | class MediaModerationPhotoDNAServiceProvider implements IMediaModerationPhotoDNAServiceProvider { |
25 | |
26 | use MediaModerationPhotoDNAResponseHandler; |
27 | |
28 | public const CONSTRUCTOR_OPTIONS = [ |
29 | 'MediaModerationPhotoDNAUrl', |
30 | 'MediaModerationPhotoDNASubscriptionKey', |
31 | 'MediaModerationHttpProxy', |
32 | ]; |
33 | |
34 | private HttpRequestFactory $httpRequestFactory; |
35 | private StatsdDataFactoryInterface $perDbNameStatsdDataFactory; |
36 | private MediaModerationImageContentsLookup $mediaModerationImageContentsLookup; |
37 | private StatusFormatter $statusFormatter; |
38 | private string $photoDNAUrl; |
39 | private ?string $httpProxy; |
40 | private string $photoDNASubscriptionKey; |
41 | |
42 | /** |
43 | * @param ServiceOptions $options |
44 | * @param HttpRequestFactory $httpRequestFactory |
45 | * @param StatsdDataFactoryInterface $perDbNameStatsdDataFactory |
46 | * @param MediaModerationImageContentsLookup $mediaModerationImageContentsLookup |
47 | * @param StatusFormatter $statusFormatter |
48 | */ |
49 | public function __construct( |
50 | ServiceOptions $options, |
51 | HttpRequestFactory $httpRequestFactory, |
52 | StatsdDataFactoryInterface $perDbNameStatsdDataFactory, |
53 | MediaModerationImageContentsLookup $mediaModerationImageContentsLookup, |
54 | StatusFormatter $statusFormatter |
55 | ) { |
56 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
57 | $this->httpRequestFactory = $httpRequestFactory; |
58 | $this->photoDNAUrl = $options->get( 'MediaModerationPhotoDNAUrl' ); |
59 | $this->photoDNASubscriptionKey = $options->get( 'MediaModerationPhotoDNASubscriptionKey' ); |
60 | $this->httpProxy = $options->get( 'MediaModerationHttpProxy' ); |
61 | $this->perDbNameStatsdDataFactory = $perDbNameStatsdDataFactory; |
62 | $this->mediaModerationImageContentsLookup = $mediaModerationImageContentsLookup; |
63 | $this->statusFormatter = $statusFormatter; |
64 | } |
65 | |
66 | /** @inheritDoc */ |
67 | public function check( $file ): StatusValue { |
68 | $requestStatus = $this->getRequest( $file ); |
69 | if ( !$requestStatus->isGood() ) { |
70 | return $requestStatus; |
71 | } |
72 | /** @var MWHttpRequest $request */ |
73 | $request = $requestStatus->getValue(); |
74 | $start = microtime( true ); |
75 | $status = $request->execute(); |
76 | $delay = microtime( true ) - $start; |
77 | $this->perDbNameStatsdDataFactory->timing( |
78 | 'MediaModeration.PhotoDNAServiceProviderRequestTime', |
79 | 1000 * $delay |
80 | ); |
81 | if ( $status->isOK() ) { |
82 | $this->perDbNameStatsdDataFactory->increment( 'MediaModeration.PhotoDNAServiceProvider.Execute.OK' ); |
83 | } else { |
84 | $this->perDbNameStatsdDataFactory->increment( |
85 | 'MediaModeration.PhotoDNAServiceProvider.Execute.Error.' . $request->getStatus() |
86 | ); |
87 | } |
88 | if ( !$status->isOK() ) { |
89 | // Something went badly wrong. |
90 | $errorMessage = FormatJson::decode( $request->getContent(), true ); |
91 | if ( is_array( $errorMessage ) && isset( $errorMessage['message'] ) ) { |
92 | $errorMessage = $errorMessage['message']; |
93 | } else { |
94 | $errorMessage = 'Unable to get JSON in response from PhotoDNA'; |
95 | } |
96 | |
97 | return StatusValue::newFatal( |
98 | new RawMessage( |
99 | 'PhotoDNA returned HTTP ' . $request->getStatus() . ' error: ' . $errorMessage |
100 | ) |
101 | ); |
102 | } |
103 | $rawResponse = $request->getContent(); |
104 | $jsonParseStatus = FormatJson::parse( $rawResponse, FormatJson::FORCE_ASSOC ); |
105 | $responseJson = $jsonParseStatus->getValue(); |
106 | if ( !$jsonParseStatus->isOK() || !is_array( $responseJson ) ) { |
107 | $this->perDbNameStatsdDataFactory->increment( |
108 | 'MediaModeration.PhotoDNAServiceProvider.Execute.InvalidJsonResponse' |
109 | ); |
110 | return StatusValue::newFatal( new RawMessage( |
111 | 'PhotoDNA returned an invalid JSON body for $1. Parse error: $2', |
112 | [ $file->getName(), $this->statusFormatter->getWikiText( $jsonParseStatus ) ] |
113 | ) ); |
114 | } |
115 | $response = Response::newFromArray( $responseJson, $rawResponse ); |
116 | $this->perDbNameStatsdDataFactory->increment( |
117 | 'MediaModeration.PhotoDNAServiceProvider.Execute.StatusCode' . $response->getStatusCode() |
118 | ); |
119 | return $this->createStatusFromResponse( $response ); |
120 | } |
121 | |
122 | /** |
123 | * @param File|ArchivedFile $file |
124 | * @return StatusValue |
125 | */ |
126 | private function getRequest( $file ): StatusValue { |
127 | $imageContentsStatus = $this->mediaModerationImageContentsLookup->getImageContents( $file ); |
128 | if ( !$imageContentsStatus->isOK() ) { |
129 | // Hide the thumbnail contents and mime type from the caller of ::getRequest by |
130 | // creating a standard StatusValue and merging the ImageContentsStatus into it. |
131 | // This is done as these values will be null if we have reached here. |
132 | return StatusValue::newGood()->merge( $imageContentsStatus ); |
133 | } |
134 | $options = [ |
135 | 'method' => 'POST', |
136 | 'postData' => $imageContentsStatus->getImageContents() |
137 | ]; |
138 | if ( $this->httpProxy ) { |
139 | $options['proxy'] = $this->httpProxy; |
140 | } |
141 | $request = $this->httpRequestFactory->create( |
142 | $this->photoDNAUrl, |
143 | $options |
144 | ); |
145 | $request->setHeader( 'Content-Type', $imageContentsStatus->getMimeType() ); |
146 | $request->setHeader( 'Ocp-Apim-Subscription-Key', $this->photoDNASubscriptionKey ); |
147 | return StatusValue::newGood( $request ); |
148 | } |
149 | } |