PageRenderTime 53ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/htdocs/wp-includes/ID3/module.tag.apetag.php

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