1<?php
2
3/////////////////////////////////////////////////////////////////
4/// getID3() by James Heinrich <info@getid3.org>               //
5//  available at https://github.com/JamesHeinrich/getID3       //
6//            or https://www.getid3.org                        //
7//            or http://getid3.sourceforge.net                 //
8//  see readme.txt for more details                            //
9/////////////////////////////////////////////////////////////////
10///                                                            //
11// write.id3v2.php                                             //
12// module for writing ID3v2 tags                               //
13// dependencies: module.tag.id3v2.php                          //
14//                                                            ///
15/////////////////////////////////////////////////////////////////
16
17if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
18	exit;
19}
20getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true);
21
22class getid3_write_id3v2
23{
24	/**
25	 * @var string
26	 */
27	public $filename;
28
29	/**
30	 * @var array|null
31	 */
32	public $tag_data;
33
34	/**
35	 * Read buffer size in bytes.
36	 *
37	 * @var int
38	 */
39	public $fread_buffer_size           = 32768;
40
41	/**
42	 * Minimum length of ID3v2 tag in bytes.
43	 *
44	 * @var int
45	 */
46	public $paddedlength                = 4096;
47
48	/**
49	 * ID3v2 major version (2, 3 (recommended), 4).
50	 *
51	 * @var int
52	 */
53	public $majorversion                = 3;
54
55	/**
56	 * ID3v2 minor version - always 0.
57	 *
58	 * @var int
59	 */
60	public $minorversion                = 0;
61
62	/**
63	 * If true, merge new data with existing tags; if false, delete old tag data and only write new tags.
64	 *
65	 * @var bool
66	 */
67	public $merge_existing_data         = false;
68
69	/**
70	 * Default text encoding (ISO-8859-1) if not explicitly passed.
71	 *
72	 * @var int
73	 */
74	public $id3v2_default_encodingid    = 0;
75
76	/**
77	 * The specs say it should be TRUE, but most other ID3v2-aware programs are broken if unsynchronization is used,
78	 * so by default don't use it.
79	 *
80	 * @var bool
81	 */
82	public $id3v2_use_unsynchronisation = false;
83
84	/**
85	 * Any non-critical errors will be stored here.
86	 *
87	 * @var array
88	 */
89	public $warnings                    = array();
90
91	/**
92	 * Any critical errors will be stored here.
93	 *
94	 * @var array
95	 */
96	public $errors                      = array();
97
98	public function __construct() {
99	}
100
101	/**
102	 * @return bool
103	 */
104	public function WriteID3v2() {
105		// File MUST be writeable - CHMOD(646) at least. It's best if the
106		// directory is also writeable, because that method is both faster and less susceptible to errors.
107
108		if (!empty($this->filename) && (getID3::is_writable($this->filename) || (!file_exists($this->filename) && getID3::is_writable(dirname($this->filename))))) {
109			// Initialize getID3 engine
110			$getID3 = new getID3;
111			$OldThisFileInfo = $getID3->analyze($this->filename);
112			if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) {
113				$this->errors[] = 'Unable to write ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
114				return false;
115			}
116			if ($this->merge_existing_data) {
117				// merge with existing data
118				if (!empty($OldThisFileInfo['id3v2'])) {
119					$this->tag_data = $this->array_join_merge($OldThisFileInfo['id3v2'], $this->tag_data);
120				}
121			}
122			$this->paddedlength = (isset($OldThisFileInfo['id3v2']['headerlength']) ? max($OldThisFileInfo['id3v2']['headerlength'], $this->paddedlength) : $this->paddedlength);
123
124			if ($NewID3v2Tag = $this->GenerateID3v2Tag()) {
125
126				if (file_exists($this->filename) && getID3::is_writable($this->filename) && isset($OldThisFileInfo['id3v2']['headerlength']) && ($OldThisFileInfo['id3v2']['headerlength'] == strlen($NewID3v2Tag))) {
127
128					// best and fastest method - insert-overwrite existing tag (padded to length of old tag if neccesary)
129					if (file_exists($this->filename)) {
130
131						if (is_readable($this->filename) && getID3::is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'r+b'))) {
132							rewind($fp);
133							fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag));
134							fclose($fp);
135						} else {
136							$this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")';
137						}
138
139					} else {
140
141						if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'wb'))) {
142							rewind($fp);
143							fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag));
144							fclose($fp);
145						} else {
146							$this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")';
147						}
148
149					}
150
151				} else {
152
153					if ($tempfilename = tempnam(GETID3_TEMP_DIR, 'getID3')) {
154						if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) {
155							if (getID3::is_writable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) {
156
157								fwrite($fp_temp, $NewID3v2Tag, strlen($NewID3v2Tag));
158
159								rewind($fp_source);
160								if (!empty($OldThisFileInfo['avdataoffset'])) {
161									fseek($fp_source, $OldThisFileInfo['avdataoffset']);
162								}
163
164								while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
165									fwrite($fp_temp, $buffer, strlen($buffer));
166								}
167
168								fclose($fp_temp);
169								fclose($fp_source);
170								copy($tempfilename, $this->filename);
171								unlink($tempfilename);
172								return true;
173
174							} else {
175								$this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")';
176							}
177							fclose($fp_source);
178
179						} else {
180							$this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")';
181						}
182					}
183					return false;
184
185				}
186
187			} else {
188
189				$this->errors[] = '$this->GenerateID3v2Tag() failed';
190
191			}
192
193			if (!empty($this->errors)) {
194				return false;
195			}
196			return true;
197		} else {
198			$this->errors[] = 'WriteID3v2() failed: !is_writeable('.$this->filename.')';
199		}
200		return false;
201	}
202
203	/**
204	 * @return bool
205	 */
206	public function RemoveID3v2() {
207		// File MUST be writeable - CHMOD(646) at least. It's best if the
208		// directory is also writeable, because that method is both faster and less susceptible to errors.
209		if (getID3::is_writable(dirname($this->filename))) {
210
211			// preferred method - only one copying operation, minimal chance of corrupting
212			// original file if script is interrupted, but required directory to be writeable
213			if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) {
214
215				// Initialize getID3 engine
216				$getID3 = new getID3;
217				$OldThisFileInfo = $getID3->analyze($this->filename);
218				if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) {
219					$this->errors[] = 'Unable to remove ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
220					fclose($fp_source);
221					return false;
222				}
223				rewind($fp_source);
224				if ($OldThisFileInfo['avdataoffset'] !== false) {
225					fseek($fp_source, $OldThisFileInfo['avdataoffset']);
226				}
227				if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp_temp = fopen($this->filename.'getid3tmp', 'w+b'))) {
228					while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
229						fwrite($fp_temp, $buffer, strlen($buffer));
230					}
231					fclose($fp_temp);
232				} else {
233					$this->errors[] = 'Could not fopen("'.$this->filename.'getid3tmp", "w+b")';
234				}
235				fclose($fp_source);
236			} else {
237				$this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")';
238			}
239			if (file_exists($this->filename)) {
240				unlink($this->filename);
241			}
242			rename($this->filename.'getid3tmp', $this->filename);
243
244		} elseif (getID3::is_writable($this->filename)) {
245
246			// less desirable alternate method - double-copies the file, overwrites original file
247			// and could corrupt source file if the script is interrupted or an error occurs.
248			if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) {
249
250				// Initialize getID3 engine
251				$getID3 = new getID3;
252				$OldThisFileInfo = $getID3->analyze($this->filename);
253				if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) {
254					$this->errors[] = 'Unable to remove ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
255					fclose($fp_source);
256					return false;
257				}
258				rewind($fp_source);
259				if ($OldThisFileInfo['avdataoffset'] !== false) {
260					fseek($fp_source, $OldThisFileInfo['avdataoffset']);
261				}
262				if ($fp_temp = tmpfile()) {
263					while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
264						fwrite($fp_temp, $buffer, strlen($buffer));
265					}
266					fclose($fp_source);
267					if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'wb'))) {
268						rewind($fp_temp);
269						while ($buffer = fread($fp_temp, $this->fread_buffer_size)) {
270							fwrite($fp_source, $buffer, strlen($buffer));
271						}
272						fseek($fp_temp, -128, SEEK_END);
273						fclose($fp_source);
274					} else {
275						$this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")';
276					}
277					fclose($fp_temp);
278				} else {
279					$this->errors[] = 'Could not create tmpfile()';
280				}
281			} else {
282				$this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")';
283			}
284
285		} else {
286
287			$this->errors[] = 'Directory and file both not writeable';
288
289		}
290
291		if (!empty($this->errors)) {
292			return false;
293		}
294		return true;
295	}
296
297	/**
298	 * @param array $flags
299	 *
300	 * @return string|false
301	 */
302	public function GenerateID3v2TagFlags($flags) {
303		$flag = null;
304		switch ($this->majorversion) {
305			case 4:
306				// %abcd0000
307				$flag  = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation
308				$flag .= (!empty($flags['extendedheader']   ) ? '1' : '0'); // b - Extended header
309				$flag .= (!empty($flags['experimental']     ) ? '1' : '0'); // c - Experimental indicator
310				$flag .= (!empty($flags['footer']           ) ? '1' : '0'); // d - Footer present
311				$flag .= '0000';
312				break;
313
314			case 3:
315				// %abc00000
316				$flag  = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation
317				$flag .= (!empty($flags['extendedheader']   ) ? '1' : '0'); // b - Extended header
318				$flag .= (!empty($flags['experimental']     ) ? '1' : '0'); // c - Experimental indicator
319				$flag .= '00000';
320				break;
321
322			case 2:
323				// %ab000000
324				$flag  = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation
325				$flag .= (!empty($flags['compression']      ) ? '1' : '0'); // b - Compression
326				$flag .= '000000';
327				break;
328
329			default:
330				return false;
331		}
332		return chr(bindec($flag));
333	}
334
335	/**
336	 * @param bool $TagAlter
337	 * @param bool $FileAlter
338	 * @param bool $ReadOnly
339	 * @param bool $Compression
340	 * @param bool $Encryption
341	 * @param bool $GroupingIdentity
342	 * @param bool $Unsynchronisation
343	 * @param bool $DataLengthIndicator
344	 *
345	 * @return string|false
346	 */
347	public function GenerateID3v2FrameFlags($TagAlter=false, $FileAlter=false, $ReadOnly=false, $Compression=false, $Encryption=false, $GroupingIdentity=false, $Unsynchronisation=false, $DataLengthIndicator=false) {
348		$flag1 = null;
349		$flag2 = null;
350		switch ($this->majorversion) {
351			case 4:
352				// %0abc0000 %0h00kmnp
353				$flag1  = '0';
354				$flag1 .= $TagAlter  ? '1' : '0'; // a - Tag alter preservation (true == discard)
355				$flag1 .= $FileAlter ? '1' : '0'; // b - File alter preservation (true == discard)
356				$flag1 .= $ReadOnly  ? '1' : '0'; // c - Read only (true == read only)
357				$flag1 .= '0000';
358
359				$flag2  = '0';
360				$flag2 .= $GroupingIdentity    ? '1' : '0'; // h - Grouping identity (true == contains group information)
361				$flag2 .= '00';
362				$flag2 .= $Compression         ? '1' : '0'; // k - Compression (true == compressed)
363				$flag2 .= $Encryption          ? '1' : '0'; // m - Encryption (true == encrypted)
364				$flag2 .= $Unsynchronisation   ? '1' : '0'; // n - Unsynchronisation (true == unsynchronised)
365				$flag2 .= $DataLengthIndicator ? '1' : '0'; // p - Data length indicator (true == data length indicator added)
366				break;
367
368			case 3:
369				// %abc00000 %ijk00000
370				$flag1  = $TagAlter  ? '1' : '0';  // a - Tag alter preservation (true == discard)
371				$flag1 .= $FileAlter ? '1' : '0';  // b - File alter preservation (true == discard)
372				$flag1 .= $ReadOnly  ? '1' : '0';  // c - Read only (true == read only)
373				$flag1 .= '00000';
374
375				$flag2  = $Compression      ? '1' : '0';      // i - Compression (true == compressed)
376				$flag2 .= $Encryption       ? '1' : '0';      // j - Encryption (true == encrypted)
377				$flag2 .= $GroupingIdentity ? '1' : '0';      // k - Grouping identity (true == contains group information)
378				$flag2 .= '00000';
379				break;
380
381			default:
382				return false;
383
384		}
385		return chr(bindec($flag1)).chr(bindec($flag2));
386	}
387
388	/**
389	 * @param string $frame_name
390	 * @param array  $source_data_array
391	 *
392	 * @return string|false
393	 */
394	public function GenerateID3v2FrameData($frame_name, $source_data_array) {
395		if (!getid3_id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) {
396			return false;
397		}
398		$framedata = '';
399
400		if (($this->majorversion < 3) || ($this->majorversion > 4)) {
401
402			$this->errors[] = 'Only ID3v2.3 and ID3v2.4 are supported in GenerateID3v2FrameData()';
403
404		} else { // $this->majorversion 3 or 4
405
406			switch ($frame_name) {
407				case 'UFID':
408					// 4.1   UFID Unique file identifier
409					// Owner identifier        <text string> $00
410					// Identifier              <up to 64 bytes binary data>
411					if (strlen($source_data_array['data']) > 64) {
412						$this->errors[] = 'Identifier not allowed to be longer than 64 bytes in '.$frame_name.' (supplied data was '.strlen($source_data_array['data']).' bytes long)';
413					} else {
414						$framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
415						$framedata .= substr($source_data_array['data'], 0, 64); // max 64 bytes - truncate anything longer
416					}
417					break;
418
419				case 'TXXX':
420					// 4.2.2 TXXX User defined text information frame
421					// Text encoding     $xx
422					// Description       <text string according to encoding> $00 (00)
423					// Value             <text string according to encoding>
424					$source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
425					if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
426						$this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
427					} else {
428						$framedata .= chr($source_data_array['encodingid']);
429						$framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
430						$framedata .= $source_data_array['data'];
431					}
432					break;
433
434				case 'WXXX':
435					// 4.3.2 WXXX User defined URL link frame
436					// Text encoding     $xx
437					// Description       <text string according to encoding> $00 (00)
438					// URL               <text string>
439					$source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
440					if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
441						$this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
442					} elseif (!isset($source_data_array['data']) || !$this->IsValidURL($source_data_array['data'], false)) {
443						//$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
444						// probably should be an error, need to rewrite IsValidURL() to handle other encodings
445						$this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
446					} else {
447						$framedata .= chr($source_data_array['encodingid']);
448						$framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
449						$framedata .= $source_data_array['data'];
450					}
451					break;
452
453				case 'IPLS':
454					// 4.4  IPLS Involved people list (ID3v2.3 only)
455					// Text encoding     $xx
456					// People list strings    <textstrings>
457					$source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
458					if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
459						$this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
460					} else {
461						$framedata .= chr($source_data_array['encodingid']);
462						$framedata .= $source_data_array['data'];
463					}
464					break;
465
466				case 'MCDI':
467					// 4.4   MCDI Music CD identifier
468					// CD TOC                <binary data>
469					$framedata .= $source_data_array['data'];
470					break;
471
472				case 'ETCO':
473					// 4.5   ETCO Event timing codes
474					// Time stamp format    $xx
475					//   Where time stamp format is:
476					// $01  (32-bit value) MPEG frames from beginning of file
477					// $02  (32-bit value) milliseconds from beginning of file
478					//   Followed by a list of key events in the following format:
479					// Type of event   $xx
480					// Time stamp      $xx (xx ...)
481					//   The 'Time stamp' is set to zero if directly at the beginning of the sound
482					//   or after the previous event. All events MUST be sorted in chronological order.
483					if (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) {
484						$this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')';
485					} else {
486						$framedata .= chr($source_data_array['timestampformat']);
487						foreach ($source_data_array as $key => $val) {
488							if (!$this->ID3v2IsValidETCOevent($val['typeid'])) {
489								$this->errors[] = 'Invalid Event Type byte in '.$frame_name.' ('.$val['typeid'].')';
490							} elseif (($key != 'timestampformat') && ($key != 'flags')) {
491								if (($val['timestamp'] > 0) && isset($previousETCOtimestamp) && ($previousETCOtimestamp >= $val['timestamp'])) {
492									//   The 'Time stamp' is set to zero if directly at the beginning of the sound
493									//   or after the previous event. All events MUST be sorted in chronological order.
494									$this->errors[] = 'Out-of-order timestamp in '.$frame_name.' ('.$val['timestamp'].') for Event Type ('.$val['typeid'].')';
495								} else {
496									$framedata .= chr($val['typeid']);
497									$framedata .= getid3_lib::BigEndian2String($val['timestamp'], 4, false);
498									$previousETCOtimestamp = $val['timestamp'];
499								}
500							}
501						}
502					}
503					break;
504
505				case 'MLLT':
506					// 4.6   MLLT MPEG location lookup table
507					// MPEG frames between reference  $xx xx
508					// Bytes between reference        $xx xx xx
509					// Milliseconds between reference $xx xx xx
510					// Bits for bytes deviation       $xx
511					// Bits for milliseconds dev.     $xx
512					//   Then for every reference the following data is included;
513					// Deviation in bytes         %xxx....
514					// Deviation in milliseconds  %xxx....
515					if (($source_data_array['framesbetweenreferences'] > 0) && ($source_data_array['framesbetweenreferences'] <= 65535)) {
516						$framedata .= getid3_lib::BigEndian2String($source_data_array['framesbetweenreferences'], 2, false);
517					} else {
518						$this->errors[] = 'Invalid MPEG Frames Between References in '.$frame_name.' ('.$source_data_array['framesbetweenreferences'].')';
519					}
520					if (($source_data_array['bytesbetweenreferences'] > 0) && ($source_data_array['bytesbetweenreferences'] <= 16777215)) {
521						$framedata .= getid3_lib::BigEndian2String($source_data_array['bytesbetweenreferences'], 3, false);
522					} else {
523						$this->errors[] = 'Invalid bytes Between References in '.$frame_name.' ('.$source_data_array['bytesbetweenreferences'].')';
524					}
525					if (($source_data_array['msbetweenreferences'] > 0) && ($source_data_array['msbetweenreferences'] <= 16777215)) {
526						$framedata .= getid3_lib::BigEndian2String($source_data_array['msbetweenreferences'], 3, false);
527					} else {
528						$this->errors[] = 'Invalid Milliseconds Between References in '.$frame_name.' ('.$source_data_array['msbetweenreferences'].')';
529					}
530					if (!$this->IsWithinBitRange($source_data_array['bitsforbytesdeviation'], 8, false)) {
531						if (($source_data_array['bitsforbytesdeviation'] % 4) == 0) {
532							$framedata .= chr($source_data_array['bitsforbytesdeviation']);
533						} else {
534							$this->errors[] = 'Bits For Bytes Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].') must be a multiple of 4.';
535						}
536					} else {
537						$this->errors[] = 'Invalid Bits For Bytes Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].')';
538					}
539					if (!$this->IsWithinBitRange($source_data_array['bitsformsdeviation'], 8, false)) {
540						if (($source_data_array['bitsformsdeviation'] % 4) == 0) {
541							$framedata .= chr($source_data_array['bitsformsdeviation']);
542						} else {
543							$this->errors[] = 'Bits For Milliseconds Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].') must be a multiple of 4.';
544						}
545					} else {
546						$this->errors[] = 'Invalid Bits For Milliseconds Deviation in '.$frame_name.' ('.$source_data_array['bitsformsdeviation'].')';
547					}
548					$unwrittenbitstream = '';
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(getid3_lib::Dec2Bin($val['bytedeviation']), $source_data_array['bitsforbytesdeviation'], '0', STR_PAD_LEFT);
552							$unwrittenbitstream .= str_pad(getid3_lib::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 .= getid3_lib::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 (getid3_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'].getid3_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 (getid3_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'].getid3_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'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
643							$framedata .= getid3_lib::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 (getid3_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'].getid3_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 .= getid3_lib::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 .= getid3_lib::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 .= getid3_lib::BigEndian2String($source_data_array['volumechange']['right'], ceil($source_data_array['bitsvolume'] / 8), false);
723						$framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['left'],  ceil($source_data_array['bitsvolume'] / 8), false);
724						$framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['right'], ceil($source_data_array['bitsvolume'] / 8), false);
725						$framedata .= getid3_lib::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 .= getid3_lib::BigEndian2String($source_data_array['volumechange']['rightrear'], ceil($source_data_array['bitsvolume']/8), false);
731								$framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['leftrear'],  ceil($source_data_array['bitsvolume']/8), false);
732								$framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['rightrear'], ceil($source_data_array['bitsvolume']/8), false);
733								$framedata .= getid3_lib::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 .= getid3_lib::BigEndian2String($source_data_array['volumechange']['center'], ceil($source_data_array['bitsvolume']/8), false);
738								$framedata .= getid3_lib::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 .= getid3_lib::BigEndian2String($source_data_array['volumechange']['bass'], ceil($source_data_array['bitsvolume']/8), false);
742								$framedata .= getid3_lib::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 .= getid3_lib::BigEndian2String(intval(round($key * 2)), 2, false);
763							$framedata .= getid3_lib::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 .= getid3_lib::BigEndian2String($key, 2, false);
791									$framedata .= getid3_lib::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 .= getid3_lib::BigEndian2String($source_data_array['left'], 2, false);
832						$framedata .= getid3_lib::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->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))) {
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'] : '').getid3_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'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
889						$framedata .= $source_data_array['description'].getid3_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 .= getid3_lib::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->IsValidEmail($source_data_array['email'])) {
910						// https://github.com/JamesHeinrich/getID3/issues/216
911						// https://en.wikipedia.org/wiki/ID3#ID3v2_rating_tag_issue
912						// ID3v2 specs say it should be an email address, but Windows instead uses string like "Windows Media Player 9 Series"
913						$this->warnings[] = 'Invalid Email in '.$frame_name.' ('.$source_data_array['email'].')';
914					}
915					if (!$this->IsWithinBitRange($source_data_array['rating'], 8, false)) {
916						$this->errors[] = 'Invalid Rating byte in '.$frame_name.' ('.$source_data_array['rating'].') (range = 0 to 255)';
917					} else {
918						$framedata .= str_replace("\x00", '', $source_data_array['email'])."\x00";
919						$framedata .= chr($source_data_array['rating']);
920						$framedata .= getid3_lib::BigEndian2String($source_data_array['data'], 4, false);
921					}
922					break;
923
924				case 'RBUF':
925					// 4.18  RBUF Recommended buffer size
926					// Buffer size               $xx xx xx
927					// Embedded info flag        %0000000x
928					// Offset to next tag        $xx xx xx xx
929					if (!$this->IsWithinBitRange($source_data_array['buffersize'], 24, false)) {
930						$this->errors[] = 'Invalid Buffer Size in '.$frame_name;
931					} elseif (!$this->IsWithinBitRange($source_data_array['nexttagoffset'], 32, false)) {
932						$this->errors[] = 'Invalid Offset To Next Tag in '.$frame_name;
933					} else {
934						$framedata .= getid3_lib::BigEndian2String($source_data_array['buffersize'], 3, false);
935						$flag  = '0000000';
936						$flag .= $source_data_array['flags']['embededinfo'] ? '1' : '0';
937						$framedata .= chr(bindec($flag));
938						$framedata .= getid3_lib::BigEndian2String($source_data_array['nexttagoffset'], 4, false);
939					}
940					break;
941
942				case 'AENC':
943					// 4.19  AENC Audio encryption
944					// Owner identifier   <text string> $00
945					// Preview start      $xx xx
946					// Preview length     $xx xx
947					// Encryption info    <binary data>
948					if (!$this->IsWithinBitRange($source_data_array['previewstart'], 16, false)) {
949						$this->errors[] = 'Invalid Preview Start in '.$frame_name.' ('.$source_data_array['previewstart'].')';
950					} elseif (!$this->IsWithinBitRange($source_data_array['previewlength'], 16, false)) {
951						$this->errors[] = 'Invalid Preview Length in '.$frame_name.' ('.$source_data_array['previewlength'].')';
952					} else {
953						$framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
954						$framedata .= getid3_lib::BigEndian2String($source_data_array['previewstart'], 2, false);
955						$framedata .= getid3_lib::BigEndian2String($source_data_array['previewlength'], 2, false);
956						$framedata .= $source_data_array['encryptioninfo'];
957					}
958					break;
959
960				case 'LINK':
961					// 4.20  LINK Linked information
962					// Frame identifier               $xx xx xx xx
963					// URL                            <text string> $00
964					// ID and additional data         <text string(s)>
965					if (!getid3_id3v2::IsValidID3v2FrameName($source_data_array['frameid'], $this->majorversion)) {
966						$this->errors[] = 'Invalid Frame Identifier in '.$frame_name.' ('.$source_data_array['frameid'].')';
967					} elseif (!$this->IsValidURL($source_data_array['data'], true)) {
968						//$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
969						// probably should be an error, need to rewrite IsValidURL() to handle other encodings
970						$this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
971					} 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'] == '')) {
972						$this->errors[] = 'Content Descriptor must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
973					} elseif (($source_data_array['frameid'] == 'USER') && (getid3_id3v2::LanguageLookup($source_data_array['additionaldata'], true) == '')) {
974						$this->errors[] = 'Language must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
975					} elseif (($source_data_array['frameid'] == 'PRIV') && ($source_data_array['additionaldata'] == '')) {
976						$this->errors[] = 'Owner Identifier must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
977					} elseif ((($source_data_array['frameid'] == 'COMM') || ($source_data_array['frameid'] == 'SYLT') || ($source_data_array['frameid'] == 'USLT')) && ((getid3_id3v2::LanguageLookup(substr($source_data_array['additionaldata'], 0, 3), true) == '') || (substr($source_data_array['additionaldata'], 3) == ''))) {
978						$this->errors[] = 'Language followed by Content Descriptor must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
979					} else {
980						$framedata .= $source_data_array['frameid'];
981						$framedata .= str_replace("\x00", '', $source_data_array['data'])."\x00";
982						switch ($source_data_array['frameid']) {
983							case 'COMM':
984							case 'SYLT':
985							case 'USLT':
986							case 'PRIV':
987							case 'USER':
988							case 'AENC':
989							case 'APIC':
990							case 'GEOB':
991							case 'TXXX':
992								$framedata .= $source_data_array['additionaldata'];
993								break;
994							case 'ASPI':
995							case 'ETCO':
996							case 'EQU2':
997							case 'MCID':
998							case 'MLLT':
999							case 'OWNE':
1000							case 'RVA2':
1001							case 'RVRB':
1002							case 'SYTC':
1003							case 'IPLS':
1004							case 'RVAD':
1005							case 'EQUA':
1006								// no additional data required
1007								break;
1008							case 'RBUF':
1009								if ($this->majorversion == 3) {
1010									// no additional data required
1011								} else {
1012									$this->errors[] = $source_data_array['frameid'].' is not a valid Frame Identifier in '.$frame_name.' (in ID3v2.'.$this->majorversion.')';
1013								}
1014								break;
1015
1016							default:
1017								if ((substr($source_data_array['frameid'], 0, 1) == 'T') || (substr($source_data_array['frameid'], 0, 1) == 'W')) {
1018									// no additional data required
1019								} else {
1020									$this->errors[] = $source_data_array['frameid'].' is not a valid Frame Identifier in '.$frame_name.' (in ID3v2.'.$this->majorversion.')';
1021								}
1022								break;
1023						}
1024					}
1025					break;
1026
1027				case 'POSS':
1028					// 4.21  POSS Position synchronisation frame (ID3v2.3+ only)
1029					// Time stamp format         $xx
1030					// Position                  $xx (xx ...)
1031					if (($source_data_array['timestampformat'] < 1) || ($source_data_array['timestampformat'] > 2)) {
1032						$this->errors[] = 'Invalid Time Stamp Format in '.$frame_name.' ('.$source_data_array['timestampformat'].') (valid = 1 or 2)';
1033					} elseif (!$this->IsWithinBitRange($source_data_array['position'], 32, false)) {
1034						$this->errors[] = 'Invalid Position in '.$frame_name.' ('.$source_data_array['position'].') (range = 0 to 4294967295)';
1035					} else {
1036						$framedata .= chr($source_data_array['timestampformat']);
1037						$framedata .= getid3_lib::BigEndian2String($source_data_array['position'], 4, false);
1038					}
1039					break;
1040
1041				case 'USER':
1042					// 4.22  USER Terms of use (ID3v2.3+ only)
1043					// Text encoding        $xx
1044					// Language             $xx xx xx
1045					// The actual text      <text string according to encoding>
1046					$source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
1047					if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
1048						$this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')';
1049					} elseif (getid3_id3v2::LanguageLookup($source_data_array['language'], true) == '') {
1050						$this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')';
1051					} else {
1052						$framedata .= chr($source_data_array['encodingid']);
1053						$framedata .= strtolower($source_data_array['language']);
1054						$framedata .= $source_data_array['data'];
1055					}
1056					break;
1057
1058				case 'OWNE':
1059					// 4.23  OWNE Ownership frame (ID3v2.3+ only)
1060					// Text encoding     $xx
1061					// Price paid        <text string> $00
1062					// Date of purch.    <text string>
1063					// Seller            <text string according to encoding>
1064					$source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
1065					if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
1066						$this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')';
1067					} elseif (!getid3_id3v2::IsANumber($source_data_array['pricepaid']['value'], false)) {
1068						$this->errors[] = 'Invalid Price Paid in '.$frame_name.' ('.$source_data_array['pricepaid']['value'].')';
1069					} elseif (!getid3_id3v2::IsValidDateStampString($source_data_array['purchasedate'])) {
1070						$this->errors[] = 'Invalid Date Of Purchase in '.$frame_name.' ('.$source_data_array['purchasedate'].') (format = YYYYMMDD)';
1071					} else {
1072						$framedata .= chr($source_data_array['encodingid']);
1073						$framedata .= str_replace("\x00", '', $source_data_array['pricepaid']['value'])."\x00";
1074						$framedata .= $source_data_array['purchasedate'];
1075						$framedata .= $source_data_array['seller'];
1076					}
1077					break;
1078
1079				case 'COMR':
1080					// 4.24  COMR Commercial frame (ID3v2.3+ only)
1081					// Text encoding      $xx
1082					// Price string       <text string> $00
1083					// Valid until        <text string>
1084					// Contact URL        <text string> $00
1085					// Received as        $xx
1086					// Name of seller     <text string according to encoding> $00 (00)
1087					// Description        <text string according to encoding> $00 (00)
1088					// Picture MIME type  <string> $00
1089					// Seller logo        <binary data>
1090					$source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
1091					if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
1092						$this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')';
1093					} elseif (!getid3_id3v2::IsValidDateStampString($source_data_array['pricevaliduntil'])) {
1094						$this->errors[] = 'Invalid Valid Until date in '.$frame_name.' ('.$source_data_array['pricevaliduntil'].') (format = YYYYMMDD)';
1095					} elseif (!$this->IsValidURL($source_data_array['contacturl'], false)) {
1096						$this->errors[] = 'Invalid Contact URL in '.$frame_name.' ('.$source_data_array['contacturl'].') (allowed schemes: http, https, ftp, mailto)';
1097					} elseif (!$this->ID3v2IsValidCOMRreceivedAs($source_data_array['receivedasid'])) {
1098						$this->errors[] = 'Invalid Received As byte in '.$frame_name.' ('.$source_data_array['contacturl'].') (range = 0 to 8)';
1099					} elseif (!$this->IsValidMIMEstring($source_data_array['mime'])) {
1100						$this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].')';
1101					} else {
1102						$framedata .= chr($source_data_array['encodingid']);
1103						$pricestrings = array();
1104						foreach ($source_data_array['price'] as $key => $val) {
1105							if ($this->ID3v2IsValidPriceString($key.$val['value'])) {
1106								$pricestrings[] = $key.$val['value'];
1107							} else {
1108								$this->errors[] = 'Invalid Price String in '.$frame_name.' ('.$key.$val['value'].')';
1109							}
1110						}
1111						$framedata .= implode('/', $pricestrings);
1112						$framedata .= $source_data_array['pricevaliduntil'];
1113						$framedata .= str_replace("\x00", '', $source_data_array['contacturl'])."\x00";
1114						$framedata .= chr($source_data_array['receivedasid']);
1115						$framedata .= $source_data_array['sellername'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
1116						$framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
1117						$framedata .= $source_data_array['mime']."\x00";
1118						$framedata .= $source_data_array['logo'];
1119					}
1120					break;
1121
1122				case 'ENCR':
1123					// 4.25  ENCR Encryption method registration (ID3v2.3+ only)
1124					// Owner identifier    <text string> $00
1125					// Method symbol       $xx
1126					// Encryption data     <binary data>
1127					if (!$this->IsWithinBitRange($source_data_array['methodsymbol'], 8, false)) {
1128						$this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['methodsymbol'].') (range = 0 to 255)';
1129					} else {
1130						$framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
1131						$framedata .= ord($source_data_array['methodsymbol']);
1132						$framedata .= $source_data_array['data'];
1133					}
1134					break;
1135
1136				case 'GRID':
1137					// 4.26  GRID Group identification registration (ID3v2.3+ only)
1138					// Owner identifier      <text string> $00
1139					// Group symbol          $xx
1140					// Group dependent data  <binary data>
1141					if (!$this->IsWithinBitRange($source_data_array['groupsymbol'], 8, false)) {
1142						$this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['groupsymbol'].') (range = 0 to 255)';
1143					} else {
1144						$framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
1145						$framedata .= ord($source_data_array['groupsymbol']);
1146						$framedata .= $source_data_array['data'];
1147					}
1148					break;
1149
1150				case 'PRIV':
1151					// 4.27  PRIV Private frame (ID3v2.3+ only)
1152					// Owner identifier      <text string> $00
1153					// The private data      <binary data>
1154					$framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
1155					$framedata .= $source_data_array['data'];
1156					break;
1157
1158				case 'SIGN':
1159					// 4.28  SIGN Signature frame (ID3v2.4+ only)
1160					// Group symbol      $xx
1161					// Signature         <binary data>
1162					if (!$this->IsWithinBitRange($source_data_array['groupsymbol'], 8, false)) {
1163						$this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['groupsymbol'].') (range = 0 to 255)';
1164					} else {
1165						$framedata .= ord($source_data_array['groupsymbol']);
1166						$framedata .= $source_data_array['data'];
1167					}
1168					break;
1169
1170				case 'SEEK':
1171					// 4.29  SEEK Seek frame (ID3v2.4+ only)
1172					// Minimum offset to next tag       $xx xx xx xx
1173					if (!$this->IsWithinBitRange($source_data_array['data'], 32, false)) {
1174						$this->errors[] = 'Invalid Minimum Offset in '.$frame_name.' ('.$source_data_array['data'].') (range = 0 to 4294967295)';
1175					} else {
1176						$framedata .= getid3_lib::BigEndian2String($source_data_array['data'], 4, false);
1177					}
1178					break;
1179
1180				case 'ASPI':
1181					// 4.30  ASPI Audio seek point index (ID3v2.4+ only)
1182					// Indexed data start (S)         $xx xx xx xx
1183					// Indexed data length (L)        $xx xx xx xx
1184					// Number of index points (N)     $xx xx
1185					// Bits per index point (b)       $xx
1186					//   Then for every index point the following data is included:
1187					// Fraction at index (Fi)          $xx (xx)
1188					if (!$this->IsWithinBitRange($source_data_array['datastart'], 32, false)) {
1189						$this->errors[] = 'Invalid Indexed Data Start in '.$frame_name.' ('.$source_data_array['datastart'].') (range = 0 to 4294967295)';
1190					} elseif (!$this->IsWithinBitRange($source_data_array['datalength'], 32, false)) {
1191						$this->errors[] = 'Invalid Indexed Data Length in '.$frame_name.' ('.$source_data_array['datalength'].') (range = 0 to 4294967295)';
1192					} elseif (!$this->IsWithinBitRange($source_data_array['indexpoints'], 16, false)) {
1193						$this->errors[] = 'Invalid Number Of Index Points in '.$frame_name.' ('.$source_data_array['indexpoints'].') (range = 0 to 65535)';
1194					} elseif (!$this->IsWithinBitRange($source_data_array['bitsperpoint'], 8, false)) {
1195						$this->errors[] = 'Invalid Bits Per Index Point in '.$frame_name.' ('.$source_data_array['bitsperpoint'].') (range = 0 to 255)';
1196					} elseif ($source_data_array['indexpoints'] != count($source_data_array['indexes'])) {
1197						$this->errors[] = 'Number Of Index Points does not match actual supplied data in '.$frame_name;
1198					} else {
1199						$framedata .= getid3_lib::BigEndian2String($source_data_array['datastart'], 4, false);
1200						$framedata .= getid3_lib::BigEndian2String($source_data_array['datalength'], 4, false);
1201						$framedata .= getid3_lib::BigEndian2String($source_data_array['indexpoints'], 2, false);
1202						$framedata .= getid3_lib::BigEndian2String($source_data_array['bitsperpoint'], 1, false);
1203						foreach ($source_data_array['indexes'] as $key => $val) {
1204							$framedata .= getid3_lib::BigEndian2String($val, ceil($source_data_array['bitsperpoint'] / 8), false);
1205						}
1206					}
1207					break;
1208
1209				case 'RGAD':
1210					//   RGAD Replay Gain Adjustment
1211					//   http://privatewww.essex.ac.uk/~djmrob/replaygain/
1212					// Peak Amplitude                     $xx $xx $xx $xx
1213					// Radio Replay Gain Adjustment        %aaabbbcd %dddddddd
1214					// Audiophile Replay Gain Adjustment   %aaabbbcd %dddddddd
1215					//   a - name code
1216					//   b - originator code
1217					//   c - sign bit
1218					//   d - replay gain adjustment
1219
1220					if (($source_data_array['track_adjustment'] > 51) || ($source_data_array['track_adjustment'] < -51)) {
1221						$this->errors[] = 'Invalid Track Adjustment in '.$frame_name.' ('.$source_data_array['track_adjustment'].') (range = -51.0 to +51.0)';
1222					} elseif (($source_data_array['album_adjustment'] > 51) || ($source_data_array['album_adjustment'] < -51)) {
1223						$this->errors[] = 'Invalid Album Adjustment in '.$frame_name.' ('.$source_data_array['album_adjustment'].') (range = -51.0 to +51.0)';
1224					} elseif (!$this->ID3v2IsValidRGADname($source_data_array['raw']['track_name'])) {
1225						$this->errors[] = 'Invalid Track Name Code in '.$frame_name.' ('.$source_data_array['raw']['track_name'].') (range = 0 to 2)';
1226					} elseif (!$this->ID3v2IsValidRGADname($source_data_array['raw']['album_name'])) {
1227						$this->errors[] = 'Invalid Album Name Code in '.$frame_name.' ('.$source_data_array['raw']['album_name'].') (range = 0 to 2)';
1228					} elseif (!$this->ID3v2IsValidRGADoriginator($source_data_array['raw']['track_originator'])) {
1229						$this->errors[] = 'Invalid Track Originator Code in '.$frame_name.' ('.$source_data_array['raw']['track_originator'].') (range = 0 to 3)';
1230					} elseif (!$this->ID3v2IsValidRGADoriginator($source_data_array['raw']['album_originator'])) {
1231						$this->errors[] = 'Invalid Album Originator Code in '.$frame_name.' ('.$source_data_array['raw']['album_originator'].') (range = 0 to 3)';
1232					} else {
1233						$framedata .= getid3_lib::Float2String($source_data_array['peakamplitude'], 32);
1234						$framedata .= getid3_lib::RGADgainString($source_data_array['raw']['track_name'], $source_data_array['raw']['track_originator'], $source_data_array['track_adjustment']);
1235						$framedata .= getid3_lib::RGADgainString($source_data_array['raw']['album_name'], $source_data_array['raw']['album_originator'], $source_data_array['album_adjustment']);
1236					}
1237					break;
1238
1239				default:
1240					if (/*(($this->majorversion == 2) && (strlen($frame_name) != 3)) || (($this->majorversion > 2) && (*/strlen($frame_name) != 4/*))*/) {
1241						$this->errors[] = 'Invalid frame name "'.$frame_name.'" for ID3v2.'.$this->majorversion;
1242					} elseif ($frame_name[0] == 'T') {
1243						// 4.2. T???  Text information frames
1244						// Text encoding                $xx
1245						// Information                  <text string(s) according to encoding>
1246						$source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
1247						if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
1248							$this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
1249						} else {
1250							$framedata .= chr($source_data_array['encodingid']);
1251							$framedata .= $source_data_array['data'];
1252						}
1253					} elseif ($frame_name[0] == 'W') {
1254						// 4.3. W???  URL link frames
1255						// URL              <text string>
1256						if (!$this->IsValidURL($source_data_array['data'], false)) {
1257							//$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
1258							// probably should be an error, need to rewrite IsValidURL() to handle other encodings
1259							$this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
1260						} else {
1261							$framedata .= $source_data_array['data'];
1262						}
1263					} else {
1264						$this->errors[] = $frame_name.' not yet supported in $this->GenerateID3v2FrameData()';
1265					}
1266					break;
1267			}
1268		}
1269		if (!empty($this->errors)) {
1270			return false;
1271		}
1272		return $framedata;
1273	}
1274
1275	/**
1276	 * @param string|null $frame_name
1277	 * @param array       $source_data_array
1278	 *
1279	 * @return bool
1280	 */
1281	public function ID3v2FrameIsAllowed($frame_name, $source_data_array) {
1282		static $PreviousFrames = array();
1283
1284		if ($frame_name === null) {
1285			// if the writing functions are called multiple times, the static array needs to be
1286			// cleared - this can be done by calling $this->ID3v2FrameIsAllowed(null, '')
1287			$PreviousFrames = array();
1288			return true;
1289		}
1290		if ($this->majorversion == 4) {
1291			switch ($frame_name) {
1292				case 'UFID':
1293				case 'AENC':
1294				case 'ENCR':
1295				case 'GRID':
1296					if (!isset($source_data_array['ownerid'])) {
1297						$this->errors[] = '[ownerid] not specified for '.$frame_name;
1298					} elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) {
1299						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')';
1300					} else {
1301						$PreviousFrames[] = $frame_name.$source_data_array['ownerid'];
1302					}
1303					break;
1304
1305				case 'TXXX':
1306				case 'WXXX':
1307				case 'RVA2':
1308				case 'EQU2':
1309				case 'APIC':
1310				case 'GEOB':
1311					if (!isset($source_data_array['description'])) {
1312						$this->errors[] = '[description] not specified for '.$frame_name;
1313					} elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) {
1314						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')';
1315					} else {
1316						$PreviousFrames[] = $frame_name.$source_data_array['description'];
1317					}
1318					break;
1319
1320				case 'USER':
1321					if (!isset($source_data_array['language'])) {
1322						$this->errors[] = '[language] not specified for '.$frame_name;
1323					} elseif (in_array($frame_name.$source_data_array['language'], $PreviousFrames)) {
1324						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language ('.$source_data_array['language'].')';
1325					} else {
1326						$PreviousFrames[] = $frame_name.$source_data_array['language'];
1327					}
1328					break;
1329
1330				case 'USLT':
1331				case 'SYLT':
1332				case 'COMM':
1333					if (!isset($source_data_array['language'])) {
1334						$this->errors[] = '[language] not specified for '.$frame_name;
1335					} elseif (!isset($source_data_array['description'])) {
1336						$this->errors[] = '[description] not specified for '.$frame_name;
1337					} elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) {
1338						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')';
1339					} else {
1340						$PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description'];
1341					}
1342					break;
1343
1344				case 'POPM':
1345					if (!isset($source_data_array['email'])) {
1346						$this->errors[] = '[email] not specified for '.$frame_name;
1347					} elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) {
1348						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')';
1349					} else {
1350						$PreviousFrames[] = $frame_name.$source_data_array['email'];
1351					}
1352					break;
1353
1354				case 'IPLS':
1355				case 'MCDI':
1356				case 'ETCO':
1357				case 'MLLT':
1358				case 'SYTC':
1359				case 'RVRB':
1360				case 'PCNT':
1361				case 'RBUF':
1362				case 'POSS':
1363				case 'OWNE':
1364				case 'SEEK':
1365				case 'ASPI':
1366				case 'RGAD':
1367					if (in_array($frame_name, $PreviousFrames)) {
1368						$this->errors[] = 'Only one '.$frame_name.' tag allowed';
1369					} else {
1370						$PreviousFrames[] = $frame_name;
1371					}
1372					break;
1373
1374				case 'LINK':
1375					// this isn't implemented quite right (yet) - it should check the target frame data for compliance
1376					// but right now it just allows one linked frame of each type, to be safe.
1377					if (!isset($source_data_array['frameid'])) {
1378						$this->errors[] = '[frameid] not specified for '.$frame_name;
1379					} elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) {
1380						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')';
1381					} elseif (in_array($source_data_array['frameid'], $PreviousFrames)) {
1382						// no links to singleton tags
1383						$this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')';
1384					} else {
1385						$PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type
1386						$PreviousFrames[] = $source_data_array['frameid'];             // no non-linked singleton tags of this type
1387					}
1388					break;
1389
1390				case 'COMR':
1391					//   There may be more than one 'commercial frame' in a tag, but no two may be identical
1392					// Checking isn't implemented at all (yet) - just assumes that it's OK.
1393					break;
1394
1395				case 'PRIV':
1396				case 'SIGN':
1397					if (!isset($source_data_array['ownerid'])) {
1398						$this->errors[] = '[ownerid] not specified for '.$frame_name;
1399					} elseif (!isset($source_data_array['data'])) {
1400						$this->errors[] = '[data] not specified for '.$frame_name;
1401					} elseif (in_array($frame_name.$source_data_array['ownerid'].$source_data_array['data'], $PreviousFrames)) {
1402						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID + Data ('.$source_data_array['ownerid'].' + '.$source_data_array['data'].')';
1403					} else {
1404						$PreviousFrames[] = $frame_name.$source_data_array['ownerid'].$source_data_array['data'];
1405					}
1406					break;
1407
1408				default:
1409					if (($frame_name[0] != 'T') && ($frame_name[0] != 'W')) {
1410						$this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name;
1411					}
1412					break;
1413			}
1414
1415		} elseif ($this->majorversion == 3) {
1416
1417			switch ($frame_name) {
1418				case 'UFID':
1419				case 'AENC':
1420				case 'ENCR':
1421				case 'GRID':
1422					if (!isset($source_data_array['ownerid'])) {
1423						$this->errors[] = '[ownerid] not specified for '.$frame_name;
1424					} elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) {
1425						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')';
1426					} else {
1427						$PreviousFrames[] = $frame_name.$source_data_array['ownerid'];
1428					}
1429					break;
1430
1431				case 'TXXX':
1432				case 'WXXX':
1433				case 'APIC':
1434				case 'GEOB':
1435					if (!isset($source_data_array['description'])) {
1436						$this->errors[] = '[description] not specified for '.$frame_name;
1437					} elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) {
1438						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')';
1439					} else {
1440						$PreviousFrames[] = $frame_name.$source_data_array['description'];
1441					}
1442					break;
1443
1444				case 'USER':
1445					if (!isset($source_data_array['language'])) {
1446						$this->errors[] = '[language] not specified for '.$frame_name;
1447					} elseif (in_array($frame_name.$source_data_array['language'], $PreviousFrames)) {
1448						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language ('.$source_data_array['language'].')';
1449					} else {
1450						$PreviousFrames[] = $frame_name.$source_data_array['language'];
1451					}
1452					break;
1453
1454				case 'USLT':
1455				case 'SYLT':
1456				case 'COMM':
1457					if (!isset($source_data_array['language'])) {
1458						$this->errors[] = '[language] not specified for '.$frame_name;
1459					} elseif (!isset($source_data_array['description'])) {
1460						$this->errors[] = '[description] not specified for '.$frame_name;
1461					} elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) {
1462						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')';
1463					} else {
1464						$PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description'];
1465					}
1466					break;
1467
1468				case 'POPM':
1469					if (!isset($source_data_array['email'])) {
1470						$this->errors[] = '[email] not specified for '.$frame_name;
1471					} elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) {
1472						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')';
1473					} else {
1474						$PreviousFrames[] = $frame_name.$source_data_array['email'];
1475					}
1476					break;
1477
1478				case 'IPLS':
1479				case 'MCDI':
1480				case 'ETCO':
1481				case 'MLLT':
1482				case 'SYTC':
1483				case 'RVAD':
1484				case 'EQUA':
1485				case 'RVRB':
1486				case 'PCNT':
1487				case 'RBUF':
1488				case 'POSS':
1489				case 'OWNE':
1490				case 'RGAD':
1491					if (in_array($frame_name, $PreviousFrames)) {
1492						$this->errors[] = 'Only one '.$frame_name.' tag allowed';
1493					} else {
1494						$PreviousFrames[] = $frame_name;
1495					}
1496					break;
1497
1498				case 'LINK':
1499					// this isn't implemented quite right (yet) - it should check the target frame data for compliance
1500					// but right now it just allows one linked frame of each type, to be safe.
1501					if (!isset($source_data_array['frameid'])) {
1502						$this->errors[] = '[frameid] not specified for '.$frame_name;
1503					} elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) {
1504						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')';
1505					} elseif (in_array($source_data_array['frameid'], $PreviousFrames)) {
1506						// no links to singleton tags
1507						$this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')';
1508					} else {
1509						$PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type
1510						$PreviousFrames[] = $source_data_array['frameid'];             // no non-linked singleton tags of this type
1511					}
1512					break;
1513
1514				case 'COMR':
1515					//   There may be more than one 'commercial frame' in a tag, but no two may be identical
1516					// Checking isn't implemented at all (yet) - just assumes that it's OK.
1517					break;
1518
1519				case 'PRIV':
1520					if (!isset($source_data_array['ownerid'])) {
1521						$this->errors[] = '[ownerid] not specified for '.$frame_name;
1522					} elseif (!isset($source_data_array['data'])) {
1523						$this->errors[] = '[data] not specified for '.$frame_name;
1524					} elseif (in_array($frame_name.$source_data_array['ownerid'].$source_data_array['data'], $PreviousFrames)) {
1525						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID + Data ('.$source_data_array['ownerid'].' + '.$source_data_array['data'].')';
1526					} else {
1527						$PreviousFrames[] = $frame_name.$source_data_array['ownerid'].$source_data_array['data'];
1528					}
1529					break;
1530
1531				default:
1532					if (($frame_name[0] != 'T') && ($frame_name[0] != 'W')) {
1533						$this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name;
1534					}
1535					break;
1536			}
1537
1538		} elseif ($this->majorversion == 2) {
1539
1540			switch ($frame_name) {
1541				case 'UFI':
1542				case 'CRM':
1543				case 'CRA':
1544					if (!isset($source_data_array['ownerid'])) {
1545						$this->errors[] = '[ownerid] not specified for '.$frame_name;
1546					} elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) {
1547						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')';
1548					} else {
1549						$PreviousFrames[] = $frame_name.$source_data_array['ownerid'];
1550					}
1551					break;
1552
1553				case 'TXX':
1554				case 'WXX':
1555				case 'PIC':
1556				case 'GEO':
1557					if (!isset($source_data_array['description'])) {
1558						$this->errors[] = '[description] not specified for '.$frame_name;
1559					} elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) {
1560						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')';
1561					} else {
1562						$PreviousFrames[] = $frame_name.$source_data_array['description'];
1563					}
1564					break;
1565
1566				case 'ULT':
1567				case 'SLT':
1568				case 'COM':
1569					if (!isset($source_data_array['language'])) {
1570						$this->errors[] = '[language] not specified for '.$frame_name;
1571					} elseif (!isset($source_data_array['description'])) {
1572						$this->errors[] = '[description] not specified for '.$frame_name;
1573					} elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) {
1574						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')';
1575					} else {
1576						$PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description'];
1577					}
1578					break;
1579
1580				case 'POP':
1581					if (!isset($source_data_array['email'])) {
1582						$this->errors[] = '[email] not specified for '.$frame_name;
1583					} elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) {
1584						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')';
1585					} else {
1586						$PreviousFrames[] = $frame_name.$source_data_array['email'];
1587					}
1588					break;
1589
1590				case 'IPL':
1591				case 'MCI':
1592				case 'ETC':
1593				case 'MLL':
1594				case 'STC':
1595				case 'RVA':
1596				case 'EQU':
1597				case 'REV':
1598				case 'CNT':
1599				case 'BUF':
1600					if (in_array($frame_name, $PreviousFrames)) {
1601						$this->errors[] = 'Only one '.$frame_name.' tag allowed';
1602					} else {
1603						$PreviousFrames[] = $frame_name;
1604					}
1605					break;
1606
1607				case 'LNK':
1608					// this isn't implemented quite right (yet) - it should check the target frame data for compliance
1609					// but right now it just allows one linked frame of each type, to be safe.
1610					if (!isset($source_data_array['frameid'])) {
1611						$this->errors[] = '[frameid] not specified for '.$frame_name;
1612					} elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) {
1613						$this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')';
1614					} elseif (in_array($source_data_array['frameid'], $PreviousFrames)) {
1615						// no links to singleton tags
1616						$this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')';
1617					} else {
1618						$PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type
1619						$PreviousFrames[] = $source_data_array['frameid'];             // no non-linked singleton tags of this type
1620					}
1621					break;
1622
1623				default:
1624					if (($frame_name[0] != 'T') && ($frame_name[0] != 'W')) {
1625						$this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name;
1626					}
1627					break;
1628			}
1629		}
1630
1631		if (!empty($this->errors)) {
1632			return false;
1633		}
1634		return true;
1635	}
1636
1637	/**
1638	 * @param bool $noerrorsonly
1639	 *
1640	 * @return string|false
1641	 */
1642	public function GenerateID3v2Tag($noerrorsonly=true) {
1643		$this->ID3v2FrameIsAllowed(null, ''); // clear static array in case this isn't the first call to $this->GenerateID3v2Tag()
1644
1645		$tagstring = '';
1646		if (is_array($this->tag_data)) {
1647			foreach ($this->tag_data as $frame_name => $frame_rawinputdata) {
1648				foreach ($frame_rawinputdata as $irrelevantindex => $source_data_array) {
1649					if (getid3_id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) {
1650						unset($frame_length);
1651						unset($frame_flags);
1652						$frame_data = false;
1653						if ($this->ID3v2FrameIsAllowed($frame_name, $source_data_array)) {
1654							if(array_key_exists('description', $source_data_array) && array_key_exists('encodingid', $source_data_array) && array_key_exists('encoding', $this->tag_data)) {
1655								$source_data_array['description'] = getid3_lib::iconv_fallback($this->tag_data['encoding'], $source_data_array['encoding'], $source_data_array['description']);
1656							}
1657							if ($frame_data = $this->GenerateID3v2FrameData($frame_name, $source_data_array)) {
1658								$FrameUnsynchronisation = false;
1659								if ($this->majorversion >= 4) {
1660									// frame-level unsynchronisation
1661									$unsynchdata = $frame_data;
1662									if ($this->id3v2_use_unsynchronisation) {
1663										$unsynchdata = $this->Unsynchronise($frame_data);
1664									}
1665									if (strlen($unsynchdata) != strlen($frame_data)) {
1666										// unsynchronisation needed
1667										$FrameUnsynchronisation = true;
1668										$frame_data = $unsynchdata;
1669										if (isset($TagUnsynchronisation) && $TagUnsynchronisation === false) {
1670											// only set to true if ALL frames are unsynchronised
1671										} else {
1672											$TagUnsynchronisation = true;
1673										}
1674									} else {
1675										if (isset($TagUnsynchronisation)) {
1676											$TagUnsynchronisation = false;
1677										}
1678									}
1679									unset($unsynchdata);
1680
1681									$frame_length = getid3_lib::BigEndian2String(strlen($frame_data), 4, true);
1682								} else {
1683									$frame_length = getid3_lib::BigEndian2String(strlen($frame_data), 4, false);
1684								}
1685								$frame_flags  = $this->GenerateID3v2FrameFlags($this->ID3v2FrameFlagsLookupTagAlter($frame_name), $this->ID3v2FrameFlagsLookupFileAlter($frame_name), false, false, false, false, $FrameUnsynchronisation, false);
1686							}
1687						} else {
1688							$this->errors[] = 'Frame "'.$frame_name.'" is NOT allowed';
1689						}
1690						if ($frame_data === false) {
1691							$this->errors[] = '$this->GenerateID3v2FrameData() failed for "'.$frame_name.'"';
1692							if ($noerrorsonly) {
1693								return false;
1694							} else {
1695								$frame_name = null;
1696							}
1697						}
1698					} else {
1699						// ignore any invalid frame names, including 'title', 'header', etc
1700						$this->warnings[] = 'Ignoring invalid ID3v2 frame type: "'.$frame_name.'"';
1701						$frame_name = null;
1702						unset($frame_length);
1703						unset($frame_flags);
1704						unset($frame_data);
1705					}
1706					if (null !== $frame_name && isset($frame_length) && isset($frame_flags) && isset($frame_data)) {
1707						$tagstring .= $frame_name.$frame_length.$frame_flags.$frame_data;
1708					}
1709				}
1710			}
1711
1712			if (!isset($TagUnsynchronisation)) {
1713				$TagUnsynchronisation = false;
1714			}
1715			if (($this->majorversion <= 3) && $this->id3v2_use_unsynchronisation) {
1716				// tag-level unsynchronisation
1717				$unsynchdata = $this->Unsynchronise($tagstring);
1718				if (strlen($unsynchdata) != strlen($tagstring)) {
1719					// unsynchronisation needed
1720					$TagUnsynchronisation = true;
1721					$tagstring = $unsynchdata;
1722				}
1723			}
1724
1725			while ($this->paddedlength < (strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion))) {
1726				$this->paddedlength += 1024;
1727			}
1728
1729			$footer = false; // ID3v2 footers not yet supported in getID3()
1730			if (/*!$footer && */($this->paddedlength > (strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion)))) {
1731				// pad up to $paddedlength bytes if unpadded tag is shorter than $paddedlength
1732				// "Furthermore it MUST NOT have any padding when a tag footer is added to the tag."
1733				if (($this->paddedlength - strlen($tagstring) - getid3_id3v2::ID3v2HeaderLength($this->majorversion)) > 0) {
1734					$tagstring .= str_repeat("\x00", $this->paddedlength - strlen($tagstring) - getid3_id3v2::ID3v2HeaderLength($this->majorversion));
1735				}
1736			}
1737			if ($this->id3v2_use_unsynchronisation && (substr($tagstring, strlen($tagstring) - 1, 1) == "\xFF")) {
1738				// special unsynchronisation case:
1739				// if last byte == $FF then appended a $00
1740				$TagUnsynchronisation = true;
1741				$tagstring .= "\x00";
1742			}
1743
1744			$tagheader  = 'ID3';
1745			$tagheader .= chr($this->majorversion);
1746			$tagheader .= chr($this->minorversion);
1747			$tagheader .= $this->GenerateID3v2TagFlags(array('unsynchronisation'=>$TagUnsynchronisation));
1748			$tagheader .= getid3_lib::BigEndian2String(strlen($tagstring), 4, true);
1749
1750			return $tagheader.$tagstring;
1751		}
1752		$this->errors[] = 'tag_data is not an array in GenerateID3v2Tag()';
1753		return false;
1754	}
1755
1756	/**
1757	 * @param string $pricestring
1758	 *
1759	 * @return bool
1760	 */
1761	public function ID3v2IsValidPriceString($pricestring) {
1762		if (getid3_id3v2::LanguageLookup(substr($pricestring, 0, 3), true) == '') {
1763			return false;
1764		} elseif (!getid3_id3v2::IsANumber(substr($pricestring, 3), true)) {
1765			return false;
1766		}
1767		return true;
1768	}
1769
1770	/**
1771	 * @param string $framename
1772	 *
1773	 * @return bool
1774	 */
1775	public function ID3v2FrameFlagsLookupTagAlter($framename) {
1776		// unfinished
1777		switch ($framename) {
1778			case 'RGAD':
1779				$allow = true;
1780				break;
1781			default:
1782				$allow = false;
1783				break;
1784		}
1785		return $allow;
1786	}
1787
1788	/**
1789	 * @param string $framename
1790	 *
1791	 * @return bool
1792	 */
1793	public function ID3v2FrameFlagsLookupFileAlter($framename) {
1794		// unfinished
1795		switch ($framename) {
1796			case 'RGAD':
1797				return false;
1798
1799			default:
1800				return false;
1801		}
1802	}
1803
1804	/**
1805	 * @param int $eventid
1806	 *
1807	 * @return bool
1808	 */
1809	public function ID3v2IsValidETCOevent($eventid) {
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		return true;
1827	}
1828
1829	/**
1830	 * @param int $contenttype
1831	 *
1832	 * @return bool
1833	 */
1834	public function ID3v2IsValidSYLTtype($contenttype) {
1835		if (($contenttype >= 0) && ($contenttype <= 8) && ($this->majorversion == 4)) {
1836			return true;
1837		} elseif (($contenttype >= 0) && ($contenttype <= 6) && ($this->majorversion == 3)) {
1838			return true;
1839		}
1840		return false;
1841	}
1842
1843	/**
1844	 * @param int $channeltype
1845	 *
1846	 * @return bool
1847	 */
1848	public function ID3v2IsValidRVA2channeltype($channeltype) {
1849		if (($channeltype >= 0) && ($channeltype <= 8) && ($this->majorversion == 4)) {
1850			return true;
1851		}
1852		return false;
1853	}
1854
1855	/**
1856	 * @param int $picturetype
1857	 *
1858	 * @return bool
1859	 */
1860	public function ID3v2IsValidAPICpicturetype($picturetype) {
1861		if (($picturetype >= 0) && ($picturetype <= 0x14) && ($this->majorversion >= 2) && ($this->majorversion <= 4)) {
1862			return true;
1863		}
1864		return false;
1865	}
1866
1867	/**
1868	 * @param int|string $imageformat
1869	 *
1870	 * @return bool
1871	 */
1872	public function ID3v2IsValidAPICimageformat($imageformat) {
1873		if ($imageformat == '-->') {
1874			return true;
1875		} elseif ($this->majorversion == 2) {
1876			if ((strlen($imageformat) == 3) && ($imageformat == strtoupper($imageformat))) {
1877				return true;
1878			}
1879		} elseif (($this->majorversion == 3) || ($this->majorversion == 4)) {
1880			if ($this->IsValidMIMEstring($imageformat)) {
1881				return true;
1882			}
1883		}
1884		return false;
1885	}
1886
1887	/**
1888	 * @param int $receivedas
1889	 *
1890	 * @return bool
1891	 */
1892	public function ID3v2IsValidCOMRreceivedAs($receivedas) {
1893		if (($this->majorversion >= 3) && ($receivedas >= 0) && ($receivedas <= 8)) {
1894			return true;
1895		}
1896		return false;
1897	}
1898
1899	/**
1900	 * @param int $RGADname
1901	 *
1902	 * @return bool
1903	 */
1904	public static function ID3v2IsValidRGADname($RGADname) {
1905		if (($RGADname >= 0) && ($RGADname <= 2)) {
1906			return true;
1907		}
1908		return false;
1909	}
1910
1911	/**
1912	 * @param int $RGADoriginator
1913	 *
1914	 * @return bool
1915	 */
1916	public static function ID3v2IsValidRGADoriginator($RGADoriginator) {
1917		if (($RGADoriginator >= 0) && ($RGADoriginator <= 3)) {
1918			return true;
1919		}
1920		return false;
1921	}
1922
1923	/**
1924	 * @param int $textencodingbyte
1925	 *
1926	 * @return bool
1927	 */
1928	public function ID3v2IsValidTextEncoding($textencodingbyte) {
1929		// 0 = ISO-8859-1
1930		// 1 = UTF-16 with BOM
1931		// 2 = UTF-16BE without BOM
1932		// 3 = UTF-8
1933		static $ID3v2IsValidTextEncoding_cache = array(
1934			2 => array(true, true),              // ID3v2.2 - allow 0=ISO-8859-1, 1=UTF-16
1935			3 => array(true, true),              // ID3v2.3 - allow 0=ISO-8859-1, 1=UTF-16
1936			4 => array(true, true, true, true),  // ID3v2.4 - allow 0=ISO-8859-1, 1=UTF-16, 2=UTF-16BE, 3=UTF-8
1937		);
1938		return isset($ID3v2IsValidTextEncoding_cache[$this->majorversion][$textencodingbyte]);
1939	}
1940
1941	/**
1942	 * @param string $data
1943	 *
1944	 * @return string
1945	 */
1946	public static function Unsynchronise($data) {
1947		// Whenever a false synchronisation is found within the tag, one zeroed
1948		// byte is inserted after the first false synchronisation byte. The
1949		// format of a correct sync that should be altered by ID3 encoders is as
1950		// follows:
1951		//      %11111111 111xxxxx
1952		// And should be replaced with:
1953		//      %11111111 00000000 111xxxxx
1954		// This has the side effect that all $FF 00 combinations have to be
1955		// altered, so they won't be affected by the decoding process. Therefore
1956		// all the $FF 00 combinations have to be replaced with the $FF 00 00
1957		// combination during the unsynchronisation.
1958
1959		$data = str_replace("\xFF\x00", "\xFF\x00\x00", $data);
1960		$unsyncheddata = '';
1961		$datalength = strlen($data);
1962		for ($i = 0; $i < $datalength; $i++) {
1963			$thischar = $data[$i];
1964			$unsyncheddata .= $thischar;
1965			if ($thischar == "\xFF") {
1966				$nextchar = ord($data[$i + 1]);
1967				if (($nextchar & 0xE0) == 0xE0) {
1968					// previous byte = 11111111, this byte = 111?????
1969					$unsyncheddata .= "\x00";
1970				}
1971			}
1972		}
1973		return $unsyncheddata;
1974	}
1975
1976	/**
1977	 * @param mixed $var
1978	 *
1979	 * @return bool
1980	 */
1981	public function is_hash($var) {
1982		// written by dev-nullØchristophe*vg
1983		// taken from http://www.php.net/manual/en/function.array-merge-recursive.php
1984		if (is_array($var)) {
1985			$keys = array_keys($var);
1986			$all_num = true;
1987			for ($i = 0; $i < count($keys); $i++) {
1988				if (is_string($keys[$i])) {
1989					return true;
1990				}
1991			}
1992		}
1993		return false;
1994	}
1995
1996	/**
1997	 * @param mixed $arr1
1998	 * @param mixed $arr2
1999	 *
2000	 * @return array
2001	 */
2002	public function array_join_merge($arr1, $arr2) {
2003		// written by dev-nullØchristophe*vg
2004		// taken from http://www.php.net/manual/en/function.array-merge-recursive.php
2005		if (is_array($arr1) && is_array($arr2)) {
2006			// the same -> merge
2007			$new_array = array();
2008
2009			if ($this->is_hash($arr1) && $this->is_hash($arr2)) {
2010				// hashes -> merge based on keys
2011				$keys = array_merge(array_keys($arr1), array_keys($arr2));
2012				foreach ($keys as $key) {
2013					$new_array[$key] = $this->array_join_merge((isset($arr1[$key]) ? $arr1[$key] : ''), (isset($arr2[$key]) ? $arr2[$key] : ''));
2014				}
2015			} else {
2016				// two real arrays -> merge
2017				$new_array = array_reverse(array_unique(array_reverse(array_merge($arr1, $arr2))));
2018			}
2019			return $new_array;
2020		} else {
2021			// not the same ... take new one if defined, else the old one stays
2022			return $arr2 ? $arr2 : $arr1;
2023		}
2024	}
2025
2026	/**
2027	 * @param string $mimestring
2028	 *
2029	 * @return false|int
2030	 */
2031	public static function IsValidMIMEstring($mimestring) {
2032		return preg_match('#^.+/.+$#', $mimestring);
2033	}
2034
2035	/**
2036	 * @param int  $number
2037	 * @param int  $maxbits
2038	 * @param bool $signed
2039	 *
2040	 * @return bool
2041	 */
2042	public static function IsWithinBitRange($number, $maxbits, $signed=false) {
2043		if ($signed) {
2044			if (($number > (0 - pow(2, $maxbits - 1))) && ($number <= pow(2, $maxbits - 1))) {
2045				return true;
2046			}
2047		} else {
2048			if (($number >= 0) && ($number <= pow(2, $maxbits))) {
2049				return true;
2050			}
2051		}
2052		return false;
2053	}
2054
2055	/**
2056	 * @param string $email
2057	 *
2058	 * @return false|int|mixed
2059	 */
2060	public static function IsValidEmail($email) {
2061		if (function_exists('filter_var')) {
2062			return filter_var($email, FILTER_VALIDATE_EMAIL);
2063		}
2064		// VERY crude email validation
2065		return preg_match('#^[^ ]+@[a-z\\-\\.]+\\.[a-z]{2,}$#', $email);
2066	}
2067
2068	/**
2069	 * @param string $url
2070	 * @param bool   $allowUserPass
2071	 *
2072	 * @return bool
2073	 */
2074	public static function IsValidURL($url, $allowUserPass=false) {
2075		if ($url == '') {
2076			return false;
2077		}
2078		if ($allowUserPass !== true) {
2079			if (strstr($url, '@')) {
2080				// in the format http://user:pass@example.com  or http://user@example.com
2081				// but could easily be somebody incorrectly entering an email address in place of a URL
2082				return false;
2083			}
2084		}
2085		// 2016-06-08: relax URL checking to avoid falsely rejecting valid URLs, leave URL validation to the user
2086		// https://www.getid3.org/phpBB3/viewtopic.php?t=1926
2087		return true;
2088		/*
2089		if ($parts = $this->safe_parse_url($url)) {
2090			if (($parts['scheme'] != 'http') && ($parts['scheme'] != 'https') && ($parts['scheme'] != 'ftp') && ($parts['scheme'] != 'gopher')) {
2091				return false;
2092			} 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'])) {
2093				return false;
2094			} elseif (!preg_match('#^([[:alnum:]-]|[\\_])*$#i', $parts['user'], $regs)) {
2095				return false;
2096			} elseif (!preg_match('#^([[:alnum:]-]|[\\_])*$#i', $parts['pass'], $regs)) {
2097				return false;
2098			} elseif (!preg_match('#^[[:alnum:]/_\\.@~-]*$#i', $parts['path'], $regs)) {
2099				return false;
2100			} elseif (!empty($parts['query']) && !preg_match('#^[[:alnum:]?&=+:;_()%\\#/,\\.-]*$#i', $parts['query'], $regs)) {
2101				return false;
2102			} else {
2103				return true;
2104			}
2105		}
2106		return false;
2107		*/
2108	}
2109
2110	/**
2111	 * @param string $url
2112	 *
2113	 * @return array
2114	 */
2115	public static function safe_parse_url($url) {
2116		$parts = @parse_url($url);
2117		$parts['scheme'] = (isset($parts['scheme']) ? $parts['scheme'] : '');
2118		$parts['host']   = (isset($parts['host'])   ? $parts['host']   : '');
2119		$parts['user']   = (isset($parts['user'])   ? $parts['user']   : '');
2120		$parts['pass']   = (isset($parts['pass'])   ? $parts['pass']   : '');
2121		$parts['path']   = (isset($parts['path'])   ? $parts['path']   : '');
2122		$parts['query']  = (isset($parts['query'])  ? $parts['query']  : '');
2123		return $parts;
2124	}
2125
2126	/**
2127	 * @param int    $majorversion
2128	 * @param string $long_description
2129	 *
2130	 * @return string
2131	 */
2132	public static function ID3v2ShortFrameNameLookup($majorversion, $long_description) {
2133		$long_description = str_replace(' ', '_', strtolower(trim($long_description)));
2134		static $ID3v2ShortFrameNameLookup = array();
2135		if (empty($ID3v2ShortFrameNameLookup)) {
2136
2137			// The following are unique to ID3v2.2
2138			$ID3v2ShortFrameNameLookup[2]['recommended_buffer_size']           = 'BUF';
2139			$ID3v2ShortFrameNameLookup[2]['comment']                           = 'COM';
2140			$ID3v2ShortFrameNameLookup[2]['audio_encryption']                  = 'CRA';
2141			$ID3v2ShortFrameNameLookup[2]['encrypted_meta_frame']              = 'CRM';
2142			$ID3v2ShortFrameNameLookup[2]['equalisation']                      = 'EQU';
2143			$ID3v2ShortFrameNameLookup[2]['event_timing_codes']                = 'ETC';
2144			$ID3v2ShortFrameNameLookup[2]['general_encapsulated_object']       = 'GEO';
2145			$ID3v2ShortFrameNameLookup[2]['involved_people_list']              = 'IPL';
2146			$ID3v2ShortFrameNameLookup[2]['linked_information']                = 'LNK';
2147			$ID3v2ShortFrameNameLookup[2]['music_cd_identifier']               = 'MCI';
2148			$ID3v2ShortFrameNameLookup[2]['mpeg_location_lookup_table']        = 'MLL';
2149			$ID3v2ShortFrameNameLookup[2]['attached_picture']                  = 'PIC';
2150			$ID3v2ShortFrameNameLookup[2]['popularimeter']                     = 'POP';
2151			$ID3v2ShortFrameNameLookup[2]['reverb']                            = 'REV';
2152			$ID3v2ShortFrameNameLookup[2]['relative_volume_adjustment']        = 'RVA';
2153			$ID3v2ShortFrameNameLookup[2]['synchronised_lyric']                = 'SLT';
2154			$ID3v2ShortFrameNameLookup[2]['synchronised_tempo_codes']          = 'STC';
2155			$ID3v2ShortFrameNameLookup[2]['album']                             = 'TAL';
2156			$ID3v2ShortFrameNameLookup[2]['beats_per_minute']                  = 'TBP';
2157			$ID3v2ShortFrameNameLookup[2]['bpm']                               = 'TBP';
2158			$ID3v2ShortFrameNameLookup[2]['composer']                          = 'TCM';
2159			$ID3v2ShortFrameNameLookup[2]['genre']                             = 'TCO';
2160			$ID3v2ShortFrameNameLookup[2]['part_of_a_compilation']             = 'TCP';
2161			$ID3v2ShortFrameNameLookup[2]['copyright_message']                 = 'TCR';
2162			$ID3v2ShortFrameNameLookup[2]['date']                              = 'TDA';
2163			$ID3v2ShortFrameNameLookup[2]['playlist_delay']                    = 'TDY';
2164			$ID3v2ShortFrameNameLookup[2]['encoded_by']                        = 'TEN';
2165			$ID3v2ShortFrameNameLookup[2]['file_type']                         = 'TFT';
2166			$ID3v2ShortFrameNameLookup[2]['time']                              = 'TIM';
2167			$ID3v2ShortFrameNameLookup[2]['initial_key']                       = 'TKE';
2168			$ID3v2ShortFrameNameLookup[2]['language']                          = 'TLA';
2169			$ID3v2ShortFrameNameLookup[2]['length']                            = 'TLE';
2170			$ID3v2ShortFrameNameLookup[2]['media_type']                        = 'TMT';
2171			$ID3v2ShortFrameNameLookup[2]['original_artist']                   = 'TOA';
2172			$ID3v2ShortFrameNameLookup[2]['original_filename']                 = 'TOF';
2173			$ID3v2ShortFrameNameLookup[2]['original_lyricist']                 = 'TOL';
2174			$ID3v2ShortFrameNameLookup[2]['original_year']                     = 'TOR';
2175			$ID3v2ShortFrameNameLookup[2]['original_album']                    = 'TOT';
2176			$ID3v2ShortFrameNameLookup[2]['artist']                            = 'TP1';
2177			$ID3v2ShortFrameNameLookup[2]['band']                              = 'TP2';
2178			$ID3v2ShortFrameNameLookup[2]['conductor']                         = 'TP3';
2179			$ID3v2ShortFrameNameLookup[2]['remixer']                           = 'TP4';
2180			$ID3v2ShortFrameNameLookup[2]['part_of_a_set']                     = 'TPA';
2181			$ID3v2ShortFrameNameLookup[2]['publisher']                         = 'TPB';
2182			$ID3v2ShortFrameNameLookup[2]['isrc']                              = 'TRC';
2183			$ID3v2ShortFrameNameLookup[2]['recording_dates']                   = 'TRD';
2184			$ID3v2ShortFrameNameLookup[2]['tracknumber']                       = 'TRK';
2185			$ID3v2ShortFrameNameLookup[2]['track_number']                      = 'TRK';
2186			$ID3v2ShortFrameNameLookup[2]['album_artist_sort_order']           = 'TS2';
2187			$ID3v2ShortFrameNameLookup[2]['album_sort_order']                  = 'TSA';
2188			$ID3v2ShortFrameNameLookup[2]['composer_sort_order']               = 'TSC';
2189			$ID3v2ShortFrameNameLookup[2]['size']                              = 'TSI';
2190			$ID3v2ShortFrameNameLookup[2]['performer_sort_order']              = 'TSP';
2191			$ID3v2ShortFrameNameLookup[2]['encoder_settings']                  = 'TSS';
2192			$ID3v2ShortFrameNameLookup[2]['title_sort_order']                  = 'TST';
2193			$ID3v2ShortFrameNameLookup[2]['content_group_description']         = 'TT1';
2194			$ID3v2ShortFrameNameLookup[2]['title']                             = 'TT2';
2195			$ID3v2ShortFrameNameLookup[2]['subtitle']                          = 'TT3';
2196			$ID3v2ShortFrameNameLookup[2]['lyricist']                          = 'TXT';
2197			$ID3v2ShortFrameNameLookup[2]['text']                              = 'TXX';
2198			$ID3v2ShortFrameNameLookup[2]['year']                              = 'TYE';
2199			$ID3v2ShortFrameNameLookup[2]['unique_file_identifier']            = 'UFI';
2200			$ID3v2ShortFrameNameLookup[2]['unsynchronised_lyric']              = 'ULT';
2201			$ID3v2ShortFrameNameLookup[2]['url_file']                          = 'WAF';
2202			$ID3v2ShortFrameNameLookup[2]['url_artist']                        = 'WAR';
2203			$ID3v2ShortFrameNameLookup[2]['url_source']                        = 'WAS';
2204			$ID3v2ShortFrameNameLookup[2]['commercial_information']            = 'WCM';
2205			$ID3v2ShortFrameNameLookup[2]['copyright']                         = 'WCP';
2206			$ID3v2ShortFrameNameLookup[2]['url_publisher']                     = 'WPB';
2207			$ID3v2ShortFrameNameLookup[2]['url_user']                          = 'WXX';
2208
2209			// The following are common to ID3v2.3 and ID3v2.4
2210			$ID3v2ShortFrameNameLookup[3]['audio_encryption']                  = 'AENC';
2211			$ID3v2ShortFrameNameLookup[3]['attached_picture']                  = 'APIC';
2212			$ID3v2ShortFrameNameLookup[3]['picture']                           = 'APIC';
2213			$ID3v2ShortFrameNameLookup[3]['comment']                           = 'COMM';
2214			$ID3v2ShortFrameNameLookup[3]['commercial_frame']                  = 'COMR';
2215			$ID3v2ShortFrameNameLookup[3]['encryption_method_registration']    = 'ENCR';
2216			$ID3v2ShortFrameNameLookup[3]['event_timing_codes']                = 'ETCO';
2217			$ID3v2ShortFrameNameLookup[3]['general_encapsulated_object']       = 'GEOB';
2218			$ID3v2ShortFrameNameLookup[3]['group_identification_registration'] = 'GRID';
2219			$ID3v2ShortFrameNameLookup[3]['linked_information']                = 'LINK';
2220			$ID3v2ShortFrameNameLookup[3]['music_cd_identifier']               = 'MCDI';
2221			$ID3v2ShortFrameNameLookup[3]['mpeg_location_lookup_table']        = 'MLLT';
2222			$ID3v2ShortFrameNameLookup[3]['ownership_frame']                   = 'OWNE';
2223			$ID3v2ShortFrameNameLookup[3]['play_counter']                      = 'PCNT';
2224			$ID3v2ShortFrameNameLookup[3]['popularimeter']                     = 'POPM';
2225			$ID3v2ShortFrameNameLookup[3]['position_synchronisation_frame']    = 'POSS';
2226			$ID3v2ShortFrameNameLookup[3]['private_frame']                     = 'PRIV';
2227			$ID3v2ShortFrameNameLookup[3]['recommended_buffer_size']           = 'RBUF';
2228			$ID3v2ShortFrameNameLookup[3]['replay_gain_adjustment']            = 'RGAD';
2229			$ID3v2ShortFrameNameLookup[3]['reverb']                            = 'RVRB';
2230			$ID3v2ShortFrameNameLookup[3]['synchronised_lyric']                = 'SYLT';
2231			$ID3v2ShortFrameNameLookup[3]['synchronised_tempo_codes']          = 'SYTC';
2232			$ID3v2ShortFrameNameLookup[3]['album']                             = 'TALB';
2233			$ID3v2ShortFrameNameLookup[3]['beats_per_minute']                  = 'TBPM';
2234			$ID3v2ShortFrameNameLookup[3]['bpm']                               = 'TBPM';
2235			$ID3v2ShortFrameNameLookup[3]['part_of_a_compilation']             = 'TCMP';
2236			$ID3v2ShortFrameNameLookup[3]['composer']                          = 'TCOM';
2237			$ID3v2ShortFrameNameLookup[3]['genre']                             = 'TCON';
2238			$ID3v2ShortFrameNameLookup[3]['copyright_message']                 = 'TCOP';
2239			$ID3v2ShortFrameNameLookup[3]['playlist_delay']                    = 'TDLY';
2240			$ID3v2ShortFrameNameLookup[3]['encoded_by']                        = 'TENC';
2241			$ID3v2ShortFrameNameLookup[3]['lyricist']                          = 'TEXT';
2242			$ID3v2ShortFrameNameLookup[3]['file_type']                         = 'TFLT';
2243			$ID3v2ShortFrameNameLookup[3]['content_group_description']         = 'TIT1';
2244			$ID3v2ShortFrameNameLookup[3]['title']                             = 'TIT2';
2245			$ID3v2ShortFrameNameLookup[3]['subtitle']                          = 'TIT3';
2246			$ID3v2ShortFrameNameLookup[3]['initial_key']                       = 'TKEY';
2247			$ID3v2ShortFrameNameLookup[3]['language']                          = 'TLAN';
2248			$ID3v2ShortFrameNameLookup[3]['length']                            = 'TLEN';
2249			$ID3v2ShortFrameNameLookup[3]['media_type']                        = 'TMED';
2250			$ID3v2ShortFrameNameLookup[3]['original_album']                    = 'TOAL';
2251			$ID3v2ShortFrameNameLookup[3]['original_filename']                 = 'TOFN';
2252			$ID3v2ShortFrameNameLookup[3]['original_lyricist']                 = 'TOLY';
2253			$ID3v2ShortFrameNameLookup[3]['original_artist']                   = 'TOPE';
2254			$ID3v2ShortFrameNameLookup[3]['file_owner']                        = 'TOWN';
2255			$ID3v2ShortFrameNameLookup[3]['artist']                            = 'TPE1';
2256			$ID3v2ShortFrameNameLookup[3]['band']                              = 'TPE2';
2257			$ID3v2ShortFrameNameLookup[3]['conductor']                         = 'TPE3';
2258			$ID3v2ShortFrameNameLookup[3]['remixer']                           = 'TPE4';
2259			$ID3v2ShortFrameNameLookup[3]['part_of_a_set']                     = 'TPOS';
2260			$ID3v2ShortFrameNameLookup[3]['publisher']                         = 'TPUB';
2261			$ID3v2ShortFrameNameLookup[3]['tracknumber']                       = 'TRCK';
2262			$ID3v2ShortFrameNameLookup[3]['track_number']                      = 'TRCK';
2263			$ID3v2ShortFrameNameLookup[3]['internet_radio_station_name']       = 'TRSN';
2264			$ID3v2ShortFrameNameLookup[3]['internet_radio_station_owner']      = 'TRSO';
2265			$ID3v2ShortFrameNameLookup[3]['album_artist_sort_order']           = 'TSO2';
2266			$ID3v2ShortFrameNameLookup[3]['album_sort_order']                  = 'TSOA';
2267			$ID3v2ShortFrameNameLookup[3]['composer_sort_order']               = 'TSOC';
2268			$ID3v2ShortFrameNameLookup[3]['performer_sort_order']              = 'TSOP';
2269			$ID3v2ShortFrameNameLookup[3]['title_sort_order']                  = 'TSOT';
2270			$ID3v2ShortFrameNameLookup[3]['isrc']                              = 'TSRC';
2271			$ID3v2ShortFrameNameLookup[3]['encoder_settings']                  = 'TSSE';
2272			$ID3v2ShortFrameNameLookup[3]['text']                              = 'TXXX';
2273			$ID3v2ShortFrameNameLookup[3]['unique_file_identifier']            = 'UFID';
2274			$ID3v2ShortFrameNameLookup[3]['terms_of_use']                      = 'USER';
2275			$ID3v2ShortFrameNameLookup[3]['unsynchronised_lyric']              = 'USLT';
2276			$ID3v2ShortFrameNameLookup[3]['commercial_information']            = 'WCOM';
2277			$ID3v2ShortFrameNameLookup[3]['copyright']                         = 'WCOP';
2278			$ID3v2ShortFrameNameLookup[3]['url_file']                          = 'WOAF';
2279			$ID3v2ShortFrameNameLookup[3]['url_artist']                        = 'WOAR';
2280			$ID3v2ShortFrameNameLookup[3]['url_source']                        = 'WOAS';
2281			$ID3v2ShortFrameNameLookup[3]['url_station']                       = 'WORS';
2282			$ID3v2ShortFrameNameLookup[3]['url_payment']                       = 'WPAY';
2283			$ID3v2ShortFrameNameLookup[3]['url_publisher']                     = 'WPUB';
2284			$ID3v2ShortFrameNameLookup[3]['url_user']                          = 'WXXX';
2285
2286			// The above are common to ID3v2.3 and ID3v2.4
2287			// so copy them to ID3v2.4 before adding specifics for 2.3 and 2.4
2288			$ID3v2ShortFrameNameLookup[4] = $ID3v2ShortFrameNameLookup[3];
2289
2290			// The following are unique to ID3v2.3
2291			$ID3v2ShortFrameNameLookup[3]['equalisation']                      = 'EQUA';
2292			$ID3v2ShortFrameNameLookup[3]['involved_people_list']              = 'IPLS';
2293			$ID3v2ShortFrameNameLookup[3]['relative_volume_adjustment']        = 'RVAD';
2294			$ID3v2ShortFrameNameLookup[3]['date']                              = 'TDAT';
2295			$ID3v2ShortFrameNameLookup[3]['time']                              = 'TIME';
2296			$ID3v2ShortFrameNameLookup[3]['original_year']                     = 'TORY';
2297			$ID3v2ShortFrameNameLookup[3]['recording_dates']                   = 'TRDA';
2298			$ID3v2ShortFrameNameLookup[3]['size']                              = 'TSIZ';
2299			$ID3v2ShortFrameNameLookup[3]['year']                              = 'TYER';
2300
2301
2302			// The following are unique to ID3v2.4
2303			$ID3v2ShortFrameNameLookup[4]['audio_seek_point_index']            = 'ASPI';
2304			$ID3v2ShortFrameNameLookup[4]['equalisation']                      = 'EQU2';
2305			$ID3v2ShortFrameNameLookup[4]['relative_volume_adjustment']        = 'RVA2';
2306			$ID3v2ShortFrameNameLookup[4]['seek_frame']                        = 'SEEK';
2307			$ID3v2ShortFrameNameLookup[4]['signature_frame']                   = 'SIGN';
2308			$ID3v2ShortFrameNameLookup[4]['encoding_time']                     = 'TDEN';
2309			$ID3v2ShortFrameNameLookup[4]['original_release_time']             = 'TDOR';
2310			$ID3v2ShortFrameNameLookup[4]['recording_time']                    = 'TDRC';
2311			$ID3v2ShortFrameNameLookup[4]['release_time']                      = 'TDRL';
2312			$ID3v2ShortFrameNameLookup[4]['tagging_time']                      = 'TDTG';
2313			$ID3v2ShortFrameNameLookup[4]['involved_people_list']              = 'TIPL';
2314			$ID3v2ShortFrameNameLookup[4]['musician_credits_list']             = 'TMCL';
2315			$ID3v2ShortFrameNameLookup[4]['mood']                              = 'TMOO';
2316			$ID3v2ShortFrameNameLookup[4]['produced_notice']                   = 'TPRO';
2317			$ID3v2ShortFrameNameLookup[4]['album_sort_order']                  = 'TSOA';
2318			$ID3v2ShortFrameNameLookup[4]['performer_sort_order']              = 'TSOP';
2319			$ID3v2ShortFrameNameLookup[4]['title_sort_order']                  = 'TSOT';
2320			$ID3v2ShortFrameNameLookup[4]['set_subtitle']                      = 'TSST';
2321			$ID3v2ShortFrameNameLookup[4]['year']                              = 'TDRC'; // subset of ISO 8601: valid timestamps are yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddTHH, yyyy-MM-ddTHH:mm and yyyy-MM-ddTHH:mm:ss. All time stamps are UTC
2322		}
2323		return (isset($ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)]) ? $ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)] : '');
2324
2325	}
2326
2327}
2328
2329