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