[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Formatting of image metadata values into human readable form. 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 * @ingroup Media 21 * @author Ævar Arnfjörð Bjarmason <[email protected]> 22 * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason, 2009 Brent Garber, 2010 Brian Wolff 23 * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 24 * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification 25 * @file 26 */ 27 28 /** 29 * Format Image metadata values into a human readable form. 30 * 31 * Note lots of these messages use the prefix 'exif' even though 32 * they may not be exif properties. For example 'exif-ImageDescription' 33 * can be the Exif ImageDescription, or it could be the iptc-iim caption 34 * property, or it could be the xmp dc:description property. This 35 * is because these messages should be independent of how the data is 36 * stored, sine the user doesn't care if the description is stored in xmp, 37 * exif, etc only that its a description. (Additionally many of these properties 38 * are merged together following the MWG standard, such that for example, 39 * exif properties override XMP properties that mean the same thing if 40 * there is a conflict). 41 * 42 * It should perhaps use a prefix like 'metadata' instead, but there 43 * is already a large number of messages using the 'exif' prefix. 44 * 45 * @ingroup Media 46 * @since 1.23 the class extends ContextSource and various formerly-public 47 * internal methods are private 48 */ 49 class FormatMetadata extends ContextSource { 50 /** 51 * Only output a single language for multi-language fields 52 * @var bool 53 * @since 1.23 54 */ 55 protected $singleLang = false; 56 57 /** 58 * Trigger only outputting single language for multilanguage fields 59 * 60 * @param bool $val 61 * @since 1.23 62 */ 63 public function setSingleLanguage( $val ) { 64 $this->singleLang = $val; 65 } 66 67 /** 68 * Numbers given by Exif user agents are often magical, that is they 69 * should be replaced by a detailed explanation depending on their 70 * value which most of the time are plain integers. This function 71 * formats Exif (and other metadata) values into human readable form. 72 * 73 * This is the usual entry point for this class. 74 * 75 * @param array $tags The Exif data to format ( as returned by 76 * Exif::getFilteredData() or BitmapMetadataHandler ) 77 * @param bool|IContextSource $context Context to use (optional) 78 * @return array 79 */ 80 public static function getFormattedData( $tags, $context = false ) { 81 $obj = new FormatMetadata; 82 if ( $context ) { 83 $obj->setContext( $context ); 84 } 85 86 return $obj->makeFormattedData( $tags ); 87 } 88 89 /** 90 * Numbers given by Exif user agents are often magical, that is they 91 * should be replaced by a detailed explanation depending on their 92 * value which most of the time are plain integers. This function 93 * formats Exif (and other metadata) values into human readable form. 94 * 95 * @param array $tags The Exif data to format ( as returned by 96 * Exif::getFilteredData() or BitmapMetadataHandler ) 97 * @return array 98 * @since 1.23 99 */ 100 public function makeFormattedData( $tags ) { 101 $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3; 102 unset( $tags['ResolutionUnit'] ); 103 104 foreach ( $tags as $tag => &$vals ) { 105 106 // This seems ugly to wrap non-array's in an array just to unwrap again, 107 // especially when most of the time it is not an array 108 if ( !is_array( $tags[$tag] ) ) { 109 $vals = array( $vals ); 110 } 111 112 // _type is a special value to say what array type 113 if ( isset( $tags[$tag]['_type'] ) ) { 114 $type = $tags[$tag]['_type']; 115 unset( $vals['_type'] ); 116 } else { 117 $type = 'ul'; // default unordered list. 118 } 119 120 //This is done differently as the tag is an array. 121 if ( $tag == 'GPSTimeStamp' && count( $vals ) === 3 ) { 122 //hour min sec array 123 124 $h = explode( '/', $vals[0] ); 125 $m = explode( '/', $vals[1] ); 126 $s = explode( '/', $vals[2] ); 127 128 // this should already be validated 129 // when loaded from file, but it could 130 // come from a foreign repo, so be 131 // paranoid. 132 if ( !isset( $h[1] ) 133 || !isset( $m[1] ) 134 || !isset( $s[1] ) 135 || $h[1] == 0 136 || $m[1] == 0 137 || $s[1] == 0 138 ) { 139 continue; 140 } 141 $tags[$tag] = str_pad( intval( $h[0] / $h[1] ), 2, '0', STR_PAD_LEFT ) 142 . ':' . str_pad( intval( $m[0] / $m[1] ), 2, '0', STR_PAD_LEFT ) 143 . ':' . str_pad( intval( $s[0] / $s[1] ), 2, '0', STR_PAD_LEFT ); 144 145 try { 146 $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] ); 147 // the 1971:01:01 is just a placeholder, and not shown to user. 148 if ( $time && intval( $time ) > 0 ) { 149 $tags[$tag] = $this->getLanguage()->time( $time ); 150 } 151 } catch ( TimestampException $e ) { 152 // This shouldn't happen, but we've seen bad formats 153 // such as 4-digit seconds in the wild. 154 // leave $tags[$tag] as-is 155 } 156 continue; 157 } 158 159 // The contact info is a multi-valued field 160 // instead of the other props which are single 161 // valued (mostly) so handle as a special case. 162 if ( $tag === 'Contact' ) { 163 $vals = $this->collapseContactInfo( $vals ); 164 continue; 165 } 166 167 foreach ( $vals as &$val ) { 168 169 switch ( $tag ) { 170 case 'Compression': 171 switch ( $val ) { 172 case 1: 173 case 2: 174 case 3: 175 case 4: 176 case 5: 177 case 6: 178 case 7: 179 case 8: 180 case 32773: 181 case 32946: 182 case 34712: 183 $val = $this->exifMsg( $tag, $val ); 184 break; 185 default: 186 /* If not recognized, display as is. */ 187 break; 188 } 189 break; 190 191 case 'PhotometricInterpretation': 192 switch ( $val ) { 193 case 2: 194 case 6: 195 $val = $this->exifMsg( $tag, $val ); 196 break; 197 default: 198 /* If not recognized, display as is. */ 199 break; 200 } 201 break; 202 203 case 'Orientation': 204 switch ( $val ) { 205 case 1: 206 case 2: 207 case 3: 208 case 4: 209 case 5: 210 case 6: 211 case 7: 212 case 8: 213 $val = $this->exifMsg( $tag, $val ); 214 break; 215 default: 216 /* If not recognized, display as is. */ 217 break; 218 } 219 break; 220 221 case 'PlanarConfiguration': 222 switch ( $val ) { 223 case 1: 224 case 2: 225 $val = $this->exifMsg( $tag, $val ); 226 break; 227 default: 228 /* If not recognized, display as is. */ 229 break; 230 } 231 break; 232 233 // TODO: YCbCrSubSampling 234 case 'YCbCrPositioning': 235 switch ( $val ) { 236 case 1: 237 case 2: 238 $val = $this->exifMsg( $tag, $val ); 239 break; 240 default: 241 /* If not recognized, display as is. */ 242 break; 243 } 244 break; 245 246 case 'XResolution': 247 case 'YResolution': 248 switch ( $resolutionunit ) { 249 case 2: 250 $val = $this->exifMsg( 'XYResolution', 'i', $this->formatNum( $val ) ); 251 break; 252 case 3: 253 $val = $this->exifMsg( 'XYResolution', 'c', $this->formatNum( $val ) ); 254 break; 255 default: 256 /* If not recognized, display as is. */ 257 break; 258 } 259 break; 260 261 // TODO: YCbCrCoefficients #p27 (see annex E) 262 case 'ExifVersion': 263 case 'FlashpixVersion': 264 $val = "$val" / 100; 265 break; 266 267 case 'ColorSpace': 268 switch ( $val ) { 269 case 1: 270 case 65535: 271 $val = $this->exifMsg( $tag, $val ); 272 break; 273 default: 274 /* If not recognized, display as is. */ 275 break; 276 } 277 break; 278 279 case 'ComponentsConfiguration': 280 switch ( $val ) { 281 case 0: 282 case 1: 283 case 2: 284 case 3: 285 case 4: 286 case 5: 287 case 6: 288 $val = $this->exifMsg( $tag, $val ); 289 break; 290 default: 291 /* If not recognized, display as is. */ 292 break; 293 } 294 break; 295 296 case 'DateTime': 297 case 'DateTimeOriginal': 298 case 'DateTimeDigitized': 299 case 'DateTimeReleased': 300 case 'DateTimeExpires': 301 case 'GPSDateStamp': 302 case 'dc-date': 303 case 'DateTimeMetadata': 304 if ( $val == '0000:00:00 00:00:00' || $val == ' : : : : ' ) { 305 $val = $this->msg( 'exif-unknowndate' )->text(); 306 } elseif ( preg_match( 307 '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D', 308 $val 309 ) ) { 310 // Full date. 311 $time = wfTimestamp( TS_MW, $val ); 312 if ( $time && intval( $time ) > 0 ) { 313 $val = $this->getLanguage()->timeanddate( $time ); 314 } 315 } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) { 316 // No second field. Still format the same 317 // since timeanddate doesn't include seconds anyways, 318 // but second still available in api 319 $time = wfTimestamp( TS_MW, $val . ':00' ); 320 if ( $time && intval( $time ) > 0 ) { 321 $val = $this->getLanguage()->timeanddate( $time ); 322 } 323 } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d)$/D', $val ) ) { 324 // If only the date but not the time is filled in. 325 $time = wfTimestamp( TS_MW, substr( $val, 0, 4 ) 326 . substr( $val, 5, 2 ) 327 . substr( $val, 8, 2 ) 328 . '000000' ); 329 if ( $time && intval( $time ) > 0 ) { 330 $val = $this->getLanguage()->date( $time ); 331 } 332 } 333 // else it will just output $val without formatting it. 334 break; 335 336 case 'ExposureProgram': 337 switch ( $val ) { 338 case 0: 339 case 1: 340 case 2: 341 case 3: 342 case 4: 343 case 5: 344 case 6: 345 case 7: 346 case 8: 347 $val = $this->exifMsg( $tag, $val ); 348 break; 349 default: 350 /* If not recognized, display as is. */ 351 break; 352 } 353 break; 354 355 case 'SubjectDistance': 356 $val = $this->exifMsg( $tag, '', $this->formatNum( $val ) ); 357 break; 358 359 case 'MeteringMode': 360 switch ( $val ) { 361 case 0: 362 case 1: 363 case 2: 364 case 3: 365 case 4: 366 case 5: 367 case 6: 368 case 7: 369 case 255: 370 $val = $this->exifMsg( $tag, $val ); 371 break; 372 default: 373 /* If not recognized, display as is. */ 374 break; 375 } 376 break; 377 378 case 'LightSource': 379 switch ( $val ) { 380 case 0: 381 case 1: 382 case 2: 383 case 3: 384 case 4: 385 case 9: 386 case 10: 387 case 11: 388 case 12: 389 case 13: 390 case 14: 391 case 15: 392 case 17: 393 case 18: 394 case 19: 395 case 20: 396 case 21: 397 case 22: 398 case 23: 399 case 24: 400 case 255: 401 $val = $this->exifMsg( $tag, $val ); 402 break; 403 default: 404 /* If not recognized, display as is. */ 405 break; 406 } 407 break; 408 409 case 'Flash': 410 $flashDecode = array( 411 'fired' => $val & bindec( '00000001' ), 412 'return' => ( $val & bindec( '00000110' ) ) >> 1, 413 'mode' => ( $val & bindec( '00011000' ) ) >> 3, 414 'function' => ( $val & bindec( '00100000' ) ) >> 5, 415 'redeye' => ( $val & bindec( '01000000' ) ) >> 6, 416 // 'reserved' => ($val & bindec( '10000000' )) >> 7, 417 ); 418 $flashMsgs = array(); 419 # We do not need to handle unknown values since all are used. 420 foreach ( $flashDecode as $subTag => $subValue ) { 421 # We do not need any message for zeroed values. 422 if ( $subTag != 'fired' && $subValue == 0 ) { 423 continue; 424 } 425 $fullTag = $tag . '-' . $subTag; 426 $flashMsgs[] = $this->exifMsg( $fullTag, $subValue ); 427 } 428 $val = $this->getLanguage()->commaList( $flashMsgs ); 429 break; 430 431 case 'FocalPlaneResolutionUnit': 432 switch ( $val ) { 433 case 2: 434 $val = $this->exifMsg( $tag, $val ); 435 break; 436 default: 437 /* If not recognized, display as is. */ 438 break; 439 } 440 break; 441 442 case 'SensingMethod': 443 switch ( $val ) { 444 case 1: 445 case 2: 446 case 3: 447 case 4: 448 case 5: 449 case 7: 450 case 8: 451 $val = $this->exifMsg( $tag, $val ); 452 break; 453 default: 454 /* If not recognized, display as is. */ 455 break; 456 } 457 break; 458 459 case 'FileSource': 460 switch ( $val ) { 461 case 3: 462 $val = $this->exifMsg( $tag, $val ); 463 break; 464 default: 465 /* If not recognized, display as is. */ 466 break; 467 } 468 break; 469 470 case 'SceneType': 471 switch ( $val ) { 472 case 1: 473 $val = $this->exifMsg( $tag, $val ); 474 break; 475 default: 476 /* If not recognized, display as is. */ 477 break; 478 } 479 break; 480 481 case 'CustomRendered': 482 switch ( $val ) { 483 case 0: 484 case 1: 485 $val = $this->exifMsg( $tag, $val ); 486 break; 487 default: 488 /* If not recognized, display as is. */ 489 break; 490 } 491 break; 492 493 case 'ExposureMode': 494 switch ( $val ) { 495 case 0: 496 case 1: 497 case 2: 498 $val = $this->exifMsg( $tag, $val ); 499 break; 500 default: 501 /* If not recognized, display as is. */ 502 break; 503 } 504 break; 505 506 case 'WhiteBalance': 507 switch ( $val ) { 508 case 0: 509 case 1: 510 $val = $this->exifMsg( $tag, $val ); 511 break; 512 default: 513 /* If not recognized, display as is. */ 514 break; 515 } 516 break; 517 518 case 'SceneCaptureType': 519 switch ( $val ) { 520 case 0: 521 case 1: 522 case 2: 523 case 3: 524 $val = $this->exifMsg( $tag, $val ); 525 break; 526 default: 527 /* If not recognized, display as is. */ 528 break; 529 } 530 break; 531 532 case 'GainControl': 533 switch ( $val ) { 534 case 0: 535 case 1: 536 case 2: 537 case 3: 538 case 4: 539 $val = $this->exifMsg( $tag, $val ); 540 break; 541 default: 542 /* If not recognized, display as is. */ 543 break; 544 } 545 break; 546 547 case 'Contrast': 548 switch ( $val ) { 549 case 0: 550 case 1: 551 case 2: 552 $val = $this->exifMsg( $tag, $val ); 553 break; 554 default: 555 /* If not recognized, display as is. */ 556 break; 557 } 558 break; 559 560 case 'Saturation': 561 switch ( $val ) { 562 case 0: 563 case 1: 564 case 2: 565 $val = $this->exifMsg( $tag, $val ); 566 break; 567 default: 568 /* If not recognized, display as is. */ 569 break; 570 } 571 break; 572 573 case 'Sharpness': 574 switch ( $val ) { 575 case 0: 576 case 1: 577 case 2: 578 $val = $this->exifMsg( $tag, $val ); 579 break; 580 default: 581 /* If not recognized, display as is. */ 582 break; 583 } 584 break; 585 586 case 'SubjectDistanceRange': 587 switch ( $val ) { 588 case 0: 589 case 1: 590 case 2: 591 case 3: 592 $val = $this->exifMsg( $tag, $val ); 593 break; 594 default: 595 /* If not recognized, display as is. */ 596 break; 597 } 598 break; 599 600 //The GPS...Ref values are kept for compatibility, probably won't be reached. 601 case 'GPSLatitudeRef': 602 case 'GPSDestLatitudeRef': 603 switch ( $val ) { 604 case 'N': 605 case 'S': 606 $val = $this->exifMsg( 'GPSLatitude', $val ); 607 break; 608 default: 609 /* If not recognized, display as is. */ 610 break; 611 } 612 break; 613 614 case 'GPSLongitudeRef': 615 case 'GPSDestLongitudeRef': 616 switch ( $val ) { 617 case 'E': 618 case 'W': 619 $val = $this->exifMsg( 'GPSLongitude', $val ); 620 break; 621 default: 622 /* If not recognized, display as is. */ 623 break; 624 } 625 break; 626 627 case 'GPSAltitude': 628 if ( $val < 0 ) { 629 $val = $this->exifMsg( 'GPSAltitude', 'below-sealevel', $this->formatNum( -$val, 3 ) ); 630 } else { 631 $val = $this->exifMsg( 'GPSAltitude', 'above-sealevel', $this->formatNum( $val, 3 ) ); 632 } 633 break; 634 635 case 'GPSStatus': 636 switch ( $val ) { 637 case 'A': 638 case 'V': 639 $val = $this->exifMsg( $tag, $val ); 640 break; 641 default: 642 /* If not recognized, display as is. */ 643 break; 644 } 645 break; 646 647 case 'GPSMeasureMode': 648 switch ( $val ) { 649 case 2: 650 case 3: 651 $val = $this->exifMsg( $tag, $val ); 652 break; 653 default: 654 /* If not recognized, display as is. */ 655 break; 656 } 657 break; 658 659 case 'GPSTrackRef': 660 case 'GPSImgDirectionRef': 661 case 'GPSDestBearingRef': 662 switch ( $val ) { 663 case 'T': 664 case 'M': 665 $val = $this->exifMsg( 'GPSDirection', $val ); 666 break; 667 default: 668 /* If not recognized, display as is. */ 669 break; 670 } 671 break; 672 673 case 'GPSLatitude': 674 case 'GPSDestLatitude': 675 $val = $this->formatCoords( $val, 'latitude' ); 676 break; 677 case 'GPSLongitude': 678 case 'GPSDestLongitude': 679 $val = $this->formatCoords( $val, 'longitude' ); 680 break; 681 682 case 'GPSSpeedRef': 683 switch ( $val ) { 684 case 'K': 685 case 'M': 686 case 'N': 687 $val = $this->exifMsg( 'GPSSpeed', $val ); 688 break; 689 default: 690 /* If not recognized, display as is. */ 691 break; 692 } 693 break; 694 695 case 'GPSDestDistanceRef': 696 switch ( $val ) { 697 case 'K': 698 case 'M': 699 case 'N': 700 $val = $this->exifMsg( 'GPSDestDistance', $val ); 701 break; 702 default: 703 /* If not recognized, display as is. */ 704 break; 705 } 706 break; 707 708 case 'GPSDOP': 709 // See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS) 710 if ( $val <= 2 ) { 711 $val = $this->exifMsg( $tag, 'excellent', $this->formatNum( $val ) ); 712 } elseif ( $val <= 5 ) { 713 $val = $this->exifMsg( $tag, 'good', $this->formatNum( $val ) ); 714 } elseif ( $val <= 10 ) { 715 $val = $this->exifMsg( $tag, 'moderate', $this->formatNum( $val ) ); 716 } elseif ( $val <= 20 ) { 717 $val = $this->exifMsg( $tag, 'fair', $this->formatNum( $val ) ); 718 } else { 719 $val = $this->exifMsg( $tag, 'poor', $this->formatNum( $val ) ); 720 } 721 break; 722 723 // This is not in the Exif standard, just a special 724 // case for our purposes which enables wikis to wikify 725 // the make, model and software name to link to their articles. 726 case 'Make': 727 case 'Model': 728 $val = $this->exifMsg( $tag, '', $val ); 729 break; 730 731 case 'Software': 732 if ( is_array( $val ) ) { 733 //if its a software, version array. 734 $val = $this->msg( 'exif-software-version-value', $val[0], $val[1] )->text(); 735 } else { 736 $val = $this->exifMsg( $tag, '', $val ); 737 } 738 break; 739 740 case 'ExposureTime': 741 // Show the pretty fraction as well as decimal version 742 $val = $this->msg( 'exif-exposuretime-format', 743 $this->formatFraction( $val ), $this->formatNum( $val ) )->text(); 744 break; 745 case 'ISOSpeedRatings': 746 // If its = 65535 that means its at the 747 // limit of the size of Exif::short and 748 // is really higher. 749 if ( $val == '65535' ) { 750 $val = $this->exifMsg( $tag, 'overflow' ); 751 } else { 752 $val = $this->formatNum( $val ); 753 } 754 break; 755 case 'FNumber': 756 $val = $this->msg( 'exif-fnumber-format', 757 $this->formatNum( $val ) )->text(); 758 break; 759 760 case 'FocalLength': 761 case 'FocalLengthIn35mmFilm': 762 $val = $this->msg( 'exif-focallength-format', 763 $this->formatNum( $val ) )->text(); 764 break; 765 766 case 'MaxApertureValue': 767 if ( strpos( $val, '/' ) !== false ) { 768 // need to expand this earlier to calculate fNumber 769 list( $n, $d ) = explode( '/', $val ); 770 if ( is_numeric( $n ) && is_numeric( $d ) ) { 771 $val = $n / $d; 772 } 773 } 774 if ( is_numeric( $val ) ) { 775 $fNumber = pow( 2, $val / 2 ); 776 if ( $fNumber !== false ) { 777 $val = $this->msg( 'exif-maxaperturevalue-value', 778 $this->formatNum( $val ), 779 $this->formatNum( $fNumber, 2 ) 780 )->text(); 781 } 782 } 783 break; 784 785 case 'iimCategory': 786 switch ( strtolower( $val ) ) { 787 // See pg 29 of IPTC photo 788 // metadata standard. 789 case 'ace': 790 case 'clj': 791 case 'dis': 792 case 'fin': 793 case 'edu': 794 case 'evn': 795 case 'hth': 796 case 'hum': 797 case 'lab': 798 case 'lif': 799 case 'pol': 800 case 'rel': 801 case 'sci': 802 case 'soi': 803 case 'spo': 804 case 'war': 805 case 'wea': 806 $val = $this->exifMsg( 807 'iimcategory', 808 $val 809 ); 810 } 811 break; 812 case 'SubjectNewsCode': 813 // Essentially like iimCategory. 814 // 8 (numeric) digit hierarchical 815 // classification. We decode the 816 // first 2 digits, which provide 817 // a broad category. 818 $val = $this->convertNewsCode( $val ); 819 break; 820 case 'Urgency': 821 // 1-8 with 1 being highest, 5 normal 822 // 0 is reserved, and 9 is 'user-defined'. 823 $urgency = ''; 824 if ( $val == 0 || $val == 9 ) { 825 $urgency = 'other'; 826 } elseif ( $val < 5 && $val > 1 ) { 827 $urgency = 'high'; 828 } elseif ( $val == 5 ) { 829 $urgency = 'normal'; 830 } elseif ( $val <= 8 && $val > 5 ) { 831 $urgency = 'low'; 832 } 833 834 if ( $urgency !== '' ) { 835 $val = $this->exifMsg( 'urgency', 836 $urgency, $val 837 ); 838 } 839 break; 840 841 // Things that have a unit of pixels. 842 case 'OriginalImageHeight': 843 case 'OriginalImageWidth': 844 case 'PixelXDimension': 845 case 'PixelYDimension': 846 case 'ImageWidth': 847 case 'ImageLength': 848 $val = $this->formatNum( $val ) . ' ' . $this->msg( 'unit-pixel' )->text(); 849 break; 850 851 // Do not transform fields with pure text. 852 // For some languages the formatNum() 853 // conversion results to wrong output like 854 // foo,bar@example,com or foo٫bar@example٫com. 855 // Also some 'numeric' things like Scene codes 856 // are included here as we really don't want 857 // commas inserted. 858 case 'ImageDescription': 859 case 'Artist': 860 case 'Copyright': 861 case 'RelatedSoundFile': 862 case 'ImageUniqueID': 863 case 'SpectralSensitivity': 864 case 'GPSSatellites': 865 case 'GPSVersionID': 866 case 'GPSMapDatum': 867 case 'Keywords': 868 case 'WorldRegionDest': 869 case 'CountryDest': 870 case 'CountryCodeDest': 871 case 'ProvinceOrStateDest': 872 case 'CityDest': 873 case 'SublocationDest': 874 case 'WorldRegionCreated': 875 case 'CountryCreated': 876 case 'CountryCodeCreated': 877 case 'ProvinceOrStateCreated': 878 case 'CityCreated': 879 case 'SublocationCreated': 880 case 'ObjectName': 881 case 'SpecialInstructions': 882 case 'Headline': 883 case 'Credit': 884 case 'Source': 885 case 'EditStatus': 886 case 'FixtureIdentifier': 887 case 'LocationDest': 888 case 'LocationDestCode': 889 case 'Writer': 890 case 'JPEGFileComment': 891 case 'iimSupplementalCategory': 892 case 'OriginalTransmissionRef': 893 case 'Identifier': 894 case 'dc-contributor': 895 case 'dc-coverage': 896 case 'dc-publisher': 897 case 'dc-relation': 898 case 'dc-rights': 899 case 'dc-source': 900 case 'dc-type': 901 case 'Lens': 902 case 'SerialNumber': 903 case 'CameraOwnerName': 904 case 'Label': 905 case 'Nickname': 906 case 'RightsCertificate': 907 case 'CopyrightOwner': 908 case 'UsageTerms': 909 case 'WebStatement': 910 case 'OriginalDocumentID': 911 case 'LicenseUrl': 912 case 'MorePermissionsUrl': 913 case 'AttributionUrl': 914 case 'PreferredAttributionName': 915 case 'PNGFileComment': 916 case 'Disclaimer': 917 case 'ContentWarning': 918 case 'GIFFileComment': 919 case 'SceneCode': 920 case 'IntellectualGenre': 921 case 'Event': 922 case 'OrginisationInImage': 923 case 'PersonInImage': 924 925 $val = htmlspecialchars( $val ); 926 break; 927 928 case 'ObjectCycle': 929 switch ( $val ) { 930 case 'a': 931 case 'p': 932 case 'b': 933 $val = $this->exifMsg( $tag, $val ); 934 break; 935 default: 936 $val = htmlspecialchars( $val ); 937 break; 938 } 939 break; 940 case 'Copyrighted': 941 switch ( $val ) { 942 case 'True': 943 case 'False': 944 $val = $this->exifMsg( $tag, $val ); 945 break; 946 } 947 break; 948 case 'Rating': 949 if ( $val == '-1' ) { 950 $val = $this->exifMsg( $tag, 'rejected' ); 951 } else { 952 $val = $this->formatNum( $val ); 953 } 954 break; 955 956 case 'LanguageCode': 957 $lang = Language::fetchLanguageName( strtolower( $val ), $this->getLanguage()->getCode() ); 958 if ( $lang ) { 959 $val = htmlspecialchars( $lang ); 960 } else { 961 $val = htmlspecialchars( $val ); 962 } 963 break; 964 965 default: 966 $val = $this->formatNum( $val ); 967 break; 968 } 969 } 970 // End formatting values, start flattening arrays. 971 $vals = $this->flattenArrayReal( $vals, $type ); 972 } 973 974 return $tags; 975 } 976 977 /** 978 * Flatten an array, using the content language for any messages. 979 * 980 * @param array $vals Array of values 981 * @param string $type Type of array (either lang, ul, ol). 982 * lang = language assoc array with keys being the lang code 983 * ul = unordered list, ol = ordered list 984 * type can also come from the '_type' member of $vals. 985 * @param bool $noHtml If to avoid returning anything resembling HTML. 986 * (Ugly hack for backwards compatibility with old MediaWiki). 987 * @param bool|IContextSource $context 988 * @return string Single value (in wiki-syntax). 989 * @since 1.23 990 */ 991 public static function flattenArrayContentLang( $vals, $type = 'ul', 992 $noHtml = false, $context = false 993 ) { 994 global $wgContLang; 995 $obj = new FormatMetadata; 996 if ( $context ) { 997 $obj->setContext( $context ); 998 } 999 $context = new DerivativeContext( $obj->getContext() ); 1000 $context->setLanguage( $wgContLang ); 1001 $obj->setContext( $context ); 1002 1003 return $obj->flattenArrayReal( $vals, $type, $noHtml ); 1004 } 1005 1006 /** 1007 * Flatten an array, using the user language for any messages. 1008 * 1009 * @param array $vals Array of values 1010 * @param string $type Type of array (either lang, ul, ol). 1011 * lang = language assoc array with keys being the lang code 1012 * ul = unordered list, ol = ordered list 1013 * type can also come from the '_type' member of $vals. 1014 * @param bool $noHtml If to avoid returning anything resembling HTML. 1015 * (Ugly hack for backwards compatibility with old MediaWiki). 1016 * @param bool|IContextSource $context 1017 * @return string Single value (in wiki-syntax). 1018 */ 1019 public static function flattenArray( $vals, $type = 'ul', $noHtml = false, $context = false ) { 1020 $obj = new FormatMetadata; 1021 if ( $context ) { 1022 $obj->setContext( $context ); 1023 } 1024 1025 return $obj->flattenArrayReal( $vals, $type, $noHtml ); 1026 } 1027 1028 /** 1029 * A function to collapse multivalued tags into a single value. 1030 * This turns an array of (for example) authors into a bulleted list. 1031 * 1032 * This is public on the basis it might be useful outside of this class. 1033 * 1034 * @param array $vals Array of values 1035 * @param string $type Type of array (either lang, ul, ol). 1036 * lang = language assoc array with keys being the lang code 1037 * ul = unordered list, ol = ordered list 1038 * type can also come from the '_type' member of $vals. 1039 * @param bool $noHtml If to avoid returning anything resembling HTML. 1040 * (Ugly hack for backwards compatibility with old mediawiki). 1041 * @return string Single value (in wiki-syntax). 1042 * @since 1.23 1043 */ 1044 public function flattenArrayReal( $vals, $type = 'ul', $noHtml = false ) { 1045 if ( !is_array( $vals ) ) { 1046 return $vals; // do nothing if not an array; 1047 } 1048 1049 if ( isset( $vals['_type'] ) ) { 1050 $type = $vals['_type']; 1051 unset( $vals['_type'] ); 1052 } 1053 1054 if ( !is_array( $vals ) ) { 1055 return $vals; // do nothing if not an array; 1056 } elseif ( count( $vals ) === 1 && $type !== 'lang' ) { 1057 return $vals[0]; 1058 } elseif ( count( $vals ) === 0 ) { 1059 wfDebug( __METHOD__ . " metadata array with 0 elements!\n" ); 1060 1061 return ""; // paranoia. This should never happen 1062 } else { 1063 /* @todo FIXME: This should hide some of the list entries if there are 1064 * say more than four. Especially if a field is translated into 20 1065 * languages, we don't want to show them all by default 1066 */ 1067 switch ( $type ) { 1068 case 'lang': 1069 // Display default, followed by ContLang, 1070 // followed by the rest in no particular 1071 // order. 1072 1073 // Todo: hide some items if really long list. 1074 1075 $content = ''; 1076 1077 $priorityLanguages = $this->getPriorityLanguages(); 1078 $defaultItem = false; 1079 $defaultLang = false; 1080 1081 // If default is set, save it for later, 1082 // as we don't know if it's equal to 1083 // one of the lang codes. (In xmp 1084 // you specify the language for a 1085 // default property by having both 1086 // a default prop, and one in the language 1087 // that are identical) 1088 if ( isset( $vals['x-default'] ) ) { 1089 $defaultItem = $vals['x-default']; 1090 unset( $vals['x-default'] ); 1091 } 1092 foreach ( $priorityLanguages as $pLang ) { 1093 if ( isset( $vals[$pLang] ) ) { 1094 $isDefault = false; 1095 if ( $vals[$pLang] === $defaultItem ) { 1096 $defaultItem = false; 1097 $isDefault = true; 1098 } 1099 $content .= $this->langItem( 1100 $vals[$pLang], $pLang, 1101 $isDefault, $noHtml ); 1102 1103 unset( $vals[$pLang] ); 1104 1105 if ( $this->singleLang ) { 1106 return Html::rawElement( 'span', 1107 array( 'lang' => $pLang ), $vals[$pLang] ); 1108 } 1109 } 1110 } 1111 1112 // Now do the rest. 1113 foreach ( $vals as $lang => $item ) { 1114 if ( $item === $defaultItem ) { 1115 $defaultLang = $lang; 1116 continue; 1117 } 1118 $content .= $this->langItem( $item, 1119 $lang, false, $noHtml ); 1120 if ( $this->singleLang ) { 1121 return Html::rawElement( 'span', 1122 array( 'lang' => $lang ), $item ); 1123 } 1124 } 1125 if ( $defaultItem !== false ) { 1126 $content = $this->langItem( $defaultItem, 1127 $defaultLang, true, $noHtml ) . 1128 $content; 1129 if ( $this->singleLang ) { 1130 return $defaultItem; 1131 } 1132 } 1133 if ( $noHtml ) { 1134 return $content; 1135 } 1136 1137 return '<ul class="metadata-langlist">' . 1138 $content . 1139 '</ul>'; 1140 case 'ol': 1141 if ( $noHtml ) { 1142 return "\n#" . implode( "\n#", $vals ); 1143 } 1144 1145 return "<ol><li>" . implode( "</li>\n<li>", $vals ) . '</li></ol>'; 1146 case 'ul': 1147 default: 1148 if ( $noHtml ) { 1149 return "\n*" . implode( "\n*", $vals ); 1150 } 1151 1152 return "<ul><li>" . implode( "</li>\n<li>", $vals ) . '</li></ul>'; 1153 } 1154 } 1155 } 1156 1157 /** Helper function for creating lists of translations. 1158 * 1159 * @param string $value Value (this is not escaped) 1160 * @param string $lang Lang code of item or false 1161 * @param bool $default If it is default value. 1162 * @param bool $noHtml If to avoid html (for back-compat) 1163 * @throws MWException 1164 * @return string Language item (Note: despite how this looks, this is 1165 * treated as wikitext, not as HTML). 1166 */ 1167 private function langItem( $value, $lang, $default = false, $noHtml = false ) { 1168 if ( $lang === false && $default === false ) { 1169 throw new MWException( '$lang and $default cannot both ' 1170 . 'be false.' ); 1171 } 1172 1173 if ( $noHtml ) { 1174 $wrappedValue = $value; 1175 } else { 1176 $wrappedValue = '<span class="mw-metadata-lang-value">' 1177 . $value . '</span>'; 1178 } 1179 1180 if ( $lang === false ) { 1181 $msg = $this->msg( 'metadata-langitem-default', $wrappedValue ); 1182 if ( $noHtml ) { 1183 return $msg->text() . "\n\n"; 1184 } /* else */ 1185 1186 return '<li class="mw-metadata-lang-default">' 1187 . $msg->text() 1188 . "</li>\n"; 1189 } 1190 1191 $lowLang = strtolower( $lang ); 1192 $langName = Language::fetchLanguageName( $lowLang ); 1193 if ( $langName === '' ) { 1194 //try just the base language name. (aka en-US -> en ). 1195 list( $langPrefix ) = explode( '-', $lowLang, 2 ); 1196 $langName = Language::fetchLanguageName( $langPrefix ); 1197 if ( $langName === '' ) { 1198 // give up. 1199 $langName = $lang; 1200 } 1201 } 1202 // else we have a language specified 1203 1204 $msg = $this->msg( 'metadata-langitem', $wrappedValue, $langName, $lang ); 1205 if ( $noHtml ) { 1206 return '*' . $msg->text(); 1207 } /* else: */ 1208 1209 $item = '<li class="mw-metadata-lang-code-' 1210 . $lang; 1211 if ( $default ) { 1212 $item .= ' mw-metadata-lang-default'; 1213 } 1214 $item .= '" lang="' . $lang . '">'; 1215 $item .= $msg->text(); 1216 $item .= "</li>\n"; 1217 1218 return $item; 1219 } 1220 1221 /** 1222 * Convenience function for getFormattedData() 1223 * 1224 * @param string $tag The tag name to pass on 1225 * @param string $val The value of the tag 1226 * @param string $arg An argument to pass ($1) 1227 * @param string $arg2 A 2nd argument to pass ($2) 1228 * @return string The text content of "exif-$tag-$val" message in lower case 1229 */ 1230 private function exifMsg( $tag, $val, $arg = null, $arg2 = null ) { 1231 global $wgContLang; 1232 1233 if ( $val === '' ) { 1234 $val = 'value'; 1235 } 1236 1237 return $this->msg( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 )->text(); 1238 } 1239 1240 /** 1241 * Format a number, convert numbers from fractions into floating point 1242 * numbers, joins arrays of numbers with commas. 1243 * 1244 * @param mixed $num The value to format 1245 * @param float|int|bool $round Digits to round to or false. 1246 * @return mixed A floating point number or whatever we were fed 1247 */ 1248 private function formatNum( $num, $round = false ) { 1249 $m = array(); 1250 if ( is_array( $num ) ) { 1251 $out = array(); 1252 foreach ( $num as $number ) { 1253 $out[] = $this->formatNum( $number ); 1254 } 1255 1256 return $this->getLanguage()->commaList( $out ); 1257 } 1258 if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) { 1259 if ( $m[2] != 0 ) { 1260 $newNum = $m[1] / $m[2]; 1261 if ( $round !== false ) { 1262 $newNum = round( $newNum, $round ); 1263 } 1264 } else { 1265 $newNum = $num; 1266 } 1267 1268 return $this->getLanguage()->formatNum( $newNum ); 1269 } else { 1270 if ( is_numeric( $num ) && $round !== false ) { 1271 $num = round( $num, $round ); 1272 } 1273 1274 return $this->getLanguage()->formatNum( $num ); 1275 } 1276 } 1277 1278 /** 1279 * Format a rational number, reducing fractions 1280 * 1281 * @param mixed $num The value to format 1282 * @return mixed A floating point number or whatever we were fed 1283 */ 1284 private function formatFraction( $num ) { 1285 $m = array(); 1286 if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) { 1287 $numerator = intval( $m[1] ); 1288 $denominator = intval( $m[2] ); 1289 $gcd = $this->gcd( abs( $numerator ), $denominator ); 1290 if ( $gcd != 0 ) { 1291 // 0 shouldn't happen! ;) 1292 return $this->formatNum( $numerator / $gcd ) . '/' . $this->formatNum( $denominator / $gcd ); 1293 } 1294 } 1295 1296 return $this->formatNum( $num ); 1297 } 1298 1299 /** 1300 * Calculate the greatest common divisor of two integers. 1301 * 1302 * @param int $a Numerator 1303 * @param int $b Denominator 1304 * @return int 1305 */ 1306 private function gcd( $a, $b ) { 1307 /* 1308 // http://en.wikipedia.org/wiki/Euclidean_algorithm 1309 // Recursive form would be: 1310 if( $b == 0 ) 1311 return $a; 1312 else 1313 return gcd( $b, $a % $b ); 1314 */ 1315 while ( $b != 0 ) { 1316 $remainder = $a % $b; 1317 1318 // tail recursion... 1319 $a = $b; 1320 $b = $remainder; 1321 } 1322 1323 return $a; 1324 } 1325 1326 /** 1327 * Fetch the human readable version of a news code. 1328 * A news code is an 8 digit code. The first two 1329 * digits are a general classification, so we just 1330 * translate that. 1331 * 1332 * Note, leading 0's are significant, so this is 1333 * a string, not an int. 1334 * 1335 * @param string $val The 8 digit news code. 1336 * @return string The human readable form 1337 */ 1338 private function convertNewsCode( $val ) { 1339 if ( !preg_match( '/^\d{8}$/D', $val ) ) { 1340 // Not a valid news code. 1341 return $val; 1342 } 1343 $cat = ''; 1344 switch ( substr( $val, 0, 2 ) ) { 1345 case '01': 1346 $cat = 'ace'; 1347 break; 1348 case '02': 1349 $cat = 'clj'; 1350 break; 1351 case '03': 1352 $cat = 'dis'; 1353 break; 1354 case '04': 1355 $cat = 'fin'; 1356 break; 1357 case '05': 1358 $cat = 'edu'; 1359 break; 1360 case '06': 1361 $cat = 'evn'; 1362 break; 1363 case '07': 1364 $cat = 'hth'; 1365 break; 1366 case '08': 1367 $cat = 'hum'; 1368 break; 1369 case '09': 1370 $cat = 'lab'; 1371 break; 1372 case '10': 1373 $cat = 'lif'; 1374 break; 1375 case '11': 1376 $cat = 'pol'; 1377 break; 1378 case '12': 1379 $cat = 'rel'; 1380 break; 1381 case '13': 1382 $cat = 'sci'; 1383 break; 1384 case '14': 1385 $cat = 'soi'; 1386 break; 1387 case '15': 1388 $cat = 'spo'; 1389 break; 1390 case '16': 1391 $cat = 'war'; 1392 break; 1393 case '17': 1394 $cat = 'wea'; 1395 break; 1396 } 1397 if ( $cat !== '' ) { 1398 $catMsg = $this->exifMsg( 'iimcategory', $cat ); 1399 $val = $this->exifMsg( 'subjectnewscode', '', $val, $catMsg ); 1400 } 1401 1402 return $val; 1403 } 1404 1405 /** 1406 * Format a coordinate value, convert numbers from floating point 1407 * into degree minute second representation. 1408 * 1409 * @param int $coord Degrees, minutes and seconds 1410 * @param string $type Latitude or longitude (for if its a NWS or E) 1411 * @return mixed A floating point number or whatever we were fed 1412 */ 1413 private function formatCoords( $coord, $type ) { 1414 $ref = ''; 1415 if ( $coord < 0 ) { 1416 $nCoord = -$coord; 1417 if ( $type === 'latitude' ) { 1418 $ref = 'S'; 1419 } elseif ( $type === 'longitude' ) { 1420 $ref = 'W'; 1421 } 1422 } else { 1423 $nCoord = $coord; 1424 if ( $type === 'latitude' ) { 1425 $ref = 'N'; 1426 } elseif ( $type === 'longitude' ) { 1427 $ref = 'E'; 1428 } 1429 } 1430 1431 $deg = floor( $nCoord ); 1432 $min = floor( ( $nCoord - $deg ) * 60.0 ); 1433 $sec = round( ( ( $nCoord - $deg ) - $min / 60 ) * 3600, 2 ); 1434 1435 $deg = $this->formatNum( $deg ); 1436 $min = $this->formatNum( $min ); 1437 $sec = $this->formatNum( $sec ); 1438 1439 return $this->msg( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text(); 1440 } 1441 1442 /** 1443 * Format the contact info field into a single value. 1444 * 1445 * This function might be called from 1446 * JpegHandler::convertMetadataVersion which is why it is 1447 * public. 1448 * 1449 * @param array $vals Array with fields of the ContactInfo 1450 * struct defined in the IPTC4XMP spec. Or potentially 1451 * an array with one element that is a free form text 1452 * value from the older iptc iim 1:118 prop. 1453 * @return string HTML-ish looking wikitext 1454 * @since 1.23 no longer static 1455 */ 1456 public function collapseContactInfo( $vals ) { 1457 if ( !( isset( $vals['CiAdrExtadr'] ) 1458 || isset( $vals['CiAdrCity'] ) 1459 || isset( $vals['CiAdrCtry'] ) 1460 || isset( $vals['CiEmailWork'] ) 1461 || isset( $vals['CiTelWork'] ) 1462 || isset( $vals['CiAdrPcode'] ) 1463 || isset( $vals['CiAdrRegion'] ) 1464 || isset( $vals['CiUrlWork'] ) 1465 ) ) { 1466 // We don't have any sub-properties 1467 // This could happen if its using old 1468 // iptc that just had this as a free-form 1469 // text value. 1470 // Note: We run this through htmlspecialchars 1471 // partially to be consistent, and partially 1472 // because people often insert >, etc into 1473 // the metadata which should not be interpreted 1474 // but we still want to auto-link urls. 1475 foreach ( $vals as &$val ) { 1476 $val = htmlspecialchars( $val ); 1477 } 1478 1479 return $this->flattenArrayReal( $vals ); 1480 } else { 1481 // We have a real ContactInfo field. 1482 // Its unclear if all these fields have to be 1483 // set, so assume they do not. 1484 $url = $tel = $street = $city = $country = ''; 1485 $email = $postal = $region = ''; 1486 1487 // Also note, some of the class names this uses 1488 // are similar to those used by hCard. This is 1489 // mostly because they're sensible names. This 1490 // does not (and does not attempt to) output 1491 // stuff in the hCard microformat. However it 1492 // might output in the adr microformat. 1493 1494 if ( isset( $vals['CiAdrExtadr'] ) ) { 1495 // Todo: This can potentially be multi-line. 1496 // Need to check how that works in XMP. 1497 $street = '<span class="extended-address">' 1498 . htmlspecialchars( 1499 $vals['CiAdrExtadr'] ) 1500 . '</span>'; 1501 } 1502 if ( isset( $vals['CiAdrCity'] ) ) { 1503 $city = '<span class="locality">' 1504 . htmlspecialchars( $vals['CiAdrCity'] ) 1505 . '</span>'; 1506 } 1507 if ( isset( $vals['CiAdrCtry'] ) ) { 1508 $country = '<span class="country-name">' 1509 . htmlspecialchars( $vals['CiAdrCtry'] ) 1510 . '</span>'; 1511 } 1512 if ( isset( $vals['CiEmailWork'] ) ) { 1513 $emails = array(); 1514 // Have to split multiple emails at commas/new lines. 1515 $splitEmails = explode( "\n", $vals['CiEmailWork'] ); 1516 foreach ( $splitEmails as $e1 ) { 1517 // Also split on comma 1518 foreach ( explode( ',', $e1 ) as $e2 ) { 1519 $finalEmail = trim( $e2 ); 1520 if ( $finalEmail == ',' || $finalEmail == '' ) { 1521 continue; 1522 } 1523 if ( strpos( $finalEmail, '<' ) !== false ) { 1524 // Don't do fancy formatting to 1525 // "My name" <[email protected]> style stuff 1526 $emails[] = $finalEmail; 1527 } else { 1528 $emails[] = '[mailto:' 1529 . $finalEmail 1530 . ' <span class="email">' 1531 . $finalEmail 1532 . '</span>]'; 1533 } 1534 } 1535 } 1536 $email = implode( ', ', $emails ); 1537 } 1538 if ( isset( $vals['CiTelWork'] ) ) { 1539 $tel = '<span class="tel">' 1540 . htmlspecialchars( $vals['CiTelWork'] ) 1541 . '</span>'; 1542 } 1543 if ( isset( $vals['CiAdrPcode'] ) ) { 1544 $postal = '<span class="postal-code">' 1545 . htmlspecialchars( 1546 $vals['CiAdrPcode'] ) 1547 . '</span>'; 1548 } 1549 if ( isset( $vals['CiAdrRegion'] ) ) { 1550 // Note this is province/state. 1551 $region = '<span class="region">' 1552 . htmlspecialchars( 1553 $vals['CiAdrRegion'] ) 1554 . '</span>'; 1555 } 1556 if ( isset( $vals['CiUrlWork'] ) ) { 1557 $url = '<span class="url">' 1558 . htmlspecialchars( $vals['CiUrlWork'] ) 1559 . '</span>'; 1560 } 1561 1562 return $this->msg( 'exif-contact-value', $email, $url, 1563 $street, $city, $region, $postal, $country, 1564 $tel )->text(); 1565 } 1566 } 1567 1568 /** 1569 * Get a list of fields that are visible by default. 1570 * 1571 * @return array 1572 * @since 1.23 1573 */ 1574 public static function getVisibleFields() { 1575 $fields = array(); 1576 $lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() ); 1577 foreach ( $lines as $line ) { 1578 $matches = array(); 1579 if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { 1580 $fields[] = $matches[1]; 1581 } 1582 } 1583 $fields = array_map( 'strtolower', $fields ); 1584 1585 return $fields; 1586 } 1587 1588 /** 1589 * Get an array of extended metadata. (See the imageinfo API for format.) 1590 * 1591 * @param File $file File to use 1592 * @return array [<property name> => ['value' => <value>]], or [] on error 1593 * @since 1.23 1594 */ 1595 public function fetchExtendedMetadata( File $file ) { 1596 global $wgMemc; 1597 1598 wfProfileIn( __METHOD__ ); 1599 1600 // If revision deleted, exit immediately 1601 if ( $file->isDeleted( File::DELETED_FILE ) ) { 1602 wfProfileOut( __METHOD__ ); 1603 1604 return array(); 1605 } 1606 1607 $cacheKey = wfMemcKey( 1608 'getExtendedMetadata', 1609 $this->getLanguage()->getCode(), 1610 (int)$this->singleLang, 1611 $file->getSha1() 1612 ); 1613 1614 $cachedValue = $wgMemc->get( $cacheKey ); 1615 if ( 1616 $cachedValue 1617 && wfRunHooks( 'ValidateExtendedMetadataCache', array( $cachedValue['timestamp'], $file ) ) 1618 ) { 1619 $extendedMetadata = $cachedValue['data']; 1620 } else { 1621 $maxCacheTime = ( $file instanceof ForeignAPIFile ) ? 60 * 60 * 12 : 60 * 60 * 24 * 30; 1622 $fileMetadata = $this->getExtendedMetadataFromFile( $file ); 1623 $extendedMetadata = $this->getExtendedMetadataFromHook( $file, $fileMetadata, $maxCacheTime ); 1624 if ( $this->singleLang ) { 1625 $this->resolveMultilangMetadata( $extendedMetadata ); 1626 } 1627 // Make sure the metadata won't break the API when an XML format is used. 1628 // This is an API-specific function so it would be cleaner to call it from 1629 // outside fetchExtendedMetadata, but this way we don't need to redo the 1630 // computation on a cache hit. 1631 $this->sanitizeArrayForXml( $extendedMetadata ); 1632 $valueToCache = array( 'data' => $extendedMetadata, 'timestamp' => wfTimestampNow() ); 1633 $wgMemc->set( $cacheKey, $valueToCache, $maxCacheTime ); 1634 } 1635 1636 wfProfileOut( __METHOD__ ); 1637 1638 return $extendedMetadata; 1639 } 1640 1641 /** 1642 * Get file-based metadata in standardized format. 1643 * 1644 * Note that for a remote file, this might return metadata supplied by extensions. 1645 * 1646 * @param File $file File to use 1647 * @return array [<property name> => ['value' => <value>]], or [] on error 1648 * @since 1.23 1649 */ 1650 protected function getExtendedMetadataFromFile( File $file ) { 1651 // If this is a remote file accessed via an API request, we already 1652 // have remote metadata so we just ignore any local one 1653 if ( $file instanceof ForeignAPIFile ) { 1654 // In case of error we pretend no metadata - this will get cached. 1655 // Might or might not be a good idea. 1656 return $file->getExtendedMetadata() ?: array(); 1657 } 1658 1659 wfProfileIn( __METHOD__ ); 1660 1661 $uploadDate = wfTimestamp( TS_ISO_8601, $file->getTimestamp() ); 1662 1663 $fileMetadata = array( 1664 // This is modification time, which is close to "upload" time. 1665 'DateTime' => array( 1666 'value' => $uploadDate, 1667 'source' => 'mediawiki-metadata', 1668 ), 1669 ); 1670 1671 $title = $file->getTitle(); 1672 if ( $title ) { 1673 $text = $title->getText(); 1674 $pos = strrpos( $text, '.' ); 1675 1676 if ( $pos ) { 1677 $name = substr( $text, 0, $pos ); 1678 } else { 1679 $name = $text; 1680 } 1681 1682 $fileMetadata['ObjectName'] = array( 1683 'value' => $name, 1684 'source' => 'mediawiki-metadata', 1685 ); 1686 } 1687 1688 $common = $file->getCommonMetaArray(); 1689 1690 if ( $common !== false ) { 1691 foreach ( $common as $key => $value ) { 1692 $fileMetadata[$key] = array( 1693 'value' => $value, 1694 'source' => 'file-metadata', 1695 ); 1696 } 1697 } 1698 1699 wfProfileOut( __METHOD__ ); 1700 1701 return $fileMetadata; 1702 } 1703 1704 /** 1705 * Get additional metadata from hooks in standardized format. 1706 * 1707 * @param File $file File to use 1708 * @param array $extendedMetadata 1709 * @param int $maxCacheTime Hook handlers might use this parameter to override cache time 1710 * 1711 * @return array [<property name> => ['value' => <value>]], or [] on error 1712 * @since 1.23 1713 */ 1714 protected function getExtendedMetadataFromHook( File $file, array $extendedMetadata, 1715 &$maxCacheTime 1716 ) { 1717 wfProfileIn( __METHOD__ ); 1718 1719 wfRunHooks( 'GetExtendedMetadata', array( 1720 &$extendedMetadata, 1721 $file, 1722 $this->getContext(), 1723 $this->singleLang, 1724 &$maxCacheTime 1725 ) ); 1726 1727 $visible = array_flip( self::getVisibleFields() ); 1728 foreach ( $extendedMetadata as $key => $value ) { 1729 if ( !isset( $visible[strtolower( $key )] ) ) { 1730 $extendedMetadata[$key]['hidden'] = ''; 1731 } 1732 } 1733 1734 wfProfileOut( __METHOD__ ); 1735 1736 return $extendedMetadata; 1737 } 1738 1739 /** 1740 * Turns an XMP-style multilang array into a single value. 1741 * If the value is not a multilang array, it is returned unchanged. 1742 * See mediawiki.org/wiki/Manual:File_metadata_handling#Multi-language_array_format 1743 * @param mixed $value 1744 * @return mixed Value in best language, null if there were no languages at all 1745 * @since 1.23 1746 */ 1747 protected function resolveMultilangValue( $value ) { 1748 if ( 1749 !is_array( $value ) 1750 || !isset( $value['_type'] ) 1751 || $value['_type'] != 'lang' 1752 ) { 1753 return $value; // do nothing if not a multilang array 1754 } 1755 1756 // choose the language best matching user or site settings 1757 $priorityLanguages = $this->getPriorityLanguages(); 1758 foreach ( $priorityLanguages as $lang ) { 1759 if ( isset( $value[$lang] ) ) { 1760 return $value[$lang]; 1761 } 1762 } 1763 1764 // otherwise go with the default language, if set 1765 if ( isset( $value['x-default'] ) ) { 1766 return $value['x-default']; 1767 } 1768 1769 // otherwise just return any one language 1770 unset( $value['_type'] ); 1771 if ( !empty( $value ) ) { 1772 return reset( $value ); 1773 } 1774 1775 // this should not happen; signal error 1776 return null; 1777 } 1778 1779 /** 1780 * Takes an array returned by the getExtendedMetadata* functions, 1781 * and resolves multi-language values in it. 1782 * @param array $metadata 1783 * @since 1.23 1784 */ 1785 protected function resolveMultilangMetadata( &$metadata ) { 1786 if ( !is_array( $metadata ) ) { 1787 return; 1788 } 1789 foreach ( $metadata as &$field ) { 1790 if ( isset( $field['value'] ) ) { 1791 $field['value'] = $this->resolveMultilangValue( $field['value'] ); 1792 } 1793 } 1794 } 1795 1796 /** 1797 * Makes sure the given array is a valid API response fragment 1798 * (can be transformed into XML) 1799 * @param array $arr 1800 */ 1801 protected function sanitizeArrayForXml( &$arr ) { 1802 if ( !is_array( $arr ) ) { 1803 return; 1804 } 1805 1806 $counter = 1; 1807 foreach ( $arr as $key => &$value ) { 1808 $sanitizedKey = $this->sanitizeKeyForXml( $key ); 1809 if ( $sanitizedKey !== $key ) { 1810 if ( isset( $arr[$sanitizedKey] ) ) { 1811 // Make the sanitized keys hopefully unique. 1812 // To make it definitely unique would be too much effort, given that 1813 // sanitizing is only needed for misformatted metadata anyway, but 1814 // this at least covers the case when $arr is numeric. 1815 $sanitizedKey .= $counter; 1816 ++$counter; 1817 } 1818 $arr[$sanitizedKey] = $arr[$key]; 1819 unset( $arr[$key] ); 1820 } 1821 if ( is_array( $value ) ) { 1822 $this->sanitizeArrayForXml( $value ); 1823 } 1824 } 1825 } 1826 1827 /** 1828 * Turns a string into a valid XML identifier. 1829 * Used to ensure that keys of an associative array in the 1830 * API response do not break the XML formatter. 1831 * @param string $key 1832 * @return string 1833 * @since 1.23 1834 */ 1835 protected function sanitizeKeyForXml( $key ) { 1836 // drop all characters which are not valid in an XML tag name 1837 // a bunch of non-ASCII letters would be valid but probably won't 1838 // be used so we take the easy way 1839 $key = preg_replace( '/[^a-zA-z0-9_:.-]/', '', $key ); 1840 // drop characters which are invalid at the first position 1841 $key = preg_replace( '/^[\d-.]+/', '', $key ); 1842 1843 if ( $key == '' ) { 1844 $key = '_'; 1845 } 1846 1847 // special case for an internal keyword 1848 if ( $key == '_element' ) { 1849 $key = 'element'; 1850 } 1851 1852 return $key; 1853 } 1854 1855 /** 1856 * Returns a list of languages (first is best) to use when formatting multilang fields, 1857 * based on user and site preferences. 1858 * @return array 1859 * @since 1.23 1860 */ 1861 protected function getPriorityLanguages() { 1862 $priorityLanguages = 1863 Language::getFallbacksIncludingSiteLanguage( $this->getLanguage()->getCode() ); 1864 $priorityLanguages = array_merge( 1865 (array)$this->getLanguage()->getCode(), 1866 $priorityLanguages[0], 1867 $priorityLanguages[1] 1868 ); 1869 1870 return $priorityLanguages; 1871 } 1872 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |