[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

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


Generated: Sat Nov 23 20:47:33 2019 Cross-referenced by PHPXref 0.7