[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ID3/ -> module.tag.apetag.php (source)

   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  }


Generated : Sat Apr 18 08:20:10 2026 Cross-referenced by PHPXref