Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
131 / 131 |
|
100.00% |
11 / 11 |
CRAP | |
100.00% |
1 / 1 |
Validate | |
100.00% |
131 / 131 |
|
100.00% |
11 / 11 |
67 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validateBoolean | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
validateRational | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
validateRating | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
validateInteger | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
validateClosed | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
9 | |||
validateReal | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
8 | |||
validateFlash | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
7 | |||
validateLangCode | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
validateDate | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
15 | |||
validateGPS | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
8 |
1 | <?php |
2 | /** |
3 | * Methods for validating XMP properties. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup Media |
22 | */ |
23 | |
24 | namespace Wikimedia\XMPReader; |
25 | |
26 | use Psr\Log\LoggerAwareInterface; |
27 | use Psr\Log\LoggerAwareTrait; |
28 | use Psr\Log\LoggerInterface; |
29 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
30 | |
31 | /** |
32 | * This contains some static methods for |
33 | * validating XMP properties. See XMPInfo and Reader classes. |
34 | * |
35 | * Each of these functions take the same parameters |
36 | * * an info array which is a subset of the XMPInfo::items array |
37 | * * A value (passed as reference) to validate. This can be either a |
38 | * simple value or an array |
39 | * * A boolean to determine if this is validating a simple or complex values |
40 | * |
41 | * It should be noted that when an array is being validated, typically the validation |
42 | * function is called once for each value, and then once at the end for the entire array. |
43 | * |
44 | * These validation functions can also be used to modify the data. See the gps and flash one's |
45 | * for example. |
46 | * |
47 | * @see https://www.adobe.com/devnet/xmp.html |
48 | * @see https://wwwimages2.adobe.com/content/dam/acom/en/devnet/xmp/pdfs/ |
49 | * XMP%20SDK%20Release%20cc-2016-08/XMPSpecificationPart1.pdf starting at pg 28 |
50 | * @see https://wwwimages2.adobe.com/content/dam/acom/en/devnet/xmp/pdfs/ |
51 | * XMP%20SDK%20Release%20cc-2016-08/XMPSpecificationPart2.pdf starting at pg 11 |
52 | */ |
53 | class Validate implements LoggerAwareInterface { |
54 | use LoggerAwareTrait; |
55 | |
56 | /** |
57 | * Creates a new instance, with a logger |
58 | * |
59 | * @param LoggerInterface $logger |
60 | */ |
61 | public function __construct( LoggerInterface $logger ) { |
62 | $this->setLogger( $logger ); |
63 | } |
64 | |
65 | /** |
66 | * Function to validate boolean properties ( True or False ) |
67 | * |
68 | * @param array $info Information about current property |
69 | * @param mixed &$val Current value to validate |
70 | * @param bool $standalone If this is a simple property or array |
71 | */ |
72 | public function validateBoolean( $info, &$val, $standalone ): void { |
73 | if ( !$standalone ) { |
74 | // this only validates standalone properties, not arrays, etc |
75 | // @codeCoverageIgnoreStart |
76 | return; |
77 | // @codeCoverageIgnoreEnd |
78 | } |
79 | if ( $val !== 'True' && $val !== 'False' ) { |
80 | $this->logger->info( __METHOD__ . " Expected True or False but got $val" ); |
81 | $val = null; |
82 | } |
83 | } |
84 | |
85 | /** |
86 | * function to validate rational properties ( 12/10 ) |
87 | * |
88 | * @param array $info Information about current property |
89 | * @param mixed &$val Current value to validate |
90 | * @param bool $standalone If this is a simple property or array |
91 | */ |
92 | public function validateRational( $info, &$val, $standalone ): void { |
93 | if ( !$standalone ) { |
94 | // this only validates standalone properties, not arrays, etc |
95 | // @codeCoverageIgnoreStart |
96 | return; |
97 | // @codeCoverageIgnoreEnd |
98 | } |
99 | if ( !preg_match( '/^(?:-?\d+)\/(?:\d+[1-9]|[1-9]\d*)$/D', $val ) ) { |
100 | $this->logger->info( __METHOD__ . " Expected rational but got $val" ); |
101 | $val = null; |
102 | } |
103 | } |
104 | |
105 | /** |
106 | * function to validate rating properties -1, 0-5 |
107 | * |
108 | * if its outside of range, put it into range. |
109 | * |
110 | * @see MWG spec |
111 | * @param array $info Information about current property |
112 | * @param mixed &$val Current value to validate |
113 | * @param bool $standalone If this is a simple property or array |
114 | */ |
115 | public function validateRating( $info, &$val, $standalone ): void { |
116 | if ( !$standalone ) { |
117 | // this only validates standalone properties, not arrays, etc |
118 | // @codeCoverageIgnoreStart |
119 | return; |
120 | // @codeCoverageIgnoreEnd |
121 | } |
122 | if ( !preg_match( '/^[-+]?\d*(?:\.?\d*)$/D', $val ) |
123 | || !is_numeric( $val ) |
124 | ) { |
125 | $this->logger->info( __METHOD__ . " Expected rating but got $val" ); |
126 | $val = null; |
127 | |
128 | return; |
129 | } |
130 | |
131 | $nVal = (float)$val; |
132 | if ( $nVal < 0 ) { |
133 | // We do < 0 here instead of < -1 here, since |
134 | // the values between 0 and -1 are also illegal |
135 | // as -1 is meant as a special reject rating. |
136 | $this->logger->info( __METHOD__ . " Rating too low, setting to -1 (Rejected)" ); |
137 | $val = '-1'; |
138 | |
139 | return; |
140 | } |
141 | if ( $nVal > 5 ) { |
142 | $this->logger->info( __METHOD__ . " Rating too high, setting to 5" ); |
143 | $val = '5'; |
144 | } |
145 | } |
146 | |
147 | /** |
148 | * function to validate integers |
149 | * |
150 | * @param array $info Information about current property |
151 | * @param mixed &$val Current value to validate |
152 | * @param bool $standalone If this is a simple property or array |
153 | */ |
154 | public function validateInteger( $info, &$val, $standalone ): void { |
155 | if ( !$standalone ) { |
156 | // this only validates standalone properties, not arrays, etc |
157 | // @codeCoverageIgnoreStart |
158 | return; |
159 | // @codeCoverageIgnoreEnd |
160 | } |
161 | if ( !preg_match( '/^[-+]?\d+$/D', $val ) ) { |
162 | $this->logger->info( __METHOD__ . " Expected integer but got $val" ); |
163 | $val = null; |
164 | } |
165 | } |
166 | |
167 | /** |
168 | * function to validate properties with a fixed number of allowed |
169 | * choices. (closed choice) |
170 | * |
171 | * @param array $info Information about current property |
172 | * @param mixed &$val Current value to validate |
173 | * @param bool $standalone If this is a simple property or array |
174 | */ |
175 | public function validateClosed( $info, &$val, $standalone ): void { |
176 | if ( !$standalone ) { |
177 | // this only validates standalone properties, not arrays, etc |
178 | // @codeCoverageIgnoreStart |
179 | return; |
180 | // @codeCoverageIgnoreEnd |
181 | } |
182 | |
183 | // check if it's in a numeric range |
184 | $inRange = false; |
185 | if ( is_numeric( $val ) |
186 | && isset( $info['rangeLow'] ) && isset( $info['rangeHigh'] ) |
187 | && ( (int)$val <= $info['rangeHigh'] ) && ( (int)$val >= $info['rangeLow'] ) |
188 | ) { |
189 | $inRange = true; |
190 | } |
191 | |
192 | if ( !isset( $info['choices'][$val] ) && !$inRange ) { |
193 | $this->logger->info( __METHOD__ . " Expected closed choice, but got $val" ); |
194 | $val = null; |
195 | } |
196 | } |
197 | |
198 | /** |
199 | * function to validate and modify real numbers, with ranges |
200 | * |
201 | * @param array $info Information about current property |
202 | * @param mixed &$val Current value to validate |
203 | * @param bool $standalone If this is a simple property or array |
204 | */ |
205 | public function validateReal( $info, &$val, $standalone ): void { |
206 | if ( !$standalone ) { |
207 | // this only validates standalone properties, not arrays, etc |
208 | // @codeCoverageIgnoreStart |
209 | return; |
210 | // @codeCoverageIgnoreEnd |
211 | } |
212 | |
213 | $isReal = is_numeric( $val ) && (float)$val; |
214 | if ( !$isReal ) { |
215 | $this->logger->info( __METHOD__ . " Expected real, but got $val" ); |
216 | $val = null; |
217 | return; |
218 | } |
219 | |
220 | // check if it's in a numeric range |
221 | if ( isset( $info['rangeLow'] ) && isset( $info['rangeHigh'] ) |
222 | && ( (float)$val > $info['rangeHigh'] || (float)$val < $info['rangeLow'] ) |
223 | ) { |
224 | $this->logger->info( |
225 | __METHOD__ |
226 | . " Expected value within range of {$info['rangeLow']}-{$info['rangeHigh']}, but got $val" |
227 | ); |
228 | $val = null; |
229 | } |
230 | } |
231 | |
232 | /** |
233 | * function to validate and modify flash structure |
234 | * |
235 | * @param array $info Information about current property |
236 | * @param mixed &$val Current value to validate |
237 | * @param bool $standalone If this is a simple property or array |
238 | */ |
239 | public function validateFlash( $info, &$val, $standalone ): void { |
240 | if ( $standalone ) { |
241 | // this only validates flash structs, not individual properties |
242 | // @codeCoverageIgnoreStart |
243 | return; |
244 | // @codeCoverageIgnoreEnd |
245 | } |
246 | if ( !isset( $val['Fired'] ) || |
247 | !isset( $val['Function'] ) || |
248 | !isset( $val['Mode'] ) || |
249 | !isset( $val['RedEyeMode'] ) || |
250 | !isset( $val['Return'] ) |
251 | ) { |
252 | $this->logger->info( __METHOD__ . ' Flash structure did not have all the required components' ); |
253 | $val = null; |
254 | } else { |
255 | // @phan-suppress-next-line PhanTypeInvalidRightOperandOfBitwiseOp |
256 | $val = ( 0 | ( $val['Fired'] === 'True' ) |
257 | | ( (int)$val['Return'] << 1 ) |
258 | | ( (int)$val['Mode'] << 3 ) |
259 | // @phan-suppress-next-line PhanTypeInvalidLeftOperandOfIntegerOp |
260 | | ( ( $val['Function'] === 'True' ) << 5 ) |
261 | // @phan-suppress-next-line PhanTypeInvalidLeftOperandOfIntegerOp |
262 | | ( ( $val['RedEyeMode'] === 'True' ) << 6 ) ); |
263 | } |
264 | } |
265 | |
266 | /** |
267 | * function to validate LangCode properties ( en-GB, etc. ) |
268 | * |
269 | * This is just a naive check to make sure it somewhat looks like a lang code. |
270 | * |
271 | * @see BCP 47 |
272 | * @see https://wwwimages2.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/ |
273 | * XMP%20SDK%20Release%20cc-2014-12/XMPSpecificationPart1.pdf page 22 (section 8.2.2.4) |
274 | * |
275 | * @param array $info Information about current property |
276 | * @param mixed &$val Current value to validate |
277 | * @param bool $standalone If this is a simple property or array |
278 | */ |
279 | public function validateLangCode( $info, &$val, $standalone ): void { |
280 | if ( !$standalone ) { |
281 | // this only validates standalone properties, not arrays, etc |
282 | // @codeCoverageIgnoreStart |
283 | return; |
284 | // @codeCoverageIgnoreEnd |
285 | } |
286 | if ( !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $val ) ) { |
287 | // this is a rather naive check. |
288 | $this->logger->info( __METHOD__ . " Expected Lang code but got $val" ); |
289 | $val = null; |
290 | } |
291 | } |
292 | |
293 | /** |
294 | * function to validate date properties, and convert to (partial) Exif format. |
295 | * |
296 | * Dates can be one of the following formats: |
297 | * YYYY |
298 | * YYYY-MM |
299 | * YYYY-MM-DD |
300 | * YYYY-MM-DDThh:mmTZD |
301 | * YYYY-MM-DDThh:mm:ssTZD |
302 | * YYYY-MM-DDThh:mm:ss.sTZD |
303 | * |
304 | * @param array $info Information about current property |
305 | * @param mixed &$val Current value to validate. Converts to TS_EXIF as a side effect. |
306 | * in cases where there's only a partial date, it will give things like |
307 | * 2011:04. |
308 | * @param bool $standalone If this is a simple property or array |
309 | */ |
310 | public function validateDate( $info, &$val, $standalone ): void { |
311 | if ( !$standalone ) { |
312 | // this only validates standalone properties, not arrays, etc |
313 | // @codeCoverageIgnoreStart |
314 | return; |
315 | // @codeCoverageIgnoreEnd |
316 | } |
317 | $res = []; |
318 | if ( !preg_match( |
319 | /* ahh! scary regex... */ |
320 | // phpcs:ignore Generic.Files.LineLength |
321 | '/^([0-3]\d{3})(?:-([01]\d)(?:-([0-3]\d)(?:T([0-2]\d):([0-6]\d)(?::([0-6]\d)(?:\.\d+)?)?([-+]\d{2}:\d{2}|Z)?)?)?)?$/D', |
322 | $val, $res ) |
323 | ) { |
324 | $this->logger->info( __METHOD__ . " Expected date but got $val" ); |
325 | $val = null; |
326 | return; |
327 | } |
328 | |
329 | /* |
330 | * $res is formatted as follows: |
331 | * 0 -> full date. |
332 | * 1 -> year, 2-> month, 3-> day, 4-> hour, 5-> minute, 6->second |
333 | * 7-> Timezone specifier (Z or something like +12:30 ) |
334 | * many parts are optional, some aren't. For example if you specify |
335 | * minute, you must specify hour, day, month, and year but not second or TZ. |
336 | */ |
337 | |
338 | /* |
339 | * First of all, if year = 0000, Something is wrong-ish, |
340 | * so don't extract. This seems to happen when |
341 | * some programs convert between metadata formats. |
342 | */ |
343 | if ( $res[1] === '0000' ) { |
344 | $this->logger->info( __METHOD__ . " Invalid date (year 0): $val" ); |
345 | $val = null; |
346 | |
347 | return; |
348 | } |
349 | |
350 | // hour |
351 | if ( !isset( $res[4] ) ) { |
352 | // just have the year month day (if that) |
353 | $val = $res[1]; |
354 | if ( isset( $res[2] ) ) { |
355 | $val .= ':' . $res[2]; |
356 | } |
357 | if ( isset( $res[3] ) ) { |
358 | $val .= ':' . $res[3]; |
359 | } |
360 | |
361 | return; |
362 | } |
363 | |
364 | if ( !isset( $res[7] ) || $res[7] === 'Z' ) { |
365 | // if hour is set, then minute must also be or regex above will fail. |
366 | $val = $res[1] . ':' . $res[2] . ':' . $res[3] |
367 | . ' ' . $res[4] . ':' . $res[5]; |
368 | if ( isset( $res[6] ) && $res[6] !== '' ) { |
369 | $val .= ':' . $res[6]; |
370 | } |
371 | |
372 | return; |
373 | } |
374 | |
375 | // Extra check for empty string necessary due to TZ but no second case. |
376 | $stripSeconds = false; |
377 | if ( !isset( $res[6] ) || $res[6] === '' ) { |
378 | $res[6] = '00'; |
379 | $stripSeconds = true; |
380 | } |
381 | |
382 | // Do timezone processing. We've already done the case that tz = Z. |
383 | |
384 | // We know that if we got to this step, year, month day hour and min must be set |
385 | // by virtue of regex not failing. |
386 | |
387 | $unix = ConvertibleTimestamp::convert( TS_UNIX, |
388 | $res[1] . $res[2] . $res[3] . $res[4] . $res[5] . $res[6] |
389 | ); |
390 | $offset = (int)substr( $res[7], 1, 2 ) * 60 * 60; |
391 | $offset += (int)substr( $res[7], 4, 2 ) * 60; |
392 | if ( substr( $res[7], 0, 1 ) === '-' ) { |
393 | $offset = -$offset; |
394 | } |
395 | $val = ConvertibleTimestamp::convert( TS_EXIF, (int)$unix + $offset ); |
396 | |
397 | if ( $stripSeconds ) { |
398 | // If seconds weren't specified, remove the trailing ':00'. |
399 | $val = substr( $val, 0, -3 ); |
400 | } |
401 | } |
402 | |
403 | /** function to validate, and more importantly |
404 | * translate the XMP DMS form of gps coords to |
405 | * the decimal form we use. |
406 | * |
407 | * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf |
408 | * section 1.2.7.4 on page 23 |
409 | * |
410 | * @param array $info Unused (info about prop) |
411 | * @param string &$val GPS string in either DDD,MM,SSk or |
412 | * or DDD,MM.mmk form |
413 | * @param bool $standalone If it's a simple prop (should always be true) |
414 | */ |
415 | public function validateGPS( $info, &$val, $standalone ): void { |
416 | if ( !$standalone ) { |
417 | // this only validates standalone properties, not arrays, etc |
418 | // @codeCoverageIgnoreStart |
419 | return; |
420 | // @codeCoverageIgnoreEnd |
421 | } |
422 | |
423 | $m = []; |
424 | if ( preg_match( |
425 | '/(\d{1,3}),(\d{1,2}),(\d{1,2})([NWSE])/D', |
426 | $val, $m ) |
427 | ) { |
428 | $coord = (int)$m[1]; |
429 | $coord += (int)$m[2] * ( 1 / 60 ); |
430 | $coord += (int)$m[3] * ( 1 / 3600 ); |
431 | if ( $m[4] === 'S' || $m[4] === 'W' ) { |
432 | $coord = -$coord; |
433 | } |
434 | $val = $coord; |
435 | |
436 | return; |
437 | } |
438 | |
439 | if ( preg_match( |
440 | '/(\d{1,3}),(\d{1,2}(?:.\d*)?)([NWSE])/D', |
441 | $val, $m ) |
442 | ) { |
443 | $coord = (int)$m[1]; |
444 | $coord += (float)$m[2] * ( 1 / 60 ); |
445 | if ( $m[3] === 'S' || $m[3] === 'W' ) { |
446 | $coord = -$coord; |
447 | } |
448 | $val = $coord; |
449 | |
450 | return; |
451 | } |
452 | |
453 | $this->logger->info( __METHOD__ |
454 | . " Expected GPSCoordinate, but got $val." ); |
455 | $val = null; |
456 | } |
457 | } |