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