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