WordPress 4.1.4-scripts
[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 3: // Locator (URL, filename, etc), UTF-8 encoded
142                                         $thisfile_ape_items_current['data'] = explode("\x00", trim($thisfile_ape_items_current['data']));
143                                         break;
144
145                                 default: // binary data
146                                         break;
147                         }
148
149                         switch (strtolower($item_key)) {
150                                 case 'replaygain_track_gain':
151                                         $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
152                                         $thisfile_replaygain['track']['originator'] = 'unspecified';
153                                         break;
154
155                                 case 'replaygain_track_peak':
156                                         $thisfile_replaygain['track']['peak']       = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
157                                         $thisfile_replaygain['track']['originator'] = 'unspecified';
158                                         if ($thisfile_replaygain['track']['peak'] <= 0) {
159                                                 $info['warning'][] = 'ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
160                                         }
161                                         break;
162
163                                 case 'replaygain_album_gain':
164                                         $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
165                                         $thisfile_replaygain['album']['originator'] = 'unspecified';
166                                         break;
167
168                                 case 'replaygain_album_peak':
169                                         $thisfile_replaygain['album']['peak']       = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero!
170                                         $thisfile_replaygain['album']['originator'] = 'unspecified';
171                                         if ($thisfile_replaygain['album']['peak'] <= 0) {
172                                                 $info['warning'][] = 'ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")';
173                                         }
174                                         break;
175
176                                 case 'mp3gain_undo':
177                                         list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]);
178                                         $thisfile_replaygain['mp3gain']['undo_left']  = intval($mp3gain_undo_left);
179                                         $thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right);
180                                         $thisfile_replaygain['mp3gain']['undo_wrap']  = (($mp3gain_undo_wrap == 'Y') ? true : false);
181                                         break;
182
183                                 case 'mp3gain_minmax':
184                                         list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]);
185                                         $thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min);
186                                         $thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max);
187                                         break;
188
189                                 case 'mp3gain_album_minmax':
190                                         list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]);
191                                         $thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min);
192                                         $thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max);
193                                         break;
194
195                                 case 'tracknumber':
196                                         if (is_array($thisfile_ape_items_current['data'])) {
197                                                 foreach ($thisfile_ape_items_current['data'] as $comment) {
198                                                         $thisfile_ape['comments']['track'][] = $comment;
199                                                 }
200                                         }
201                                         break;
202
203                                 case 'cover art (artist)':
204                                 case 'cover art (back)':
205                                 case 'cover art (band logo)':
206                                 case 'cover art (band)':
207                                 case 'cover art (colored fish)':
208                                 case 'cover art (composer)':
209                                 case 'cover art (conductor)':
210                                 case 'cover art (front)':
211                                 case 'cover art (icon)':
212                                 case 'cover art (illustration)':
213                                 case 'cover art (lead)':
214                                 case 'cover art (leaflet)':
215                                 case 'cover art (lyricist)':
216                                 case 'cover art (media)':
217                                 case 'cover art (movie scene)':
218                                 case 'cover art (other icon)':
219                                 case 'cover art (other)':
220                                 case 'cover art (performance)':
221                                 case 'cover art (publisher logo)':
222                                 case 'cover art (recording)':
223                                 case 'cover art (studio)':
224                                         // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html
225                                         list($thisfile_ape_items_current['filename'], $thisfile_ape_items_current['data']) = explode("\x00", $thisfile_ape_items_current['data'], 2);
226                                         $thisfile_ape_items_current['data_offset'] = $thisfile_ape_items_current['offset'] + strlen($thisfile_ape_items_current['filename']."\x00");
227                                         $thisfile_ape_items_current['data_length'] = strlen($thisfile_ape_items_current['data']);
228
229                                         $thisfile_ape_items_current['image_mime'] = '';
230                                         $imageinfo = array();
231                                         $imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo);
232                                         $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]);
233
234                                         do {
235                                                 if ($this->inline_attachments === false) {
236                                                         // skip entirely
237                                                         unset($thisfile_ape_items_current['data']);
238                                                         break;
239                                                 }
240                                                 if ($this->inline_attachments === true) {
241                                                         // great
242                                                 } elseif (is_int($this->inline_attachments)) {
243                                                         if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) {
244                                                                 // too big, skip
245                                                                 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' is too large to process inline ('.number_format($thisfile_ape_items_current['data_length']).' bytes)';
246                                                                 unset($thisfile_ape_items_current['data']);
247                                                                 break;
248                                                         }
249                                                 } elseif (is_string($this->inline_attachments)) {
250                                                         $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR);
251                                                         if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) {
252                                                                 // cannot write, skip
253                                                                 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)';
254                                                                 unset($thisfile_ape_items_current['data']);
255                                                                 break;
256                                                         }
257                                                 }
258                                                 // if we get this far, must be OK
259                                                 if (is_string($this->inline_attachments)) {
260                                                         $destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$thisfile_ape_items_current['data_offset'];
261                                                         if (!file_exists($destination_filename) || is_writable($destination_filename)) {
262                                                                 file_put_contents($destination_filename, $thisfile_ape_items_current['data']);
263                                                         } else {
264                                                                 $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)';
265                                                         }
266                                                         $thisfile_ape_items_current['data_filename'] = $destination_filename;
267                                                         unset($thisfile_ape_items_current['data']);
268                                                 } else {
269                                                         if (!isset($info['ape']['comments']['picture'])) {
270                                                                 $info['ape']['comments']['picture'] = array();
271                                                         }
272                                                         $info['ape']['comments']['picture'][] = array('data'=>$thisfile_ape_items_current['data'], 'image_mime'=>$thisfile_ape_items_current['image_mime']);
273                                                 }
274                                         } while (false);
275                                         break;
276
277                                 default:
278                                         if (is_array($thisfile_ape_items_current['data'])) {
279                                                 foreach ($thisfile_ape_items_current['data'] as $comment) {
280                                                         $thisfile_ape['comments'][strtolower($item_key)][] = $comment;
281                                                 }
282                                         }
283                                         break;
284                         }
285
286                 }
287                 if (empty($thisfile_replaygain)) {
288                         unset($info['replay_gain']);
289                 }
290                 return true;
291         }
292
293         public function parseAPEheaderFooter($APEheaderFooterData) {
294                 // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html
295
296                 // shortcut
297                 $headerfooterinfo['raw'] = array();
298                 $headerfooterinfo_raw = &$headerfooterinfo['raw'];
299
300                 $headerfooterinfo_raw['footer_tag']   =                  substr($APEheaderFooterData,  0, 8);
301                 if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') {
302                         return false;
303                 }
304                 $headerfooterinfo_raw['version']      = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData,  8, 4));
305                 $headerfooterinfo_raw['tagsize']      = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4));
306                 $headerfooterinfo_raw['tag_items']    = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4));
307                 $headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4));
308                 $headerfooterinfo_raw['reserved']     =                              substr($APEheaderFooterData, 24, 8);
309
310                 $headerfooterinfo['tag_version']         = $headerfooterinfo_raw['version'] / 1000;
311                 if ($headerfooterinfo['tag_version'] >= 2) {
312                         $headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']);
313                 }
314                 return $headerfooterinfo;
315         }
316
317         public function parseAPEtagFlags($rawflagint) {
318                 // "Note: APE Tags 1.0 do not use any of the APE Tag flags.
319                 // All are set to zero on creation and ignored on reading."
320                 // http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html
321                 $flags['header']            = (bool) ($rawflagint & 0x80000000);
322                 $flags['footer']            = (bool) ($rawflagint & 0x40000000);
323                 $flags['this_is_header']    = (bool) ($rawflagint & 0x20000000);
324                 $flags['item_contents_raw'] =        ($rawflagint & 0x00000006) >> 1;
325                 $flags['read_only']         = (bool) ($rawflagint & 0x00000001);
326
327                 $flags['item_contents']     = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']);
328
329                 return $flags;
330         }
331
332         public function APEcontentTypeFlagLookup($contenttypeid) {
333                 static $APEcontentTypeFlagLookup = array(
334                         0 => 'utf-8',
335                         1 => 'binary',
336                         2 => 'external',
337                         3 => 'reserved'
338                 );
339                 return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid');
340         }
341
342         public function APEtagItemIsUTF8Lookup($itemkey) {
343                 static $APEtagItemIsUTF8Lookup = array(
344                         'title',
345                         'subtitle',
346                         'artist',
347                         'album',
348                         'debut album',
349                         'publisher',
350                         'conductor',
351                         'track',
352                         'composer',
353                         'comment',
354                         'copyright',
355                         'publicationright',
356                         'file',
357                         'year',
358                         'record date',
359                         'record location',
360                         'genre',
361                         'media',
362                         'related',
363                         'isrc',
364                         'abstract',
365                         'language',
366                         'bibliography'
367                 );
368                 return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup);
369         }
370
371 }