ILIAS  release_5-2 Revision v5.2.25-18-g3f80b828510
Id3v2.php
Go to the documentation of this file.
1 <?php
2 
3 namespace GetId3\Write;
4 
8 
11 // available at http://getid3.sourceforge.net //
12 // or http://www.getid3.org //
14 // See readme.txt for more details //
17 // write.id3v2.php //
18 // module for writing ID3v2 tags //
19 // dependencies: module.tag.id3v2.php //
20 // ///
22 
31 class Id3v2
32 {
33  public $filename;
34  public $tag_data;
39  public $fread_buffer_size = 32768; // read buffer size in bytes
44  public $paddedlength = 4096; // minimum length of ID3v2 tag in bytes
49  public $majorversion = 3; // ID3v2 major version (2, 3 (recommended), 4)
54  public $minorversion = 0; // ID3v2 minor version - always 0
59  public $merge_existing_data = false; // if true, merge new data with existing tags; if false, delete old tag data and only write new tags
64  public $id3v2_default_encodingid = 0; // default text encoding (ISO-8859-1) if not explicitly passed
69  public $id3v2_use_unsynchronisation = false; // the specs say it should be TRUE, but most other ID3v2-aware programs are broken if unsynchronization is used, so by default don't use it.
74  public $warnings = array(); // any non-critical errors will be stored here
79  public $errors = array(); // any critical errors will be stored here
80 
85  public function __construct()
86  {
87  return true;
88  }
89 
94  public function WriteID3v2()
95  {
96  // File MUST be writeable - CHMOD(646) at least. It's best if the
97  // directory is also writeable, because that method is both faster and less susceptible to errors.
98 
99  if (!empty($this->filename) && (is_writeable($this->filename) || (!file_exists($this->filename) && is_writeable(dirname($this->filename))))) {
100  // Initialize GetId3 engine
101  $getID3 = new GetId3Core();
102  $OldThisFileInfo = $getID3->analyze($this->filename);
103  if (!Helper::intValueSupported($OldThisFileInfo['filesize'])) {
104  $this->errors[] = 'Unable to write ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
105  fclose($fp_source);
106 
107  return false;
108  }
109  if ($this->merge_existing_data) {
110  // merge with existing data
111  if (!empty($OldThisFileInfo['id3v2'])) {
112  $this->tag_data = $this->array_join_merge($OldThisFileInfo['id3v2'], $this->tag_data);
113  }
114  }
115  $this->paddedlength = (isset($OldThisFileInfo['id3v2']['headerlength']) ? max($OldThisFileInfo['id3v2']['headerlength'], $this->paddedlength) : $this->paddedlength);
116 
117  if ($NewID3v2Tag = $this->GenerateID3v2Tag()) {
118 
119  if (file_exists($this->filename) && is_writeable($this->filename) && isset($OldThisFileInfo['id3v2']['headerlength']) && ($OldThisFileInfo['id3v2']['headerlength'] == strlen($NewID3v2Tag))) {
120 
121  // best and fastest method - insert-overwrite existing tag (padded to length of old tag if neccesary)
122  if (file_exists($this->filename)) {
123 
124  if (is_readable($this->filename) && is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'r+b'))) {
125  rewind($fp);
126  fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag));
127  fclose($fp);
128  } else {
129  $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")';
130  }
131 
132  } else {
133 
134  if (is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'wb'))) {
135  rewind($fp);
136  fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag));
137  fclose($fp);
138  } else {
139  $this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")';
140  }
141 
142  }
143 
144  } else {
145 
146  if ($tempfilename = tempnam(GetId3Core::getTempDir(), 'getID3')) {
147  if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) {
148  if (is_writable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) {
149 
150  fwrite($fp_temp, $NewID3v2Tag, strlen($NewID3v2Tag));
151 
152  rewind($fp_source);
153  if (!empty($OldThisFileInfo['avdataoffset'])) {
154  fseek($fp_source, $OldThisFileInfo['avdataoffset'], SEEK_SET);
155  }
156 
157  while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
158  fwrite($fp_temp, $buffer, strlen($buffer));
159  }
160 
161  fclose($fp_temp);
162  fclose($fp_source);
163  copy($tempfilename, $this->filename);
164  unlink($tempfilename);
165 
166  return true;
167 
168  } else {
169  $this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")';
170  }
171  fclose($fp_source);
172 
173  } else {
174  $this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")';
175  }
176  }
177 
178  return false;
179 
180  }
181 
182  } else {
183 
184  $this->errors[] = '$this->GenerateID3v2Tag() failed';
185 
186  }
187 
188  if (!empty($this->errors)) {
189  return false;
190  }
191 
192  return true;
193  } else {
194  $this->errors[] = 'WriteID3v2() failed: !is_writeable('.$this->filename.')';
195  }
196 
197  return false;
198  }
199 
200  public function RemoveID3v2()
201  {
202  // File MUST be writeable - CHMOD(646) at least. It's best if the
203  // directory is also writeable, because that method is both faster and less susceptible to errors.
204  if (is_writeable(dirname($this->filename))) {
205 
206  // preferred method - only one copying operation, minimal chance of corrupting
207  // original file if script is interrupted, but required directory to be writeable
208  if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) {
209 
210  // Initialize GetId3 engine
211  $getID3 = new GetId3Core();
212  $OldThisFileInfo = $getID3->analyze($this->filename);
213  if (!Helper::intValueSupported($OldThisFileInfo['filesize'])) {
214  $this->errors[] = 'Unable to remove ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
215  fclose($fp_source);
216 
217  return false;
218  }
219  rewind($fp_source);
220  if ($OldThisFileInfo['avdataoffset'] !== false) {
221  fseek($fp_source, $OldThisFileInfo['avdataoffset'], SEEK_SET);
222  }
223  if (is_writable($this->filename) && is_file($this->filename) && ($fp_temp = fopen($this->filename.'getid3tmp', 'w+b'))) {
224  while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
225  fwrite($fp_temp, $buffer, strlen($buffer));
226  }
227  fclose($fp_temp);
228  } else {
229  $this->errors[] = 'Could not fopen("'.$this->filename.'getid3tmp", "w+b")';
230  }
231  fclose($fp_source);
232  } else {
233  $this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")';
234  }
235  if (file_exists($this->filename)) {
236  unlink($this->filename);
237  }
238  rename($this->filename.'getid3tmp', $this->filename);
239 
240  } elseif (is_writable($this->filename)) {
241 
242  // less desirable alternate method - double-copies the file, overwrites original file
243  // and could corrupt source file if the script is interrupted or an error occurs.
244  if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) {
245 
246  // Initialize GetId3 engine
247  $getID3 = new GetId3Core();
248  $OldThisFileInfo = $getID3->analyze($this->filename);
249  if (!Helper::intValueSupported($OldThisFileInfo['filesize'])) {
250  $this->errors[] = 'Unable to remove ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
251  fclose($fp_source);
252 
253  return false;
254  }
255  rewind($fp_source);
256  if ($OldThisFileInfo['avdataoffset'] !== false) {
257  fseek($fp_source, $OldThisFileInfo['avdataoffset'], SEEK_SET);
258  }
259  if ($fp_temp = tmpfile()) {
260  while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
261  fwrite($fp_temp, $buffer, strlen($buffer));
262  }
263  fclose($fp_source);
264  if (is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'wb'))) {
265  rewind($fp_temp);
266  while ($buffer = fread($fp_temp, $this->fread_buffer_size)) {
267  fwrite($fp_source, $buffer, strlen($buffer));
268  }
269  fseek($fp_temp, -128, SEEK_END);
270  fclose($fp_source);
271  } else {
272  $this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")';
273  }
274  fclose($fp_temp);
275  } else {
276  $this->errors[] = 'Could not create tmpfile()';
277  }
278  } else {
279  $this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")';
280  }
281 
282  } else {
283 
284  $this->errors[] = 'Directory and file both not writeable';
285 
286  }
287 
288  if (!empty($this->errors)) {
289  return false;
290  }
291 
292  return true;
293  }
294 
300  public function GenerateID3v2TagFlags($flags)
301  {
302  switch ($this->majorversion) {
303  case 4:
304  // %abcd0000
305  $flag = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation
306  $flag .= (!empty($flags['extendedheader'] ) ? '1' : '0'); // b - Extended header
307  $flag .= (!empty($flags['experimental'] ) ? '1' : '0'); // c - Experimental indicator
308  $flag .= (!empty($flags['footer'] ) ? '1' : '0'); // d - Footer present
309  $flag .= '0000';
310  break;
311 
312  case 3:
313  // %abc00000
314  $flag = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation
315  $flag .= (!empty($flags['extendedheader'] ) ? '1' : '0'); // b - Extended header
316  $flag .= (!empty($flags['experimental'] ) ? '1' : '0'); // c - Experimental indicator
317  $flag .= '00000';
318  break;
319 
320  case 2:
321  // %ab000000
322  $flag = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation
323  $flag .= (!empty($flags['compression'] ) ? '1' : '0'); // b - Compression
324  $flag .= '000000';
325  break;
326 
327  default:
328  return false;
329  break;
330  }
331 
332  return chr(bindec($flag));
333  }
334 
347  public function GenerateID3v2FrameFlags($TagAlter=false, $FileAlter=false, $ReadOnly=false, $Compression=false, $Encryption=false, $GroupingIdentity=false, $Unsynchronisation=false, $DataLengthIndicator=false)
348  {
349  switch ($this->majorversion) {
350  case 4:
351  // %0abc0000 %0h00kmnp
352  $flag1 = '0';
353  $flag1 .= $TagAlter ? '1' : '0'; // a - Tag alter preservation (true == discard)
354  $flag1 .= $FileAlter ? '1' : '0'; // b - File alter preservation (true == discard)
355  $flag1 .= $ReadOnly ? '1' : '0'; // c - Read only (true == read only)
356  $flag1 .= '0000';
357 
358  $flag2 = '0';
359  $flag2 .= $GroupingIdentity ? '1' : '0'; // h - Grouping identity (true == contains group information)
360  $flag2 .= '00';
361  $flag2 .= $Compression ? '1' : '0'; // k - Compression (true == compressed)
362  $flag2 .= $Encryption ? '1' : '0'; // m - Encryption (true == encrypted)
363  $flag2 .= $Unsynchronisation ? '1' : '0'; // n - Unsynchronisation (true == unsynchronised)
364  $flag2 .= $DataLengthIndicator ? '1' : '0'; // p - Data length indicator (true == data length indicator added)
365  break;
366 
367  case 3:
368  // %abc00000 %ijk00000
369  $flag1 = $TagAlter ? '1' : '0'; // a - Tag alter preservation (true == discard)
370  $flag1 .= $FileAlter ? '1' : '0'; // b - File alter preservation (true == discard)
371  $flag1 .= $ReadOnly ? '1' : '0'; // c - Read only (true == read only)
372  $flag1 .= '00000';
373 
374  $flag2 = $Compression ? '1' : '0'; // i - Compression (true == compressed)
375  $flag2 .= $Encryption ? '1' : '0'; // j - Encryption (true == encrypted)
376  $flag2 .= $GroupingIdentity ? '1' : '0'; // k - Grouping identity (true == contains group information)
377  $flag2 .= '00000';
378  break;
379 
380  default:
381  return false;
382  break;
383 
384  }
385 
386  return chr(bindec($flag1)).chr(bindec($flag2));
387  }
388 
395  public function GenerateID3v2FrameData($frame_name, $source_data_array)
396  {
397  if (!Tag\Id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) {
398  return false;
399  }
400  $framedata = '';
401 
402  if (($this->majorversion < 3) || ($this->majorversion > 4)) {
403 
404  $this->errors[] = 'Only ID3v2.3 and ID3v2.4 are supported in GenerateID3v2FrameData()';
405 
406  } else { // $this->majorversion 3 or 4
407 
408  switch ($frame_name) {
409  case 'UFID':
410  // 4.1 UFID Unique file identifier
411  // Owner identifier <text string> $00
412  // Identifier <up to 64 bytes binary data>
413  if (strlen($source_data_array['data']) > 64) {
414  $this->errors[] = 'Identifier not allowed to be longer than 64 bytes in '.$frame_name.' (supplied data was '.strlen($source_data_array['data']).' bytes long)';
415  } else {
416  $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
417  $framedata .= substr($source_data_array['data'], 0, 64); // max 64 bytes - truncate anything longer
418  }
419  break;
420 
421  case 'TXXX':
422  // 4.2.2 TXXX User defined text information frame
423  // Text encoding $xx
424  // Description <text string according to encoding> $00 (00)
425  // Value <text string according to encoding>
426  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
427  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) {
428  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
429  } else {
430  $framedata .= chr($source_data_array['encodingid']);
431  $framedata .= $source_data_array['description'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
432  $framedata .= $source_data_array['data'];
433  }
434  break;
435 
436  case 'WXXX':
437  // 4.3.2 WXXX User defined URL link frame
438  // Text encoding $xx
439  // Description <text string according to encoding> $00 (00)
440  // URL <text string>
441  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
442  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) {
443  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
444  } elseif (!isset($source_data_array['data']) || !$this->IsValidURL($source_data_array['data'], false, false)) {
445  //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
446  // probably should be an error, need to rewrite IsValidURL() to handle other encodings
447  $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
448  } else {
449  $framedata .= chr($source_data_array['encodingid']);
450  $framedata .= $source_data_array['description'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
451  $framedata .= $source_data_array['data'];
452  }
453  break;
454 
455  case 'IPLS':
456  // 4.4 IPLS Involved people list (ID3v2.3 only)
457  // Text encoding $xx
458  // People list strings <textstrings>
459  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
460  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) {
461  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
462  } else {
463  $framedata .= chr($source_data_array['encodingid']);
464  $framedata .= $source_data_array['data'];
465  }
466  break;
467 
468  case 'MCDI':
469  // 4.4 MCDI Music CD identifier
470  // CD TOC <binary data>
471  $framedata .= $source_data_array['data'];
472  break;
473 
474  case 'ETCO':
475  // 4.5 ETCO Event timing codes
476  // Time stamp format $xx
477  // Where time stamp format is:
478  // $01 (32-bit value) MPEG frames from beginning of file
479  // $02 (32-bit value) milliseconds from beginning of file
480  // Followed by a list of key events in the following format:
481  // Type of event $xx
482  // Time stamp $xx (xx ...)
483  // The 'Time stamp' is set to zero if directly at the beginning of the sound
484  // or after the previous event. All events MUST be sorted in chronological order.
485  if (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) {
486  $this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')';
487  } else {
488  $framedata .= chr($source_data_array['timestampformat']);
489  foreach ($source_data_array as $key => $val) {
490  if (!$this->ID3v2IsValidETCOevent($val['typeid'])) {
491  $this->errors[] = 'Invalid Event Type byte in '.$frame_name.' ('.$val['typeid'].')';
492  } elseif (($key != 'timestampformat') && ($key != 'flags')) {
493  if (($val['timestamp'] > 0) && ($previousETCOtimestamp >= $val['timestamp'])) {
494  // The 'Time stamp' is set to zero if directly at the beginning of the sound
495  // or after the previous event. All events MUST be sorted in chronological order.
496  $this->errors[] = 'Out-of-order timestamp in '.$frame_name.' ('.$val['timestamp'].') for Event Type ('.$val['typeid'].')';
497  } else {
498  $framedata .= chr($val['typeid']);
499  $framedata .= Helper::BigEndian2String($val['timestamp'], 4, false);
500  }
501  }
502  }
503  }
504  break;
505 
506  case 'MLLT':
507  // 4.6 MLLT MPEG location lookup table
508  // MPEG frames between reference $xx xx
509  // Bytes between reference $xx xx xx
510  // Milliseconds between reference $xx xx xx
511  // Bits for bytes deviation $xx
512  // Bits for milliseconds dev. $xx
513  // Then for every reference the following data is included;
514  // Deviation in bytes %xxx....
515  // Deviation in milliseconds %xxx....
516  if (($source_data_array['framesbetweenreferences'] > 0) && ($source_data_array['framesbetweenreferences'] <= 65535)) {
517  $framedata .= Helper::BigEndian2String($source_data_array['framesbetweenreferences'], 2, false);
518  } else {
519  $this->errors[] = 'Invalid MPEG Frames Between References in '.$frame_name.' ('.$source_data_array['framesbetweenreferences'].')';
520  }
521  if (($source_data_array['bytesbetweenreferences'] > 0) && ($source_data_array['bytesbetweenreferences'] <= 16777215)) {
522  $framedata .= Helper::BigEndian2String($source_data_array['bytesbetweenreferences'], 3, false);
523  } else {
524  $this->errors[] = 'Invalid bytes Between References in '.$frame_name.' ('.$source_data_array['bytesbetweenreferences'].')';
525  }
526  if (($source_data_array['msbetweenreferences'] > 0) && ($source_data_array['msbetweenreferences'] <= 16777215)) {
527  $framedata .= Helper::BigEndian2String($source_data_array['msbetweenreferences'], 3, false);
528  } else {
529  $this->errors[] = 'Invalid Milliseconds Between References in '.$frame_name.' ('.$source_data_array['msbetweenreferences'].')';
530  }
531  if (!$this->IsWithinBitRange($source_data_array['bitsforbytesdeviation'], 8, false)) {
532  if (($source_data_array['bitsforbytesdeviation'] % 4) == 0) {
533  $framedata .= chr($source_data_array['bitsforbytesdeviation']);
534  } else {
535  $this->errors[] = 'Bits For Bytes Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].') must be a multiple of 4.';
536  }
537  } else {
538  $this->errors[] = 'Invalid Bits For Bytes Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].')';
539  }
540  if (!$this->IsWithinBitRange($source_data_array['bitsformsdeviation'], 8, false)) {
541  if (($source_data_array['bitsformsdeviation'] % 4) == 0) {
542  $framedata .= chr($source_data_array['bitsformsdeviation']);
543  } else {
544  $this->errors[] = 'Bits For Milliseconds Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].') must be a multiple of 4.';
545  }
546  } else {
547  $this->errors[] = 'Invalid Bits For Milliseconds Deviation in '.$frame_name.' ('.$source_data_array['bitsformsdeviation'].')';
548  }
549  foreach ($source_data_array as $key => $val) {
550  if (($key != 'framesbetweenreferences') && ($key != 'bytesbetweenreferences') && ($key != 'msbetweenreferences') && ($key != 'bitsforbytesdeviation') && ($key != 'bitsformsdeviation') && ($key != 'flags')) {
551  $unwrittenbitstream .= str_pad(Helper::Dec2Bin($val['bytedeviation']), $source_data_array['bitsforbytesdeviation'], '0', STR_PAD_LEFT);
552  $unwrittenbitstream .= str_pad(Helper::Dec2Bin($val['msdeviation']), $source_data_array['bitsformsdeviation'], '0', STR_PAD_LEFT);
553  }
554  }
555  for ($i = 0; $i < strlen($unwrittenbitstream); $i += 8) {
556  $highnibble = bindec(substr($unwrittenbitstream, $i, 4)) << 4;
557  $lownibble = bindec(substr($unwrittenbitstream, $i + 4, 4));
558  $framedata .= chr($highnibble & $lownibble);
559  }
560  break;
561 
562  case 'SYTC':
563  // 4.7 SYTC Synchronised tempo codes
564  // Time stamp format $xx
565  // Tempo data <binary data>
566  // Where time stamp format is:
567  // $01 (32-bit value) MPEG frames from beginning of file
568  // $02 (32-bit value) milliseconds from beginning of file
569  if (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) {
570  $this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')';
571  } else {
572  $framedata .= chr($source_data_array['timestampformat']);
573  foreach ($source_data_array as $key => $val) {
574  if (!$this->ID3v2IsValidETCOevent($val['typeid'])) {
575  $this->errors[] = 'Invalid Event Type byte in '.$frame_name.' ('.$val['typeid'].')';
576  } elseif (($key != 'timestampformat') && ($key != 'flags')) {
577  if (($val['tempo'] < 0) || ($val['tempo'] > 510)) {
578  $this->errors[] = 'Invalid Tempo (max = 510) in '.$frame_name.' ('.$val['tempo'].') at timestamp ('.$val['timestamp'].')';
579  } else {
580  if ($val['tempo'] > 255) {
581  $framedata .= chr(255);
582  $val['tempo'] -= 255;
583  }
584  $framedata .= chr($val['tempo']);
585  $framedata .= Helper::BigEndian2String($val['timestamp'], 4, false);
586  }
587  }
588  }
589  }
590  break;
591 
592  case 'USLT':
593  // 4.8 USLT Unsynchronised lyric/text transcription
594  // Text encoding $xx
595  // Language $xx xx xx
596  // Content descriptor <text string according to encoding> $00 (00)
597  // Lyrics/text <full text string according to encoding>
598  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
599  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
600  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
601  } elseif (Tag\Id3v2::LanguageLookup($source_data_array['language'], true) == '') {
602  $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')';
603  } else {
604  $framedata .= chr($source_data_array['encodingid']);
605  $framedata .= strtolower($source_data_array['language']);
606  $framedata .= $source_data_array['description'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
607  $framedata .= $source_data_array['data'];
608  }
609  break;
610 
611  case 'SYLT':
612  // 4.9 SYLT Synchronised lyric/text
613  // Text encoding $xx
614  // Language $xx xx xx
615  // Time stamp format $xx
616  // $01 (32-bit value) MPEG frames from beginning of file
617  // $02 (32-bit value) milliseconds from beginning of file
618  // Content type $xx
619  // Content descriptor <text string according to encoding> $00 (00)
620  // Terminated text to be synced (typically a syllable)
621  // Sync identifier (terminator to above string) $00 (00)
622  // Time stamp $xx (xx ...)
623  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
624  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
625  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
626  } elseif (Tag\Id3v2::LanguageLookup($source_data_array['language'], true) == '') {
627  $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')';
628  } elseif (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) {
629  $this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')';
630  } elseif (!$this->ID3v2IsValidSYLTtype($source_data_array['contenttypeid'])) {
631  $this->errors[] = 'Invalid Content Type byte in '.$frame_name.' ('.$source_data_array['contenttypeid'].')';
632  } elseif (!is_array($source_data_array['data'])) {
633  $this->errors[] = 'Invalid Lyric/Timestamp data in '.$frame_name.' (must be an array)';
634  } else {
635  $framedata .= chr($source_data_array['encodingid']);
636  $framedata .= strtolower($source_data_array['language']);
637  $framedata .= chr($source_data_array['timestampformat']);
638  $framedata .= chr($source_data_array['contenttypeid']);
639  $framedata .= $source_data_array['description'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
640  ksort($source_data_array['data']);
641  foreach ($source_data_array['data'] as $key => $val) {
642  $framedata .= $val['data'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
643  $framedata .= Helper::BigEndian2String($val['timestamp'], 4, false);
644  }
645  }
646  break;
647 
648  case 'COMM':
649  // 4.10 COMM Comments
650  // Text encoding $xx
651  // Language $xx xx xx
652  // Short content descrip. <text string according to encoding> $00 (00)
653  // The actual text <full text string according to encoding>
654  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
655  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
656  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
657  } elseif (Tag\Id3v2::LanguageLookup($source_data_array['language'], true) == '') {
658  $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')';
659  } else {
660  $framedata .= chr($source_data_array['encodingid']);
661  $framedata .= strtolower($source_data_array['language']);
662  $framedata .= $source_data_array['description'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
663  $framedata .= $source_data_array['data'];
664  }
665  break;
666 
667  case 'RVA2':
668  // 4.11 RVA2 Relative volume adjustment (2) (ID3v2.4+ only)
669  // Identification <text string> $00
670  // The 'identification' string is used to identify the situation and/or
671  // device where this adjustment should apply. The following is then
672  // repeated for every channel:
673  // Type of channel $xx
674  // Volume adjustment $xx xx
675  // Bits representing peak $xx
676  // Peak volume $xx (xx ...)
677  $framedata .= str_replace("\x00", '', $source_data_array['description'])."\x00";
678  foreach ($source_data_array as $key => $val) {
679  if ($key != 'description') {
680  $framedata .= chr($val['channeltypeid']);
681  $framedata .= Helper::BigEndian2String($val['volumeadjust'], 2, false, true); // signed 16-bit
682  if (!$this->IsWithinBitRange($source_data_array['bitspeakvolume'], 8, false)) {
683  $framedata .= chr($val['bitspeakvolume']);
684  if ($val['bitspeakvolume'] > 0) {
685  $framedata .= Helper::BigEndian2String($val['peakvolume'], ceil($val['bitspeakvolume'] / 8), false, false);
686  }
687  } else {
688  $this->errors[] = 'Invalid Bits Representing Peak Volume in '.$frame_name.' ('.$val['bitspeakvolume'].') (range = 0 to 255)';
689  }
690  }
691  }
692  break;
693 
694  case 'RVAD':
695  // 4.12 RVAD Relative volume adjustment (ID3v2.3 only)
696  // Increment/decrement %00fedcba
697  // Bits used for volume descr. $xx
698  // Relative volume change, right $xx xx (xx ...) // a
699  // Relative volume change, left $xx xx (xx ...) // b
700  // Peak volume right $xx xx (xx ...)
701  // Peak volume left $xx xx (xx ...)
702  // Relative volume change, right back $xx xx (xx ...) // c
703  // Relative volume change, left back $xx xx (xx ...) // d
704  // Peak volume right back $xx xx (xx ...)
705  // Peak volume left back $xx xx (xx ...)
706  // Relative volume change, center $xx xx (xx ...) // e
707  // Peak volume center $xx xx (xx ...)
708  // Relative volume change, bass $xx xx (xx ...) // f
709  // Peak volume bass $xx xx (xx ...)
710  if (!$this->IsWithinBitRange($source_data_array['bitsvolume'], 8, false)) {
711  $this->errors[] = 'Invalid Bits For Volume Description byte in '.$frame_name.' ('.$source_data_array['bitsvolume'].') (range = 1 to 255)';
712  } else {
713  $incdecflag .= '00';
714  $incdecflag .= $source_data_array['incdec']['right'] ? '1' : '0'; // a - Relative volume change, right
715  $incdecflag .= $source_data_array['incdec']['left'] ? '1' : '0'; // b - Relative volume change, left
716  $incdecflag .= $source_data_array['incdec']['rightrear'] ? '1' : '0'; // c - Relative volume change, right back
717  $incdecflag .= $source_data_array['incdec']['leftrear'] ? '1' : '0'; // d - Relative volume change, left back
718  $incdecflag .= $source_data_array['incdec']['center'] ? '1' : '0'; // e - Relative volume change, center
719  $incdecflag .= $source_data_array['incdec']['bass'] ? '1' : '0'; // f - Relative volume change, bass
720  $framedata .= chr(bindec($incdecflag));
721  $framedata .= chr($source_data_array['bitsvolume']);
722  $framedata .= Helper::BigEndian2String($source_data_array['volumechange']['right'], ceil($source_data_array['bitsvolume'] / 8), false);
723  $framedata .= Helper::BigEndian2String($source_data_array['volumechange']['left'], ceil($source_data_array['bitsvolume'] / 8), false);
724  $framedata .= Helper::BigEndian2String($source_data_array['peakvolume']['right'], ceil($source_data_array['bitsvolume'] / 8), false);
725  $framedata .= Helper::BigEndian2String($source_data_array['peakvolume']['left'], ceil($source_data_array['bitsvolume'] / 8), false);
726  if ($source_data_array['volumechange']['rightrear'] || $source_data_array['volumechange']['leftrear'] ||
727  $source_data_array['peakvolume']['rightrear'] || $source_data_array['peakvolume']['leftrear'] ||
728  $source_data_array['volumechange']['center'] || $source_data_array['peakvolume']['center'] ||
729  $source_data_array['volumechange']['bass'] || $source_data_array['peakvolume']['bass']) {
730  $framedata .= Helper::BigEndian2String($source_data_array['volumechange']['rightrear'], ceil($source_data_array['bitsvolume']/8), false);
731  $framedata .= Helper::BigEndian2String($source_data_array['volumechange']['leftrear'], ceil($source_data_array['bitsvolume']/8), false);
732  $framedata .= Helper::BigEndian2String($source_data_array['peakvolume']['rightrear'], ceil($source_data_array['bitsvolume']/8), false);
733  $framedata .= Helper::BigEndian2String($source_data_array['peakvolume']['leftrear'], ceil($source_data_array['bitsvolume']/8), false);
734  }
735  if ($source_data_array['volumechange']['center'] || $source_data_array['peakvolume']['center'] ||
736  $source_data_array['volumechange']['bass'] || $source_data_array['peakvolume']['bass']) {
737  $framedata .= Helper::BigEndian2String($source_data_array['volumechange']['center'], ceil($source_data_array['bitsvolume']/8), false);
738  $framedata .= Helper::BigEndian2String($source_data_array['peakvolume']['center'], ceil($source_data_array['bitsvolume']/8), false);
739  }
740  if ($source_data_array['volumechange']['bass'] || $source_data_array['peakvolume']['bass']) {
741  $framedata .= Helper::BigEndian2String($source_data_array['volumechange']['bass'], ceil($source_data_array['bitsvolume']/8), false);
742  $framedata .= Helper::BigEndian2String($source_data_array['peakvolume']['bass'], ceil($source_data_array['bitsvolume']/8), false);
743  }
744  }
745  break;
746 
747  case 'EQU2':
748  // 4.12 EQU2 Equalisation (2) (ID3v2.4+ only)
749  // Interpolation method $xx
750  // $00 Band
751  // $01 Linear
752  // Identification <text string> $00
753  // The following is then repeated for every adjustment point
754  // Frequency $xx xx
755  // Volume adjustment $xx xx
756  if (($source_data_array['interpolationmethod'] < 0) || ($source_data_array['interpolationmethod'] > 1)) {
757  $this->errors[] = 'Invalid Interpolation Method byte in '.$frame_name.' ('.$source_data_array['interpolationmethod'].') (valid = 0 or 1)';
758  } else {
759  $framedata .= chr($source_data_array['interpolationmethod']);
760  $framedata .= str_replace("\x00", '', $source_data_array['description'])."\x00";
761  foreach ($source_data_array['data'] as $key => $val) {
762  $framedata .= Helper::BigEndian2String(intval(round($key * 2)), 2, false);
763  $framedata .= Helper::BigEndian2String($val, 2, false, true); // signed 16-bit
764  }
765  }
766  break;
767 
768  case 'EQUA':
769  // 4.12 EQUA Equalisation (ID3v2.3 only)
770  // Adjustment bits $xx
771  // This is followed by 2 bytes + ('adjustment bits' rounded up to the
772  // nearest byte) for every equalisation band in the following format,
773  // giving a frequency range of 0 - 32767Hz:
774  // Increment/decrement %x (MSB of the Frequency)
775  // Frequency (lower 15 bits)
776  // Adjustment $xx (xx ...)
777  if (!$this->IsWithinBitRange($source_data_array['bitsvolume'], 8, false)) {
778  $this->errors[] = 'Invalid Adjustment Bits byte in '.$frame_name.' ('.$source_data_array['bitsvolume'].') (range = 1 to 255)';
779  } else {
780  $framedata .= chr($source_data_array['adjustmentbits']);
781  foreach ($source_data_array as $key => $val) {
782  if ($key != 'bitsvolume') {
783  if (($key > 32767) || ($key < 0)) {
784  $this->errors[] = 'Invalid Frequency in '.$frame_name.' ('.$key.') (range = 0 to 32767)';
785  } else {
786  if ($val >= 0) {
787  // put MSB of frequency to 1 if increment, 0 if decrement
788  $key |= 0x8000;
789  }
790  $framedata .= Helper::BigEndian2String($key, 2, false);
791  $framedata .= Helper::BigEndian2String($val, ceil($source_data_array['adjustmentbits'] / 8), false);
792  }
793  }
794  }
795  }
796  break;
797 
798  case 'RVRB':
799  // 4.13 RVRB Reverb
800  // Reverb left (ms) $xx xx
801  // Reverb right (ms) $xx xx
802  // Reverb bounces, left $xx
803  // Reverb bounces, right $xx
804  // Reverb feedback, left to left $xx
805  // Reverb feedback, left to right $xx
806  // Reverb feedback, right to right $xx
807  // Reverb feedback, right to left $xx
808  // Premix left to right $xx
809  // Premix right to left $xx
810  if (!$this->IsWithinBitRange($source_data_array['left'], 16, false)) {
811  $this->errors[] = 'Invalid Reverb Left in '.$frame_name.' ('.$source_data_array['left'].') (range = 0 to 65535)';
812  } elseif (!$this->IsWithinBitRange($source_data_array['right'], 16, false)) {
813  $this->errors[] = 'Invalid Reverb Left in '.$frame_name.' ('.$source_data_array['right'].') (range = 0 to 65535)';
814  } elseif (!$this->IsWithinBitRange($source_data_array['bouncesL'], 8, false)) {
815  $this->errors[] = 'Invalid Reverb Bounces, Left in '.$frame_name.' ('.$source_data_array['bouncesL'].') (range = 0 to 255)';
816  } elseif (!$this->IsWithinBitRange($source_data_array['bouncesR'], 8, false)) {
817  $this->errors[] = 'Invalid Reverb Bounces, Right in '.$frame_name.' ('.$source_data_array['bouncesR'].') (range = 0 to 255)';
818  } elseif (!$this->IsWithinBitRange($source_data_array['feedbackLL'], 8, false)) {
819  $this->errors[] = 'Invalid Reverb Feedback, Left-To-Left in '.$frame_name.' ('.$source_data_array['feedbackLL'].') (range = 0 to 255)';
820  } elseif (!$this->IsWithinBitRange($source_data_array['feedbackLR'], 8, false)) {
821  $this->errors[] = 'Invalid Reverb Feedback, Left-To-Right in '.$frame_name.' ('.$source_data_array['feedbackLR'].') (range = 0 to 255)';
822  } elseif (!$this->IsWithinBitRange($source_data_array['feedbackRR'], 8, false)) {
823  $this->errors[] = 'Invalid Reverb Feedback, Right-To-Right in '.$frame_name.' ('.$source_data_array['feedbackRR'].') (range = 0 to 255)';
824  } elseif (!$this->IsWithinBitRange($source_data_array['feedbackRL'], 8, false)) {
825  $this->errors[] = 'Invalid Reverb Feedback, Right-To-Left in '.$frame_name.' ('.$source_data_array['feedbackRL'].') (range = 0 to 255)';
826  } elseif (!$this->IsWithinBitRange($source_data_array['premixLR'], 8, false)) {
827  $this->errors[] = 'Invalid Premix, Left-To-Right in '.$frame_name.' ('.$source_data_array['premixLR'].') (range = 0 to 255)';
828  } elseif (!$this->IsWithinBitRange($source_data_array['premixRL'], 8, false)) {
829  $this->errors[] = 'Invalid Premix, Right-To-Left in '.$frame_name.' ('.$source_data_array['premixRL'].') (range = 0 to 255)';
830  } else {
831  $framedata .= Helper::BigEndian2String($source_data_array['left'], 2, false);
832  $framedata .= Helper::BigEndian2String($source_data_array['right'], 2, false);
833  $framedata .= chr($source_data_array['bouncesL']);
834  $framedata .= chr($source_data_array['bouncesR']);
835  $framedata .= chr($source_data_array['feedbackLL']);
836  $framedata .= chr($source_data_array['feedbackLR']);
837  $framedata .= chr($source_data_array['feedbackRR']);
838  $framedata .= chr($source_data_array['feedbackRL']);
839  $framedata .= chr($source_data_array['premixLR']);
840  $framedata .= chr($source_data_array['premixRL']);
841  }
842  break;
843 
844  case 'APIC':
845  // 4.14 APIC Attached picture
846  // Text encoding $xx
847  // MIME type <text string> $00
848  // Picture type $xx
849  // Description <text string according to encoding> $00 (00)
850  // Picture data <binary data>
851  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
852  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
853  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
854  } elseif (!$this->ID3v2IsValidAPICpicturetype($source_data_array['picturetypeid'])) {
855  $this->errors[] = 'Invalid Picture Type byte in '.$frame_name.' ('.$source_data_array['picturetypeid'].') for ID3v2.'.$this->majorversion;
856  } elseif (($this->majorversion >= 3) && (!$this->ID3v2IsValidAPICimageformat($source_data_array['mime']))) {
857  $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].') for ID3v2.'.$this->majorversion;
858  } elseif (($source_data_array['mime'] == '-->') && (!$this->IsValidURL($source_data_array['data'], false, false))) {
859  //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
860  // probably should be an error, need to rewrite IsValidURL() to handle other encodings
861  $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
862  } else {
863  $framedata .= chr($source_data_array['encodingid']);
864  $framedata .= str_replace("\x00", '', $source_data_array['mime'])."\x00";
865  $framedata .= chr($source_data_array['picturetypeid']);
866  $framedata .= (!empty($source_data_array['description']) ? $source_data_array['description'] : '').Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
867  $framedata .= $source_data_array['data'];
868  }
869  break;
870 
871  case 'GEOB':
872  // 4.15 GEOB General encapsulated object
873  // Text encoding $xx
874  // MIME type <text string> $00
875  // Filename <text string according to encoding> $00 (00)
876  // Content description <text string according to encoding> $00 (00)
877  // Encapsulated object <binary data>
878  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
879  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
880  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
881  } elseif (!$this->IsValidMIMEstring($source_data_array['mime'])) {
882  $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].')';
883  } elseif (!$source_data_array['description']) {
884  $this->errors[] = 'Missing Description in '.$frame_name;
885  } else {
886  $framedata .= chr($source_data_array['encodingid']);
887  $framedata .= str_replace("\x00", '', $source_data_array['mime'])."\x00";
888  $framedata .= $source_data_array['filename'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
889  $framedata .= $source_data_array['description'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
890  $framedata .= $source_data_array['data'];
891  }
892  break;
893 
894  case 'PCNT':
895  // 4.16 PCNT Play counter
896  // When the counter reaches all one's, one byte is inserted in
897  // front of the counter thus making the counter eight bits bigger
898  // Counter $xx xx xx xx (xx ...)
899  $framedata .= Helper::BigEndian2String($source_data_array['data'], 4, false);
900  break;
901 
902  case 'POPM':
903  // 4.17 POPM Popularimeter
904  // When the counter reaches all one's, one byte is inserted in
905  // front of the counter thus making the counter eight bits bigger
906  // Email to user <text string> $00
907  // Rating $xx
908  // Counter $xx xx xx xx (xx ...)
909  if (!$this->IsWithinBitRange($source_data_array['rating'], 8, false)) {
910  $this->errors[] = 'Invalid Rating byte in '.$frame_name.' ('.$source_data_array['rating'].') (range = 0 to 255)';
911  } elseif (!IsValidEmail($source_data_array['email'])) {
912  $this->errors[] = 'Invalid Email in '.$frame_name.' ('.$source_data_array['email'].')';
913  } else {
914  $framedata .= str_replace("\x00", '', $source_data_array['email'])."\x00";
915  $framedata .= chr($source_data_array['rating']);
916  $framedata .= Helper::BigEndian2String($source_data_array['data'], 4, false);
917  }
918  break;
919 
920  case 'RBUF':
921  // 4.18 RBUF Recommended buffer size
922  // Buffer size $xx xx xx
923  // Embedded info flag %0000000x
924  // Offset to next tag $xx xx xx xx
925  if (!$this->IsWithinBitRange($source_data_array['buffersize'], 24, false)) {
926  $this->errors[] = 'Invalid Buffer Size in '.$frame_name;
927  } elseif (!$this->IsWithinBitRange($source_data_array['nexttagoffset'], 32, false)) {
928  $this->errors[] = 'Invalid Offset To Next Tag in '.$frame_name;
929  } else {
930  $framedata .= Helper::BigEndian2String($source_data_array['buffersize'], 3, false);
931  $flag .= '0000000';
932  $flag .= $source_data_array['flags']['embededinfo'] ? '1' : '0';
933  $framedata .= chr(bindec($flag));
934  $framedata .= Helper::BigEndian2String($source_data_array['nexttagoffset'], 4, false);
935  }
936  break;
937 
938  case 'AENC':
939  // 4.19 AENC Audio encryption
940  // Owner identifier <text string> $00
941  // Preview start $xx xx
942  // Preview length $xx xx
943  // Encryption info <binary data>
944  if (!$this->IsWithinBitRange($source_data_array['previewstart'], 16, false)) {
945  $this->errors[] = 'Invalid Preview Start in '.$frame_name.' ('.$source_data_array['previewstart'].')';
946  } elseif (!$this->IsWithinBitRange($source_data_array['previewlength'], 16, false)) {
947  $this->errors[] = 'Invalid Preview Length in '.$frame_name.' ('.$source_data_array['previewlength'].')';
948  } else {
949  $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
950  $framedata .= Helper::BigEndian2String($source_data_array['previewstart'], 2, false);
951  $framedata .= Helper::BigEndian2String($source_data_array['previewlength'], 2, false);
952  $framedata .= $source_data_array['encryptioninfo'];
953  }
954  break;
955 
956  case 'LINK':
957  // 4.20 LINK Linked information
958  // Frame identifier $xx xx xx xx
959  // URL <text string> $00
960  // ID and additional data <text string(s)>
961  if (!Tag\Id3v2::IsValidID3v2FrameName($source_data_array['frameid'], $this->majorversion)) {
962  $this->errors[] = 'Invalid Frame Identifier in '.$frame_name.' ('.$source_data_array['frameid'].')';
963  } elseif (!$this->IsValidURL($source_data_array['data'], true, false)) {
964  //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
965  // probably should be an error, need to rewrite IsValidURL() to handle other encodings
966  $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
967  } elseif ((($source_data_array['frameid'] == 'AENC') || ($source_data_array['frameid'] == 'APIC') || ($source_data_array['frameid'] == 'GEOB') || ($source_data_array['frameid'] == 'TXXX')) && ($source_data_array['additionaldata'] == '')) {
968  $this->errors[] = 'Content Descriptor must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
969  } elseif (($source_data_array['frameid'] == 'USER') && (Tag\Id3v2::LanguageLookup($source_data_array['additionaldata'], true) == '')) {
970  $this->errors[] = 'Language must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
971  } elseif (($source_data_array['frameid'] == 'PRIV') && ($source_data_array['additionaldata'] == '')) {
972  $this->errors[] = 'Owner Identifier must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
973  } elseif ((($source_data_array['frameid'] == 'COMM') || ($source_data_array['frameid'] == 'SYLT') || ($source_data_array['frameid'] == 'USLT')) && ((Tag\Id3v2::LanguageLookup(substr($source_data_array['additionaldata'], 0, 3), true) == '') || (substr($source_data_array['additionaldata'], 3) == ''))) {
974  $this->errors[] = 'Language followed by Content Descriptor must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
975  } else {
976  $framedata .= $source_data_array['frameid'];
977  $framedata .= str_replace("\x00", '', $source_data_array['data'])."\x00";
978  switch ($source_data_array['frameid']) {
979  case 'COMM':
980  case 'SYLT':
981  case 'USLT':
982  case 'PRIV':
983  case 'USER':
984  case 'AENC':
985  case 'APIC':
986  case 'GEOB':
987  case 'TXXX':
988  $framedata .= $source_data_array['additionaldata'];
989  break;
990  case 'ASPI':
991  case 'ETCO':
992  case 'EQU2':
993  case 'MCID':
994  case 'MLLT':
995  case 'OWNE':
996  case 'RVA2':
997  case 'RVRB':
998  case 'SYTC':
999  case 'IPLS':
1000  case 'RVAD':
1001  case 'EQUA':
1002  // no additional data required
1003  break;
1004  case 'RBUF':
1005  if ($this->majorversion == 3) {
1006  // no additional data required
1007  } else {
1008  $this->errors[] = $source_data_array['frameid'].' is not a valid Frame Identifier in '.$frame_name.' (in ID3v2.'.$this->majorversion.')';
1009  }
1010 
1011  default:
1012  if ((substr($source_data_array['frameid'], 0, 1) == 'T') || (substr($source_data_array['frameid'], 0, 1) == 'W')) {
1013  // no additional data required
1014  } else {
1015  $this->errors[] = $source_data_array['frameid'].' is not a valid Frame Identifier in '.$frame_name.' (in ID3v2.'.$this->majorversion.')';
1016  }
1017  break;
1018  }
1019  }
1020  break;
1021 
1022  case 'POSS':
1023  // 4.21 POSS Position synchronisation frame (ID3v2.3+ only)
1024  // Time stamp format $xx
1025  // Position $xx (xx ...)
1026  if (($source_data_array['timestampformat'] < 1) || ($source_data_array['timestampformat'] > 2)) {
1027  $this->errors[] = 'Invalid Time Stamp Format in '.$frame_name.' ('.$source_data_array['timestampformat'].') (valid = 1 or 2)';
1028  } elseif (!$this->IsWithinBitRange($source_data_array['position'], 32, false)) {
1029  $this->errors[] = 'Invalid Position in '.$frame_name.' ('.$source_data_array['position'].') (range = 0 to 4294967295)';
1030  } else {
1031  $framedata .= chr($source_data_array['timestampformat']);
1032  $framedata .= Helper::BigEndian2String($source_data_array['position'], 4, false);
1033  }
1034  break;
1035 
1036  case 'USER':
1037  // 4.22 USER Terms of use (ID3v2.3+ only)
1038  // Text encoding $xx
1039  // Language $xx xx xx
1040  // The actual text <text string according to encoding>
1041  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
1042  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
1043  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')';
1044  } elseif (Tag\Id3v2::LanguageLookup($source_data_array['language'], true) == '') {
1045  $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')';
1046  } else {
1047  $framedata .= chr($source_data_array['encodingid']);
1048  $framedata .= strtolower($source_data_array['language']);
1049  $framedata .= $source_data_array['data'];
1050  }
1051  break;
1052 
1053  case 'OWNE':
1054  // 4.23 OWNE Ownership frame (ID3v2.3+ only)
1055  // Text encoding $xx
1056  // Price paid <text string> $00
1057  // Date of purch. <text string>
1058  // Seller <text string according to encoding>
1059  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
1060  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
1061  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')';
1062  } elseif (!$this->IsANumber($source_data_array['pricepaid']['value'], false)) {
1063  $this->errors[] = 'Invalid Price Paid in '.$frame_name.' ('.$source_data_array['pricepaid']['value'].')';
1064  } elseif (!$this->IsValidDateStampString($source_data_array['purchasedate'])) {
1065  $this->errors[] = 'Invalid Date Of Purchase in '.$frame_name.' ('.$source_data_array['purchasedate'].') (format = YYYYMMDD)';
1066  } else {
1067  $framedata .= chr($source_data_array['encodingid']);
1068  $framedata .= str_replace("\x00", '', $source_data_array['pricepaid']['value'])."\x00";
1069  $framedata .= $source_data_array['purchasedate'];
1070  $framedata .= $source_data_array['seller'];
1071  }
1072  break;
1073 
1074  case 'COMR':
1075  // 4.24 COMR Commercial frame (ID3v2.3+ only)
1076  // Text encoding $xx
1077  // Price string <text string> $00
1078  // Valid until <text string>
1079  // Contact URL <text string> $00
1080  // Received as $xx
1081  // Name of seller <text string according to encoding> $00 (00)
1082  // Description <text string according to encoding> $00 (00)
1083  // Picture MIME type <string> $00
1084  // Seller logo <binary data>
1085  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
1086  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
1087  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')';
1088  } elseif (!$this->IsValidDateStampString($source_data_array['pricevaliduntil'])) {
1089  $this->errors[] = 'Invalid Valid Until date in '.$frame_name.' ('.$source_data_array['pricevaliduntil'].') (format = YYYYMMDD)';
1090  } elseif (!$this->IsValidURL($source_data_array['contacturl'], false, true)) {
1091  $this->errors[] = 'Invalid Contact URL in '.$frame_name.' ('.$source_data_array['contacturl'].') (allowed schemes: http, https, ftp, mailto)';
1092  } elseif (!$this->ID3v2IsValidCOMRreceivedAs($source_data_array['receivedasid'])) {
1093  $this->errors[] = 'Invalid Received As byte in '.$frame_name.' ('.$source_data_array['contacturl'].') (range = 0 to 8)';
1094  } elseif (!$this->IsValidMIMEstring($source_data_array['mime'])) {
1095  $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].')';
1096  } else {
1097  $framedata .= chr($source_data_array['encodingid']);
1098  unset($pricestring);
1099  foreach ($source_data_array['price'] as $key => $val) {
1100  if ($this->ID3v2IsValidPriceString($key.$val['value'])) {
1101  $pricestrings[] = $key.$val['value'];
1102  } else {
1103  $this->errors[] = 'Invalid Price String in '.$frame_name.' ('.$key.$val['value'].')';
1104  }
1105  }
1106  $framedata .= implode('/', $pricestrings);
1107  $framedata .= $source_data_array['pricevaliduntil'];
1108  $framedata .= str_replace("\x00", '', $source_data_array['contacturl'])."\x00";
1109  $framedata .= chr($source_data_array['receivedasid']);
1110  $framedata .= $source_data_array['sellername'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
1111  $framedata .= $source_data_array['description'].Tag\Id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
1112  $framedata .= $source_data_array['mime']."\x00";
1113  $framedata .= $source_data_array['logo'];
1114  }
1115  break;
1116 
1117  case 'ENCR':
1118  // 4.25 ENCR Encryption method registration (ID3v2.3+ only)
1119  // Owner identifier <text string> $00
1120  // Method symbol $xx
1121  // Encryption data <binary data>
1122  if (!$this->IsWithinBitRange($source_data_array['methodsymbol'], 8, false)) {
1123  $this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['methodsymbol'].') (range = 0 to 255)';
1124  } else {
1125  $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
1126  $framedata .= ord($source_data_array['methodsymbol']);
1127  $framedata .= $source_data_array['data'];
1128  }
1129  break;
1130 
1131  case 'GRID':
1132  // 4.26 GRID Group identification registration (ID3v2.3+ only)
1133  // Owner identifier <text string> $00
1134  // Group symbol $xx
1135  // Group dependent data <binary data>
1136  if (!$this->IsWithinBitRange($source_data_array['groupsymbol'], 8, false)) {
1137  $this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['groupsymbol'].') (range = 0 to 255)';
1138  } else {
1139  $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
1140  $framedata .= ord($source_data_array['groupsymbol']);
1141  $framedata .= $source_data_array['data'];
1142  }
1143  break;
1144 
1145  case 'PRIV':
1146  // 4.27 PRIV Private frame (ID3v2.3+ only)
1147  // Owner identifier <text string> $00
1148  // The private data <binary data>
1149  $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
1150  $framedata .= $source_data_array['data'];
1151  break;
1152 
1153  case 'SIGN':
1154  // 4.28 SIGN Signature frame (ID3v2.4+ only)
1155  // Group symbol $xx
1156  // Signature <binary data>
1157  if (!$this->IsWithinBitRange($source_data_array['groupsymbol'], 8, false)) {
1158  $this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['groupsymbol'].') (range = 0 to 255)';
1159  } else {
1160  $framedata .= ord($source_data_array['groupsymbol']);
1161  $framedata .= $source_data_array['data'];
1162  }
1163  break;
1164 
1165  case 'SEEK':
1166  // 4.29 SEEK Seek frame (ID3v2.4+ only)
1167  // Minimum offset to next tag $xx xx xx xx
1168  if (!$this->IsWithinBitRange($source_data_array['data'], 32, false)) {
1169  $this->errors[] = 'Invalid Minimum Offset in '.$frame_name.' ('.$source_data_array['data'].') (range = 0 to 4294967295)';
1170  } else {
1171  $framedata .= Helper::BigEndian2String($source_data_array['data'], 4, false);
1172  }
1173  break;
1174 
1175  case 'ASPI':
1176  // 4.30 ASPI Audio seek point index (ID3v2.4+ only)
1177  // Indexed data start (S) $xx xx xx xx
1178  // Indexed data length (L) $xx xx xx xx
1179  // Number of index points (N) $xx xx
1180  // Bits per index point (b) $xx
1181  // Then for every index point the following data is included:
1182  // Fraction at index (Fi) $xx (xx)
1183  if (!$this->IsWithinBitRange($source_data_array['datastart'], 32, false)) {
1184  $this->errors[] = 'Invalid Indexed Data Start in '.$frame_name.' ('.$source_data_array['datastart'].') (range = 0 to 4294967295)';
1185  } elseif (!$this->IsWithinBitRange($source_data_array['datalength'], 32, false)) {
1186  $this->errors[] = 'Invalid Indexed Data Length in '.$frame_name.' ('.$source_data_array['datalength'].') (range = 0 to 4294967295)';
1187  } elseif (!$this->IsWithinBitRange($source_data_array['indexpoints'], 16, false)) {
1188  $this->errors[] = 'Invalid Number Of Index Points in '.$frame_name.' ('.$source_data_array['indexpoints'].') (range = 0 to 65535)';
1189  } elseif (!$this->IsWithinBitRange($source_data_array['bitsperpoint'], 8, false)) {
1190  $this->errors[] = 'Invalid Bits Per Index Point in '.$frame_name.' ('.$source_data_array['bitsperpoint'].') (range = 0 to 255)';
1191  } elseif ($source_data_array['indexpoints'] != count($source_data_array['indexes'])) {
1192  $this->errors[] = 'Number Of Index Points does not match actual supplied data in '.$frame_name;
1193  } else {
1194  $framedata .= Helper::BigEndian2String($source_data_array['datastart'], 4, false);
1195  $framedata .= Helper::BigEndian2String($source_data_array['datalength'], 4, false);
1196  $framedata .= Helper::BigEndian2String($source_data_array['indexpoints'], 2, false);
1197  $framedata .= Helper::BigEndian2String($source_data_array['bitsperpoint'], 1, false);
1198  foreach ($source_data_array['indexes'] as $key => $val) {
1199  $framedata .= Helper::BigEndian2String($val, ceil($source_data_array['bitsperpoint'] / 8), false);
1200  }
1201  }
1202  break;
1203 
1204  case 'RGAD':
1205  // RGAD Replay Gain Adjustment
1206  // http://privatewww.essex.ac.uk/~djmrob/replaygain/
1207  // Peak Amplitude $xx $xx $xx $xx
1208  // Radio Replay Gain Adjustment %aaabbbcd %dddddddd
1209  // Audiophile Replay Gain Adjustment %aaabbbcd %dddddddd
1210  // a - name code
1211  // b - originator code
1212  // c - sign bit
1213  // d - replay gain adjustment
1214 
1215  if (($source_data_array['track_adjustment'] > 51) || ($source_data_array['track_adjustment'] < -51)) {
1216  $this->errors[] = 'Invalid Track Adjustment in '.$frame_name.' ('.$source_data_array['track_adjustment'].') (range = -51.0 to +51.0)';
1217  } elseif (($source_data_array['album_adjustment'] > 51) || ($source_data_array['album_adjustment'] < -51)) {
1218  $this->errors[] = 'Invalid Album Adjustment in '.$frame_name.' ('.$source_data_array['album_adjustment'].') (range = -51.0 to +51.0)';
1219  } elseif (!$this->ID3v2IsValidRGADname($source_data_array['raw']['track_name'])) {
1220  $this->errors[] = 'Invalid Track Name Code in '.$frame_name.' ('.$source_data_array['raw']['track_name'].') (range = 0 to 2)';
1221  } elseif (!$this->ID3v2IsValidRGADname($source_data_array['raw']['album_name'])) {
1222  $this->errors[] = 'Invalid Album Name Code in '.$frame_name.' ('.$source_data_array['raw']['album_name'].') (range = 0 to 2)';
1223  } elseif (!$this->ID3v2IsValidRGADoriginator($source_data_array['raw']['track_originator'])) {
1224  $this->errors[] = 'Invalid Track Originator Code in '.$frame_name.' ('.$source_data_array['raw']['track_originator'].') (range = 0 to 3)';
1225  } elseif (!$this->ID3v2IsValidRGADoriginator($source_data_array['raw']['album_originator'])) {
1226  $this->errors[] = 'Invalid Album Originator Code in '.$frame_name.' ('.$source_data_array['raw']['album_originator'].') (range = 0 to 3)';
1227  } else {
1228  $framedata .= Helper::Float2String($source_data_array['peakamplitude'], 32);
1229  $framedata .= Helper::RGADgainString($source_data_array['raw']['track_name'], $source_data_array['raw']['track_originator'], $source_data_array['track_adjustment']);
1230  $framedata .= Helper::RGADgainString($source_data_array['raw']['album_name'], $source_data_array['raw']['album_originator'], $source_data_array['album_adjustment']);
1231  }
1232  break;
1233 
1234  default:
1235  if ((($this->majorversion == 2) && (strlen($frame_name) != 3)) || (($this->majorversion > 2) && (strlen($frame_name) != 4))) {
1236  $this->errors[] = 'Invalid frame name "'.$frame_name.'" for ID3v2.'.$this->majorversion;
1237  } elseif ($frame_name{0} == 'T') {
1238  // 4.2. T??? Text information frames
1239  // Text encoding $xx
1240  // Information <text string(s) according to encoding>
1241  $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
1242  if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
1243  $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
1244  } else {
1245  $framedata .= chr($source_data_array['encodingid']);
1246  $framedata .= $source_data_array['data'];
1247  }
1248  } elseif ($frame_name{0} == 'W') {
1249  // 4.3. W??? URL link frames
1250  // URL <text string>
1251  if (!$this->IsValidURL($source_data_array['data'], false, false)) {
1252  //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
1253  // probably should be an error, need to rewrite IsValidURL() to handle other encodings
1254  $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
1255  } else {
1256  $framedata .= $source_data_array['data'];
1257  }
1258  } else {
1259  $this->errors[] = $frame_name.' not yet supported in $this->GenerateID3v2FrameData()';
1260  }
1261  break;
1262  }
1263  }
1264  if (!empty($this->errors)) {
1265  return false;
1266  }
1267 
1268  return $framedata;
1269  }
1270 
1271  public function ID3v2FrameIsAllowed($frame_name, $source_data_array)
1272  {
1273  static $PreviousFrames = array();
1274 
1275  if ($frame_name === null) {
1276  // if the writing functions are called multiple times, the static array needs to be
1277  // cleared - this can be done by calling $this->ID3v2FrameIsAllowed(null, '')
1278  $PreviousFrames = array();
1279 
1280  return true;
1281  }
1282 
1283  if ($this->majorversion == 4) {
1284  switch ($frame_name) {
1285  case 'UFID':
1286  case 'AENC':
1287  case 'ENCR':
1288  case 'GRID':
1289  if (!isset($source_data_array['ownerid'])) {
1290  $this->errors[] = '[ownerid] not specified for '.$frame_name;
1291  } elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) {
1292  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')';
1293  } else {
1294  $PreviousFrames[] = $frame_name.$source_data_array['ownerid'];
1295  }
1296  break;
1297 
1298  case 'TXXX':
1299  case 'WXXX':
1300  case 'RVA2':
1301  case 'EQU2':
1302  case 'APIC':
1303  case 'GEOB':
1304  if (!isset($source_data_array['description'])) {
1305  $this->errors[] = '[description] not specified for '.$frame_name;
1306  } elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) {
1307  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')';
1308  } else {
1309  $PreviousFrames[] = $frame_name.$source_data_array['description'];
1310  }
1311  break;
1312 
1313  case 'USER':
1314  if (!isset($source_data_array['language'])) {
1315  $this->errors[] = '[language] not specified for '.$frame_name;
1316  } elseif (in_array($frame_name.$source_data_array['language'], $PreviousFrames)) {
1317  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language ('.$source_data_array['language'].')';
1318  } else {
1319  $PreviousFrames[] = $frame_name.$source_data_array['language'];
1320  }
1321  break;
1322 
1323  case 'USLT':
1324  case 'SYLT':
1325  case 'COMM':
1326  if (!isset($source_data_array['language'])) {
1327  $this->errors[] = '[language] not specified for '.$frame_name;
1328  } elseif (!isset($source_data_array['description'])) {
1329  $this->errors[] = '[description] not specified for '.$frame_name;
1330  } elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) {
1331  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')';
1332  } else {
1333  $PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description'];
1334  }
1335  break;
1336 
1337  case 'POPM':
1338  if (!isset($source_data_array['email'])) {
1339  $this->errors[] = '[email] not specified for '.$frame_name;
1340  } elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) {
1341  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')';
1342  } else {
1343  $PreviousFrames[] = $frame_name.$source_data_array['email'];
1344  }
1345  break;
1346 
1347  case 'IPLS':
1348  case 'MCDI':
1349  case 'ETCO':
1350  case 'MLLT':
1351  case 'SYTC':
1352  case 'RVRB':
1353  case 'PCNT':
1354  case 'RBUF':
1355  case 'POSS':
1356  case 'OWNE':
1357  case 'SEEK':
1358  case 'ASPI':
1359  case 'RGAD':
1360  if (in_array($frame_name, $PreviousFrames)) {
1361  $this->errors[] = 'Only one '.$frame_name.' tag allowed';
1362  } else {
1363  $PreviousFrames[] = $frame_name;
1364  }
1365  break;
1366 
1367  case 'LINK':
1368  // this isn't implemented quite right (yet) - it should check the target frame data for compliance
1369  // but right now it just allows one linked frame of each type, to be safe.
1370  if (!isset($source_data_array['frameid'])) {
1371  $this->errors[] = '[frameid] not specified for '.$frame_name;
1372  } elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) {
1373  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')';
1374  } elseif (in_array($source_data_array['frameid'], $PreviousFrames)) {
1375  // no links to singleton tags
1376  $this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')';
1377  } else {
1378  $PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type
1379  $PreviousFrames[] = $source_data_array['frameid']; // no non-linked singleton tags of this type
1380  }
1381  break;
1382 
1383  case 'COMR':
1384  // There may be more than one 'commercial frame' in a tag, but no two may be identical
1385  // Checking isn't implemented at all (yet) - just assumes that it's OK.
1386  break;
1387 
1388  case 'PRIV':
1389  case 'SIGN':
1390  if (!isset($source_data_array['ownerid'])) {
1391  $this->errors[] = '[ownerid] not specified for '.$frame_name;
1392  } elseif (!isset($source_data_array['data'])) {
1393  $this->errors[] = '[data] not specified for '.$frame_name;
1394  } elseif (in_array($frame_name.$source_data_array['ownerid'].$source_data_array['data'], $PreviousFrames)) {
1395  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID + Data ('.$source_data_array['ownerid'].' + '.$source_data_array['data'].')';
1396  } else {
1397  $PreviousFrames[] = $frame_name.$source_data_array['ownerid'].$source_data_array['data'];
1398  }
1399  break;
1400 
1401  default:
1402  if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) {
1403  $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name;
1404  }
1405  break;
1406  }
1407 
1408  } elseif ($this->majorversion == 3) {
1409 
1410  switch ($frame_name) {
1411  case 'UFID':
1412  case 'AENC':
1413  case 'ENCR':
1414  case 'GRID':
1415  if (!isset($source_data_array['ownerid'])) {
1416  $this->errors[] = '[ownerid] not specified for '.$frame_name;
1417  } elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) {
1418  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')';
1419  } else {
1420  $PreviousFrames[] = $frame_name.$source_data_array['ownerid'];
1421  }
1422  break;
1423 
1424  case 'TXXX':
1425  case 'WXXX':
1426  case 'APIC':
1427  case 'GEOB':
1428  if (!isset($source_data_array['description'])) {
1429  $this->errors[] = '[description] not specified for '.$frame_name;
1430  } elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) {
1431  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')';
1432  } else {
1433  $PreviousFrames[] = $frame_name.$source_data_array['description'];
1434  }
1435  break;
1436 
1437  case 'USER':
1438  if (!isset($source_data_array['language'])) {
1439  $this->errors[] = '[language] not specified for '.$frame_name;
1440  } elseif (in_array($frame_name.$source_data_array['language'], $PreviousFrames)) {
1441  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language ('.$source_data_array['language'].')';
1442  } else {
1443  $PreviousFrames[] = $frame_name.$source_data_array['language'];
1444  }
1445  break;
1446 
1447  case 'USLT':
1448  case 'SYLT':
1449  case 'COMM':
1450  if (!isset($source_data_array['language'])) {
1451  $this->errors[] = '[language] not specified for '.$frame_name;
1452  } elseif (!isset($source_data_array['description'])) {
1453  $this->errors[] = '[description] not specified for '.$frame_name;
1454  } elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) {
1455  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')';
1456  } else {
1457  $PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description'];
1458  }
1459  break;
1460 
1461  case 'POPM':
1462  if (!isset($source_data_array['email'])) {
1463  $this->errors[] = '[email] not specified for '.$frame_name;
1464  } elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) {
1465  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')';
1466  } else {
1467  $PreviousFrames[] = $frame_name.$source_data_array['email'];
1468  }
1469  break;
1470 
1471  case 'IPLS':
1472  case 'MCDI':
1473  case 'ETCO':
1474  case 'MLLT':
1475  case 'SYTC':
1476  case 'RVAD':
1477  case 'EQUA':
1478  case 'RVRB':
1479  case 'PCNT':
1480  case 'RBUF':
1481  case 'POSS':
1482  case 'OWNE':
1483  case 'RGAD':
1484  if (in_array($frame_name, $PreviousFrames)) {
1485  $this->errors[] = 'Only one '.$frame_name.' tag allowed';
1486  } else {
1487  $PreviousFrames[] = $frame_name;
1488  }
1489  break;
1490 
1491  case 'LINK':
1492  // this isn't implemented quite right (yet) - it should check the target frame data for compliance
1493  // but right now it just allows one linked frame of each type, to be safe.
1494  if (!isset($source_data_array['frameid'])) {
1495  $this->errors[] = '[frameid] not specified for '.$frame_name;
1496  } elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) {
1497  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')';
1498  } elseif (in_array($source_data_array['frameid'], $PreviousFrames)) {
1499  // no links to singleton tags
1500  $this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')';
1501  } else {
1502  $PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type
1503  $PreviousFrames[] = $source_data_array['frameid']; // no non-linked singleton tags of this type
1504  }
1505  break;
1506 
1507  case 'COMR':
1508  // There may be more than one 'commercial frame' in a tag, but no two may be identical
1509  // Checking isn't implemented at all (yet) - just assumes that it's OK.
1510  break;
1511 
1512  case 'PRIV':
1513  if (!isset($source_data_array['ownerid'])) {
1514  $this->errors[] = '[ownerid] not specified for '.$frame_name;
1515  } elseif (!isset($source_data_array['data'])) {
1516  $this->errors[] = '[data] not specified for '.$frame_name;
1517  } elseif (in_array($frame_name.$source_data_array['ownerid'].$source_data_array['data'], $PreviousFrames)) {
1518  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID + Data ('.$source_data_array['ownerid'].' + '.$source_data_array['data'].')';
1519  } else {
1520  $PreviousFrames[] = $frame_name.$source_data_array['ownerid'].$source_data_array['data'];
1521  }
1522  break;
1523 
1524  default:
1525  if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) {
1526  $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name;
1527  }
1528  break;
1529  }
1530 
1531  } elseif ($this->majorversion == 2) {
1532 
1533  switch ($frame_name) {
1534  case 'UFI':
1535  case 'CRM':
1536  case 'CRA':
1537  if (!isset($source_data_array['ownerid'])) {
1538  $this->errors[] = '[ownerid] not specified for '.$frame_name;
1539  } elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) {
1540  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')';
1541  } else {
1542  $PreviousFrames[] = $frame_name.$source_data_array['ownerid'];
1543  }
1544  break;
1545 
1546  case 'TXX':
1547  case 'WXX':
1548  case 'PIC':
1549  case 'GEO':
1550  if (!isset($source_data_array['description'])) {
1551  $this->errors[] = '[description] not specified for '.$frame_name;
1552  } elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) {
1553  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')';
1554  } else {
1555  $PreviousFrames[] = $frame_name.$source_data_array['description'];
1556  }
1557  break;
1558 
1559  case 'ULT':
1560  case 'SLT':
1561  case 'COM':
1562  if (!isset($source_data_array['language'])) {
1563  $this->errors[] = '[language] not specified for '.$frame_name;
1564  } elseif (!isset($source_data_array['description'])) {
1565  $this->errors[] = '[description] not specified for '.$frame_name;
1566  } elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) {
1567  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')';
1568  } else {
1569  $PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description'];
1570  }
1571  break;
1572 
1573  case 'POP':
1574  if (!isset($source_data_array['email'])) {
1575  $this->errors[] = '[email] not specified for '.$frame_name;
1576  } elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) {
1577  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')';
1578  } else {
1579  $PreviousFrames[] = $frame_name.$source_data_array['email'];
1580  }
1581  break;
1582 
1583  case 'IPL':
1584  case 'MCI':
1585  case 'ETC':
1586  case 'MLL':
1587  case 'STC':
1588  case 'RVA':
1589  case 'EQU':
1590  case 'REV':
1591  case 'CNT':
1592  case 'BUF':
1593  if (in_array($frame_name, $PreviousFrames)) {
1594  $this->errors[] = 'Only one '.$frame_name.' tag allowed';
1595  } else {
1596  $PreviousFrames[] = $frame_name;
1597  }
1598  break;
1599 
1600  case 'LNK':
1601  // this isn't implemented quite right (yet) - it should check the target frame data for compliance
1602  // but right now it just allows one linked frame of each type, to be safe.
1603  if (!isset($source_data_array['frameid'])) {
1604  $this->errors[] = '[frameid] not specified for '.$frame_name;
1605  } elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) {
1606  $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')';
1607  } elseif (in_array($source_data_array['frameid'], $PreviousFrames)) {
1608  // no links to singleton tags
1609  $this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')';
1610  } else {
1611  $PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type
1612  $PreviousFrames[] = $source_data_array['frameid']; // no non-linked singleton tags of this type
1613  }
1614  break;
1615 
1616  default:
1617  if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) {
1618  $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name;
1619  }
1620  break;
1621  }
1622  }
1623 
1624  if (!empty($this->errors)) {
1625  return false;
1626  }
1627 
1628  return true;
1629  }
1630 
1636  public function GenerateID3v2Tag($noerrorsonly=true)
1637  {
1638  $this->ID3v2FrameIsAllowed(null, ''); // clear static array in case this isn't the first call to $this->GenerateID3v2Tag()
1639 
1640  $tagstring = '';
1641  if (is_array($this->tag_data)) {
1642  foreach ($this->tag_data as $frame_name => $frame_rawinputdata) {
1643  foreach ($frame_rawinputdata as $irrelevantindex => $source_data_array) {
1644  if (Tag\Id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) {
1645  unset($frame_length);
1646  unset($frame_flags);
1647  $frame_data = false;
1648  if ($this->ID3v2FrameIsAllowed($frame_name, $source_data_array)) {
1649  if ($frame_data = $this->GenerateID3v2FrameData($frame_name, $source_data_array)) {
1650  $FrameUnsynchronisation = false;
1651  if ($this->majorversion >= 4) {
1652  // frame-level unsynchronisation
1653  $unsynchdata = $frame_data;
1654  if ($this->id3v2_use_unsynchronisation) {
1655  $unsynchdata = $this->Unsynchronise($frame_data);
1656  }
1657  if (strlen($unsynchdata) != strlen($frame_data)) {
1658  // unsynchronisation needed
1659  $FrameUnsynchronisation = true;
1660  $frame_data = $unsynchdata;
1661  if (isset($TagUnsynchronisation) && $TagUnsynchronisation === false) {
1662  // only set to true if ALL frames are unsynchronised
1663  } else {
1664  $TagUnsynchronisation = true;
1665  }
1666  } else {
1667  if (isset($TagUnsynchronisation)) {
1668  $TagUnsynchronisation = false;
1669  }
1670  }
1671  unset($unsynchdata);
1672 
1673  $frame_length = Helper::BigEndian2String(strlen($frame_data), 4, true);
1674  } else {
1675  $frame_length = Helper::BigEndian2String(strlen($frame_data), 4, false);
1676  }
1677  $frame_flags = $this->GenerateID3v2FrameFlags($this->ID3v2FrameFlagsLookupTagAlter($frame_name), $this->ID3v2FrameFlagsLookupFileAlter($frame_name), false, false, false, false, $FrameUnsynchronisation, false);
1678  }
1679  } else {
1680  $this->errors[] = 'Frame "'.$frame_name.'" is NOT allowed';
1681  }
1682  if ($frame_data === false) {
1683  $this->errors[] = '$this->GenerateID3v2FrameData() failed for "'.$frame_name.'"';
1684  if ($noerrorsonly) {
1685  return false;
1686  } else {
1687  unset($frame_name);
1688  }
1689  }
1690  } else {
1691  // ignore any invalid frame names, including 'title', 'header', etc
1692  $this->warnings[] = 'Ignoring invalid ID3v2 frame type: "'.$frame_name.'"';
1693  unset($frame_name);
1694  unset($frame_length);
1695  unset($frame_flags);
1696  unset($frame_data);
1697  }
1698  if (isset($frame_name) && isset($frame_length) && isset($frame_flags) && isset($frame_data)) {
1699  $tagstring .= $frame_name.$frame_length.$frame_flags.$frame_data;
1700  }
1701  }
1702  }
1703 
1704  if (!isset($TagUnsynchronisation)) {
1705  $TagUnsynchronisation = false;
1706  }
1707  if (($this->majorversion <= 3) && $this->id3v2_use_unsynchronisation) {
1708  // tag-level unsynchronisation
1709  $unsynchdata = $this->Unsynchronise($tagstring);
1710  if (strlen($unsynchdata) != strlen($tagstring)) {
1711  // unsynchronisation needed
1712  $TagUnsynchronisation = true;
1713  $tagstring = $unsynchdata;
1714  }
1715  }
1716 
1717  while ($this->paddedlength < (strlen($tagstring) + Tag\Id3v2::ID3v2HeaderLength($this->majorversion))) {
1718  $this->paddedlength += 1024;
1719  }
1720 
1721  $footer = false; // ID3v2 footers not yet supported in GetId3Core()
1722  if (!$footer && ($this->paddedlength > (strlen($tagstring) + Tag\Id3v2::ID3v2HeaderLength($this->majorversion)))) {
1723  // pad up to $paddedlength bytes if unpadded tag is shorter than $paddedlength
1724  // "Furthermore it MUST NOT have any padding when a tag footer is added to the tag."
1725  if (($this->paddedlength - strlen($tagstring) - Tag\Id3v2::ID3v2HeaderLength($this->majorversion)) > 0) {
1726  $tagstring .= str_repeat("\x00", $this->paddedlength - strlen($tagstring) - Tag\Id3v2::ID3v2HeaderLength($this->majorversion));
1727  }
1728  }
1729  if ($this->id3v2_use_unsynchronisation && (substr($tagstring, strlen($tagstring) - 1, 1) == "\xFF")) {
1730  // special unsynchronisation case:
1731  // if last byte == $FF then appended a $00
1732  $TagUnsynchronisation = true;
1733  $tagstring .= "\x00";
1734  }
1735 
1736  $tagheader = 'ID3';
1737  $tagheader .= chr($this->majorversion);
1738  $tagheader .= chr($this->minorversion);
1739  $tagheader .= $this->GenerateID3v2TagFlags(array('unsynchronisation'=>$TagUnsynchronisation));
1740  $tagheader .= Helper::BigEndian2String(strlen($tagstring), 4, true);
1741 
1742  return $tagheader.$tagstring;
1743  }
1744  $this->errors[] = 'tag_data is not an array in GenerateID3v2Tag()';
1745 
1746  return false;
1747  }
1748 
1754  public function ID3v2IsValidPriceString($pricestring)
1755  {
1756  if (Tag\Id3v2::LanguageLookup(substr($pricestring, 0, 3), true) == '') {
1757  return false;
1758  } elseif (!$this->IsANumber(substr($pricestring, 3), true)) {
1759  return false;
1760  }
1761 
1762  return true;
1763  }
1764 
1770  public function ID3v2FrameFlagsLookupTagAlter($framename)
1771  {
1772  // unfinished
1773  switch ($framename) {
1774  case 'RGAD':
1775  $allow = true;
1776  default:
1777  $allow = false;
1778  break;
1779  }
1780 
1781  return $allow;
1782  }
1783 
1789  public function ID3v2FrameFlagsLookupFileAlter($framename)
1790  {
1791  // unfinished
1792  switch ($framename) {
1793  case 'RGAD':
1794  return false;
1795  break;
1796 
1797  default:
1798  return false;
1799  break;
1800  }
1801  }
1802 
1808  public function ID3v2IsValidETCOevent($eventid)
1809  {
1810  if (($eventid < 0) || ($eventid > 0xFF)) {
1811  // outside range of 1 byte
1812  return false;
1813  } elseif (($eventid >= 0xF0) && ($eventid <= 0xFC)) {
1814  // reserved for future use
1815  return false;
1816  } elseif (($eventid >= 0x17) && ($eventid <= 0xDF)) {
1817  // reserved for future use
1818  return false;
1819  } elseif (($eventid >= 0x0E) && ($eventid <= 0x16) && ($this->majorversion == 2)) {
1820  // not defined in ID3v2.2
1821  return false;
1822  } elseif (($eventid >= 0x15) && ($eventid <= 0x16) && ($this->majorversion == 3)) {
1823  // not defined in ID3v2.3
1824  return false;
1825  }
1826 
1827  return true;
1828  }
1829 
1835  public function ID3v2IsValidSYLTtype($contenttype)
1836  {
1837  if (($contenttype >= 0) && ($contenttype <= 8) && ($this->majorversion == 4)) {
1838  return true;
1839  } elseif (($contenttype >= 0) && ($contenttype <= 6) && ($this->majorversion == 3)) {
1840  return true;
1841  }
1842 
1843  return false;
1844  }
1845 
1851  public function ID3v2IsValidRVA2channeltype($channeltype)
1852  {
1853  if (($channeltype >= 0) && ($channeltype <= 8) && ($this->majorversion == 4)) {
1854  return true;
1855  }
1856 
1857  return false;
1858  }
1859 
1865  public function ID3v2IsValidAPICpicturetype($picturetype)
1866  {
1867  if (($picturetype >= 0) && ($picturetype <= 0x14) && ($this->majorversion >= 2) && ($this->majorversion <= 4)) {
1868  return true;
1869  }
1870 
1871  return false;
1872  }
1873 
1879  public function ID3v2IsValidAPICimageformat($imageformat)
1880  {
1881  if ($imageformat == '-->') {
1882  return true;
1883  } elseif ($this->majorversion == 2) {
1884  if ((strlen($imageformat) == 3) && ($imageformat == strtoupper($imageformat))) {
1885  return true;
1886  }
1887  } elseif (($this->majorversion == 3) || ($this->majorversion == 4)) {
1888  if ($this->IsValidMIMEstring($imageformat)) {
1889  return true;
1890  }
1891  }
1892 
1893  return false;
1894  }
1895 
1901  public function ID3v2IsValidCOMRreceivedAs($receivedas)
1902  {
1903  if (($this->majorversion >= 3) && ($receivedas >= 0) && ($receivedas <= 8)) {
1904  return true;
1905  }
1906 
1907  return false;
1908  }
1909 
1915  public function ID3v2IsValidRGADname($RGADname)
1916  {
1917  if (($RGADname >= 0) && ($RGADname <= 2)) {
1918  return true;
1919  }
1920 
1921  return false;
1922  }
1923 
1929  public function ID3v2IsValidRGADoriginator($RGADoriginator)
1930  {
1931  if (($RGADoriginator >= 0) && ($RGADoriginator <= 3)) {
1932  return true;
1933  }
1934 
1935  return false;
1936  }
1937 
1944  public function ID3v2IsValidTextEncoding($textencodingbyte)
1945  {
1946  static $ID3v2IsValidTextEncoding_cache = array(
1947  2 => array(true, true),
1948  3 => array(true, true),
1949  4 => array(true, true, true, true));
1950 
1951  return isset($ID3v2IsValidTextEncoding_cache[$this->majorversion][$textencodingbyte]);
1952  }
1953 
1959  public function Unsynchronise($data)
1960  {
1961  // Whenever a false synchronisation is found within the tag, one zeroed
1962  // byte is inserted after the first false synchronisation byte. The
1963  // format of a correct sync that should be altered by ID3 encoders is as
1964  // follows:
1965  // %11111111 111xxxxx
1966  // And should be replaced with:
1967  // %11111111 00000000 111xxxxx
1968  // This has the side effect that all $FF 00 combinations have to be
1969  // altered, so they won't be affected by the decoding process. Therefore
1970  // all the $FF 00 combinations have to be replaced with the $FF 00 00
1971  // combination during the unsynchronisation.
1972 
1973  $data = str_replace("\xFF\x00", "\xFF\x00\x00", $data);
1974  $unsyncheddata = '';
1975  $datalength = strlen($data);
1976  for ($i = 0; $i < $datalength; $i++) {
1977  $thischar = $data{$i};
1978  $unsyncheddata .= $thischar;
1979  if ($thischar == "\xFF") {
1980  $nextchar = ord($data{$i + 1});
1981  if (($nextchar & 0xE0) == 0xE0) {
1982  // previous byte = 11111111, this byte = 111?????
1983  $unsyncheddata .= "\x00";
1984  }
1985  }
1986  }
1987 
1988  return $unsyncheddata;
1989  }
1990 
1996  public function is_hash($var)
1997  {
1998  // written by dev-nullØchristophe*vg
1999  // taken from http://www.php.net/manual/en/function.array-merge-recursive.php
2000  if (is_array($var)) {
2001  $keys = array_keys($var);
2002  $all_num = true;
2003  for ($i = 0; $i < count($keys); $i++) {
2004  if (is_string($keys[$i])) {
2005  return true;
2006  }
2007  }
2008  }
2009 
2010  return false;
2011  }
2012 
2019  public function array_join_merge($arr1, $arr2)
2020  {
2021  // written by dev-nullØchristophe*vg
2022  // taken from http://www.php.net/manual/en/function.array-merge-recursive.php
2023  if (is_array($arr1) && is_array($arr2)) {
2024  // the same -> merge
2025  $new_array = array();
2026 
2027  if ($this->is_hash($arr1) && $this->is_hash($arr2)) {
2028  // hashes -> merge based on keys
2029  $keys = array_merge(array_keys($arr1), array_keys($arr2));
2030  foreach ($keys as $key) {
2031  $new_array[$key] = $this->array_join_merge((isset($arr1[$key]) ? $arr1[$key] : ''), (isset($arr2[$key]) ? $arr2[$key] : ''));
2032  }
2033  } else {
2034  // two real arrays -> merge
2035  $new_array = array_reverse(array_unique(array_reverse(array_merge($arr1, $arr2))));
2036  }
2037 
2038  return $new_array;
2039  } else {
2040  // not the same ... take new one if defined, else the old one stays
2041  return $arr2 ? $arr2 : $arr1;
2042  }
2043  }
2044 
2050  public function IsValidMIMEstring($mimestring)
2051  {
2052  if ((strlen($mimestring) >= 3) && (strpos($mimestring, '/') > 0) && (strpos($mimestring, '/') < (strlen($mimestring) - 1))) {
2053  return true;
2054  }
2055 
2056  return false;
2057  }
2058 
2066  public function IsWithinBitRange($number, $maxbits, $signed=false)
2067  {
2068  if ($signed) {
2069  if (($number > (0 - pow(2, $maxbits - 1))) && ($number <= pow(2, $maxbits - 1))) {
2070  return true;
2071  }
2072  } else {
2073  if (($number >= 0) && ($number <= pow(2, $maxbits))) {
2074  return true;
2075  }
2076  }
2077 
2078  return false;
2079  }
2080 
2086  public function safe_parse_url($url)
2087  {
2088  $parts = @parse_url($url);
2089  $parts['scheme'] = (isset($parts['scheme']) ? $parts['scheme'] : '');
2090  $parts['host'] = (isset($parts['host']) ? $parts['host'] : '');
2091  $parts['user'] = (isset($parts['user']) ? $parts['user'] : '');
2092  $parts['pass'] = (isset($parts['pass']) ? $parts['pass'] : '');
2093  $parts['path'] = (isset($parts['path']) ? $parts['path'] : '');
2094  $parts['query'] = (isset($parts['query']) ? $parts['query'] : '');
2095 
2096  return $parts;
2097  }
2098 
2105  public function IsValidURL($url, $allowUserPass=false)
2106  {
2107  if ($url == '') {
2108  return false;
2109  }
2110  if ($allowUserPass !== true) {
2111  if (strstr($url, '@')) {
2112  // in the format http://user:pass@example.com or http://user@example.com
2113  // but could easily be somebody incorrectly entering an email address in place of a URL
2114  return false;
2115  }
2116  }
2117  if ($parts = $this->safe_parse_url($url)) {
2118  if (($parts['scheme'] != 'http') && ($parts['scheme'] != 'https') && ($parts['scheme'] != 'ftp') && ($parts['scheme'] != 'gopher')) {
2119  return false;
2120  } elseif (!preg_match('#^[[:alnum:]]([-.]?[0-9a-z])*\\.[a-z]{2,3}$#i', $parts['host'], $regs) && !preg_match('#^[0-9]{1,3}(\\.[0-9]{1,3}){3}$#', $parts['host'])) {
2121  return false;
2122  } elseif (!preg_match('#^([[:alnum:]-]|[\\_])*$#i', $parts['user'], $regs)) {
2123  return false;
2124  } elseif (!preg_match('#^([[:alnum:]-]|[\\_])*$#i', $parts['pass'], $regs)) {
2125  return false;
2126  } elseif (!preg_match('#^[[:alnum:]/_\\.@~-]*$#i', $parts['path'], $regs)) {
2127  return false;
2128  } elseif (!empty($parts['query']) && !preg_match('#^[[:alnum:]?&=+:;_()%\\#/,\\.-]*$#i', $parts['query'], $regs)) {
2129  return false;
2130  } else {
2131  return true;
2132  }
2133  }
2134 
2135  return false;
2136  }
2137 
2145  public static function ID3v2ShortFrameNameLookup($majorversion, $long_description)
2146  {
2147  $long_description = str_replace(' ', '_', strtolower(trim($long_description)));
2148  static $ID3v2ShortFrameNameLookup = array();
2149  if (empty($ID3v2ShortFrameNameLookup)) {
2150 
2151  // The following are unique to ID3v2.2
2152  $ID3v2ShortFrameNameLookup[2]['comment'] = 'COM';
2153  $ID3v2ShortFrameNameLookup[2]['album'] = 'TAL';
2154  $ID3v2ShortFrameNameLookup[2]['beats_per_minute'] = 'TBP';
2155  $ID3v2ShortFrameNameLookup[2]['composer'] = 'TCM';
2156  $ID3v2ShortFrameNameLookup[2]['genre'] = 'TCO';
2157  $ID3v2ShortFrameNameLookup[2]['itunescompilation'] = 'TCP';
2158  $ID3v2ShortFrameNameLookup[2]['copyright'] = 'TCR';
2159  $ID3v2ShortFrameNameLookup[2]['encoded_by'] = 'TEN';
2160  $ID3v2ShortFrameNameLookup[2]['language'] = 'TLA';
2161  $ID3v2ShortFrameNameLookup[2]['length'] = 'TLE';
2162  $ID3v2ShortFrameNameLookup[2]['original_artist'] = 'TOA';
2163  $ID3v2ShortFrameNameLookup[2]['original_filename'] = 'TOF';
2164  $ID3v2ShortFrameNameLookup[2]['original_lyricist'] = 'TOL';
2165  $ID3v2ShortFrameNameLookup[2]['original_album_title'] = 'TOT';
2166  $ID3v2ShortFrameNameLookup[2]['artist'] = 'TP1';
2167  $ID3v2ShortFrameNameLookup[2]['band'] = 'TP2';
2168  $ID3v2ShortFrameNameLookup[2]['conductor'] = 'TP3';
2169  $ID3v2ShortFrameNameLookup[2]['remixer'] = 'TP4';
2170  $ID3v2ShortFrameNameLookup[2]['publisher'] = 'TPB';
2171  $ID3v2ShortFrameNameLookup[2]['isrc'] = 'TRC';
2172  $ID3v2ShortFrameNameLookup[2]['tracknumber'] = 'TRK';
2173  $ID3v2ShortFrameNameLookup[2]['size'] = 'TSI';
2174  $ID3v2ShortFrameNameLookup[2]['encoder_settings'] = 'TSS';
2175  $ID3v2ShortFrameNameLookup[2]['description'] = 'TT1';
2176  $ID3v2ShortFrameNameLookup[2]['title'] = 'TT2';
2177  $ID3v2ShortFrameNameLookup[2]['subtitle'] = 'TT3';
2178  $ID3v2ShortFrameNameLookup[2]['lyricist'] = 'TXT';
2179  $ID3v2ShortFrameNameLookup[2]['user_text'] = 'TXX';
2180  $ID3v2ShortFrameNameLookup[2]['year'] = 'TYE';
2181  $ID3v2ShortFrameNameLookup[2]['unique_file_identifier'] = 'UFI';
2182  $ID3v2ShortFrameNameLookup[2]['unsynchronised_lyrics'] = 'ULT';
2183  $ID3v2ShortFrameNameLookup[2]['url_file'] = 'WAF';
2184  $ID3v2ShortFrameNameLookup[2]['url_artist'] = 'WAR';
2185  $ID3v2ShortFrameNameLookup[2]['url_source'] = 'WAS';
2186  $ID3v2ShortFrameNameLookup[2]['copyright_information'] = 'WCP';
2187  $ID3v2ShortFrameNameLookup[2]['url_publisher'] = 'WPB';
2188  $ID3v2ShortFrameNameLookup[2]['url_user'] = 'WXX';
2189 
2190  // The following are common to ID3v2.3 and ID3v2.4
2191  $ID3v2ShortFrameNameLookup[3]['audio_encryption'] = 'AENC';
2192  $ID3v2ShortFrameNameLookup[3]['attached_picture'] = 'APIC';
2193  $ID3v2ShortFrameNameLookup[3]['comment'] = 'COMM';
2194  $ID3v2ShortFrameNameLookup[3]['commercial'] = 'COMR';
2195  $ID3v2ShortFrameNameLookup[3]['encryption_method_registration'] = 'ENCR';
2196  $ID3v2ShortFrameNameLookup[3]['event_timing_codes'] = 'ETCO';
2197  $ID3v2ShortFrameNameLookup[3]['general_encapsulated_object'] = 'GEOB';
2198  $ID3v2ShortFrameNameLookup[3]['group_identification_registration'] = 'GRID';
2199  $ID3v2ShortFrameNameLookup[3]['linked_information'] = 'LINK';
2200  $ID3v2ShortFrameNameLookup[3]['music_cd_identifier'] = 'MCDI';
2201  $ID3v2ShortFrameNameLookup[3]['mpeg_location_lookup_table'] = 'MLLT';
2202  $ID3v2ShortFrameNameLookup[3]['ownership'] = 'OWNE';
2203  $ID3v2ShortFrameNameLookup[3]['play_counter'] = 'PCNT';
2204  $ID3v2ShortFrameNameLookup[3]['popularimeter'] = 'POPM';
2205  $ID3v2ShortFrameNameLookup[3]['position_synchronisation'] = 'POSS';
2206  $ID3v2ShortFrameNameLookup[3]['private'] = 'PRIV';
2207  $ID3v2ShortFrameNameLookup[3]['recommended_buffer_size'] = 'RBUF';
2208  $ID3v2ShortFrameNameLookup[3]['reverb'] = 'RVRB';
2209  $ID3v2ShortFrameNameLookup[3]['synchronised_lyrics'] = 'SYLT';
2210  $ID3v2ShortFrameNameLookup[3]['synchronised_tempo_codes'] = 'SYTC';
2211  $ID3v2ShortFrameNameLookup[3]['album'] = 'TALB';
2212  $ID3v2ShortFrameNameLookup[3]['beats_per_minute'] = 'TBPM';
2213  $ID3v2ShortFrameNameLookup[3]['itunescompilation'] = 'TCMP';
2214  $ID3v2ShortFrameNameLookup[3]['composer'] = 'TCOM';
2215  $ID3v2ShortFrameNameLookup[3]['genre'] = 'TCON';
2216  $ID3v2ShortFrameNameLookup[3]['copyright'] = 'TCOP';
2217  $ID3v2ShortFrameNameLookup[3]['playlist_delay'] = 'TDLY';
2218  $ID3v2ShortFrameNameLookup[3]['encoded_by'] = 'TENC';
2219  $ID3v2ShortFrameNameLookup[3]['lyricist'] = 'TEXT';
2220  $ID3v2ShortFrameNameLookup[3]['file_type'] = 'TFLT';
2221  $ID3v2ShortFrameNameLookup[3]['content_group_description'] = 'TIT1';
2222  $ID3v2ShortFrameNameLookup[3]['title'] = 'TIT2';
2223  $ID3v2ShortFrameNameLookup[3]['subtitle'] = 'TIT3';
2224  $ID3v2ShortFrameNameLookup[3]['initial_key'] = 'TKEY';
2225  $ID3v2ShortFrameNameLookup[3]['language'] = 'TLAN';
2226  $ID3v2ShortFrameNameLookup[3]['length'] = 'TLEN';
2227  $ID3v2ShortFrameNameLookup[3]['media_type'] = 'TMED';
2228  $ID3v2ShortFrameNameLookup[3]['original_album_title'] = 'TOAL';
2229  $ID3v2ShortFrameNameLookup[3]['original_filename'] = 'TOFN';
2230  $ID3v2ShortFrameNameLookup[3]['original_lyricist'] = 'TOLY';
2231  $ID3v2ShortFrameNameLookup[3]['original_artist'] = 'TOPE';
2232  $ID3v2ShortFrameNameLookup[3]['file_owner'] = 'TOWN';
2233  $ID3v2ShortFrameNameLookup[3]['artist'] = 'TPE1';
2234  $ID3v2ShortFrameNameLookup[3]['band'] = 'TPE2';
2235  $ID3v2ShortFrameNameLookup[3]['conductor'] = 'TPE3';
2236  $ID3v2ShortFrameNameLookup[3]['remixer'] = 'TPE4';
2237  $ID3v2ShortFrameNameLookup[3]['part_of_a_set'] = 'TPOS';
2238  $ID3v2ShortFrameNameLookup[3]['publisher'] = 'TPUB';
2239  $ID3v2ShortFrameNameLookup[3]['tracknumber'] = 'TRCK';
2240  $ID3v2ShortFrameNameLookup[3]['internet_radio_station_name'] = 'TRSN';
2241  $ID3v2ShortFrameNameLookup[3]['internet_radio_station_owner'] = 'TRSO';
2242  $ID3v2ShortFrameNameLookup[3]['isrc'] = 'TSRC';
2243  $ID3v2ShortFrameNameLookup[3]['encoder_settings'] = 'TSSE';
2244  $ID3v2ShortFrameNameLookup[3]['user_text'] = 'TXXX';
2245  $ID3v2ShortFrameNameLookup[3]['unique_file_identifier'] = 'UFID';
2246  $ID3v2ShortFrameNameLookup[3]['terms_of_use'] = 'USER';
2247  $ID3v2ShortFrameNameLookup[3]['unsynchronised_lyrics'] = 'USLT';
2248  $ID3v2ShortFrameNameLookup[3]['commercial'] = 'WCOM';
2249  $ID3v2ShortFrameNameLookup[3]['copyright_information'] = 'WCOP';
2250  $ID3v2ShortFrameNameLookup[3]['url_file'] = 'WOAF';
2251  $ID3v2ShortFrameNameLookup[3]['url_artist'] = 'WOAR';
2252  $ID3v2ShortFrameNameLookup[3]['url_source'] = 'WOAS';
2253  $ID3v2ShortFrameNameLookup[3]['url_station'] = 'WORS';
2254  $ID3v2ShortFrameNameLookup[3]['payment'] = 'WPAY';
2255  $ID3v2ShortFrameNameLookup[3]['url_publisher'] = 'WPUB';
2256  $ID3v2ShortFrameNameLookup[3]['url_user'] = 'WXXX';
2257 
2258  // The above are common to ID3v2.3 and ID3v2.4
2259  // so copy them to ID3v2.4 before adding specifics for 2.3 and 2.4
2260  $ID3v2ShortFrameNameLookup[4] = $ID3v2ShortFrameNameLookup[3];
2261 
2262  // The following are unique to ID3v2.3
2263  $ID3v2ShortFrameNameLookup[3]['equalisation'] = 'EQUA';
2264  $ID3v2ShortFrameNameLookup[3]['involved_people_list'] = 'IPLS';
2265  $ID3v2ShortFrameNameLookup[3]['relative_volume_adjustment'] = 'RVAD';
2266  $ID3v2ShortFrameNameLookup[3]['date'] = 'TDAT';
2267  $ID3v2ShortFrameNameLookup[3]['time'] = 'TIME';
2268  $ID3v2ShortFrameNameLookup[3]['original_release_year'] = 'TORY';
2269  $ID3v2ShortFrameNameLookup[3]['recording_dates'] = 'TRDA';
2270  $ID3v2ShortFrameNameLookup[3]['size'] = 'TSIZ';
2271  $ID3v2ShortFrameNameLookup[3]['year'] = 'TYER';
2272 
2273  // The following are unique to ID3v2.4
2274  $ID3v2ShortFrameNameLookup[4]['audio_seek_point_index'] = 'ASPI';
2275  $ID3v2ShortFrameNameLookup[4]['equalisation'] = 'EQU2';
2276  $ID3v2ShortFrameNameLookup[4]['relative_volume_adjustment'] = 'RVA2';
2277  $ID3v2ShortFrameNameLookup[4]['seek'] = 'SEEK';
2278  $ID3v2ShortFrameNameLookup[4]['signature'] = 'SIGN';
2279  $ID3v2ShortFrameNameLookup[4]['encoding_time'] = 'TDEN';
2280  $ID3v2ShortFrameNameLookup[4]['original_release_time'] = 'TDOR';
2281  $ID3v2ShortFrameNameLookup[4]['recording_time'] = 'TDRC';
2282  $ID3v2ShortFrameNameLookup[4]['release_time'] = 'TDRL';
2283  $ID3v2ShortFrameNameLookup[4]['tagging_time'] = 'TDTG';
2284  $ID3v2ShortFrameNameLookup[4]['involved_people_list'] = 'TIPL';
2285  $ID3v2ShortFrameNameLookup[4]['musician_credits_list'] = 'TMCL';
2286  $ID3v2ShortFrameNameLookup[4]['mood'] = 'TMOO';
2287  $ID3v2ShortFrameNameLookup[4]['produced_notice'] = 'TPRO';
2288  $ID3v2ShortFrameNameLookup[4]['album_sort_order'] = 'TSOA';
2289  $ID3v2ShortFrameNameLookup[4]['performer_sort_order'] = 'TSOP';
2290  $ID3v2ShortFrameNameLookup[4]['title_sort_order'] = 'TSOT';
2291  $ID3v2ShortFrameNameLookup[4]['set_subtitle'] = 'TSST';
2292  }
2293 
2294  return (isset($ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)]) ? $ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)] : '');
2295 
2296  }
2297 
2298 }
static IsValidID3v2FrameName($framename, $id3v2majorversion)
Definition: Id3v2.php:3404
ID3v2IsValidAPICpicturetype($picturetype)
Definition: Id3v2.php:1865
IsWithinBitRange($number, $maxbits, $signed=false)
Definition: Id3v2.php:2066
GenerateID3v2TagFlags($flags)
Definition: Id3v2.php:300
GenerateID3v2Tag($noerrorsonly=true)
Definition: Id3v2.php:1636
static Float2String($floatvalue, $bits)
Definition: Helper.php:237
static BigEndian2String($number, $minbytes=1, $synchsafe=false, $signed=false)
Definition: Helper.php:444
static ID3v2ShortFrameNameLookup($majorversion, $long_description)
array $ID3v2ShortFrameNameLookup
Definition: Id3v2.php:2145
static LanguageLookup($languagecode, $casesensitive=false)
Definition: Id3v2.php:2372
ID3v2IsValidPriceString($pricestring)
Definition: Id3v2.php:1754
ID3v2IsValidCOMRreceivedAs($receivedas)
Definition: Id3v2.php:1901
static Dec2Bin($number)
Definition: Helper.php:472
ID3v2IsValidETCOevent($eventid)
Definition: Id3v2.php:1808
Unsynchronise($data)
Definition: Id3v2.php:1959
$url
Definition: shib_logout.php:72
GetId3() by James Heinrich info@getid3.org //.
Definition: Id3v2.php:30
fseek($bytes, $whence=SEEK_SET)
$id3v2_use_unsynchronisation
Definition: Id3v2.php:69
safe_parse_url($url)
Definition: Id3v2.php:2086
ID3v2FrameFlagsLookupFileAlter($framename)
Definition: Id3v2.php:1789
GetId3() by James Heinrich info@getid3.org //.
Definition: GetId3Core.php:25
ID3v2FrameFlagsLookupTagAlter($framename)
Definition: Id3v2.php:1770
static IsValidDateStampString($datestamp)
Definition: Id3v2.php:3449
ID3v2IsValidTextEncoding($textencodingbyte)
array $ID3v2IsValidTextEncoding_cache
Definition: Id3v2.php:1944
static TextEncodingTerminatorLookup($encoding)
array $TextEncodingTerminatorLookup
Definition: Id3v2.php:3362
ID3v2IsValidRVA2channeltype($channeltype)
Definition: Id3v2.php:1851
Create styles array
The data for the language used.
ID3v2IsValidRGADname($RGADname)
Definition: Id3v2.php:1915
GenerateID3v2FrameFlags($TagAlter=false, $FileAlter=false, $ReadOnly=false, $Compression=false, $Encryption=false, $GroupingIdentity=false, $Unsynchronisation=false, $DataLengthIndicator=false)
Definition: Id3v2.php:347
$errors
array_join_merge($arr1, $arr2)
Definition: Id3v2.php:2019
ID3v2IsValidSYLTtype($contenttype)
Definition: Id3v2.php:1835
static RGADgainString($namecode, $originatorcode, $replaygain)
Definition: Helper.php:1588
ID3v2FrameIsAllowed($frame_name, $source_data_array)
Definition: Id3v2.php:1271
IsValidURL($url, $allowUserPass=false)
Definition: Id3v2.php:2105
static intValueSupported($num)
null $hasINT64
Definition: Helper.php:130
IsValidMIMEstring($mimestring)
Definition: Id3v2.php:2050
ID3v2IsValidAPICimageformat($imageformat)
Definition: Id3v2.php:1879
GenerateID3v2FrameData($frame_name, $source_data_array)
Definition: Id3v2.php:395
static IsANumber($numberstring, $allowdecimal=false, $allownegative=false)
Definition: Id3v2.php:3427
static ID3v2HeaderLength($majorversion)
Definition: Id3v2.php:3484
ID3v2IsValidRGADoriginator($RGADoriginator)
Definition: Id3v2.php:1929