getid3_apetag

The WordPress Core getid3 apetag class.

Defined (1)

The class is defined in the following location(s).

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