File indexing completed on 2024-05-12 17:26:01

0001 <?php
0002 /////////////////////////////////////////////////////////////////
0003 /// getID3() by James Heinrich <info@getid3.org>               //
0004 //  available at http://getid3.sourceforge.net                 //
0005 //            or http://www.getid3.org                         //
0006 //          also https://github.com/JamesHeinrich/getID3       //
0007 /////////////////////////////////////////////////////////////////
0008 // See readme.txt for more details                             //
0009 /////////////////////////////////////////////////////////////////
0010 //                                                             //
0011 // module.tag.apetag.php                                       //
0012 // module for analyzing APE tags                               //
0013 // dependencies: NONE                                          //
0014 //                                                            ///
0015 /////////////////////////////////////////////////////////////////
0016 
0017 class getid3_apetag extends getid3_handler
0018 {
0019   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
0020   public $overrideendoffset  = 0;
0021 
0022   public function Analyze() {
0023     $info = &$this->getid3->info;
0024 
0025     if (!getid3_lib::intValueSupported($info['filesize'])) {
0026       $info['warning'][] = 'Unable to check for APEtags because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
0027       return false;
0028     }
0029 
0030     $id3v1tagsize     = 128;
0031     $apetagheadersize = 32;
0032     $lyrics3tagsize   = 10;
0033 
0034     if ($this->overrideendoffset == 0) {
0035 
0036       $this->fseek(0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END);
0037       $APEfooterID3v1 = $this->fread($id3v1tagsize + $apetagheadersize + $lyrics3tagsize);
0038 
0039       //if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) {
0040       if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') {
0041 
0042         // APE tag found before ID3v1
0043         $info['ape']['tag_offset_end'] = $info['filesize'] - $id3v1tagsize;
0044 
0045       //} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) {
0046       } elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') {
0047 
0048         // APE tag found, no ID3v1
0049         $info['ape']['tag_offset_end'] = $info['filesize'];
0050 
0051       }
0052 
0053     } else {
0054 
0055       $this->fseek($this->overrideendoffset - $apetagheadersize);
0056       if ($this->fread(8) == 'APETAGEX') {
0057         $info['ape']['tag_offset_end'] = $this->overrideendoffset;
0058       }
0059 
0060     }
0061     if (!isset($info['ape']['tag_offset_end'])) {
0062 
0063       // APE tag not found
0064       unset($info['ape']);
0065       return false;
0066 
0067     }
0068 
0069     // shortcut
0070     $thisfile_ape = &$info['ape'];
0071 
0072     $this->fseek($thisfile_ape['tag_offset_end'] - $apetagheadersize);
0073     $APEfooterData = $this->fread(32);
0074     if (!($thisfile_ape['footer'] = $this->parseAPEheaderFooter($APEfooterData))) {
0075       $info['error'][] = 'Error parsing APE footer at offset '.$thisfile_ape['tag_offset_end'];
0076       return false;
0077     }
0078 
0079     if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) {
0080       $this->fseek($thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize);
0081       $thisfile_ape['tag_offset_start'] = $this->ftell();
0082       $APEtagData = $this->fread($thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize);
0083     } else {
0084       $thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'];
0085       $this->fseek($thisfile_ape['tag_offset_start']);
0086       $APEtagData = $this->fread($thisfile_ape['footer']['raw']['tagsize']);
0087     }
0088     $info['avdataend'] = $thisfile_ape['tag_offset_start'];
0089 
0090     if (isset($info['id3v1']['tag_offset_start']) && ($info['id3v1']['tag_offset_start'] < $thisfile_ape['tag_offset_end'])) {
0091       $info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in APEtag data';
0092       unset($info['id3v1']);
0093       foreach ($info['warning'] as $key => $value) {
0094         if ($value == 'Some ID3v1 fields do not use NULL characters for padding') {
0095           unset($info['warning'][$key]);
0096           sort($info['warning']);
0097           break;
0098         }
0099       }
0100     }
0101 
0102     $offset = 0;
0103     if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) {
0104       if ($thisfile_ape['header'] = $this->parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) {
0105         $offset += $apetagheadersize;
0106       } else {
0107         $info['error'][] = 'Error parsing APE header at offset '.$thisfile_ape['tag_offset_start'];
0108         return false;
0109       }
0110     }
0111 
0112     // shortcut
0113     $info['replay_gain'] = array();
0114     $thisfile_replaygain = &$info['replay_gain'];
0115 
0116     for ($i = 0; $i < $thisfile_ape['footer']['raw']['tag_items']; $i++) {
0117       $value_size = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4));
0118       $offset += 4;
0119       $item_flags = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4));
0120       $offset += 4;
0121       if (strstr(substr($APEtagData, $offset), "\x00") === false) {
0122         $info['error'][] = 'Cannot find null-byte (0x00) seperator between ItemKey #'.$i.' and value. ItemKey starts '.$offset.' bytes into the APE tag, at file offset '.($thisfile_ape['tag_offset_start'] + $offset);
0123         return false;
0124       }
0125       $ItemKeyLength = strpos($APEtagData, "\x00", $offset) - $offset;
0126       $item_key      = strtolower(substr($APEtagData, $offset, $ItemKeyLength));
0127 
0128       // shortcut
0129       $thisfile_ape['items'][$item_key] = array();
0130       $thisfile_ape_items_current = &$thisfile_ape['items'][$item_key];
0131 
0132       $thisfile_ape_items_current['offset'] = $thisfile_ape['tag_offset_start'] + $offset;
0133 
0134       $offset += ($ItemKeyLength + 1); // skip 0x00 terminator
0135       $thisfile_ape_items_current['data'] = substr($APEtagData, $offset, $value_size);
0136       $offset += $value_size;
0137 
0138       $thisfile_ape_items_current['flags'] = $this->parseAPEtagFlags($item_flags);
0139       switch ($thisfile_ape_items_current['flags']['item_contents_raw']) {
0140         case 0: // UTF-8
0141         case 3: // Locator (URL, filename, etc), UTF-8 encoded
0142           $thisfile_ape_items_current['data'] = explode("\x00", trim($thisfile_ape_items_current['data']));
0143           break;
0144 
0145         default: // binary data
0146           break;
0147       }
0148 
0149       switch (strtolower($item_key)) {
0150         case 'replaygain_track_gain':
0151           $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
0152           $thisfile_replaygain['track']['originator'] = 'unspecified';
0153           break;
0154 
0155         case 'replaygain_track_peak':
0156           $thisfile_replaygain['track']['peak']       = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
0157           $thisfile_replaygain['track']['originator'] = 'unspecified';
0158           if ($thisfile_replaygain['track']['peak'] <= 0) {
0159             $info['warning'][] = 'ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
0160           }
0161           break;
0162 
0163         case 'replaygain_album_gain':
0164           $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
0165           $thisfile_replaygain['album']['originator'] = 'unspecified';
0166           break;
0167 
0168         case 'replaygain_album_peak':
0169           $thisfile_replaygain['album']['peak']       = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
0170           $thisfile_replaygain['album']['originator'] = 'unspecified';
0171           if ($thisfile_replaygain['album']['peak'] <= 0) {
0172             $info['warning'][] = 'ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
0173           }
0174           break;
0175 
0176         case 'mp3gain_undo':
0177           list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]);
0178           $thisfile_replaygain['mp3gain']['undo_left']  = intval($mp3gain_undo_left);
0179           $thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right);
0180           $thisfile_replaygain['mp3gain']['undo_wrap']  = (($mp3gain_undo_wrap == 'Y') ? true : false);
0181           break;
0182 
0183         case 'mp3gain_minmax':
0184           list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]);
0185           $thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min);
0186           $thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max);
0187           break;
0188 
0189         case 'mp3gain_album_minmax':
0190           list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]);
0191           $thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min);
0192           $thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max);
0193           break;
0194 
0195         case 'tracknumber':
0196           if (is_array($thisfile_ape_items_current['data'])) {
0197             foreach ($thisfile_ape_items_current['data'] as $comment) {
0198               $thisfile_ape['comments']['track'][] = $comment;
0199             }
0200           }
0201           break;
0202 
0203         case 'cover art (artist)':
0204         case 'cover art (back)':
0205         case 'cover art (band logo)':
0206         case 'cover art (band)':
0207         case 'cover art (colored fish)':
0208         case 'cover art (composer)':
0209         case 'cover art (conductor)':
0210         case 'cover art (front)':
0211         case 'cover art (icon)':
0212         case 'cover art (illustration)':
0213         case 'cover art (lead)':
0214         case 'cover art (leaflet)':
0215         case 'cover art (lyricist)':
0216         case 'cover art (media)':
0217         case 'cover art (movie scene)':
0218         case 'cover art (other icon)':
0219         case 'cover art (other)':
0220         case 'cover art (performance)':
0221         case 'cover art (publisher logo)':
0222         case 'cover art (recording)':
0223         case 'cover art (studio)':
0224           // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html
0225           list($thisfile_ape_items_current['filename'], $thisfile_ape_items_current['data']) = explode("\x00", $thisfile_ape_items_current['data'], 2);
0226           $thisfile_ape_items_current['data_offset'] = $thisfile_ape_items_current['offset'] + strlen($thisfile_ape_items_current['filename']."\x00");
0227           $thisfile_ape_items_current['data_length'] = strlen($thisfile_ape_items_current['data']);
0228 
0229           $thisfile_ape_items_current['image_mime'] = '';
0230           $imageinfo = array();
0231           $imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo);
0232           $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]);
0233 
0234           do {
0235             if ($this->inline_attachments === false) {
0236               // skip entirely
0237               unset($thisfile_ape_items_current['data']);
0238               break;
0239             }
0240             if ($this->inline_attachments === true) {
0241               // great
0242             } elseif (is_int($this->inline_attachments)) {
0243               if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) {
0244                 // too big, skip
0245                 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' is too large to process inline ('.number_format($thisfile_ape_items_current['data_length']).' bytes)';
0246                 unset($thisfile_ape_items_current['data']);
0247                 break;
0248               }
0249             } elseif (is_string($this->inline_attachments)) {
0250               $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR);
0251               if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) {
0252                 // cannot write, skip
0253                 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)';
0254                 unset($thisfile_ape_items_current['data']);
0255                 break;
0256               }
0257             }
0258             // if we get this far, must be OK
0259             if (is_string($this->inline_attachments)) {
0260               $destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$thisfile_ape_items_current['data_offset'];
0261               if (!file_exists($destination_filename) || is_writable($destination_filename)) {
0262                 file_put_contents($destination_filename, $thisfile_ape_items_current['data']);
0263               } else {
0264                 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)';
0265               }
0266               $thisfile_ape_items_current['data_filename'] = $destination_filename;
0267               unset($thisfile_ape_items_current['data']);
0268             } else {
0269               if (!isset($info['ape']['comments']['picture'])) {
0270                 $info['ape']['comments']['picture'] = array();
0271               }
0272               $info['ape']['comments']['picture'][] = array('data'=>$thisfile_ape_items_current['data'], 'image_mime'=>$thisfile_ape_items_current['image_mime']);
0273             }
0274           } while (false);
0275           break;
0276 
0277         default:
0278           if (is_array($thisfile_ape_items_current['data'])) {
0279             foreach ($thisfile_ape_items_current['data'] as $comment) {
0280               $thisfile_ape['comments'][strtolower($item_key)][] = $comment;
0281             }
0282           }
0283           break;
0284       }
0285 
0286     }
0287     if (empty($thisfile_replaygain)) {
0288       unset($info['replay_gain']);
0289     }
0290     return true;
0291   }
0292 
0293   public function parseAPEheaderFooter($APEheaderFooterData) {
0294     // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html
0295 
0296     // shortcut
0297     $headerfooterinfo['raw'] = array();
0298     $headerfooterinfo_raw = &$headerfooterinfo['raw'];
0299 
0300     $headerfooterinfo_raw['footer_tag']   =                  substr($APEheaderFooterData,  0, 8);
0301     if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') {
0302       return false;
0303     }
0304     $headerfooterinfo_raw['version']      = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData,  8, 4));
0305     $headerfooterinfo_raw['tagsize']      = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4));
0306     $headerfooterinfo_raw['tag_items']    = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4));
0307     $headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4));
0308     $headerfooterinfo_raw['reserved']     =                              substr($APEheaderFooterData, 24, 8);
0309 
0310     $headerfooterinfo['tag_version']         = $headerfooterinfo_raw['version'] / 1000;
0311     if ($headerfooterinfo['tag_version'] >= 2) {
0312       $headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']);
0313     }
0314     return $headerfooterinfo;
0315   }
0316 
0317   public function parseAPEtagFlags($rawflagint) {
0318     // "Note: APE Tags 1.0 do not use any of the APE Tag flags.
0319     // All are set to zero on creation and ignored on reading."
0320     // http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html
0321     $flags['header']            = (bool) ($rawflagint & 0x80000000);
0322     $flags['footer']            = (bool) ($rawflagint & 0x40000000);
0323     $flags['this_is_header']    = (bool) ($rawflagint & 0x20000000);
0324     $flags['item_contents_raw'] =        ($rawflagint & 0x00000006) >> 1;
0325     $flags['read_only']         = (bool) ($rawflagint & 0x00000001);
0326 
0327     $flags['item_contents']     = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']);
0328 
0329     return $flags;
0330   }
0331 
0332   public function APEcontentTypeFlagLookup($contenttypeid) {
0333     static $APEcontentTypeFlagLookup = array(
0334       0 => 'utf-8',
0335       1 => 'binary',
0336       2 => 'external',
0337       3 => 'reserved'
0338     );
0339     return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid');
0340   }
0341 
0342   public function APEtagItemIsUTF8Lookup($itemkey) {
0343     static $APEtagItemIsUTF8Lookup = array(
0344       'title',
0345       'subtitle',
0346       'artist',
0347       'album',
0348       'debut album',
0349       'publisher',
0350       'conductor',
0351       'track',
0352       'composer',
0353       'comment',
0354       'copyright',
0355       'publicationright',
0356       'file',
0357       'year',
0358       'record date',
0359       'record location',
0360       'genre',
0361       'media',
0362       'related',
0363       'isrc',
0364       'abstract',
0365       'language',
0366       'bibliography'
0367     );
0368     return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup);
0369   }
0370 
0371 }