MediaWiki
REL1_24
|
00001 <?php 00049 class FormatMetadata extends ContextSource { 00055 protected $singleLang = false; 00056 00063 public function setSingleLanguage( $val ) { 00064 $this->singleLang = $val; 00065 } 00066 00080 public static function getFormattedData( $tags, $context = false ) { 00081 $obj = new FormatMetadata; 00082 if ( $context ) { 00083 $obj->setContext( $context ); 00084 } 00085 00086 return $obj->makeFormattedData( $tags ); 00087 } 00088 00100 public function makeFormattedData( $tags ) { 00101 $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3; 00102 unset( $tags['ResolutionUnit'] ); 00103 00104 foreach ( $tags as $tag => &$vals ) { 00105 00106 // This seems ugly to wrap non-array's in an array just to unwrap again, 00107 // especially when most of the time it is not an array 00108 if ( !is_array( $tags[$tag] ) ) { 00109 $vals = array( $vals ); 00110 } 00111 00112 // _type is a special value to say what array type 00113 if ( isset( $tags[$tag]['_type'] ) ) { 00114 $type = $tags[$tag]['_type']; 00115 unset( $vals['_type'] ); 00116 } else { 00117 $type = 'ul'; // default unordered list. 00118 } 00119 00120 //This is done differently as the tag is an array. 00121 if ( $tag == 'GPSTimeStamp' && count( $vals ) === 3 ) { 00122 //hour min sec array 00123 00124 $h = explode( '/', $vals[0] ); 00125 $m = explode( '/', $vals[1] ); 00126 $s = explode( '/', $vals[2] ); 00127 00128 // this should already be validated 00129 // when loaded from file, but it could 00130 // come from a foreign repo, so be 00131 // paranoid. 00132 if ( !isset( $h[1] ) 00133 || !isset( $m[1] ) 00134 || !isset( $s[1] ) 00135 || $h[1] == 0 00136 || $m[1] == 0 00137 || $s[1] == 0 00138 ) { 00139 continue; 00140 } 00141 $tags[$tag] = str_pad( intval( $h[0] / $h[1] ), 2, '0', STR_PAD_LEFT ) 00142 . ':' . str_pad( intval( $m[0] / $m[1] ), 2, '0', STR_PAD_LEFT ) 00143 . ':' . str_pad( intval( $s[0] / $s[1] ), 2, '0', STR_PAD_LEFT ); 00144 00145 try { 00146 $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] ); 00147 // the 1971:01:01 is just a placeholder, and not shown to user. 00148 if ( $time && intval( $time ) > 0 ) { 00149 $tags[$tag] = $this->getLanguage()->time( $time ); 00150 } 00151 } catch ( TimestampException $e ) { 00152 // This shouldn't happen, but we've seen bad formats 00153 // such as 4-digit seconds in the wild. 00154 // leave $tags[$tag] as-is 00155 } 00156 continue; 00157 } 00158 00159 // The contact info is a multi-valued field 00160 // instead of the other props which are single 00161 // valued (mostly) so handle as a special case. 00162 if ( $tag === 'Contact' ) { 00163 $vals = $this->collapseContactInfo( $vals ); 00164 continue; 00165 } 00166 00167 foreach ( $vals as &$val ) { 00168 00169 switch ( $tag ) { 00170 case 'Compression': 00171 switch ( $val ) { 00172 case 1: 00173 case 2: 00174 case 3: 00175 case 4: 00176 case 5: 00177 case 6: 00178 case 7: 00179 case 8: 00180 case 32773: 00181 case 32946: 00182 case 34712: 00183 $val = $this->exifMsg( $tag, $val ); 00184 break; 00185 default: 00186 /* If not recognized, display as is. */ 00187 break; 00188 } 00189 break; 00190 00191 case 'PhotometricInterpretation': 00192 switch ( $val ) { 00193 case 2: 00194 case 6: 00195 $val = $this->exifMsg( $tag, $val ); 00196 break; 00197 default: 00198 /* If not recognized, display as is. */ 00199 break; 00200 } 00201 break; 00202 00203 case 'Orientation': 00204 switch ( $val ) { 00205 case 1: 00206 case 2: 00207 case 3: 00208 case 4: 00209 case 5: 00210 case 6: 00211 case 7: 00212 case 8: 00213 $val = $this->exifMsg( $tag, $val ); 00214 break; 00215 default: 00216 /* If not recognized, display as is. */ 00217 break; 00218 } 00219 break; 00220 00221 case 'PlanarConfiguration': 00222 switch ( $val ) { 00223 case 1: 00224 case 2: 00225 $val = $this->exifMsg( $tag, $val ); 00226 break; 00227 default: 00228 /* If not recognized, display as is. */ 00229 break; 00230 } 00231 break; 00232 00233 // TODO: YCbCrSubSampling 00234 case 'YCbCrPositioning': 00235 switch ( $val ) { 00236 case 1: 00237 case 2: 00238 $val = $this->exifMsg( $tag, $val ); 00239 break; 00240 default: 00241 /* If not recognized, display as is. */ 00242 break; 00243 } 00244 break; 00245 00246 case 'XResolution': 00247 case 'YResolution': 00248 switch ( $resolutionunit ) { 00249 case 2: 00250 $val = $this->exifMsg( 'XYResolution', 'i', $this->formatNum( $val ) ); 00251 break; 00252 case 3: 00253 $val = $this->exifMsg( 'XYResolution', 'c', $this->formatNum( $val ) ); 00254 break; 00255 default: 00256 /* If not recognized, display as is. */ 00257 break; 00258 } 00259 break; 00260 00261 // TODO: YCbCrCoefficients #p27 (see annex E) 00262 case 'ExifVersion': 00263 case 'FlashpixVersion': 00264 $val = "$val" / 100; 00265 break; 00266 00267 case 'ColorSpace': 00268 switch ( $val ) { 00269 case 1: 00270 case 65535: 00271 $val = $this->exifMsg( $tag, $val ); 00272 break; 00273 default: 00274 /* If not recognized, display as is. */ 00275 break; 00276 } 00277 break; 00278 00279 case 'ComponentsConfiguration': 00280 switch ( $val ) { 00281 case 0: 00282 case 1: 00283 case 2: 00284 case 3: 00285 case 4: 00286 case 5: 00287 case 6: 00288 $val = $this->exifMsg( $tag, $val ); 00289 break; 00290 default: 00291 /* If not recognized, display as is. */ 00292 break; 00293 } 00294 break; 00295 00296 case 'DateTime': 00297 case 'DateTimeOriginal': 00298 case 'DateTimeDigitized': 00299 case 'DateTimeReleased': 00300 case 'DateTimeExpires': 00301 case 'GPSDateStamp': 00302 case 'dc-date': 00303 case 'DateTimeMetadata': 00304 if ( $val == '0000:00:00 00:00:00' || $val == ' : : : : ' ) { 00305 $val = $this->msg( 'exif-unknowndate' )->text(); 00306 } elseif ( preg_match( 00307 '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D', 00308 $val 00309 ) ) { 00310 // Full date. 00311 $time = wfTimestamp( TS_MW, $val ); 00312 if ( $time && intval( $time ) > 0 ) { 00313 $val = $this->getLanguage()->timeanddate( $time ); 00314 } 00315 } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) { 00316 // No second field. Still format the same 00317 // since timeanddate doesn't include seconds anyways, 00318 // but second still available in api 00319 $time = wfTimestamp( TS_MW, $val . ':00' ); 00320 if ( $time && intval( $time ) > 0 ) { 00321 $val = $this->getLanguage()->timeanddate( $time ); 00322 } 00323 } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d)$/D', $val ) ) { 00324 // If only the date but not the time is filled in. 00325 $time = wfTimestamp( TS_MW, substr( $val, 0, 4 ) 00326 . substr( $val, 5, 2 ) 00327 . substr( $val, 8, 2 ) 00328 . '000000' ); 00329 if ( $time && intval( $time ) > 0 ) { 00330 $val = $this->getLanguage()->date( $time ); 00331 } 00332 } 00333 // else it will just output $val without formatting it. 00334 break; 00335 00336 case 'ExposureProgram': 00337 switch ( $val ) { 00338 case 0: 00339 case 1: 00340 case 2: 00341 case 3: 00342 case 4: 00343 case 5: 00344 case 6: 00345 case 7: 00346 case 8: 00347 $val = $this->exifMsg( $tag, $val ); 00348 break; 00349 default: 00350 /* If not recognized, display as is. */ 00351 break; 00352 } 00353 break; 00354 00355 case 'SubjectDistance': 00356 $val = $this->exifMsg( $tag, '', $this->formatNum( $val ) ); 00357 break; 00358 00359 case 'MeteringMode': 00360 switch ( $val ) { 00361 case 0: 00362 case 1: 00363 case 2: 00364 case 3: 00365 case 4: 00366 case 5: 00367 case 6: 00368 case 7: 00369 case 255: 00370 $val = $this->exifMsg( $tag, $val ); 00371 break; 00372 default: 00373 /* If not recognized, display as is. */ 00374 break; 00375 } 00376 break; 00377 00378 case 'LightSource': 00379 switch ( $val ) { 00380 case 0: 00381 case 1: 00382 case 2: 00383 case 3: 00384 case 4: 00385 case 9: 00386 case 10: 00387 case 11: 00388 case 12: 00389 case 13: 00390 case 14: 00391 case 15: 00392 case 17: 00393 case 18: 00394 case 19: 00395 case 20: 00396 case 21: 00397 case 22: 00398 case 23: 00399 case 24: 00400 case 255: 00401 $val = $this->exifMsg( $tag, $val ); 00402 break; 00403 default: 00404 /* If not recognized, display as is. */ 00405 break; 00406 } 00407 break; 00408 00409 case 'Flash': 00410 $flashDecode = array( 00411 'fired' => $val & bindec( '00000001' ), 00412 'return' => ( $val & bindec( '00000110' ) ) >> 1, 00413 'mode' => ( $val & bindec( '00011000' ) ) >> 3, 00414 'function' => ( $val & bindec( '00100000' ) ) >> 5, 00415 'redeye' => ( $val & bindec( '01000000' ) ) >> 6, 00416 // 'reserved' => ($val & bindec( '10000000' )) >> 7, 00417 ); 00418 $flashMsgs = array(); 00419 # We do not need to handle unknown values since all are used. 00420 foreach ( $flashDecode as $subTag => $subValue ) { 00421 # We do not need any message for zeroed values. 00422 if ( $subTag != 'fired' && $subValue == 0 ) { 00423 continue; 00424 } 00425 $fullTag = $tag . '-' . $subTag; 00426 $flashMsgs[] = $this->exifMsg( $fullTag, $subValue ); 00427 } 00428 $val = $this->getLanguage()->commaList( $flashMsgs ); 00429 break; 00430 00431 case 'FocalPlaneResolutionUnit': 00432 switch ( $val ) { 00433 case 2: 00434 $val = $this->exifMsg( $tag, $val ); 00435 break; 00436 default: 00437 /* If not recognized, display as is. */ 00438 break; 00439 } 00440 break; 00441 00442 case 'SensingMethod': 00443 switch ( $val ) { 00444 case 1: 00445 case 2: 00446 case 3: 00447 case 4: 00448 case 5: 00449 case 7: 00450 case 8: 00451 $val = $this->exifMsg( $tag, $val ); 00452 break; 00453 default: 00454 /* If not recognized, display as is. */ 00455 break; 00456 } 00457 break; 00458 00459 case 'FileSource': 00460 switch ( $val ) { 00461 case 3: 00462 $val = $this->exifMsg( $tag, $val ); 00463 break; 00464 default: 00465 /* If not recognized, display as is. */ 00466 break; 00467 } 00468 break; 00469 00470 case 'SceneType': 00471 switch ( $val ) { 00472 case 1: 00473 $val = $this->exifMsg( $tag, $val ); 00474 break; 00475 default: 00476 /* If not recognized, display as is. */ 00477 break; 00478 } 00479 break; 00480 00481 case 'CustomRendered': 00482 switch ( $val ) { 00483 case 0: 00484 case 1: 00485 $val = $this->exifMsg( $tag, $val ); 00486 break; 00487 default: 00488 /* If not recognized, display as is. */ 00489 break; 00490 } 00491 break; 00492 00493 case 'ExposureMode': 00494 switch ( $val ) { 00495 case 0: 00496 case 1: 00497 case 2: 00498 $val = $this->exifMsg( $tag, $val ); 00499 break; 00500 default: 00501 /* If not recognized, display as is. */ 00502 break; 00503 } 00504 break; 00505 00506 case 'WhiteBalance': 00507 switch ( $val ) { 00508 case 0: 00509 case 1: 00510 $val = $this->exifMsg( $tag, $val ); 00511 break; 00512 default: 00513 /* If not recognized, display as is. */ 00514 break; 00515 } 00516 break; 00517 00518 case 'SceneCaptureType': 00519 switch ( $val ) { 00520 case 0: 00521 case 1: 00522 case 2: 00523 case 3: 00524 $val = $this->exifMsg( $tag, $val ); 00525 break; 00526 default: 00527 /* If not recognized, display as is. */ 00528 break; 00529 } 00530 break; 00531 00532 case 'GainControl': 00533 switch ( $val ) { 00534 case 0: 00535 case 1: 00536 case 2: 00537 case 3: 00538 case 4: 00539 $val = $this->exifMsg( $tag, $val ); 00540 break; 00541 default: 00542 /* If not recognized, display as is. */ 00543 break; 00544 } 00545 break; 00546 00547 case 'Contrast': 00548 switch ( $val ) { 00549 case 0: 00550 case 1: 00551 case 2: 00552 $val = $this->exifMsg( $tag, $val ); 00553 break; 00554 default: 00555 /* If not recognized, display as is. */ 00556 break; 00557 } 00558 break; 00559 00560 case 'Saturation': 00561 switch ( $val ) { 00562 case 0: 00563 case 1: 00564 case 2: 00565 $val = $this->exifMsg( $tag, $val ); 00566 break; 00567 default: 00568 /* If not recognized, display as is. */ 00569 break; 00570 } 00571 break; 00572 00573 case 'Sharpness': 00574 switch ( $val ) { 00575 case 0: 00576 case 1: 00577 case 2: 00578 $val = $this->exifMsg( $tag, $val ); 00579 break; 00580 default: 00581 /* If not recognized, display as is. */ 00582 break; 00583 } 00584 break; 00585 00586 case 'SubjectDistanceRange': 00587 switch ( $val ) { 00588 case 0: 00589 case 1: 00590 case 2: 00591 case 3: 00592 $val = $this->exifMsg( $tag, $val ); 00593 break; 00594 default: 00595 /* If not recognized, display as is. */ 00596 break; 00597 } 00598 break; 00599 00600 //The GPS...Ref values are kept for compatibility, probably won't be reached. 00601 case 'GPSLatitudeRef': 00602 case 'GPSDestLatitudeRef': 00603 switch ( $val ) { 00604 case 'N': 00605 case 'S': 00606 $val = $this->exifMsg( 'GPSLatitude', $val ); 00607 break; 00608 default: 00609 /* If not recognized, display as is. */ 00610 break; 00611 } 00612 break; 00613 00614 case 'GPSLongitudeRef': 00615 case 'GPSDestLongitudeRef': 00616 switch ( $val ) { 00617 case 'E': 00618 case 'W': 00619 $val = $this->exifMsg( 'GPSLongitude', $val ); 00620 break; 00621 default: 00622 /* If not recognized, display as is. */ 00623 break; 00624 } 00625 break; 00626 00627 case 'GPSAltitude': 00628 if ( $val < 0 ) { 00629 $val = $this->exifMsg( 'GPSAltitude', 'below-sealevel', $this->formatNum( -$val, 3 ) ); 00630 } else { 00631 $val = $this->exifMsg( 'GPSAltitude', 'above-sealevel', $this->formatNum( $val, 3 ) ); 00632 } 00633 break; 00634 00635 case 'GPSStatus': 00636 switch ( $val ) { 00637 case 'A': 00638 case 'V': 00639 $val = $this->exifMsg( $tag, $val ); 00640 break; 00641 default: 00642 /* If not recognized, display as is. */ 00643 break; 00644 } 00645 break; 00646 00647 case 'GPSMeasureMode': 00648 switch ( $val ) { 00649 case 2: 00650 case 3: 00651 $val = $this->exifMsg( $tag, $val ); 00652 break; 00653 default: 00654 /* If not recognized, display as is. */ 00655 break; 00656 } 00657 break; 00658 00659 case 'GPSTrackRef': 00660 case 'GPSImgDirectionRef': 00661 case 'GPSDestBearingRef': 00662 switch ( $val ) { 00663 case 'T': 00664 case 'M': 00665 $val = $this->exifMsg( 'GPSDirection', $val ); 00666 break; 00667 default: 00668 /* If not recognized, display as is. */ 00669 break; 00670 } 00671 break; 00672 00673 case 'GPSLatitude': 00674 case 'GPSDestLatitude': 00675 $val = $this->formatCoords( $val, 'latitude' ); 00676 break; 00677 case 'GPSLongitude': 00678 case 'GPSDestLongitude': 00679 $val = $this->formatCoords( $val, 'longitude' ); 00680 break; 00681 00682 case 'GPSSpeedRef': 00683 switch ( $val ) { 00684 case 'K': 00685 case 'M': 00686 case 'N': 00687 $val = $this->exifMsg( 'GPSSpeed', $val ); 00688 break; 00689 default: 00690 /* If not recognized, display as is. */ 00691 break; 00692 } 00693 break; 00694 00695 case 'GPSDestDistanceRef': 00696 switch ( $val ) { 00697 case 'K': 00698 case 'M': 00699 case 'N': 00700 $val = $this->exifMsg( 'GPSDestDistance', $val ); 00701 break; 00702 default: 00703 /* If not recognized, display as is. */ 00704 break; 00705 } 00706 break; 00707 00708 case 'GPSDOP': 00709 // See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS) 00710 if ( $val <= 2 ) { 00711 $val = $this->exifMsg( $tag, 'excellent', $this->formatNum( $val ) ); 00712 } elseif ( $val <= 5 ) { 00713 $val = $this->exifMsg( $tag, 'good', $this->formatNum( $val ) ); 00714 } elseif ( $val <= 10 ) { 00715 $val = $this->exifMsg( $tag, 'moderate', $this->formatNum( $val ) ); 00716 } elseif ( $val <= 20 ) { 00717 $val = $this->exifMsg( $tag, 'fair', $this->formatNum( $val ) ); 00718 } else { 00719 $val = $this->exifMsg( $tag, 'poor', $this->formatNum( $val ) ); 00720 } 00721 break; 00722 00723 // This is not in the Exif standard, just a special 00724 // case for our purposes which enables wikis to wikify 00725 // the make, model and software name to link to their articles. 00726 case 'Make': 00727 case 'Model': 00728 $val = $this->exifMsg( $tag, '', $val ); 00729 break; 00730 00731 case 'Software': 00732 if ( is_array( $val ) ) { 00733 //if its a software, version array. 00734 $val = $this->msg( 'exif-software-version-value', $val[0], $val[1] )->text(); 00735 } else { 00736 $val = $this->exifMsg( $tag, '', $val ); 00737 } 00738 break; 00739 00740 case 'ExposureTime': 00741 // Show the pretty fraction as well as decimal version 00742 $val = $this->msg( 'exif-exposuretime-format', 00743 $this->formatFraction( $val ), $this->formatNum( $val ) )->text(); 00744 break; 00745 case 'ISOSpeedRatings': 00746 // If its = 65535 that means its at the 00747 // limit of the size of Exif::short and 00748 // is really higher. 00749 if ( $val == '65535' ) { 00750 $val = $this->exifMsg( $tag, 'overflow' ); 00751 } else { 00752 $val = $this->formatNum( $val ); 00753 } 00754 break; 00755 case 'FNumber': 00756 $val = $this->msg( 'exif-fnumber-format', 00757 $this->formatNum( $val ) )->text(); 00758 break; 00759 00760 case 'FocalLength': 00761 case 'FocalLengthIn35mmFilm': 00762 $val = $this->msg( 'exif-focallength-format', 00763 $this->formatNum( $val ) )->text(); 00764 break; 00765 00766 case 'MaxApertureValue': 00767 if ( strpos( $val, '/' ) !== false ) { 00768 // need to expand this earlier to calculate fNumber 00769 list( $n, $d ) = explode( '/', $val ); 00770 if ( is_numeric( $n ) && is_numeric( $d ) ) { 00771 $val = $n / $d; 00772 } 00773 } 00774 if ( is_numeric( $val ) ) { 00775 $fNumber = pow( 2, $val / 2 ); 00776 if ( $fNumber !== false ) { 00777 $val = $this->msg( 'exif-maxaperturevalue-value', 00778 $this->formatNum( $val ), 00779 $this->formatNum( $fNumber, 2 ) 00780 )->text(); 00781 } 00782 } 00783 break; 00784 00785 case 'iimCategory': 00786 switch ( strtolower( $val ) ) { 00787 // See pg 29 of IPTC photo 00788 // metadata standard. 00789 case 'ace': 00790 case 'clj': 00791 case 'dis': 00792 case 'fin': 00793 case 'edu': 00794 case 'evn': 00795 case 'hth': 00796 case 'hum': 00797 case 'lab': 00798 case 'lif': 00799 case 'pol': 00800 case 'rel': 00801 case 'sci': 00802 case 'soi': 00803 case 'spo': 00804 case 'war': 00805 case 'wea': 00806 $val = $this->exifMsg( 00807 'iimcategory', 00808 $val 00809 ); 00810 } 00811 break; 00812 case 'SubjectNewsCode': 00813 // Essentially like iimCategory. 00814 // 8 (numeric) digit hierarchical 00815 // classification. We decode the 00816 // first 2 digits, which provide 00817 // a broad category. 00818 $val = $this->convertNewsCode( $val ); 00819 break; 00820 case 'Urgency': 00821 // 1-8 with 1 being highest, 5 normal 00822 // 0 is reserved, and 9 is 'user-defined'. 00823 $urgency = ''; 00824 if ( $val == 0 || $val == 9 ) { 00825 $urgency = 'other'; 00826 } elseif ( $val < 5 && $val > 1 ) { 00827 $urgency = 'high'; 00828 } elseif ( $val == 5 ) { 00829 $urgency = 'normal'; 00830 } elseif ( $val <= 8 && $val > 5 ) { 00831 $urgency = 'low'; 00832 } 00833 00834 if ( $urgency !== '' ) { 00835 $val = $this->exifMsg( 'urgency', 00836 $urgency, $val 00837 ); 00838 } 00839 break; 00840 00841 // Things that have a unit of pixels. 00842 case 'OriginalImageHeight': 00843 case 'OriginalImageWidth': 00844 case 'PixelXDimension': 00845 case 'PixelYDimension': 00846 case 'ImageWidth': 00847 case 'ImageLength': 00848 $val = $this->formatNum( $val ) . ' ' . $this->msg( 'unit-pixel' )->text(); 00849 break; 00850 00851 // Do not transform fields with pure text. 00852 // For some languages the formatNum() 00853 // conversion results to wrong output like 00854 // foo,bar@example,com or fooÙ«bar@exampleÙ«com. 00855 // Also some 'numeric' things like Scene codes 00856 // are included here as we really don't want 00857 // commas inserted. 00858 case 'ImageDescription': 00859 case 'Artist': 00860 case 'Copyright': 00861 case 'RelatedSoundFile': 00862 case 'ImageUniqueID': 00863 case 'SpectralSensitivity': 00864 case 'GPSSatellites': 00865 case 'GPSVersionID': 00866 case 'GPSMapDatum': 00867 case 'Keywords': 00868 case 'WorldRegionDest': 00869 case 'CountryDest': 00870 case 'CountryCodeDest': 00871 case 'ProvinceOrStateDest': 00872 case 'CityDest': 00873 case 'SublocationDest': 00874 case 'WorldRegionCreated': 00875 case 'CountryCreated': 00876 case 'CountryCodeCreated': 00877 case 'ProvinceOrStateCreated': 00878 case 'CityCreated': 00879 case 'SublocationCreated': 00880 case 'ObjectName': 00881 case 'SpecialInstructions': 00882 case 'Headline': 00883 case 'Credit': 00884 case 'Source': 00885 case 'EditStatus': 00886 case 'FixtureIdentifier': 00887 case 'LocationDest': 00888 case 'LocationDestCode': 00889 case 'Writer': 00890 case 'JPEGFileComment': 00891 case 'iimSupplementalCategory': 00892 case 'OriginalTransmissionRef': 00893 case 'Identifier': 00894 case 'dc-contributor': 00895 case 'dc-coverage': 00896 case 'dc-publisher': 00897 case 'dc-relation': 00898 case 'dc-rights': 00899 case 'dc-source': 00900 case 'dc-type': 00901 case 'Lens': 00902 case 'SerialNumber': 00903 case 'CameraOwnerName': 00904 case 'Label': 00905 case 'Nickname': 00906 case 'RightsCertificate': 00907 case 'CopyrightOwner': 00908 case 'UsageTerms': 00909 case 'WebStatement': 00910 case 'OriginalDocumentID': 00911 case 'LicenseUrl': 00912 case 'MorePermissionsUrl': 00913 case 'AttributionUrl': 00914 case 'PreferredAttributionName': 00915 case 'PNGFileComment': 00916 case 'Disclaimer': 00917 case 'ContentWarning': 00918 case 'GIFFileComment': 00919 case 'SceneCode': 00920 case 'IntellectualGenre': 00921 case 'Event': 00922 case 'OrginisationInImage': 00923 case 'PersonInImage': 00924 00925 $val = htmlspecialchars( $val ); 00926 break; 00927 00928 case 'ObjectCycle': 00929 switch ( $val ) { 00930 case 'a': 00931 case 'p': 00932 case 'b': 00933 $val = $this->exifMsg( $tag, $val ); 00934 break; 00935 default: 00936 $val = htmlspecialchars( $val ); 00937 break; 00938 } 00939 break; 00940 case 'Copyrighted': 00941 switch ( $val ) { 00942 case 'True': 00943 case 'False': 00944 $val = $this->exifMsg( $tag, $val ); 00945 break; 00946 } 00947 break; 00948 case 'Rating': 00949 if ( $val == '-1' ) { 00950 $val = $this->exifMsg( $tag, 'rejected' ); 00951 } else { 00952 $val = $this->formatNum( $val ); 00953 } 00954 break; 00955 00956 case 'LanguageCode': 00957 $lang = Language::fetchLanguageName( strtolower( $val ), $this->getLanguage()->getCode() ); 00958 if ( $lang ) { 00959 $val = htmlspecialchars( $lang ); 00960 } else { 00961 $val = htmlspecialchars( $val ); 00962 } 00963 break; 00964 00965 default: 00966 $val = $this->formatNum( $val ); 00967 break; 00968 } 00969 } 00970 // End formatting values, start flattening arrays. 00971 $vals = $this->flattenArrayReal( $vals, $type ); 00972 } 00973 00974 return $tags; 00975 } 00976 00991 public static function flattenArrayContentLang( $vals, $type = 'ul', 00992 $noHtml = false, $context = false 00993 ) { 00994 global $wgContLang; 00995 $obj = new FormatMetadata; 00996 if ( $context ) { 00997 $obj->setContext( $context ); 00998 } 00999 $context = new DerivativeContext( $obj->getContext() ); 01000 $context->setLanguage( $wgContLang ); 01001 $obj->setContext( $context ); 01002 01003 return $obj->flattenArrayReal( $vals, $type, $noHtml ); 01004 } 01005 01019 public static function flattenArray( $vals, $type = 'ul', $noHtml = false, $context = false ) { 01020 $obj = new FormatMetadata; 01021 if ( $context ) { 01022 $obj->setContext( $context ); 01023 } 01024 01025 return $obj->flattenArrayReal( $vals, $type, $noHtml ); 01026 } 01027 01044 public function flattenArrayReal( $vals, $type = 'ul', $noHtml = false ) { 01045 if ( !is_array( $vals ) ) { 01046 return $vals; // do nothing if not an array; 01047 } 01048 01049 if ( isset( $vals['_type'] ) ) { 01050 $type = $vals['_type']; 01051 unset( $vals['_type'] ); 01052 } 01053 01054 if ( !is_array( $vals ) ) { 01055 return $vals; // do nothing if not an array; 01056 } elseif ( count( $vals ) === 1 && $type !== 'lang' ) { 01057 return $vals[0]; 01058 } elseif ( count( $vals ) === 0 ) { 01059 wfDebug( __METHOD__ . " metadata array with 0 elements!\n" ); 01060 01061 return ""; // paranoia. This should never happen 01062 } else { 01063 /* @todo FIXME: This should hide some of the list entries if there are 01064 * say more than four. Especially if a field is translated into 20 01065 * languages, we don't want to show them all by default 01066 */ 01067 switch ( $type ) { 01068 case 'lang': 01069 // Display default, followed by ContLang, 01070 // followed by the rest in no particular 01071 // order. 01072 01073 // Todo: hide some items if really long list. 01074 01075 $content = ''; 01076 01077 $priorityLanguages = $this->getPriorityLanguages(); 01078 $defaultItem = false; 01079 $defaultLang = false; 01080 01081 // If default is set, save it for later, 01082 // as we don't know if it's equal to 01083 // one of the lang codes. (In xmp 01084 // you specify the language for a 01085 // default property by having both 01086 // a default prop, and one in the language 01087 // that are identical) 01088 if ( isset( $vals['x-default'] ) ) { 01089 $defaultItem = $vals['x-default']; 01090 unset( $vals['x-default'] ); 01091 } 01092 foreach ( $priorityLanguages as $pLang ) { 01093 if ( isset( $vals[$pLang] ) ) { 01094 $isDefault = false; 01095 if ( $vals[$pLang] === $defaultItem ) { 01096 $defaultItem = false; 01097 $isDefault = true; 01098 } 01099 $content .= $this->langItem( 01100 $vals[$pLang], $pLang, 01101 $isDefault, $noHtml ); 01102 01103 unset( $vals[$pLang] ); 01104 01105 if ( $this->singleLang ) { 01106 return Html::rawElement( 'span', 01107 array( 'lang' => $pLang ), $vals[$pLang] ); 01108 } 01109 } 01110 } 01111 01112 // Now do the rest. 01113 foreach ( $vals as $lang => $item ) { 01114 if ( $item === $defaultItem ) { 01115 $defaultLang = $lang; 01116 continue; 01117 } 01118 $content .= $this->langItem( $item, 01119 $lang, false, $noHtml ); 01120 if ( $this->singleLang ) { 01121 return Html::rawElement( 'span', 01122 array( 'lang' => $lang ), $item ); 01123 } 01124 } 01125 if ( $defaultItem !== false ) { 01126 $content = $this->langItem( $defaultItem, 01127 $defaultLang, true, $noHtml ) . 01128 $content; 01129 if ( $this->singleLang ) { 01130 return $defaultItem; 01131 } 01132 } 01133 if ( $noHtml ) { 01134 return $content; 01135 } 01136 01137 return '<ul class="metadata-langlist">' . 01138 $content . 01139 '</ul>'; 01140 case 'ol': 01141 if ( $noHtml ) { 01142 return "\n#" . implode( "\n#", $vals ); 01143 } 01144 01145 return "<ol><li>" . implode( "</li>\n<li>", $vals ) . '</li></ol>'; 01146 case 'ul': 01147 default: 01148 if ( $noHtml ) { 01149 return "\n*" . implode( "\n*", $vals ); 01150 } 01151 01152 return "<ul><li>" . implode( "</li>\n<li>", $vals ) . '</li></ul>'; 01153 } 01154 } 01155 } 01156 01167 private function langItem( $value, $lang, $default = false, $noHtml = false ) { 01168 if ( $lang === false && $default === false ) { 01169 throw new MWException( '$lang and $default cannot both ' 01170 . 'be false.' ); 01171 } 01172 01173 if ( $noHtml ) { 01174 $wrappedValue = $value; 01175 } else { 01176 $wrappedValue = '<span class="mw-metadata-lang-value">' 01177 . $value . '</span>'; 01178 } 01179 01180 if ( $lang === false ) { 01181 $msg = $this->msg( 'metadata-langitem-default', $wrappedValue ); 01182 if ( $noHtml ) { 01183 return $msg->text() . "\n\n"; 01184 } /* else */ 01185 01186 return '<li class="mw-metadata-lang-default">' 01187 . $msg->text() 01188 . "</li>\n"; 01189 } 01190 01191 $lowLang = strtolower( $lang ); 01192 $langName = Language::fetchLanguageName( $lowLang ); 01193 if ( $langName === '' ) { 01194 //try just the base language name. (aka en-US -> en ). 01195 list( $langPrefix ) = explode( '-', $lowLang, 2 ); 01196 $langName = Language::fetchLanguageName( $langPrefix ); 01197 if ( $langName === '' ) { 01198 // give up. 01199 $langName = $lang; 01200 } 01201 } 01202 // else we have a language specified 01203 01204 $msg = $this->msg( 'metadata-langitem', $wrappedValue, $langName, $lang ); 01205 if ( $noHtml ) { 01206 return '*' . $msg->text(); 01207 } /* else: */ 01208 01209 $item = '<li class="mw-metadata-lang-code-' 01210 . $lang; 01211 if ( $default ) { 01212 $item .= ' mw-metadata-lang-default'; 01213 } 01214 $item .= '" lang="' . $lang . '">'; 01215 $item .= $msg->text(); 01216 $item .= "</li>\n"; 01217 01218 return $item; 01219 } 01220 01230 private function exifMsg( $tag, $val, $arg = null, $arg2 = null ) { 01231 global $wgContLang; 01232 01233 if ( $val === '' ) { 01234 $val = 'value'; 01235 } 01236 01237 return $this->msg( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 )->text(); 01238 } 01239 01248 private function formatNum( $num, $round = false ) { 01249 $m = array(); 01250 if ( is_array( $num ) ) { 01251 $out = array(); 01252 foreach ( $num as $number ) { 01253 $out[] = $this->formatNum( $number ); 01254 } 01255 01256 return $this->getLanguage()->commaList( $out ); 01257 } 01258 if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) { 01259 if ( $m[2] != 0 ) { 01260 $newNum = $m[1] / $m[2]; 01261 if ( $round !== false ) { 01262 $newNum = round( $newNum, $round ); 01263 } 01264 } else { 01265 $newNum = $num; 01266 } 01267 01268 return $this->getLanguage()->formatNum( $newNum ); 01269 } else { 01270 if ( is_numeric( $num ) && $round !== false ) { 01271 $num = round( $num, $round ); 01272 } 01273 01274 return $this->getLanguage()->formatNum( $num ); 01275 } 01276 } 01277 01284 private function formatFraction( $num ) { 01285 $m = array(); 01286 if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) { 01287 $numerator = intval( $m[1] ); 01288 $denominator = intval( $m[2] ); 01289 $gcd = $this->gcd( abs( $numerator ), $denominator ); 01290 if ( $gcd != 0 ) { 01291 // 0 shouldn't happen! ;) 01292 return $this->formatNum( $numerator / $gcd ) . '/' . $this->formatNum( $denominator / $gcd ); 01293 } 01294 } 01295 01296 return $this->formatNum( $num ); 01297 } 01298 01306 private function gcd( $a, $b ) { 01307 /* 01308 // http://en.wikipedia.org/wiki/Euclidean_algorithm 01309 // Recursive form would be: 01310 if( $b == 0 ) 01311 return $a; 01312 else 01313 return gcd( $b, $a % $b ); 01314 */ 01315 while ( $b != 0 ) { 01316 $remainder = $a % $b; 01317 01318 // tail recursion... 01319 $a = $b; 01320 $b = $remainder; 01321 } 01322 01323 return $a; 01324 } 01325 01338 private function convertNewsCode( $val ) { 01339 if ( !preg_match( '/^\d{8}$/D', $val ) ) { 01340 // Not a valid news code. 01341 return $val; 01342 } 01343 $cat = ''; 01344 switch ( substr( $val, 0, 2 ) ) { 01345 case '01': 01346 $cat = 'ace'; 01347 break; 01348 case '02': 01349 $cat = 'clj'; 01350 break; 01351 case '03': 01352 $cat = 'dis'; 01353 break; 01354 case '04': 01355 $cat = 'fin'; 01356 break; 01357 case '05': 01358 $cat = 'edu'; 01359 break; 01360 case '06': 01361 $cat = 'evn'; 01362 break; 01363 case '07': 01364 $cat = 'hth'; 01365 break; 01366 case '08': 01367 $cat = 'hum'; 01368 break; 01369 case '09': 01370 $cat = 'lab'; 01371 break; 01372 case '10': 01373 $cat = 'lif'; 01374 break; 01375 case '11': 01376 $cat = 'pol'; 01377 break; 01378 case '12': 01379 $cat = 'rel'; 01380 break; 01381 case '13': 01382 $cat = 'sci'; 01383 break; 01384 case '14': 01385 $cat = 'soi'; 01386 break; 01387 case '15': 01388 $cat = 'spo'; 01389 break; 01390 case '16': 01391 $cat = 'war'; 01392 break; 01393 case '17': 01394 $cat = 'wea'; 01395 break; 01396 } 01397 if ( $cat !== '' ) { 01398 $catMsg = $this->exifMsg( 'iimcategory', $cat ); 01399 $val = $this->exifMsg( 'subjectnewscode', '', $val, $catMsg ); 01400 } 01401 01402 return $val; 01403 } 01404 01413 private function formatCoords( $coord, $type ) { 01414 $ref = ''; 01415 if ( $coord < 0 ) { 01416 $nCoord = -$coord; 01417 if ( $type === 'latitude' ) { 01418 $ref = 'S'; 01419 } elseif ( $type === 'longitude' ) { 01420 $ref = 'W'; 01421 } 01422 } else { 01423 $nCoord = $coord; 01424 if ( $type === 'latitude' ) { 01425 $ref = 'N'; 01426 } elseif ( $type === 'longitude' ) { 01427 $ref = 'E'; 01428 } 01429 } 01430 01431 $deg = floor( $nCoord ); 01432 $min = floor( ( $nCoord - $deg ) * 60.0 ); 01433 $sec = round( ( ( $nCoord - $deg ) - $min / 60 ) * 3600, 2 ); 01434 01435 $deg = $this->formatNum( $deg ); 01436 $min = $this->formatNum( $min ); 01437 $sec = $this->formatNum( $sec ); 01438 01439 return $this->msg( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text(); 01440 } 01441 01456 public function collapseContactInfo( $vals ) { 01457 if ( !( isset( $vals['CiAdrExtadr'] ) 01458 || isset( $vals['CiAdrCity'] ) 01459 || isset( $vals['CiAdrCtry'] ) 01460 || isset( $vals['CiEmailWork'] ) 01461 || isset( $vals['CiTelWork'] ) 01462 || isset( $vals['CiAdrPcode'] ) 01463 || isset( $vals['CiAdrRegion'] ) 01464 || isset( $vals['CiUrlWork'] ) 01465 ) ) { 01466 // We don't have any sub-properties 01467 // This could happen if its using old 01468 // iptc that just had this as a free-form 01469 // text value. 01470 // Note: We run this through htmlspecialchars 01471 // partially to be consistent, and partially 01472 // because people often insert >, etc into 01473 // the metadata which should not be interpreted 01474 // but we still want to auto-link urls. 01475 foreach ( $vals as &$val ) { 01476 $val = htmlspecialchars( $val ); 01477 } 01478 01479 return $this->flattenArrayReal( $vals ); 01480 } else { 01481 // We have a real ContactInfo field. 01482 // Its unclear if all these fields have to be 01483 // set, so assume they do not. 01484 $url = $tel = $street = $city = $country = ''; 01485 $email = $postal = $region = ''; 01486 01487 // Also note, some of the class names this uses 01488 // are similar to those used by hCard. This is 01489 // mostly because they're sensible names. This 01490 // does not (and does not attempt to) output 01491 // stuff in the hCard microformat. However it 01492 // might output in the adr microformat. 01493 01494 if ( isset( $vals['CiAdrExtadr'] ) ) { 01495 // Todo: This can potentially be multi-line. 01496 // Need to check how that works in XMP. 01497 $street = '<span class="extended-address">' 01498 . htmlspecialchars( 01499 $vals['CiAdrExtadr'] ) 01500 . '</span>'; 01501 } 01502 if ( isset( $vals['CiAdrCity'] ) ) { 01503 $city = '<span class="locality">' 01504 . htmlspecialchars( $vals['CiAdrCity'] ) 01505 . '</span>'; 01506 } 01507 if ( isset( $vals['CiAdrCtry'] ) ) { 01508 $country = '<span class="country-name">' 01509 . htmlspecialchars( $vals['CiAdrCtry'] ) 01510 . '</span>'; 01511 } 01512 if ( isset( $vals['CiEmailWork'] ) ) { 01513 $emails = array(); 01514 // Have to split multiple emails at commas/new lines. 01515 $splitEmails = explode( "\n", $vals['CiEmailWork'] ); 01516 foreach ( $splitEmails as $e1 ) { 01517 // Also split on comma 01518 foreach ( explode( ',', $e1 ) as $e2 ) { 01519 $finalEmail = trim( $e2 ); 01520 if ( $finalEmail == ',' || $finalEmail == '' ) { 01521 continue; 01522 } 01523 if ( strpos( $finalEmail, '<' ) !== false ) { 01524 // Don't do fancy formatting to 01525 // "My name" <[email protected]> style stuff 01526 $emails[] = $finalEmail; 01527 } else { 01528 $emails[] = '[mailto:' 01529 . $finalEmail 01530 . ' <span class="email">' 01531 . $finalEmail 01532 . '</span>]'; 01533 } 01534 } 01535 } 01536 $email = implode( ', ', $emails ); 01537 } 01538 if ( isset( $vals['CiTelWork'] ) ) { 01539 $tel = '<span class="tel">' 01540 . htmlspecialchars( $vals['CiTelWork'] ) 01541 . '</span>'; 01542 } 01543 if ( isset( $vals['CiAdrPcode'] ) ) { 01544 $postal = '<span class="postal-code">' 01545 . htmlspecialchars( 01546 $vals['CiAdrPcode'] ) 01547 . '</span>'; 01548 } 01549 if ( isset( $vals['CiAdrRegion'] ) ) { 01550 // Note this is province/state. 01551 $region = '<span class="region">' 01552 . htmlspecialchars( 01553 $vals['CiAdrRegion'] ) 01554 . '</span>'; 01555 } 01556 if ( isset( $vals['CiUrlWork'] ) ) { 01557 $url = '<span class="url">' 01558 . htmlspecialchars( $vals['CiUrlWork'] ) 01559 . '</span>'; 01560 } 01561 01562 return $this->msg( 'exif-contact-value', $email, $url, 01563 $street, $city, $region, $postal, $country, 01564 $tel )->text(); 01565 } 01566 } 01567 01574 public static function getVisibleFields() { 01575 $fields = array(); 01576 $lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() ); 01577 foreach ( $lines as $line ) { 01578 $matches = array(); 01579 if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { 01580 $fields[] = $matches[1]; 01581 } 01582 } 01583 $fields = array_map( 'strtolower', $fields ); 01584 01585 return $fields; 01586 } 01587 01595 public function fetchExtendedMetadata( File $file ) { 01596 global $wgMemc; 01597 01598 wfProfileIn( __METHOD__ ); 01599 01600 // If revision deleted, exit immediately 01601 if ( $file->isDeleted( File::DELETED_FILE ) ) { 01602 wfProfileOut( __METHOD__ ); 01603 01604 return array(); 01605 } 01606 01607 $cacheKey = wfMemcKey( 01608 'getExtendedMetadata', 01609 $this->getLanguage()->getCode(), 01610 (int)$this->singleLang, 01611 $file->getSha1() 01612 ); 01613 01614 $cachedValue = $wgMemc->get( $cacheKey ); 01615 if ( 01616 $cachedValue 01617 && wfRunHooks( 'ValidateExtendedMetadataCache', array( $cachedValue['timestamp'], $file ) ) 01618 ) { 01619 $extendedMetadata = $cachedValue['data']; 01620 } else { 01621 $maxCacheTime = ( $file instanceof ForeignAPIFile ) ? 60 * 60 * 12 : 60 * 60 * 24 * 30; 01622 $fileMetadata = $this->getExtendedMetadataFromFile( $file ); 01623 $extendedMetadata = $this->getExtendedMetadataFromHook( $file, $fileMetadata, $maxCacheTime ); 01624 if ( $this->singleLang ) { 01625 $this->resolveMultilangMetadata( $extendedMetadata ); 01626 } 01627 // Make sure the metadata won't break the API when an XML format is used. 01628 // This is an API-specific function so it would be cleaner to call it from 01629 // outside fetchExtendedMetadata, but this way we don't need to redo the 01630 // computation on a cache hit. 01631 $this->sanitizeArrayForXml( $extendedMetadata ); 01632 $valueToCache = array( 'data' => $extendedMetadata, 'timestamp' => wfTimestampNow() ); 01633 $wgMemc->set( $cacheKey, $valueToCache, $maxCacheTime ); 01634 } 01635 01636 wfProfileOut( __METHOD__ ); 01637 01638 return $extendedMetadata; 01639 } 01640 01650 protected function getExtendedMetadataFromFile( File $file ) { 01651 // If this is a remote file accessed via an API request, we already 01652 // have remote metadata so we just ignore any local one 01653 if ( $file instanceof ForeignAPIFile ) { 01654 // In case of error we pretend no metadata - this will get cached. 01655 // Might or might not be a good idea. 01656 return $file->getExtendedMetadata() ?: array(); 01657 } 01658 01659 wfProfileIn( __METHOD__ ); 01660 01661 $uploadDate = wfTimestamp( TS_ISO_8601, $file->getTimestamp() ); 01662 01663 $fileMetadata = array( 01664 // This is modification time, which is close to "upload" time. 01665 'DateTime' => array( 01666 'value' => $uploadDate, 01667 'source' => 'mediawiki-metadata', 01668 ), 01669 ); 01670 01671 $title = $file->getTitle(); 01672 if ( $title ) { 01673 $text = $title->getText(); 01674 $pos = strrpos( $text, '.' ); 01675 01676 if ( $pos ) { 01677 $name = substr( $text, 0, $pos ); 01678 } else { 01679 $name = $text; 01680 } 01681 01682 $fileMetadata['ObjectName'] = array( 01683 'value' => $name, 01684 'source' => 'mediawiki-metadata', 01685 ); 01686 } 01687 01688 $common = $file->getCommonMetaArray(); 01689 01690 if ( $common !== false ) { 01691 foreach ( $common as $key => $value ) { 01692 $fileMetadata[$key] = array( 01693 'value' => $value, 01694 'source' => 'file-metadata', 01695 ); 01696 } 01697 } 01698 01699 wfProfileOut( __METHOD__ ); 01700 01701 return $fileMetadata; 01702 } 01703 01714 protected function getExtendedMetadataFromHook( File $file, array $extendedMetadata, 01715 &$maxCacheTime 01716 ) { 01717 wfProfileIn( __METHOD__ ); 01718 01719 wfRunHooks( 'GetExtendedMetadata', array( 01720 &$extendedMetadata, 01721 $file, 01722 $this->getContext(), 01723 $this->singleLang, 01724 &$maxCacheTime 01725 ) ); 01726 01727 $visible = array_flip( self::getVisibleFields() ); 01728 foreach ( $extendedMetadata as $key => $value ) { 01729 if ( !isset( $visible[strtolower( $key )] ) ) { 01730 $extendedMetadata[$key]['hidden'] = ''; 01731 } 01732 } 01733 01734 wfProfileOut( __METHOD__ ); 01735 01736 return $extendedMetadata; 01737 } 01738 01747 protected function resolveMultilangValue( $value ) { 01748 if ( 01749 !is_array( $value ) 01750 || !isset( $value['_type'] ) 01751 || $value['_type'] != 'lang' 01752 ) { 01753 return $value; // do nothing if not a multilang array 01754 } 01755 01756 // choose the language best matching user or site settings 01757 $priorityLanguages = $this->getPriorityLanguages(); 01758 foreach ( $priorityLanguages as $lang ) { 01759 if ( isset( $value[$lang] ) ) { 01760 return $value[$lang]; 01761 } 01762 } 01763 01764 // otherwise go with the default language, if set 01765 if ( isset( $value['x-default'] ) ) { 01766 return $value['x-default']; 01767 } 01768 01769 // otherwise just return any one language 01770 unset( $value['_type'] ); 01771 if ( !empty( $value ) ) { 01772 return reset( $value ); 01773 } 01774 01775 // this should not happen; signal error 01776 return null; 01777 } 01778 01785 protected function resolveMultilangMetadata( &$metadata ) { 01786 if ( !is_array( $metadata ) ) { 01787 return; 01788 } 01789 foreach ( $metadata as &$field ) { 01790 if ( isset( $field['value'] ) ) { 01791 $field['value'] = $this->resolveMultilangValue( $field['value'] ); 01792 } 01793 } 01794 } 01795 01801 protected function sanitizeArrayForXml( &$arr ) { 01802 if ( !is_array( $arr ) ) { 01803 return; 01804 } 01805 01806 $counter = 1; 01807 foreach ( $arr as $key => &$value ) { 01808 $sanitizedKey = $this->sanitizeKeyForXml( $key ); 01809 if ( $sanitizedKey !== $key ) { 01810 if ( isset( $arr[$sanitizedKey] ) ) { 01811 // Make the sanitized keys hopefully unique. 01812 // To make it definitely unique would be too much effort, given that 01813 // sanitizing is only needed for misformatted metadata anyway, but 01814 // this at least covers the case when $arr is numeric. 01815 $sanitizedKey .= $counter; 01816 ++$counter; 01817 } 01818 $arr[$sanitizedKey] = $arr[$key]; 01819 unset( $arr[$key] ); 01820 } 01821 if ( is_array( $value ) ) { 01822 $this->sanitizeArrayForXml( $value ); 01823 } 01824 } 01825 } 01826 01835 protected function sanitizeKeyForXml( $key ) { 01836 // drop all characters which are not valid in an XML tag name 01837 // a bunch of non-ASCII letters would be valid but probably won't 01838 // be used so we take the easy way 01839 $key = preg_replace( '/[^a-zA-z0-9_:.-]/', '', $key ); 01840 // drop characters which are invalid at the first position 01841 $key = preg_replace( '/^[\d-.]+/', '', $key ); 01842 01843 if ( $key == '' ) { 01844 $key = '_'; 01845 } 01846 01847 // special case for an internal keyword 01848 if ( $key == '_element' ) { 01849 $key = 'element'; 01850 } 01851 01852 return $key; 01853 } 01854 01861 protected function getPriorityLanguages() { 01862 $priorityLanguages = 01863 Language::getFallbacksIncludingSiteLanguage( $this->getLanguage()->getCode() ); 01864 $priorityLanguages = array_merge( 01865 (array)$this->getLanguage()->getCode(), 01866 $priorityLanguages[0], 01867 $priorityLanguages[1] 01868 ); 01869 01870 return $priorityLanguages; 01871 } 01872 }