[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

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


Generated: Mon Jul 22 01:00:03 2019 Cross-referenced by PHPXref 0.7.1