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