| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 3 ///////////////////////////////////////////////////////////////// 4 /// getID3() by James Heinrich <info@getid3.org> // 5 // available at https://github.com/JamesHeinrich/getID3 // 6 // or https://www.getid3.org // 7 // or http://getid3.sourceforge.net // 8 // see readme.txt for more details // 9 ///////////////////////////////////////////////////////////////// 10 // // 11 // module.tag.apetag.php // 12 // module for analyzing APE tags // 13 // dependencies: NONE // 14 // /// 15 ///////////////////////////////////////////////////////////////// 16 17 if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers 18 exit; 19 } 20 21 class getid3_apetag extends getid3_handler 22 { 23 /** 24 * true: return full data for all attachments; 25 * false: return no data for all attachments; 26 * integer: return data for attachments <= than this; 27 * string: save as file to this directory. 28 * 29 * @var int|bool|string 30 */ 31 public $inline_attachments = true; 32 33 public $overrideendoffset = 0; 34 35 /** 36 * @return bool 37 */ 38 public function Analyze() { 39 $info = &$this->getid3->info; 40 41 if (!getid3_lib::intValueSupported($info['filesize'])) { 42 $this->warning('Unable to check for APEtags because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'); 43 return false; 44 } 45 if (PHP_INT_MAX == 2147483647) { 46 // https://github.com/JamesHeinrich/getID3/issues/439 47 $this->warning('APEtag flags may not be parsed correctly on 32-bit PHP'); 48 } 49 50 $id3v1tagsize = 128; 51 $apetagheadersize = 32; 52 $lyrics3tagsize = 10; 53 54 if ($this->overrideendoffset == 0) { 55 56 $this->fseek(0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END); 57 $APEfooterID3v1 = $this->fread($id3v1tagsize + $apetagheadersize + $lyrics3tagsize); 58 59 //if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) { 60 if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') { 61 62 // APE tag found before ID3v1 63 $info['ape']['tag_offset_end'] = $info['filesize'] - $id3v1tagsize; 64 65 //} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) { 66 } elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') { 67 68 // APE tag found, no ID3v1 69 $info['ape']['tag_offset_end'] = $info['filesize']; 70 71 } 72 73 } else { 74 75 $this->fseek($this->overrideendoffset - $apetagheadersize); 76 if ($this->fread(8) == 'APETAGEX') { 77 $info['ape']['tag_offset_end'] = $this->overrideendoffset; 78 } 79 80 } 81 if (!isset($info['ape']['tag_offset_end'])) { 82 83 // APE tag not found 84 unset($info['ape']); 85 return false; 86 87 } 88 89 // shortcut 90 $thisfile_ape = &$info['ape']; 91 92 $this->fseek($thisfile_ape['tag_offset_end'] - $apetagheadersize); 93 $APEfooterData = $this->fread(32); 94 if (!($thisfile_ape['footer'] = $this->parseAPEheaderFooter($APEfooterData))) { 95 $this->error('Error parsing APE footer at offset '.$thisfile_ape['tag_offset_end']); 96 return false; 97 } 98 99 if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { 100 $this->fseek($thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize); 101 $thisfile_ape['tag_offset_start'] = $this->ftell(); 102 $APEtagData = $this->fread($thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize); 103 } else { 104 $thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize']; 105 $this->fseek($thisfile_ape['tag_offset_start']); 106 $APEtagData = $this->fread($thisfile_ape['footer']['raw']['tagsize']); 107 } 108 $info['avdataend'] = $thisfile_ape['tag_offset_start']; 109 110 if (isset($info['id3v1']['tag_offset_start']) && ($info['id3v1']['tag_offset_start'] < $thisfile_ape['tag_offset_end'])) { 111 $this->warning('ID3v1 tag information ignored since it appears to be a false synch in APEtag data'); 112 unset($info['id3v1']); 113 foreach ($info['warning'] as $key => $value) { 114 if ($value == 'Some ID3v1 fields do not use NULL characters for padding') { 115 unset($info['warning'][$key]); 116 sort($info['warning']); 117 break; 118 } 119 } 120 } 121 122 $offset = 0; 123 if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { 124 if ($thisfile_ape['header'] = $this->parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) { 125 $offset += $apetagheadersize; 126 } else { 127 $this->error('Error parsing APE header at offset '.$thisfile_ape['tag_offset_start']); 128 return false; 129 } 130 } 131 132 // shortcut 133 $info['replay_gain'] = array(); 134 $thisfile_replaygain = &$info['replay_gain']; 135 136 for ($i = 0; $i < $thisfile_ape['footer']['raw']['tag_items']; $i++) { 137 $value_size = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4)); 138 $offset += 4; 139 $item_flags = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4)); 140 $offset += 4; 141 if (strstr(substr($APEtagData, $offset), "\x00") === false) { 142 $this->error('Cannot find null-byte (0x00) separator between ItemKey #'.$i.' and value. ItemKey starts '.$offset.' bytes into the APE tag, at file offset '.($thisfile_ape['tag_offset_start'] + $offset)); 143 return false; 144 } 145 $ItemKeyLength = strpos($APEtagData, "\x00", $offset) - $offset; 146 $item_key = strtolower(substr($APEtagData, $offset, $ItemKeyLength)); 147 148 // shortcut 149 $thisfile_ape['items'][$item_key] = array(); 150 $thisfile_ape_items_current = &$thisfile_ape['items'][$item_key]; 151 152 $thisfile_ape_items_current['offset'] = $thisfile_ape['tag_offset_start'] + $offset; 153 154 $offset += ($ItemKeyLength + 1); // skip 0x00 terminator 155 $thisfile_ape_items_current['data'] = substr($APEtagData, $offset, $value_size); 156 $offset += $value_size; 157 158 $thisfile_ape_items_current['flags'] = $this->parseAPEtagFlags($item_flags); 159 switch ($thisfile_ape_items_current['flags']['item_contents_raw']) { 160 case 0: // UTF-8 161 case 2: // Locator (URL, filename, etc), UTF-8 encoded 162 $thisfile_ape_items_current['data'] = explode("\x00", $thisfile_ape_items_current['data']); 163 break; 164 165 case 1: // binary data 166 default: 167 break; 168 } 169 170 switch (strtolower($item_key)) { 171 // http://wiki.hydrogenaud.io/index.php?title=ReplayGain#MP3Gain 172 case 'replaygain_track_gain': 173 if (preg_match('#^([\\-\\+][0-9\\.,]{8})( dB)?$#', $thisfile_ape_items_current['data'][0], $matches)) { 174 $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $matches[1]); // float casting will see "0,95" as zero! 175 $thisfile_replaygain['track']['originator'] = 'unspecified'; 176 } else { 177 $this->warning('MP3gainTrackGain value in APEtag appears invalid: "'.$thisfile_ape_items_current['data'][0].'"'); 178 } 179 break; 180 181 case 'replaygain_track_peak': 182 if (preg_match('#^([0-9\\.,]{8})$#', $thisfile_ape_items_current['data'][0], $matches)) { 183 $thisfile_replaygain['track']['peak'] = (float) str_replace(',', '.', $matches[1]); // float casting will see "0,95" as zero! 184 $thisfile_replaygain['track']['originator'] = 'unspecified'; 185 if ($thisfile_replaygain['track']['peak'] <= 0) { 186 $this->warning('ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'); 187 } 188 } else { 189 $this->warning('MP3gainTrackPeak value in APEtag appears invalid: "'.$thisfile_ape_items_current['data'][0].'"'); 190 } 191 break; 192 193 case 'replaygain_album_gain': 194 if (preg_match('#^([\\-\\+][0-9\\.,]{8})( dB)?$#', $thisfile_ape_items_current['data'][0], $matches)) { 195 $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $matches[1]); // float casting will see "0,95" as zero! 196 $thisfile_replaygain['album']['originator'] = 'unspecified'; 197 } else { 198 $this->warning('MP3gainAlbumGain value in APEtag appears invalid: "'.$thisfile_ape_items_current['data'][0].'"'); 199 } 200 break; 201 202 case 'replaygain_album_peak': 203 if (preg_match('#^([0-9\\.,]{8})$#', $thisfile_ape_items_current['data'][0], $matches)) { 204 $thisfile_replaygain['album']['peak'] = (float) str_replace(',', '.', $matches[1]); // float casting will see "0,95" as zero! 205 $thisfile_replaygain['album']['originator'] = 'unspecified'; 206 if ($thisfile_replaygain['album']['peak'] <= 0) { 207 $this->warning('ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'); 208 } 209 } else { 210 $this->warning('MP3gainAlbumPeak value in APEtag appears invalid: "'.$thisfile_ape_items_current['data'][0].'"'); 211 } 212 break; 213 214 case 'mp3gain_undo': 215 if (preg_match('#^[\\-\\+][0-9]{3},[\\-\\+][0-9]{3},[NW]$#', $thisfile_ape_items_current['data'][0])) { 216 list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]); 217 $thisfile_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left); 218 $thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right); 219 $thisfile_replaygain['mp3gain']['undo_wrap'] = (($mp3gain_undo_wrap == 'Y') ? true : false); 220 } else { 221 $this->warning('MP3gainUndo value in APEtag appears invalid: "'.$thisfile_ape_items_current['data'][0].'"'); 222 } 223 break; 224 225 case 'mp3gain_minmax': 226 if (preg_match('#^[0-9]{3},[0-9]{3}$#', $thisfile_ape_items_current['data'][0])) { 227 list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]); 228 $thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min); 229 $thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max); 230 } else { 231 $this->warning('MP3gainMinMax value in APEtag appears invalid: "'.$thisfile_ape_items_current['data'][0].'"'); 232 } 233 break; 234 235 case 'mp3gain_album_minmax': 236 if (preg_match('#^[0-9]{3},[0-9]{3}$#', $thisfile_ape_items_current['data'][0])) { 237 list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]); 238 $thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min); 239 $thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max); 240 } else { 241 $this->warning('MP3gainAlbumMinMax value in APEtag appears invalid: "'.$thisfile_ape_items_current['data'][0].'"'); 242 } 243 break; 244 245 case 'tracknumber': 246 if (is_array($thisfile_ape_items_current['data'])) { 247 foreach ($thisfile_ape_items_current['data'] as $comment) { 248 $thisfile_ape['comments']['track_number'][] = $comment; 249 } 250 } 251 break; 252 253 case 'cover art (artist)': 254 case 'cover art (back)': 255 case 'cover art (band logo)': 256 case 'cover art (band)': 257 case 'cover art (colored fish)': 258 case 'cover art (composer)': 259 case 'cover art (conductor)': 260 case 'cover art (front)': 261 case 'cover art (icon)': 262 case 'cover art (illustration)': 263 case 'cover art (lead)': 264 case 'cover art (leaflet)': 265 case 'cover art (lyricist)': 266 case 'cover art (media)': 267 case 'cover art (movie scene)': 268 case 'cover art (other icon)': 269 case 'cover art (other)': 270 case 'cover art (performance)': 271 case 'cover art (publisher logo)': 272 case 'cover art (recording)': 273 case 'cover art (studio)': 274 // list of possible cover arts from https://github.com/mono/taglib-sharp/blob/taglib-sharp-2.0.3.2/src/TagLib/Ape/Tag.cs 275 if (is_array($thisfile_ape_items_current['data'])) { 276 $this->warning('APEtag "'.$item_key.'" should be flagged as Binary data, but was incorrectly flagged as UTF-8'); 277 $thisfile_ape_items_current['data'] = implode("\x00", $thisfile_ape_items_current['data']); 278 } 279 list($thisfile_ape_items_current['filename'], $thisfile_ape_items_current['data']) = explode("\x00", $thisfile_ape_items_current['data'], 2); 280 $thisfile_ape_items_current['data_offset'] = $thisfile_ape_items_current['offset'] + strlen($thisfile_ape_items_current['filename']."\x00"); 281 $thisfile_ape_items_current['data_length'] = strlen($thisfile_ape_items_current['data']); 282 283 do { 284 $thisfile_ape_items_current['image_mime'] = ''; 285 $imageinfo = array(); 286 $imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo); 287 if (($imagechunkcheck === false) || !isset($imagechunkcheck[2])) { 288 $this->warning('APEtag "'.$item_key.'" contains invalid image data'); 289 break; 290 } 291 $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]); 292 293 if ($this->inline_attachments === false) { 294 // skip entirely 295 unset($thisfile_ape_items_current['data']); 296 break; 297 } 298 if ($this->inline_attachments === true) { 299 // great 300 } elseif (is_int($this->inline_attachments)) { 301 if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) { 302 // too big, skip 303 $this->warning('attachment at '.$thisfile_ape_items_current['offset'].' is too large to process inline ('.number_format($thisfile_ape_items_current['data_length']).' bytes)'); 304 unset($thisfile_ape_items_current['data']); 305 break; 306 } 307 } elseif (is_string($this->inline_attachments)) { 308 $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR); 309 if (!is_dir($this->inline_attachments) || !getID3::is_writable($this->inline_attachments)) { 310 // cannot write, skip 311 $this->warning('attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)'); 312 unset($thisfile_ape_items_current['data']); 313 break; 314 } 315 } 316 // if we get this far, must be OK 317 if (is_string($this->inline_attachments)) { 318 $destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$thisfile_ape_items_current['data_offset']; 319 if (!file_exists($destination_filename) || getID3::is_writable($destination_filename)) { 320 file_put_contents($destination_filename, $thisfile_ape_items_current['data']); 321 } else { 322 $this->warning('attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)'); 323 } 324 $thisfile_ape_items_current['data_filename'] = $destination_filename; 325 unset($thisfile_ape_items_current['data']); 326 } else { 327 if (!isset($info['ape']['comments']['picture'])) { 328 $info['ape']['comments']['picture'] = array(); 329 } 330 $comments_picture_data = array(); 331 foreach (array('data', 'image_mime', 'image_width', 'image_height', 'imagetype', 'picturetype', 'description', 'datalength') as $picture_key) { 332 if (isset($thisfile_ape_items_current[$picture_key])) { 333 $comments_picture_data[$picture_key] = $thisfile_ape_items_current[$picture_key]; 334 } 335 } 336 $info['ape']['comments']['picture'][] = $comments_picture_data; 337 unset($comments_picture_data); 338 } 339 } while (false); // @phpstan-ignore-line 340 break; 341 342 default: 343 if (is_array($thisfile_ape_items_current['data'])) { 344 foreach ($thisfile_ape_items_current['data'] as $comment) { 345 $thisfile_ape['comments'][strtolower($item_key)][] = $comment; 346 } 347 } 348 break; 349 } 350 351 } 352 if (empty($thisfile_replaygain)) { 353 unset($info['replay_gain']); 354 } 355 return true; 356 } 357 358 /** 359 * @param string $APEheaderFooterData 360 * 361 * @return array|false 362 */ 363 public function parseAPEheaderFooter($APEheaderFooterData) { 364 // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html 365 366 // shortcut 367 $headerfooterinfo = array(); 368 $headerfooterinfo['raw'] = array(); 369 $headerfooterinfo_raw = &$headerfooterinfo['raw']; 370 371 $headerfooterinfo_raw['footer_tag'] = substr($APEheaderFooterData, 0, 8); 372 if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') { 373 return false; 374 } 375 $headerfooterinfo_raw['version'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 8, 4)); 376 $headerfooterinfo_raw['tagsize'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4)); 377 $headerfooterinfo_raw['tag_items'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4)); 378 $headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4)); 379 $headerfooterinfo_raw['reserved'] = substr($APEheaderFooterData, 24, 8); 380 381 $headerfooterinfo['tag_version'] = $headerfooterinfo_raw['version'] / 1000; 382 if ($headerfooterinfo['tag_version'] >= 2) { 383 $headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']); 384 } 385 return $headerfooterinfo; 386 } 387 388 /** 389 * @param int $rawflagint 390 * 391 * @return array 392 */ 393 public function parseAPEtagFlags($rawflagint) { 394 // "Note: APE Tags 1.0 do not use any of the APE Tag flags. 395 // All are set to zero on creation and ignored on reading." 396 // http://wiki.hydrogenaud.io/index.php?title=Ape_Tags_Flags 397 $flags = array(); 398 $flags['header'] = (bool) ($rawflagint & 0x80000000); 399 $flags['footer'] = (bool) ($rawflagint & 0x40000000); 400 $flags['this_is_header'] = (bool) ($rawflagint & 0x20000000); 401 $flags['item_contents_raw'] = ($rawflagint & 0x00000006) >> 1; 402 $flags['read_only'] = (bool) ($rawflagint & 0x00000001); 403 404 $flags['item_contents'] = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']); 405 406 return $flags; 407 } 408 409 /** 410 * @param int $contenttypeid 411 * 412 * @return string 413 */ 414 public function APEcontentTypeFlagLookup($contenttypeid) { 415 static $APEcontentTypeFlagLookup = array( 416 0 => 'utf-8', 417 1 => 'binary', 418 2 => 'external', 419 3 => 'reserved' 420 ); 421 return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid'); 422 } 423 424 /** 425 * @param string $itemkey 426 * 427 * @return bool 428 */ 429 public function APEtagItemIsUTF8Lookup($itemkey) { 430 static $APEtagItemIsUTF8Lookup = array( 431 'title', 432 'subtitle', 433 'artist', 434 'album', 435 'debut album', 436 'publisher', 437 'conductor', 438 'track', 439 'composer', 440 'comment', 441 'copyright', 442 'publicationright', 443 'file', 444 'year', 445 'record date', 446 'record location', 447 'genre', 448 'media', 449 'related', 450 'isrc', 451 'abstract', 452 'language', 453 'bibliography' 454 ); 455 return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup); 456 } 457 458 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Apr 18 08:20:10 2026 | Cross-referenced by PHPXref |