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


Generated : Thu Apr 18 08:20:02 2024 Cross-referenced by PHPXref