WordPress 4.3
[autoinstalls/wordpress.git] / wp-includes / ID3 / module.tag.apetag.php
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 //          also https://github.com/JamesHeinrich/getID3       //
7 /////////////////////////////////////////////////////////////////
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
17 class getid3_apetag extends getid3_handler
18 {
19         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
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                         //if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) {
40                         if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') {
41
42                                 // APE tag found before ID3v1
43                                 $info['ape']['tag_offset_end'] = $info['filesize'] - $id3v1tagsize;
44
45                         //} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) {
46                         } elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') {
47
48                                 // APE tag found, no ID3v1
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                         // APE tag not found
64                         unset($info['ape']);
65                         return false;
66
67                 }
68
69                 // shortcut
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                 // shortcut
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                         // shortcut
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); // skip 0x00 terminator
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: // UTF-8
141                                 case 2: // Locator (URL, filename, etc), UTF-8 encoded
142                                         $thisfile_ape_items_current['data'] = explode("\x00", $thisfile_ape_items_current['data']);
143                                         break;
144
145                                 case 1:  // binary data
146                                 default:
147                                         break;
148                         }
149
150                         switch (strtolower($item_key)) {
151                                 // http://wiki.hydrogenaud.io/index.php?title=ReplayGain#MP3Gain
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]); // float casting will see "0,95" as zero!
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]); // float casting will see "0,95" as zero!
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]); // float casting will see "0,95" as zero!
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]); // float casting will see "0,95" as zero!
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                                         // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html
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                                         $thisfile_ape_items_current['image_mime'] = '';
264                                         $imageinfo = array();
265                                         $imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo);
266                                         $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]);
267
268                                         do {
269                                                 if ($this->inline_attachments === false) {
270                                                         // skip entirely
271                                                         unset($thisfile_ape_items_current['data']);
272                                                         break;
273                                                 }
274                                                 if ($this->inline_attachments === true) {
275                                                         // great
276                                                 } elseif (is_int($this->inline_attachments)) {
277                                                         if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) {
278                                                                 // too big, skip
279                                                                 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' is too large to process inline ('.number_format($thisfile_ape_items_current['data_length']).' bytes)';
280                                                                 unset($thisfile_ape_items_current['data']);
281                                                                 break;
282                                                         }
283                                                 } elseif (is_string($this->inline_attachments)) {
284                                                         $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR);
285                                                         if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) {
286                                                                 // cannot write, skip
287                                                                 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)';
288                                                                 unset($thisfile_ape_items_current['data']);
289                                                                 break;
290                                                         }
291                                                 }
292                                                 // if we get this far, must be OK
293                                                 if (is_string($this->inline_attachments)) {
294                                                         $destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$thisfile_ape_items_current['data_offset'];
295                                                         if (!file_exists($destination_filename) || is_writable($destination_filename)) {
296                                                                 file_put_contents($destination_filename, $thisfile_ape_items_current['data']);
297                                                         } else {
298                                                                 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)';
299                                                         }
300                                                         $thisfile_ape_items_current['data_filename'] = $destination_filename;
301                                                         unset($thisfile_ape_items_current['data']);
302                                                 } else {
303                                                         if (!isset($info['ape']['comments']['picture'])) {
304                                                                 $info['ape']['comments']['picture'] = array();
305                                                         }
306                                                         $comments_picture_data = array();
307                                                         foreach (array('data', 'image_mime', 'image_width', 'image_height', 'imagetype', 'picturetype', 'description', 'datalength') as $picture_key) {
308                                                                 if (isset($thisfile_ape_items_current[$picture_key])) {
309                                                                         $comments_picture_data[$picture_key] = $thisfile_ape_items_current[$picture_key];
310                                                                 }
311                                                         }
312                                                         $info['ape']['comments']['picture'][] = $comments_picture_data;
313                                                         unset($comments_picture_data);
314                                                 }
315                                         } while (false);
316                                         break;
317
318                                 default:
319                                         if (is_array($thisfile_ape_items_current['data'])) {
320                                                 foreach ($thisfile_ape_items_current['data'] as $comment) {
321                                                         $thisfile_ape['comments'][strtolower($item_key)][] = $comment;
322                                                 }
323                                         }
324                                         break;
325                         }
326
327                 }
328                 if (empty($thisfile_replaygain)) {
329                         unset($info['replay_gain']);
330                 }
331                 return true;
332         }
333
334         public function parseAPEheaderFooter($APEheaderFooterData) {
335                 // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html
336
337                 // shortcut
338                 $headerfooterinfo['raw'] = array();
339                 $headerfooterinfo_raw = &$headerfooterinfo['raw'];
340
341                 $headerfooterinfo_raw['footer_tag']   =                  substr($APEheaderFooterData,  0, 8);
342                 if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') {
343                         return false;
344                 }
345                 $headerfooterinfo_raw['version']      = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData,  8, 4));
346                 $headerfooterinfo_raw['tagsize']      = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4));
347                 $headerfooterinfo_raw['tag_items']    = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4));
348                 $headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4));
349                 $headerfooterinfo_raw['reserved']     =                              substr($APEheaderFooterData, 24, 8);
350
351                 $headerfooterinfo['tag_version']         = $headerfooterinfo_raw['version'] / 1000;
352                 if ($headerfooterinfo['tag_version'] >= 2) {
353                         $headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']);
354                 }
355                 return $headerfooterinfo;
356         }
357
358         public function parseAPEtagFlags($rawflagint) {
359                 // "Note: APE Tags 1.0 do not use any of the APE Tag flags.
360                 // All are set to zero on creation and ignored on reading."
361                 // http://wiki.hydrogenaud.io/index.php?title=Ape_Tags_Flags
362                 $flags['header']            = (bool) ($rawflagint & 0x80000000);
363                 $flags['footer']            = (bool) ($rawflagint & 0x40000000);
364                 $flags['this_is_header']    = (bool) ($rawflagint & 0x20000000);
365                 $flags['item_contents_raw'] =        ($rawflagint & 0x00000006) >> 1;
366                 $flags['read_only']         = (bool) ($rawflagint & 0x00000001);
367
368                 $flags['item_contents']     = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']);
369
370                 return $flags;
371         }
372
373         public function APEcontentTypeFlagLookup($contenttypeid) {
374                 static $APEcontentTypeFlagLookup = array(
375                         0 => 'utf-8',
376                         1 => 'binary',
377                         2 => 'external',
378                         3 => 'reserved'
379                 );
380                 return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid');
381         }
382
383         public function APEtagItemIsUTF8Lookup($itemkey) {
384                 static $APEtagItemIsUTF8Lookup = array(
385                         'title',
386                         'subtitle',
387                         'artist',
388                         'album',
389                         'debut album',
390                         'publisher',
391                         'conductor',
392                         'track',
393                         'composer',
394                         'comment',
395                         'copyright',
396                         'publicationright',
397                         'file',
398                         'year',
399                         'record date',
400                         'record location',
401                         'genre',
402                         'media',
403                         'related',
404                         'isrc',
405                         'abstract',
406                         'language',
407                         'bibliography'
408                 );
409                 return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup);
410         }
411
412 }