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