File indexing completed on 2024-05-12 17:26:02

0001 <?php
0002 /////////////////////////////////////////////////////////////////
0003 /// getID3() by James Heinrich <info@getid3.org>               //
0004 //  available at http://getid3.sourceforge.net                 //
0005 //            or http://www.getid3.org                         //
0006 //          also https://github.com/JamesHeinrich/getID3       //
0007 /////////////////////////////////////////////////////////////////
0008 // See readme.txt for more details                             //
0009 /////////////////////////////////////////////////////////////////
0010 ///                                                            //
0011 // write.id3v2.php                                             //
0012 // module for writing ID3v2 tags                               //
0013 // dependencies: module.tag.id3v2.php                          //
0014 //                                                            ///
0015 /////////////////////////////////////////////////////////////////
0016 
0017 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true);
0018 
0019 class getid3_write_id3v2
0020 {
0021   public $filename;
0022   public $tag_data;
0023   public $fread_buffer_size           = 32768;    // read buffer size in bytes
0024   public $paddedlength                = 4096;     // minimum length of ID3v2 tag in bytes
0025   public $majorversion                = 3;        // ID3v2 major version (2, 3 (recommended), 4)
0026   public $minorversion                = 0;        // ID3v2 minor version - always 0
0027   public $merge_existing_data         = false;    // if true, merge new data with existing tags; if false, delete old tag data and only write new tags
0028   public $id3v2_default_encodingid    = 0;        // default text encoding (ISO-8859-1) if not explicitly passed
0029   public $id3v2_use_unsynchronisation = false;    // the specs say it should be TRUE, but most other ID3v2-aware programs are broken if unsynchronization is used, so by default don't use it.
0030   public $warnings                    = array();  // any non-critical errors will be stored here
0031   public $errors                      = array();  // any critical errors will be stored here
0032 
0033   public function getid3_write_id3v2() {
0034     return true;
0035   }
0036 
0037   public function WriteID3v2() {
0038     // File MUST be writeable - CHMOD(646) at least. It's best if the
0039     // directory is also writeable, because that method is both faster and less susceptible to errors.
0040 
0041     if (!empty($this->filename) && (is_writeable($this->filename) || (!file_exists($this->filename) && is_writeable(dirname($this->filename))))) {
0042       // Initialize getID3 engine
0043       $getID3 = new getID3;
0044       $OldThisFileInfo = $getID3->analyze($this->filename);
0045       if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) {
0046         $this->errors[] = 'Unable to write ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
0047         fclose($fp_source);
0048         return false;
0049       }
0050       if ($this->merge_existing_data) {
0051         // merge with existing data
0052         if (!empty($OldThisFileInfo['id3v2'])) {
0053           $this->tag_data = $this->array_join_merge($OldThisFileInfo['id3v2'], $this->tag_data);
0054         }
0055       }
0056       $this->paddedlength = (isset($OldThisFileInfo['id3v2']['headerlength']) ? max($OldThisFileInfo['id3v2']['headerlength'], $this->paddedlength) : $this->paddedlength);
0057 
0058       if ($NewID3v2Tag = $this->GenerateID3v2Tag()) {
0059 
0060         if (file_exists($this->filename) && is_writeable($this->filename) && isset($OldThisFileInfo['id3v2']['headerlength']) && ($OldThisFileInfo['id3v2']['headerlength'] == strlen($NewID3v2Tag))) {
0061 
0062           // best and fastest method - insert-overwrite existing tag (padded to length of old tag if neccesary)
0063           if (file_exists($this->filename)) {
0064 
0065             if (is_readable($this->filename) && is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'r+b'))) {
0066               rewind($fp);
0067               fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag));
0068               fclose($fp);
0069             } else {
0070               $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")';
0071             }
0072 
0073           } else {
0074 
0075             if (is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'wb'))) {
0076               rewind($fp);
0077               fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag));
0078               fclose($fp);
0079             } else {
0080               $this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")';
0081             }
0082 
0083           }
0084 
0085         } else {
0086 
0087           if ($tempfilename = tempnam(GETID3_TEMP_DIR, 'getID3')) {
0088             if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) {
0089               if (is_writable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) {
0090 
0091                 fwrite($fp_temp, $NewID3v2Tag, strlen($NewID3v2Tag));
0092 
0093                 rewind($fp_source);
0094                 if (!empty($OldThisFileInfo['avdataoffset'])) {
0095                   fseek($fp_source, $OldThisFileInfo['avdataoffset']);
0096                 }
0097 
0098                 while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
0099                   fwrite($fp_temp, $buffer, strlen($buffer));
0100                 }
0101 
0102                 fclose($fp_temp);
0103                 fclose($fp_source);
0104                 copy($tempfilename, $this->filename);
0105                 unlink($tempfilename);
0106                 return true;
0107 
0108               } else {
0109                 $this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")';
0110               }
0111               fclose($fp_source);
0112 
0113             } else {
0114               $this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")';
0115             }
0116           }
0117           return false;
0118 
0119         }
0120 
0121       } else {
0122 
0123         $this->errors[] = '$this->GenerateID3v2Tag() failed';
0124 
0125       }
0126 
0127       if (!empty($this->errors)) {
0128         return false;
0129       }
0130       return true;
0131     } else {
0132       $this->errors[] = 'WriteID3v2() failed: !is_writeable('.$this->filename.')';
0133     }
0134     return false;
0135   }
0136 
0137   public function RemoveID3v2() {
0138     // File MUST be writeable - CHMOD(646) at least. It's best if the
0139     // directory is also writeable, because that method is both faster and less susceptible to errors.
0140     if (is_writeable(dirname($this->filename))) {
0141 
0142       // preferred method - only one copying operation, minimal chance of corrupting
0143       // original file if script is interrupted, but required directory to be writeable
0144       if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) {
0145 
0146         // Initialize getID3 engine
0147         $getID3 = new getID3;
0148         $OldThisFileInfo = $getID3->analyze($this->filename);
0149         if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) {
0150           $this->errors[] = 'Unable to remove ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
0151           fclose($fp_source);
0152           return false;
0153         }
0154         rewind($fp_source);
0155         if ($OldThisFileInfo['avdataoffset'] !== false) {
0156           fseek($fp_source, $OldThisFileInfo['avdataoffset']);
0157         }
0158         if (is_writable($this->filename) && is_file($this->filename) && ($fp_temp = fopen($this->filename.'getid3tmp', 'w+b'))) {
0159           while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
0160             fwrite($fp_temp, $buffer, strlen($buffer));
0161           }
0162           fclose($fp_temp);
0163         } else {
0164           $this->errors[] = 'Could not fopen("'.$this->filename.'getid3tmp", "w+b")';
0165         }
0166         fclose($fp_source);
0167       } else {
0168         $this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")';
0169       }
0170       if (file_exists($this->filename)) {
0171         unlink($this->filename);
0172       }
0173       rename($this->filename.'getid3tmp', $this->filename);
0174 
0175     } elseif (is_writable($this->filename)) {
0176 
0177       // less desirable alternate method - double-copies the file, overwrites original file
0178       // and could corrupt source file if the script is interrupted or an error occurs.
0179       if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) {
0180 
0181         // Initialize getID3 engine
0182         $getID3 = new getID3;
0183         $OldThisFileInfo = $getID3->analyze($this->filename);
0184         if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) {
0185           $this->errors[] = 'Unable to remove ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB';
0186           fclose($fp_source);
0187           return false;
0188         }
0189         rewind($fp_source);
0190         if ($OldThisFileInfo['avdataoffset'] !== false) {
0191           fseek($fp_source, $OldThisFileInfo['avdataoffset']);
0192         }
0193         if ($fp_temp = tmpfile()) {
0194           while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
0195             fwrite($fp_temp, $buffer, strlen($buffer));
0196           }
0197           fclose($fp_source);
0198           if (is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'wb'))) {
0199             rewind($fp_temp);
0200             while ($buffer = fread($fp_temp, $this->fread_buffer_size)) {
0201               fwrite($fp_source, $buffer, strlen($buffer));
0202             }
0203             fseek($fp_temp, -128, SEEK_END);
0204             fclose($fp_source);
0205           } else {
0206             $this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")';
0207           }
0208           fclose($fp_temp);
0209         } else {
0210           $this->errors[] = 'Could not create tmpfile()';
0211         }
0212       } else {
0213         $this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")';
0214       }
0215 
0216     } else {
0217 
0218       $this->errors[] = 'Directory and file both not writeable';
0219 
0220     }
0221 
0222     if (!empty($this->errors)) {
0223       return false;
0224     }
0225     return true;
0226   }
0227 
0228 
0229   public function GenerateID3v2TagFlags($flags) {
0230     switch ($this->majorversion) {
0231       case 4:
0232         // %abcd0000
0233         $flag  = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation
0234         $flag .= (!empty($flags['extendedheader']   ) ? '1' : '0'); // b - Extended header
0235         $flag .= (!empty($flags['experimental']     ) ? '1' : '0'); // c - Experimental indicator
0236         $flag .= (!empty($flags['footer']           ) ? '1' : '0'); // d - Footer present
0237         $flag .= '0000';
0238         break;
0239 
0240       case 3:
0241         // %abc00000
0242         $flag  = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation
0243         $flag .= (!empty($flags['extendedheader']   ) ? '1' : '0'); // b - Extended header
0244         $flag .= (!empty($flags['experimental']     ) ? '1' : '0'); // c - Experimental indicator
0245         $flag .= '00000';
0246         break;
0247 
0248       case 2:
0249         // %ab000000
0250         $flag  = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation
0251         $flag .= (!empty($flags['compression']      ) ? '1' : '0'); // b - Compression
0252         $flag .= '000000';
0253         break;
0254 
0255       default:
0256         return false;
0257         break;
0258     }
0259     return chr(bindec($flag));
0260   }
0261 
0262 
0263   public function GenerateID3v2FrameFlags($TagAlter=false, $FileAlter=false, $ReadOnly=false, $Compression=false, $Encryption=false, $GroupingIdentity=false, $Unsynchronisation=false, $DataLengthIndicator=false) {
0264     switch ($this->majorversion) {
0265       case 4:
0266         // %0abc0000 %0h00kmnp
0267         $flag1  = '0';
0268         $flag1 .= $TagAlter  ? '1' : '0'; // a - Tag alter preservation (true == discard)
0269         $flag1 .= $FileAlter ? '1' : '0'; // b - File alter preservation (true == discard)
0270         $flag1 .= $ReadOnly  ? '1' : '0'; // c - Read only (true == read only)
0271         $flag1 .= '0000';
0272 
0273         $flag2  = '0';
0274         $flag2 .= $GroupingIdentity    ? '1' : '0'; // h - Grouping identity (true == contains group information)
0275         $flag2 .= '00';
0276         $flag2 .= $Compression         ? '1' : '0'; // k - Compression (true == compressed)
0277         $flag2 .= $Encryption          ? '1' : '0'; // m - Encryption (true == encrypted)
0278         $flag2 .= $Unsynchronisation   ? '1' : '0'; // n - Unsynchronisation (true == unsynchronised)
0279         $flag2 .= $DataLengthIndicator ? '1' : '0'; // p - Data length indicator (true == data length indicator added)
0280         break;
0281 
0282       case 3:
0283         // %abc00000 %ijk00000
0284         $flag1  = $TagAlter  ? '1' : '0';  // a - Tag alter preservation (true == discard)
0285         $flag1 .= $FileAlter ? '1' : '0';  // b - File alter preservation (true == discard)
0286         $flag1 .= $ReadOnly  ? '1' : '0';  // c - Read only (true == read only)
0287         $flag1 .= '00000';
0288 
0289         $flag2  = $Compression      ? '1' : '0';      // i - Compression (true == compressed)
0290         $flag2 .= $Encryption       ? '1' : '0';      // j - Encryption (true == encrypted)
0291         $flag2 .= $GroupingIdentity ? '1' : '0';      // k - Grouping identity (true == contains group information)
0292         $flag2 .= '00000';
0293         break;
0294 
0295       default:
0296         return false;
0297         break;
0298 
0299     }
0300     return chr(bindec($flag1)).chr(bindec($flag2));
0301   }
0302 
0303   public function GenerateID3v2FrameData($frame_name, $source_data_array) {
0304     if (!getid3_id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) {
0305       return false;
0306     }
0307     $framedata = '';
0308 
0309     if (($this->majorversion < 3) || ($this->majorversion > 4)) {
0310 
0311       $this->errors[] = 'Only ID3v2.3 and ID3v2.4 are supported in GenerateID3v2FrameData()';
0312 
0313     } else { // $this->majorversion 3 or 4
0314 
0315       switch ($frame_name) {
0316         case 'UFID':
0317           // 4.1   UFID Unique file identifier
0318           // Owner identifier        <text string> $00
0319           // Identifier              <up to 64 bytes binary data>
0320           if (strlen($source_data_array['data']) > 64) {
0321             $this->errors[] = 'Identifier not allowed to be longer than 64 bytes in '.$frame_name.' (supplied data was '.strlen($source_data_array['data']).' bytes long)';
0322           } else {
0323             $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
0324             $framedata .= substr($source_data_array['data'], 0, 64); // max 64 bytes - truncate anything longer
0325           }
0326           break;
0327 
0328         case 'TXXX':
0329           // 4.2.2 TXXX User defined text information frame
0330           // Text encoding     $xx
0331           // Description       <text string according to encoding> $00 (00)
0332           // Value             <text string according to encoding>
0333           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0334           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) {
0335             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
0336           } else {
0337             $framedata .= chr($source_data_array['encodingid']);
0338             $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
0339             $framedata .= $source_data_array['data'];
0340           }
0341           break;
0342 
0343         case 'WXXX':
0344           // 4.3.2 WXXX User defined URL link frame
0345           // Text encoding     $xx
0346           // Description       <text string according to encoding> $00 (00)
0347           // URL               <text string>
0348           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0349           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) {
0350             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
0351           } elseif (!isset($source_data_array['data']) || !$this->IsValidURL($source_data_array['data'], false, false)) {
0352             //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
0353             // probably should be an error, need to rewrite IsValidURL() to handle other encodings
0354             $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
0355           } else {
0356             $framedata .= chr($source_data_array['encodingid']);
0357             $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
0358             $framedata .= $source_data_array['data'];
0359           }
0360           break;
0361 
0362         case 'IPLS':
0363           // 4.4  IPLS Involved people list (ID3v2.3 only)
0364           // Text encoding     $xx
0365           // People list strings    <textstrings>
0366           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0367           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) {
0368             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
0369           } else {
0370             $framedata .= chr($source_data_array['encodingid']);
0371             $framedata .= $source_data_array['data'];
0372           }
0373           break;
0374 
0375         case 'MCDI':
0376           // 4.4   MCDI Music CD identifier
0377           // CD TOC                <binary data>
0378           $framedata .= $source_data_array['data'];
0379           break;
0380 
0381         case 'ETCO':
0382           // 4.5   ETCO Event timing codes
0383           // Time stamp format    $xx
0384           //   Where time stamp format is:
0385           // $01  (32-bit value) MPEG frames from beginning of file
0386           // $02  (32-bit value) milliseconds from beginning of file
0387           //   Followed by a list of key events in the following format:
0388           // Type of event   $xx
0389           // Time stamp      $xx (xx ...)
0390           //   The 'Time stamp' is set to zero if directly at the beginning of the sound
0391           //   or after the previous event. All events MUST be sorted in chronological order.
0392           if (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) {
0393             $this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')';
0394           } else {
0395             $framedata .= chr($source_data_array['timestampformat']);
0396             foreach ($source_data_array as $key => $val) {
0397               if (!$this->ID3v2IsValidETCOevent($val['typeid'])) {
0398                 $this->errors[] = 'Invalid Event Type byte in '.$frame_name.' ('.$val['typeid'].')';
0399               } elseif (($key != 'timestampformat') && ($key != 'flags')) {
0400                 if (($val['timestamp'] > 0) && ($previousETCOtimestamp >= $val['timestamp'])) {
0401                   //   The 'Time stamp' is set to zero if directly at the beginning of the sound
0402                   //   or after the previous event. All events MUST be sorted in chronological order.
0403                   $this->errors[] = 'Out-of-order timestamp in '.$frame_name.' ('.$val['timestamp'].') for Event Type ('.$val['typeid'].')';
0404                 } else {
0405                   $framedata .= chr($val['typeid']);
0406                   $framedata .= getid3_lib::BigEndian2String($val['timestamp'], 4, false);
0407                 }
0408               }
0409             }
0410           }
0411           break;
0412 
0413         case 'MLLT':
0414           // 4.6   MLLT MPEG location lookup table
0415           // MPEG frames between reference  $xx xx
0416           // Bytes between reference        $xx xx xx
0417           // Milliseconds between reference $xx xx xx
0418           // Bits for bytes deviation       $xx
0419           // Bits for milliseconds dev.     $xx
0420           //   Then for every reference the following data is included;
0421           // Deviation in bytes         %xxx....
0422           // Deviation in milliseconds  %xxx....
0423           if (($source_data_array['framesbetweenreferences'] > 0) && ($source_data_array['framesbetweenreferences'] <= 65535)) {
0424             $framedata .= getid3_lib::BigEndian2String($source_data_array['framesbetweenreferences'], 2, false);
0425           } else {
0426             $this->errors[] = 'Invalid MPEG Frames Between References in '.$frame_name.' ('.$source_data_array['framesbetweenreferences'].')';
0427           }
0428           if (($source_data_array['bytesbetweenreferences'] > 0) && ($source_data_array['bytesbetweenreferences'] <= 16777215)) {
0429             $framedata .= getid3_lib::BigEndian2String($source_data_array['bytesbetweenreferences'], 3, false);
0430           } else {
0431             $this->errors[] = 'Invalid bytes Between References in '.$frame_name.' ('.$source_data_array['bytesbetweenreferences'].')';
0432           }
0433           if (($source_data_array['msbetweenreferences'] > 0) && ($source_data_array['msbetweenreferences'] <= 16777215)) {
0434             $framedata .= getid3_lib::BigEndian2String($source_data_array['msbetweenreferences'], 3, false);
0435           } else {
0436             $this->errors[] = 'Invalid Milliseconds Between References in '.$frame_name.' ('.$source_data_array['msbetweenreferences'].')';
0437           }
0438           if (!$this->IsWithinBitRange($source_data_array['bitsforbytesdeviation'], 8, false)) {
0439             if (($source_data_array['bitsforbytesdeviation'] % 4) == 0) {
0440               $framedata .= chr($source_data_array['bitsforbytesdeviation']);
0441             } else {
0442               $this->errors[] = 'Bits For Bytes Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].') must be a multiple of 4.';
0443             }
0444           } else {
0445             $this->errors[] = 'Invalid Bits For Bytes Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].')';
0446           }
0447           if (!$this->IsWithinBitRange($source_data_array['bitsformsdeviation'], 8, false)) {
0448             if (($source_data_array['bitsformsdeviation'] % 4) == 0) {
0449               $framedata .= chr($source_data_array['bitsformsdeviation']);
0450             } else {
0451               $this->errors[] = 'Bits For Milliseconds Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].') must be a multiple of 4.';
0452             }
0453           } else {
0454             $this->errors[] = 'Invalid Bits For Milliseconds Deviation in '.$frame_name.' ('.$source_data_array['bitsformsdeviation'].')';
0455           }
0456           foreach ($source_data_array as $key => $val) {
0457             if (($key != 'framesbetweenreferences') && ($key != 'bytesbetweenreferences') && ($key != 'msbetweenreferences') && ($key != 'bitsforbytesdeviation') && ($key != 'bitsformsdeviation') && ($key != 'flags')) {
0458               $unwrittenbitstream .= str_pad(getid3_lib::Dec2Bin($val['bytedeviation']), $source_data_array['bitsforbytesdeviation'], '0', STR_PAD_LEFT);
0459               $unwrittenbitstream .= str_pad(getid3_lib::Dec2Bin($val['msdeviation']),   $source_data_array['bitsformsdeviation'],    '0', STR_PAD_LEFT);
0460             }
0461           }
0462           for ($i = 0; $i < strlen($unwrittenbitstream); $i += 8) {
0463             $highnibble = bindec(substr($unwrittenbitstream, $i, 4)) << 4;
0464             $lownibble  = bindec(substr($unwrittenbitstream, $i + 4, 4));
0465             $framedata .= chr($highnibble & $lownibble);
0466           }
0467           break;
0468 
0469         case 'SYTC':
0470           // 4.7   SYTC Synchronised tempo codes
0471           // Time stamp format   $xx
0472           // Tempo data          <binary data>
0473           //   Where time stamp format is:
0474           // $01  (32-bit value) MPEG frames from beginning of file
0475           // $02  (32-bit value) milliseconds from beginning of file
0476           if (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) {
0477             $this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')';
0478           } else {
0479             $framedata .= chr($source_data_array['timestampformat']);
0480             foreach ($source_data_array as $key => $val) {
0481               if (!$this->ID3v2IsValidETCOevent($val['typeid'])) {
0482                 $this->errors[] = 'Invalid Event Type byte in '.$frame_name.' ('.$val['typeid'].')';
0483               } elseif (($key != 'timestampformat') && ($key != 'flags')) {
0484                 if (($val['tempo'] < 0) || ($val['tempo'] > 510)) {
0485                   $this->errors[] = 'Invalid Tempo (max = 510) in '.$frame_name.' ('.$val['tempo'].') at timestamp ('.$val['timestamp'].')';
0486                 } else {
0487                   if ($val['tempo'] > 255) {
0488                     $framedata .= chr(255);
0489                     $val['tempo'] -= 255;
0490                   }
0491                   $framedata .= chr($val['tempo']);
0492                   $framedata .= getid3_lib::BigEndian2String($val['timestamp'], 4, false);
0493                 }
0494               }
0495             }
0496           }
0497           break;
0498 
0499         case 'USLT':
0500           // 4.8   USLT Unsynchronised lyric/text transcription
0501           // Text encoding        $xx
0502           // Language             $xx xx xx
0503           // Content descriptor   <text string according to encoding> $00 (00)
0504           // Lyrics/text          <full text string according to encoding>
0505           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0506           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
0507             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
0508           } elseif (getid3_id3v2::LanguageLookup($source_data_array['language'], true) == '') {
0509             $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')';
0510           } else {
0511             $framedata .= chr($source_data_array['encodingid']);
0512             $framedata .= strtolower($source_data_array['language']);
0513             $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
0514             $framedata .= $source_data_array['data'];
0515           }
0516           break;
0517 
0518         case 'SYLT':
0519           // 4.9   SYLT Synchronised lyric/text
0520           // Text encoding        $xx
0521           // Language             $xx xx xx
0522           // Time stamp format    $xx
0523           //   $01  (32-bit value) MPEG frames from beginning of file
0524           //   $02  (32-bit value) milliseconds from beginning of file
0525           // Content type         $xx
0526           // Content descriptor   <text string according to encoding> $00 (00)
0527           //   Terminated text to be synced (typically a syllable)
0528           //   Sync identifier (terminator to above string)   $00 (00)
0529           //   Time stamp                                     $xx (xx ...)
0530           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0531           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
0532             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
0533           } elseif (getid3_id3v2::LanguageLookup($source_data_array['language'], true) == '') {
0534             $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')';
0535           } elseif (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) {
0536             $this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')';
0537           } elseif (!$this->ID3v2IsValidSYLTtype($source_data_array['contenttypeid'])) {
0538             $this->errors[] = 'Invalid Content Type byte in '.$frame_name.' ('.$source_data_array['contenttypeid'].')';
0539           } elseif (!is_array($source_data_array['data'])) {
0540             $this->errors[] = 'Invalid Lyric/Timestamp data in '.$frame_name.' (must be an array)';
0541           } else {
0542             $framedata .= chr($source_data_array['encodingid']);
0543             $framedata .= strtolower($source_data_array['language']);
0544             $framedata .= chr($source_data_array['timestampformat']);
0545             $framedata .= chr($source_data_array['contenttypeid']);
0546             $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
0547             ksort($source_data_array['data']);
0548             foreach ($source_data_array['data'] as $key => $val) {
0549               $framedata .= $val['data'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
0550               $framedata .= getid3_lib::BigEndian2String($val['timestamp'], 4, false);
0551             }
0552           }
0553           break;
0554 
0555         case 'COMM':
0556           // 4.10  COMM Comments
0557           // Text encoding          $xx
0558           // Language               $xx xx xx
0559           // Short content descrip. <text string according to encoding> $00 (00)
0560           // The actual text        <full text string according to encoding>
0561           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0562           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
0563             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
0564           } elseif (getid3_id3v2::LanguageLookup($source_data_array['language'], true) == '') {
0565             $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')';
0566           } else {
0567             $framedata .= chr($source_data_array['encodingid']);
0568             $framedata .= strtolower($source_data_array['language']);
0569             $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
0570             $framedata .= $source_data_array['data'];
0571           }
0572           break;
0573 
0574         case 'RVA2':
0575           // 4.11  RVA2 Relative volume adjustment (2) (ID3v2.4+ only)
0576           // Identification          <text string> $00
0577           //   The 'identification' string is used to identify the situation and/or
0578           //   device where this adjustment should apply. The following is then
0579           //   repeated for every channel:
0580           // Type of channel         $xx
0581           // Volume adjustment       $xx xx
0582           // Bits representing peak  $xx
0583           // Peak volume             $xx (xx ...)
0584           $framedata .= str_replace("\x00", '', $source_data_array['description'])."\x00";
0585           foreach ($source_data_array as $key => $val) {
0586             if ($key != 'description') {
0587               $framedata .= chr($val['channeltypeid']);
0588               $framedata .= getid3_lib::BigEndian2String($val['volumeadjust'], 2, false, true); // signed 16-bit
0589               if (!$this->IsWithinBitRange($source_data_array['bitspeakvolume'], 8, false)) {
0590                 $framedata .= chr($val['bitspeakvolume']);
0591                 if ($val['bitspeakvolume'] > 0) {
0592                   $framedata .= getid3_lib::BigEndian2String($val['peakvolume'], ceil($val['bitspeakvolume'] / 8), false, false);
0593                 }
0594               } else {
0595                 $this->errors[] = 'Invalid Bits Representing Peak Volume in '.$frame_name.' ('.$val['bitspeakvolume'].') (range = 0 to 255)';
0596               }
0597             }
0598           }
0599           break;
0600 
0601         case 'RVAD':
0602           // 4.12  RVAD Relative volume adjustment (ID3v2.3 only)
0603           // Increment/decrement     %00fedcba
0604           // Bits used for volume descr.        $xx
0605           // Relative volume change, right      $xx xx (xx ...) // a
0606           // Relative volume change, left       $xx xx (xx ...) // b
0607           // Peak volume right                  $xx xx (xx ...)
0608           // Peak volume left                   $xx xx (xx ...)
0609           // Relative volume change, right back $xx xx (xx ...) // c
0610           // Relative volume change, left back  $xx xx (xx ...) // d
0611           // Peak volume right back             $xx xx (xx ...)
0612           // Peak volume left back              $xx xx (xx ...)
0613           // Relative volume change, center     $xx xx (xx ...) // e
0614           // Peak volume center                 $xx xx (xx ...)
0615           // Relative volume change, bass       $xx xx (xx ...) // f
0616           // Peak volume bass                   $xx xx (xx ...)
0617           if (!$this->IsWithinBitRange($source_data_array['bitsvolume'], 8, false)) {
0618             $this->errors[] = 'Invalid Bits For Volume Description byte in '.$frame_name.' ('.$source_data_array['bitsvolume'].') (range = 1 to 255)';
0619           } else {
0620             $incdecflag .= '00';
0621             $incdecflag .= $source_data_array['incdec']['right']     ? '1' : '0';     // a - Relative volume change, right
0622             $incdecflag .= $source_data_array['incdec']['left']      ? '1' : '0';      // b - Relative volume change, left
0623             $incdecflag .= $source_data_array['incdec']['rightrear'] ? '1' : '0'; // c - Relative volume change, right back
0624             $incdecflag .= $source_data_array['incdec']['leftrear']  ? '1' : '0';  // d - Relative volume change, left back
0625             $incdecflag .= $source_data_array['incdec']['center']    ? '1' : '0';    // e - Relative volume change, center
0626             $incdecflag .= $source_data_array['incdec']['bass']      ? '1' : '0';      // f - Relative volume change, bass
0627             $framedata .= chr(bindec($incdecflag));
0628             $framedata .= chr($source_data_array['bitsvolume']);
0629             $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['right'], ceil($source_data_array['bitsvolume'] / 8), false);
0630             $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['left'],  ceil($source_data_array['bitsvolume'] / 8), false);
0631             $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['right'], ceil($source_data_array['bitsvolume'] / 8), false);
0632             $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['left'],  ceil($source_data_array['bitsvolume'] / 8), false);
0633             if ($source_data_array['volumechange']['rightrear'] || $source_data_array['volumechange']['leftrear'] ||
0634               $source_data_array['peakvolume']['rightrear'] || $source_data_array['peakvolume']['leftrear'] ||
0635               $source_data_array['volumechange']['center'] || $source_data_array['peakvolume']['center'] ||
0636               $source_data_array['volumechange']['bass'] || $source_data_array['peakvolume']['bass']) {
0637                 $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['rightrear'], ceil($source_data_array['bitsvolume']/8), false);
0638                 $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['leftrear'],  ceil($source_data_array['bitsvolume']/8), false);
0639                 $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['rightrear'], ceil($source_data_array['bitsvolume']/8), false);
0640                 $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['leftrear'],  ceil($source_data_array['bitsvolume']/8), false);
0641             }
0642             if ($source_data_array['volumechange']['center'] || $source_data_array['peakvolume']['center'] ||
0643               $source_data_array['volumechange']['bass'] || $source_data_array['peakvolume']['bass']) {
0644                 $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['center'], ceil($source_data_array['bitsvolume']/8), false);
0645                 $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['center'], ceil($source_data_array['bitsvolume']/8), false);
0646             }
0647             if ($source_data_array['volumechange']['bass'] || $source_data_array['peakvolume']['bass']) {
0648                 $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['bass'], ceil($source_data_array['bitsvolume']/8), false);
0649                 $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['bass'], ceil($source_data_array['bitsvolume']/8), false);
0650             }
0651           }
0652           break;
0653 
0654         case 'EQU2':
0655           // 4.12  EQU2 Equalisation (2) (ID3v2.4+ only)
0656           // Interpolation method  $xx
0657           //   $00  Band
0658           //   $01  Linear
0659           // Identification        <text string> $00
0660           //   The following is then repeated for every adjustment point
0661           // Frequency          $xx xx
0662           // Volume adjustment  $xx xx
0663           if (($source_data_array['interpolationmethod'] < 0) || ($source_data_array['interpolationmethod'] > 1)) {
0664             $this->errors[] = 'Invalid Interpolation Method byte in '.$frame_name.' ('.$source_data_array['interpolationmethod'].') (valid = 0 or 1)';
0665           } else {
0666             $framedata .= chr($source_data_array['interpolationmethod']);
0667             $framedata .= str_replace("\x00", '', $source_data_array['description'])."\x00";
0668             foreach ($source_data_array['data'] as $key => $val) {
0669               $framedata .= getid3_lib::BigEndian2String(intval(round($key * 2)), 2, false);
0670               $framedata .= getid3_lib::BigEndian2String($val, 2, false, true); // signed 16-bit
0671             }
0672           }
0673           break;
0674 
0675         case 'EQUA':
0676           // 4.12  EQUA Equalisation (ID3v2.3 only)
0677           // Adjustment bits    $xx
0678           //   This is followed by 2 bytes + ('adjustment bits' rounded up to the
0679           //   nearest byte) for every equalisation band in the following format,
0680           //   giving a frequency range of 0 - 32767Hz:
0681           // Increment/decrement   %x (MSB of the Frequency)
0682           // Frequency             (lower 15 bits)
0683           // Adjustment            $xx (xx ...)
0684           if (!$this->IsWithinBitRange($source_data_array['bitsvolume'], 8, false)) {
0685             $this->errors[] = 'Invalid Adjustment Bits byte in '.$frame_name.' ('.$source_data_array['bitsvolume'].') (range = 1 to 255)';
0686           } else {
0687             $framedata .= chr($source_data_array['adjustmentbits']);
0688             foreach ($source_data_array as $key => $val) {
0689               if ($key != 'bitsvolume') {
0690                 if (($key > 32767) || ($key < 0)) {
0691                   $this->errors[] = 'Invalid Frequency in '.$frame_name.' ('.$key.') (range = 0 to 32767)';
0692                 } else {
0693                   if ($val >= 0) {
0694                     // put MSB of frequency to 1 if increment, 0 if decrement
0695                     $key |= 0x8000;
0696                   }
0697                   $framedata .= getid3_lib::BigEndian2String($key, 2, false);
0698                   $framedata .= getid3_lib::BigEndian2String($val, ceil($source_data_array['adjustmentbits'] / 8), false);
0699                 }
0700               }
0701             }
0702           }
0703           break;
0704 
0705         case 'RVRB':
0706           // 4.13  RVRB Reverb
0707           // Reverb left (ms)                 $xx xx
0708           // Reverb right (ms)                $xx xx
0709           // Reverb bounces, left             $xx
0710           // Reverb bounces, right            $xx
0711           // Reverb feedback, left to left    $xx
0712           // Reverb feedback, left to right   $xx
0713           // Reverb feedback, right to right  $xx
0714           // Reverb feedback, right to left   $xx
0715           // Premix left to right             $xx
0716           // Premix right to left             $xx
0717           if (!$this->IsWithinBitRange($source_data_array['left'], 16, false)) {
0718             $this->errors[] = 'Invalid Reverb Left in '.$frame_name.' ('.$source_data_array['left'].') (range = 0 to 65535)';
0719           } elseif (!$this->IsWithinBitRange($source_data_array['right'], 16, false)) {
0720             $this->errors[] = 'Invalid Reverb Left in '.$frame_name.' ('.$source_data_array['right'].') (range = 0 to 65535)';
0721           } elseif (!$this->IsWithinBitRange($source_data_array['bouncesL'], 8, false)) {
0722             $this->errors[] = 'Invalid Reverb Bounces, Left in '.$frame_name.' ('.$source_data_array['bouncesL'].') (range = 0 to 255)';
0723           } elseif (!$this->IsWithinBitRange($source_data_array['bouncesR'], 8, false)) {
0724             $this->errors[] = 'Invalid Reverb Bounces, Right in '.$frame_name.' ('.$source_data_array['bouncesR'].') (range = 0 to 255)';
0725           } elseif (!$this->IsWithinBitRange($source_data_array['feedbackLL'], 8, false)) {
0726             $this->errors[] = 'Invalid Reverb Feedback, Left-To-Left in '.$frame_name.' ('.$source_data_array['feedbackLL'].') (range = 0 to 255)';
0727           } elseif (!$this->IsWithinBitRange($source_data_array['feedbackLR'], 8, false)) {
0728             $this->errors[] = 'Invalid Reverb Feedback, Left-To-Right in '.$frame_name.' ('.$source_data_array['feedbackLR'].') (range = 0 to 255)';
0729           } elseif (!$this->IsWithinBitRange($source_data_array['feedbackRR'], 8, false)) {
0730             $this->errors[] = 'Invalid Reverb Feedback, Right-To-Right in '.$frame_name.' ('.$source_data_array['feedbackRR'].') (range = 0 to 255)';
0731           } elseif (!$this->IsWithinBitRange($source_data_array['feedbackRL'], 8, false)) {
0732             $this->errors[] = 'Invalid Reverb Feedback, Right-To-Left in '.$frame_name.' ('.$source_data_array['feedbackRL'].') (range = 0 to 255)';
0733           } elseif (!$this->IsWithinBitRange($source_data_array['premixLR'], 8, false)) {
0734             $this->errors[] = 'Invalid Premix, Left-To-Right in '.$frame_name.' ('.$source_data_array['premixLR'].') (range = 0 to 255)';
0735           } elseif (!$this->IsWithinBitRange($source_data_array['premixRL'], 8, false)) {
0736             $this->errors[] = 'Invalid Premix, Right-To-Left in '.$frame_name.' ('.$source_data_array['premixRL'].') (range = 0 to 255)';
0737           } else {
0738             $framedata .= getid3_lib::BigEndian2String($source_data_array['left'], 2, false);
0739             $framedata .= getid3_lib::BigEndian2String($source_data_array['right'], 2, false);
0740             $framedata .= chr($source_data_array['bouncesL']);
0741             $framedata .= chr($source_data_array['bouncesR']);
0742             $framedata .= chr($source_data_array['feedbackLL']);
0743             $framedata .= chr($source_data_array['feedbackLR']);
0744             $framedata .= chr($source_data_array['feedbackRR']);
0745             $framedata .= chr($source_data_array['feedbackRL']);
0746             $framedata .= chr($source_data_array['premixLR']);
0747             $framedata .= chr($source_data_array['premixRL']);
0748           }
0749           break;
0750 
0751         case 'APIC':
0752           // 4.14  APIC Attached picture
0753           // Text encoding      $xx
0754           // MIME type          <text string> $00
0755           // Picture type       $xx
0756           // Description        <text string according to encoding> $00 (00)
0757           // Picture data       <binary data>
0758           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0759           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
0760             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
0761           } elseif (!$this->ID3v2IsValidAPICpicturetype($source_data_array['picturetypeid'])) {
0762             $this->errors[] = 'Invalid Picture Type byte in '.$frame_name.' ('.$source_data_array['picturetypeid'].') for ID3v2.'.$this->majorversion;
0763           } elseif (($this->majorversion >= 3) && (!$this->ID3v2IsValidAPICimageformat($source_data_array['mime']))) {
0764             $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].') for ID3v2.'.$this->majorversion;
0765           } elseif (($source_data_array['mime'] == '-->') && (!$this->IsValidURL($source_data_array['data'], false, false))) {
0766             //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
0767             // probably should be an error, need to rewrite IsValidURL() to handle other encodings
0768             $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
0769           } else {
0770             $framedata .= chr($source_data_array['encodingid']);
0771             $framedata .= str_replace("\x00", '', $source_data_array['mime'])."\x00";
0772             $framedata .= chr($source_data_array['picturetypeid']);
0773             $framedata .= (!empty($source_data_array['description']) ? $source_data_array['description'] : '').getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
0774             $framedata .= $source_data_array['data'];
0775           }
0776           break;
0777 
0778         case 'GEOB':
0779           // 4.15  GEOB General encapsulated object
0780           // Text encoding          $xx
0781           // MIME type              <text string> $00
0782           // Filename               <text string according to encoding> $00 (00)
0783           // Content description    <text string according to encoding> $00 (00)
0784           // Encapsulated object    <binary data>
0785           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0786           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
0787             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
0788           } elseif (!$this->IsValidMIMEstring($source_data_array['mime'])) {
0789             $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].')';
0790           } elseif (!$source_data_array['description']) {
0791             $this->errors[] = 'Missing Description in '.$frame_name;
0792           } else {
0793             $framedata .= chr($source_data_array['encodingid']);
0794             $framedata .= str_replace("\x00", '', $source_data_array['mime'])."\x00";
0795             $framedata .= $source_data_array['filename'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
0796             $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
0797             $framedata .= $source_data_array['data'];
0798           }
0799           break;
0800 
0801         case 'PCNT':
0802           // 4.16  PCNT Play counter
0803           //   When the counter reaches all one's, one byte is inserted in
0804           //   front of the counter thus making the counter eight bits bigger
0805           // Counter        $xx xx xx xx (xx ...)
0806           $framedata .= getid3_lib::BigEndian2String($source_data_array['data'], 4, false);
0807           break;
0808 
0809         case 'POPM':
0810           // 4.17  POPM Popularimeter
0811           //   When the counter reaches all one's, one byte is inserted in
0812           //   front of the counter thus making the counter eight bits bigger
0813           // Email to user   <text string> $00
0814           // Rating          $xx
0815           // Counter         $xx xx xx xx (xx ...)
0816           if (!$this->IsWithinBitRange($source_data_array['rating'], 8, false)) {
0817             $this->errors[] = 'Invalid Rating byte in '.$frame_name.' ('.$source_data_array['rating'].') (range = 0 to 255)';
0818           } elseif (!IsValidEmail($source_data_array['email'])) {
0819             $this->errors[] = 'Invalid Email in '.$frame_name.' ('.$source_data_array['email'].')';
0820           } else {
0821             $framedata .= str_replace("\x00", '', $source_data_array['email'])."\x00";
0822             $framedata .= chr($source_data_array['rating']);
0823             $framedata .= getid3_lib::BigEndian2String($source_data_array['data'], 4, false);
0824           }
0825           break;
0826 
0827         case 'RBUF':
0828           // 4.18  RBUF Recommended buffer size
0829           // Buffer size               $xx xx xx
0830           // Embedded info flag        %0000000x
0831           // Offset to next tag        $xx xx xx xx
0832           if (!$this->IsWithinBitRange($source_data_array['buffersize'], 24, false)) {
0833             $this->errors[] = 'Invalid Buffer Size in '.$frame_name;
0834           } elseif (!$this->IsWithinBitRange($source_data_array['nexttagoffset'], 32, false)) {
0835             $this->errors[] = 'Invalid Offset To Next Tag in '.$frame_name;
0836           } else {
0837             $framedata .= getid3_lib::BigEndian2String($source_data_array['buffersize'], 3, false);
0838             $flag .= '0000000';
0839             $flag .= $source_data_array['flags']['embededinfo'] ? '1' : '0';
0840             $framedata .= chr(bindec($flag));
0841             $framedata .= getid3_lib::BigEndian2String($source_data_array['nexttagoffset'], 4, false);
0842           }
0843           break;
0844 
0845         case 'AENC':
0846           // 4.19  AENC Audio encryption
0847           // Owner identifier   <text string> $00
0848           // Preview start      $xx xx
0849           // Preview length     $xx xx
0850           // Encryption info    <binary data>
0851           if (!$this->IsWithinBitRange($source_data_array['previewstart'], 16, false)) {
0852             $this->errors[] = 'Invalid Preview Start in '.$frame_name.' ('.$source_data_array['previewstart'].')';
0853           } elseif (!$this->IsWithinBitRange($source_data_array['previewlength'], 16, false)) {
0854             $this->errors[] = 'Invalid Preview Length in '.$frame_name.' ('.$source_data_array['previewlength'].')';
0855           } else {
0856             $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
0857             $framedata .= getid3_lib::BigEndian2String($source_data_array['previewstart'], 2, false);
0858             $framedata .= getid3_lib::BigEndian2String($source_data_array['previewlength'], 2, false);
0859             $framedata .= $source_data_array['encryptioninfo'];
0860           }
0861           break;
0862 
0863         case 'LINK':
0864           // 4.20  LINK Linked information
0865           // Frame identifier               $xx xx xx xx
0866           // URL                            <text string> $00
0867           // ID and additional data         <text string(s)>
0868           if (!getid3_id3v2::IsValidID3v2FrameName($source_data_array['frameid'], $this->majorversion)) {
0869             $this->errors[] = 'Invalid Frame Identifier in '.$frame_name.' ('.$source_data_array['frameid'].')';
0870           } elseif (!$this->IsValidURL($source_data_array['data'], true, false)) {
0871             //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
0872             // probably should be an error, need to rewrite IsValidURL() to handle other encodings
0873             $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
0874           } 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'] == '')) {
0875             $this->errors[] = 'Content Descriptor must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
0876           } elseif (($source_data_array['frameid'] == 'USER') && (getid3_id3v2::LanguageLookup($source_data_array['additionaldata'], true) == '')) {
0877             $this->errors[] = 'Language must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
0878           } elseif (($source_data_array['frameid'] == 'PRIV') && ($source_data_array['additionaldata'] == '')) {
0879             $this->errors[] = 'Owner Identifier must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
0880           } 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) == ''))) {
0881             $this->errors[] = 'Language followed by Content Descriptor must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name;
0882           } else {
0883             $framedata .= $source_data_array['frameid'];
0884             $framedata .= str_replace("\x00", '', $source_data_array['data'])."\x00";
0885             switch ($source_data_array['frameid']) {
0886               case 'COMM':
0887               case 'SYLT':
0888               case 'USLT':
0889               case 'PRIV':
0890               case 'USER':
0891               case 'AENC':
0892               case 'APIC':
0893               case 'GEOB':
0894               case 'TXXX':
0895                 $framedata .= $source_data_array['additionaldata'];
0896                 break;
0897               case 'ASPI':
0898               case 'ETCO':
0899               case 'EQU2':
0900               case 'MCID':
0901               case 'MLLT':
0902               case 'OWNE':
0903               case 'RVA2':
0904               case 'RVRB':
0905               case 'SYTC':
0906               case 'IPLS':
0907               case 'RVAD':
0908               case 'EQUA':
0909                 // no additional data required
0910                 break;
0911               case 'RBUF':
0912                 if ($this->majorversion == 3) {
0913                   // no additional data required
0914                 } else {
0915                   $this->errors[] = $source_data_array['frameid'].' is not a valid Frame Identifier in '.$frame_name.' (in ID3v2.'.$this->majorversion.')';
0916                 }
0917 
0918               default:
0919                 if ((substr($source_data_array['frameid'], 0, 1) == 'T') || (substr($source_data_array['frameid'], 0, 1) == 'W')) {
0920                   // no additional data required
0921                 } else {
0922                   $this->errors[] = $source_data_array['frameid'].' is not a valid Frame Identifier in '.$frame_name.' (in ID3v2.'.$this->majorversion.')';
0923                 }
0924                 break;
0925             }
0926           }
0927           break;
0928 
0929         case 'POSS':
0930           // 4.21  POSS Position synchronisation frame (ID3v2.3+ only)
0931           // Time stamp format         $xx
0932           // Position                  $xx (xx ...)
0933           if (($source_data_array['timestampformat'] < 1) || ($source_data_array['timestampformat'] > 2)) {
0934             $this->errors[] = 'Invalid Time Stamp Format in '.$frame_name.' ('.$source_data_array['timestampformat'].') (valid = 1 or 2)';
0935           } elseif (!$this->IsWithinBitRange($source_data_array['position'], 32, false)) {
0936             $this->errors[] = 'Invalid Position in '.$frame_name.' ('.$source_data_array['position'].') (range = 0 to 4294967295)';
0937           } else {
0938             $framedata .= chr($source_data_array['timestampformat']);
0939             $framedata .= getid3_lib::BigEndian2String($source_data_array['position'], 4, false);
0940           }
0941           break;
0942 
0943         case 'USER':
0944           // 4.22  USER Terms of use (ID3v2.3+ only)
0945           // Text encoding        $xx
0946           // Language             $xx xx xx
0947           // The actual text      <text string according to encoding>
0948           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0949           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
0950             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')';
0951           } elseif (getid3_id3v2::LanguageLookup($source_data_array['language'], true) == '') {
0952             $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')';
0953           } else {
0954             $framedata .= chr($source_data_array['encodingid']);
0955             $framedata .= strtolower($source_data_array['language']);
0956             $framedata .= $source_data_array['data'];
0957           }
0958           break;
0959 
0960         case 'OWNE':
0961           // 4.23  OWNE Ownership frame (ID3v2.3+ only)
0962           // Text encoding     $xx
0963           // Price paid        <text string> $00
0964           // Date of purch.    <text string>
0965           // Seller            <text string according to encoding>
0966           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0967           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
0968             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')';
0969           } elseif (!$this->IsANumber($source_data_array['pricepaid']['value'], false)) {
0970             $this->errors[] = 'Invalid Price Paid in '.$frame_name.' ('.$source_data_array['pricepaid']['value'].')';
0971           } elseif (!$this->IsValidDateStampString($source_data_array['purchasedate'])) {
0972             $this->errors[] = 'Invalid Date Of Purchase in '.$frame_name.' ('.$source_data_array['purchasedate'].') (format = YYYYMMDD)';
0973           } else {
0974             $framedata .= chr($source_data_array['encodingid']);
0975             $framedata .= str_replace("\x00", '', $source_data_array['pricepaid']['value'])."\x00";
0976             $framedata .= $source_data_array['purchasedate'];
0977             $framedata .= $source_data_array['seller'];
0978           }
0979           break;
0980 
0981         case 'COMR':
0982           // 4.24  COMR Commercial frame (ID3v2.3+ only)
0983           // Text encoding      $xx
0984           // Price string       <text string> $00
0985           // Valid until        <text string>
0986           // Contact URL        <text string> $00
0987           // Received as        $xx
0988           // Name of seller     <text string according to encoding> $00 (00)
0989           // Description        <text string according to encoding> $00 (00)
0990           // Picture MIME type  <string> $00
0991           // Seller logo        <binary data>
0992           $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
0993           if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
0994             $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')';
0995           } elseif (!$this->IsValidDateStampString($source_data_array['pricevaliduntil'])) {
0996             $this->errors[] = 'Invalid Valid Until date in '.$frame_name.' ('.$source_data_array['pricevaliduntil'].') (format = YYYYMMDD)';
0997           } elseif (!$this->IsValidURL($source_data_array['contacturl'], false, true)) {
0998             $this->errors[] = 'Invalid Contact URL in '.$frame_name.' ('.$source_data_array['contacturl'].') (allowed schemes: http, https, ftp, mailto)';
0999           } elseif (!$this->ID3v2IsValidCOMRreceivedAs($source_data_array['receivedasid'])) {
1000             $this->errors[] = 'Invalid Received As byte in '.$frame_name.' ('.$source_data_array['contacturl'].') (range = 0 to 8)';
1001           } elseif (!$this->IsValidMIMEstring($source_data_array['mime'])) {
1002             $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].')';
1003           } else {
1004             $framedata .= chr($source_data_array['encodingid']);
1005             unset($pricestring);
1006             foreach ($source_data_array['price'] as $key => $val) {
1007               if ($this->ID3v2IsValidPriceString($key.$val['value'])) {
1008                 $pricestrings[] = $key.$val['value'];
1009               } else {
1010                 $this->errors[] = 'Invalid Price String in '.$frame_name.' ('.$key.$val['value'].')';
1011               }
1012             }
1013             $framedata .= implode('/', $pricestrings);
1014             $framedata .= $source_data_array['pricevaliduntil'];
1015             $framedata .= str_replace("\x00", '', $source_data_array['contacturl'])."\x00";
1016             $framedata .= chr($source_data_array['receivedasid']);
1017             $framedata .= $source_data_array['sellername'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
1018             $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']);
1019             $framedata .= $source_data_array['mime']."\x00";
1020             $framedata .= $source_data_array['logo'];
1021           }
1022           break;
1023 
1024         case 'ENCR':
1025           // 4.25  ENCR Encryption method registration (ID3v2.3+ only)
1026           // Owner identifier    <text string> $00
1027           // Method symbol       $xx
1028           // Encryption data     <binary data>
1029           if (!$this->IsWithinBitRange($source_data_array['methodsymbol'], 8, false)) {
1030             $this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['methodsymbol'].') (range = 0 to 255)';
1031           } else {
1032             $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
1033             $framedata .= ord($source_data_array['methodsymbol']);
1034             $framedata .= $source_data_array['data'];
1035           }
1036           break;
1037 
1038         case 'GRID':
1039           // 4.26  GRID Group identification registration (ID3v2.3+ only)
1040           // Owner identifier      <text string> $00
1041           // Group symbol          $xx
1042           // Group dependent data  <binary data>
1043           if (!$this->IsWithinBitRange($source_data_array['groupsymbol'], 8, false)) {
1044             $this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['groupsymbol'].') (range = 0 to 255)';
1045           } else {
1046             $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
1047             $framedata .= ord($source_data_array['groupsymbol']);
1048             $framedata .= $source_data_array['data'];
1049           }
1050           break;
1051 
1052         case 'PRIV':
1053           // 4.27  PRIV Private frame (ID3v2.3+ only)
1054           // Owner identifier      <text string> $00
1055           // The private data      <binary data>
1056           $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00";
1057           $framedata .= $source_data_array['data'];
1058           break;
1059 
1060         case 'SIGN':
1061           // 4.28  SIGN Signature frame (ID3v2.4+ only)
1062           // Group symbol      $xx
1063           // Signature         <binary data>
1064           if (!$this->IsWithinBitRange($source_data_array['groupsymbol'], 8, false)) {
1065             $this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['groupsymbol'].') (range = 0 to 255)';
1066           } else {
1067             $framedata .= ord($source_data_array['groupsymbol']);
1068             $framedata .= $source_data_array['data'];
1069           }
1070           break;
1071 
1072         case 'SEEK':
1073           // 4.29  SEEK Seek frame (ID3v2.4+ only)
1074           // Minimum offset to next tag       $xx xx xx xx
1075           if (!$this->IsWithinBitRange($source_data_array['data'], 32, false)) {
1076             $this->errors[] = 'Invalid Minimum Offset in '.$frame_name.' ('.$source_data_array['data'].') (range = 0 to 4294967295)';
1077           } else {
1078             $framedata .= getid3_lib::BigEndian2String($source_data_array['data'], 4, false);
1079           }
1080           break;
1081 
1082         case 'ASPI':
1083           // 4.30  ASPI Audio seek point index (ID3v2.4+ only)
1084           // Indexed data start (S)         $xx xx xx xx
1085           // Indexed data length (L)        $xx xx xx xx
1086           // Number of index points (N)     $xx xx
1087           // Bits per index point (b)       $xx
1088           //   Then for every index point the following data is included:
1089           // Fraction at index (Fi)          $xx (xx)
1090           if (!$this->IsWithinBitRange($source_data_array['datastart'], 32, false)) {
1091             $this->errors[] = 'Invalid Indexed Data Start in '.$frame_name.' ('.$source_data_array['datastart'].') (range = 0 to 4294967295)';
1092           } elseif (!$this->IsWithinBitRange($source_data_array['datalength'], 32, false)) {
1093             $this->errors[] = 'Invalid Indexed Data Length in '.$frame_name.' ('.$source_data_array['datalength'].') (range = 0 to 4294967295)';
1094           } elseif (!$this->IsWithinBitRange($source_data_array['indexpoints'], 16, false)) {
1095             $this->errors[] = 'Invalid Number Of Index Points in '.$frame_name.' ('.$source_data_array['indexpoints'].') (range = 0 to 65535)';
1096           } elseif (!$this->IsWithinBitRange($source_data_array['bitsperpoint'], 8, false)) {
1097             $this->errors[] = 'Invalid Bits Per Index Point in '.$frame_name.' ('.$source_data_array['bitsperpoint'].') (range = 0 to 255)';
1098           } elseif ($source_data_array['indexpoints'] != count($source_data_array['indexes'])) {
1099             $this->errors[] = 'Number Of Index Points does not match actual supplied data in '.$frame_name;
1100           } else {
1101             $framedata .= getid3_lib::BigEndian2String($source_data_array['datastart'], 4, false);
1102             $framedata .= getid3_lib::BigEndian2String($source_data_array['datalength'], 4, false);
1103             $framedata .= getid3_lib::BigEndian2String($source_data_array['indexpoints'], 2, false);
1104             $framedata .= getid3_lib::BigEndian2String($source_data_array['bitsperpoint'], 1, false);
1105             foreach ($source_data_array['indexes'] as $key => $val) {
1106               $framedata .= getid3_lib::BigEndian2String($val, ceil($source_data_array['bitsperpoint'] / 8), false);
1107             }
1108           }
1109           break;
1110 
1111         case 'RGAD':
1112           //   RGAD Replay Gain Adjustment
1113           //   http://privatewww.essex.ac.uk/~djmrob/replaygain/
1114           // Peak Amplitude                     $xx $xx $xx $xx
1115           // Radio Replay Gain Adjustment        %aaabbbcd %dddddddd
1116           // Audiophile Replay Gain Adjustment   %aaabbbcd %dddddddd
1117           //   a - name code
1118           //   b - originator code
1119           //   c - sign bit
1120           //   d - replay gain adjustment
1121 
1122           if (($source_data_array['track_adjustment'] > 51) || ($source_data_array['track_adjustment'] < -51)) {
1123             $this->errors[] = 'Invalid Track Adjustment in '.$frame_name.' ('.$source_data_array['track_adjustment'].') (range = -51.0 to +51.0)';
1124           } elseif (($source_data_array['album_adjustment'] > 51) || ($source_data_array['album_adjustment'] < -51)) {
1125             $this->errors[] = 'Invalid Album Adjustment in '.$frame_name.' ('.$source_data_array['album_adjustment'].') (range = -51.0 to +51.0)';
1126           } elseif (!$this->ID3v2IsValidRGADname($source_data_array['raw']['track_name'])) {
1127             $this->errors[] = 'Invalid Track Name Code in '.$frame_name.' ('.$source_data_array['raw']['track_name'].') (range = 0 to 2)';
1128           } elseif (!$this->ID3v2IsValidRGADname($source_data_array['raw']['album_name'])) {
1129             $this->errors[] = 'Invalid Album Name Code in '.$frame_name.' ('.$source_data_array['raw']['album_name'].') (range = 0 to 2)';
1130           } elseif (!$this->ID3v2IsValidRGADoriginator($source_data_array['raw']['track_originator'])) {
1131             $this->errors[] = 'Invalid Track Originator Code in '.$frame_name.' ('.$source_data_array['raw']['track_originator'].') (range = 0 to 3)';
1132           } elseif (!$this->ID3v2IsValidRGADoriginator($source_data_array['raw']['album_originator'])) {
1133             $this->errors[] = 'Invalid Album Originator Code in '.$frame_name.' ('.$source_data_array['raw']['album_originator'].') (range = 0 to 3)';
1134           } else {
1135             $framedata .= getid3_lib::Float2String($source_data_array['peakamplitude'], 32);
1136             $framedata .= getid3_lib::RGADgainString($source_data_array['raw']['track_name'], $source_data_array['raw']['track_originator'], $source_data_array['track_adjustment']);
1137             $framedata .= getid3_lib::RGADgainString($source_data_array['raw']['album_name'], $source_data_array['raw']['album_originator'], $source_data_array['album_adjustment']);
1138           }
1139           break;
1140 
1141         default:
1142           if ((($this->majorversion == 2) && (strlen($frame_name) != 3)) || (($this->majorversion > 2) && (strlen($frame_name) != 4))) {
1143             $this->errors[] = 'Invalid frame name "'.$frame_name.'" for ID3v2.'.$this->majorversion;
1144           } elseif ($frame_name{0} == 'T') {
1145             // 4.2. T???  Text information frames
1146             // Text encoding                $xx
1147             // Information                  <text string(s) according to encoding>
1148             $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid);
1149             if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) {
1150               $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion;
1151             } else {
1152               $framedata .= chr($source_data_array['encodingid']);
1153               $framedata .= $source_data_array['data'];
1154             }
1155           } elseif ($frame_name{0} == 'W') {
1156             // 4.3. W???  URL link frames
1157             // URL              <text string>
1158             if (!$this->IsValidURL($source_data_array['data'], false, false)) {
1159               //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
1160               // probably should be an error, need to rewrite IsValidURL() to handle other encodings
1161               $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')';
1162             } else {
1163               $framedata .= $source_data_array['data'];
1164             }
1165           } else {
1166             $this->errors[] = $frame_name.' not yet supported in $this->GenerateID3v2FrameData()';
1167           }
1168           break;
1169       }
1170     }
1171     if (!empty($this->errors)) {
1172       return false;
1173     }
1174     return $framedata;
1175   }
1176 
1177   public function ID3v2FrameIsAllowed($frame_name, $source_data_array) {
1178     static $PreviousFrames = array();
1179 
1180     if ($frame_name === null) {
1181       // if the writing functions are called multiple times, the static array needs to be
1182       // cleared - this can be done by calling $this->ID3v2FrameIsAllowed(null, '')
1183       $PreviousFrames = array();
1184       return true;
1185     }
1186 
1187     if ($this->majorversion == 4) {
1188       switch ($frame_name) {
1189         case 'UFID':
1190         case 'AENC':
1191         case 'ENCR':
1192         case 'GRID':
1193           if (!isset($source_data_array['ownerid'])) {
1194             $this->errors[] = '[ownerid] not specified for '.$frame_name;
1195           } elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) {
1196             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')';
1197           } else {
1198             $PreviousFrames[] = $frame_name.$source_data_array['ownerid'];
1199           }
1200           break;
1201 
1202         case 'TXXX':
1203         case 'WXXX':
1204         case 'RVA2':
1205         case 'EQU2':
1206         case 'APIC':
1207         case 'GEOB':
1208           if (!isset($source_data_array['description'])) {
1209             $this->errors[] = '[description] not specified for '.$frame_name;
1210           } elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) {
1211             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')';
1212           } else {
1213             $PreviousFrames[] = $frame_name.$source_data_array['description'];
1214           }
1215           break;
1216 
1217         case 'USER':
1218           if (!isset($source_data_array['language'])) {
1219             $this->errors[] = '[language] not specified for '.$frame_name;
1220           } elseif (in_array($frame_name.$source_data_array['language'], $PreviousFrames)) {
1221             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language ('.$source_data_array['language'].')';
1222           } else {
1223             $PreviousFrames[] = $frame_name.$source_data_array['language'];
1224           }
1225           break;
1226 
1227         case 'USLT':
1228         case 'SYLT':
1229         case 'COMM':
1230           if (!isset($source_data_array['language'])) {
1231             $this->errors[] = '[language] not specified for '.$frame_name;
1232           } elseif (!isset($source_data_array['description'])) {
1233             $this->errors[] = '[description] not specified for '.$frame_name;
1234           } elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) {
1235             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')';
1236           } else {
1237             $PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description'];
1238           }
1239           break;
1240 
1241         case 'POPM':
1242           if (!isset($source_data_array['email'])) {
1243             $this->errors[] = '[email] not specified for '.$frame_name;
1244           } elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) {
1245             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')';
1246           } else {
1247             $PreviousFrames[] = $frame_name.$source_data_array['email'];
1248           }
1249           break;
1250 
1251         case 'IPLS':
1252         case 'MCDI':
1253         case 'ETCO':
1254         case 'MLLT':
1255         case 'SYTC':
1256         case 'RVRB':
1257         case 'PCNT':
1258         case 'RBUF':
1259         case 'POSS':
1260         case 'OWNE':
1261         case 'SEEK':
1262         case 'ASPI':
1263         case 'RGAD':
1264           if (in_array($frame_name, $PreviousFrames)) {
1265             $this->errors[] = 'Only one '.$frame_name.' tag allowed';
1266           } else {
1267             $PreviousFrames[] = $frame_name;
1268           }
1269           break;
1270 
1271         case 'LINK':
1272           // this isn't implemented quite right (yet) - it should check the target frame data for compliance
1273           // but right now it just allows one linked frame of each type, to be safe.
1274           if (!isset($source_data_array['frameid'])) {
1275             $this->errors[] = '[frameid] not specified for '.$frame_name;
1276           } elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) {
1277             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')';
1278           } elseif (in_array($source_data_array['frameid'], $PreviousFrames)) {
1279             // no links to singleton tags
1280             $this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')';
1281           } else {
1282             $PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type
1283             $PreviousFrames[] = $source_data_array['frameid'];             // no non-linked singleton tags of this type
1284           }
1285           break;
1286 
1287         case 'COMR':
1288           //   There may be more than one 'commercial frame' in a tag, but no two may be identical
1289           // Checking isn't implemented at all (yet) - just assumes that it's OK.
1290           break;
1291 
1292         case 'PRIV':
1293         case 'SIGN':
1294           if (!isset($source_data_array['ownerid'])) {
1295             $this->errors[] = '[ownerid] not specified for '.$frame_name;
1296           } elseif (!isset($source_data_array['data'])) {
1297             $this->errors[] = '[data] not specified for '.$frame_name;
1298           } elseif (in_array($frame_name.$source_data_array['ownerid'].$source_data_array['data'], $PreviousFrames)) {
1299             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID + Data ('.$source_data_array['ownerid'].' + '.$source_data_array['data'].')';
1300           } else {
1301             $PreviousFrames[] = $frame_name.$source_data_array['ownerid'].$source_data_array['data'];
1302           }
1303           break;
1304 
1305         default:
1306           if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) {
1307             $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name;
1308           }
1309           break;
1310       }
1311 
1312     } elseif ($this->majorversion == 3) {
1313 
1314       switch ($frame_name) {
1315         case 'UFID':
1316         case 'AENC':
1317         case 'ENCR':
1318         case 'GRID':
1319           if (!isset($source_data_array['ownerid'])) {
1320             $this->errors[] = '[ownerid] not specified for '.$frame_name;
1321           } elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) {
1322             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')';
1323           } else {
1324             $PreviousFrames[] = $frame_name.$source_data_array['ownerid'];
1325           }
1326           break;
1327 
1328         case 'TXXX':
1329         case 'WXXX':
1330         case 'APIC':
1331         case 'GEOB':
1332           if (!isset($source_data_array['description'])) {
1333             $this->errors[] = '[description] not specified for '.$frame_name;
1334           } elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) {
1335             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')';
1336           } else {
1337             $PreviousFrames[] = $frame_name.$source_data_array['description'];
1338           }
1339           break;
1340 
1341         case 'USER':
1342           if (!isset($source_data_array['language'])) {
1343             $this->errors[] = '[language] not specified for '.$frame_name;
1344           } elseif (in_array($frame_name.$source_data_array['language'], $PreviousFrames)) {
1345             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language ('.$source_data_array['language'].')';
1346           } else {
1347             $PreviousFrames[] = $frame_name.$source_data_array['language'];
1348           }
1349           break;
1350 
1351         case 'USLT':
1352         case 'SYLT':
1353         case 'COMM':
1354           if (!isset($source_data_array['language'])) {
1355             $this->errors[] = '[language] not specified for '.$frame_name;
1356           } elseif (!isset($source_data_array['description'])) {
1357             $this->errors[] = '[description] not specified for '.$frame_name;
1358           } elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) {
1359             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')';
1360           } else {
1361             $PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description'];
1362           }
1363           break;
1364 
1365         case 'POPM':
1366           if (!isset($source_data_array['email'])) {
1367             $this->errors[] = '[email] not specified for '.$frame_name;
1368           } elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) {
1369             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')';
1370           } else {
1371             $PreviousFrames[] = $frame_name.$source_data_array['email'];
1372           }
1373           break;
1374 
1375         case 'IPLS':
1376         case 'MCDI':
1377         case 'ETCO':
1378         case 'MLLT':
1379         case 'SYTC':
1380         case 'RVAD':
1381         case 'EQUA':
1382         case 'RVRB':
1383         case 'PCNT':
1384         case 'RBUF':
1385         case 'POSS':
1386         case 'OWNE':
1387         case 'RGAD':
1388           if (in_array($frame_name, $PreviousFrames)) {
1389             $this->errors[] = 'Only one '.$frame_name.' tag allowed';
1390           } else {
1391             $PreviousFrames[] = $frame_name;
1392           }
1393           break;
1394 
1395         case 'LINK':
1396           // this isn't implemented quite right (yet) - it should check the target frame data for compliance
1397           // but right now it just allows one linked frame of each type, to be safe.
1398           if (!isset($source_data_array['frameid'])) {
1399             $this->errors[] = '[frameid] not specified for '.$frame_name;
1400           } elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) {
1401             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')';
1402           } elseif (in_array($source_data_array['frameid'], $PreviousFrames)) {
1403             // no links to singleton tags
1404             $this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')';
1405           } else {
1406             $PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type
1407             $PreviousFrames[] = $source_data_array['frameid'];             // no non-linked singleton tags of this type
1408           }
1409           break;
1410 
1411         case 'COMR':
1412           //   There may be more than one 'commercial frame' in a tag, but no two may be identical
1413           // Checking isn't implemented at all (yet) - just assumes that it's OK.
1414           break;
1415 
1416         case 'PRIV':
1417           if (!isset($source_data_array['ownerid'])) {
1418             $this->errors[] = '[ownerid] not specified for '.$frame_name;
1419           } elseif (!isset($source_data_array['data'])) {
1420             $this->errors[] = '[data] not specified for '.$frame_name;
1421           } elseif (in_array($frame_name.$source_data_array['ownerid'].$source_data_array['data'], $PreviousFrames)) {
1422             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID + Data ('.$source_data_array['ownerid'].' + '.$source_data_array['data'].')';
1423           } else {
1424             $PreviousFrames[] = $frame_name.$source_data_array['ownerid'].$source_data_array['data'];
1425           }
1426           break;
1427 
1428         default:
1429           if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) {
1430             $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name;
1431           }
1432           break;
1433       }
1434 
1435     } elseif ($this->majorversion == 2) {
1436 
1437       switch ($frame_name) {
1438         case 'UFI':
1439         case 'CRM':
1440         case 'CRA':
1441           if (!isset($source_data_array['ownerid'])) {
1442             $this->errors[] = '[ownerid] not specified for '.$frame_name;
1443           } elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) {
1444             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')';
1445           } else {
1446             $PreviousFrames[] = $frame_name.$source_data_array['ownerid'];
1447           }
1448           break;
1449 
1450         case 'TXX':
1451         case 'WXX':
1452         case 'PIC':
1453         case 'GEO':
1454           if (!isset($source_data_array['description'])) {
1455             $this->errors[] = '[description] not specified for '.$frame_name;
1456           } elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) {
1457             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')';
1458           } else {
1459             $PreviousFrames[] = $frame_name.$source_data_array['description'];
1460           }
1461           break;
1462 
1463         case 'ULT':
1464         case 'SLT':
1465         case 'COM':
1466           if (!isset($source_data_array['language'])) {
1467             $this->errors[] = '[language] not specified for '.$frame_name;
1468           } elseif (!isset($source_data_array['description'])) {
1469             $this->errors[] = '[description] not specified for '.$frame_name;
1470           } elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) {
1471             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')';
1472           } else {
1473             $PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description'];
1474           }
1475           break;
1476 
1477         case 'POP':
1478           if (!isset($source_data_array['email'])) {
1479             $this->errors[] = '[email] not specified for '.$frame_name;
1480           } elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) {
1481             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')';
1482           } else {
1483             $PreviousFrames[] = $frame_name.$source_data_array['email'];
1484           }
1485           break;
1486 
1487         case 'IPL':
1488         case 'MCI':
1489         case 'ETC':
1490         case 'MLL':
1491         case 'STC':
1492         case 'RVA':
1493         case 'EQU':
1494         case 'REV':
1495         case 'CNT':
1496         case 'BUF':
1497           if (in_array($frame_name, $PreviousFrames)) {
1498             $this->errors[] = 'Only one '.$frame_name.' tag allowed';
1499           } else {
1500             $PreviousFrames[] = $frame_name;
1501           }
1502           break;
1503 
1504         case 'LNK':
1505           // this isn't implemented quite right (yet) - it should check the target frame data for compliance
1506           // but right now it just allows one linked frame of each type, to be safe.
1507           if (!isset($source_data_array['frameid'])) {
1508             $this->errors[] = '[frameid] not specified for '.$frame_name;
1509           } elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) {
1510             $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')';
1511           } elseif (in_array($source_data_array['frameid'], $PreviousFrames)) {
1512             // no links to singleton tags
1513             $this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')';
1514           } else {
1515             $PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type
1516             $PreviousFrames[] = $source_data_array['frameid'];             // no non-linked singleton tags of this type
1517           }
1518           break;
1519 
1520         default:
1521           if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) {
1522             $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name;
1523           }
1524           break;
1525       }
1526     }
1527 
1528     if (!empty($this->errors)) {
1529       return false;
1530     }
1531     return true;
1532   }
1533 
1534   public function GenerateID3v2Tag($noerrorsonly=true) {
1535     $this->ID3v2FrameIsAllowed(null, ''); // clear static array in case this isn't the first call to $this->GenerateID3v2Tag()
1536 
1537     $tagstring = '';
1538     if (is_array($this->tag_data)) {
1539       foreach ($this->tag_data as $frame_name => $frame_rawinputdata) {
1540         foreach ($frame_rawinputdata as $irrelevantindex => $source_data_array) {
1541           if (getid3_id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) {
1542             unset($frame_length);
1543             unset($frame_flags);
1544             $frame_data = false;
1545             if ($this->ID3v2FrameIsAllowed($frame_name, $source_data_array)) {
1546               if ($frame_data = $this->GenerateID3v2FrameData($frame_name, $source_data_array)) {
1547                 $FrameUnsynchronisation = false;
1548                 if ($this->majorversion >= 4) {
1549                   // frame-level unsynchronisation
1550                   $unsynchdata = $frame_data;
1551                   if ($this->id3v2_use_unsynchronisation) {
1552                     $unsynchdata = $this->Unsynchronise($frame_data);
1553                   }
1554                   if (strlen($unsynchdata) != strlen($frame_data)) {
1555                     // unsynchronisation needed
1556                     $FrameUnsynchronisation = true;
1557                     $frame_data = $unsynchdata;
1558                     if (isset($TagUnsynchronisation) && $TagUnsynchronisation === false) {
1559                       // only set to true if ALL frames are unsynchronised
1560                     } else {
1561                       $TagUnsynchronisation = true;
1562                     }
1563                   } else {
1564                     if (isset($TagUnsynchronisation)) {
1565                       $TagUnsynchronisation = false;
1566                     }
1567                   }
1568                   unset($unsynchdata);
1569 
1570                   $frame_length = getid3_lib::BigEndian2String(strlen($frame_data), 4, true);
1571                 } else {
1572                   $frame_length = getid3_lib::BigEndian2String(strlen($frame_data), 4, false);
1573                 }
1574                 $frame_flags  = $this->GenerateID3v2FrameFlags($this->ID3v2FrameFlagsLookupTagAlter($frame_name), $this->ID3v2FrameFlagsLookupFileAlter($frame_name), false, false, false, false, $FrameUnsynchronisation, false);
1575               }
1576             } else {
1577               $this->errors[] = 'Frame "'.$frame_name.'" is NOT allowed';
1578             }
1579             if ($frame_data === false) {
1580               $this->errors[] = '$this->GenerateID3v2FrameData() failed for "'.$frame_name.'"';
1581               if ($noerrorsonly) {
1582                 return false;
1583               } else {
1584                 unset($frame_name);
1585               }
1586             }
1587           } else {
1588             // ignore any invalid frame names, including 'title', 'header', etc
1589             $this->warnings[] = 'Ignoring invalid ID3v2 frame type: "'.$frame_name.'"';
1590             unset($frame_name);
1591             unset($frame_length);
1592             unset($frame_flags);
1593             unset($frame_data);
1594           }
1595           if (isset($frame_name) && isset($frame_length) && isset($frame_flags) && isset($frame_data)) {
1596             $tagstring .= $frame_name.$frame_length.$frame_flags.$frame_data;
1597           }
1598         }
1599       }
1600 
1601       if (!isset($TagUnsynchronisation)) {
1602         $TagUnsynchronisation = false;
1603       }
1604       if (($this->majorversion <= 3) && $this->id3v2_use_unsynchronisation) {
1605         // tag-level unsynchronisation
1606         $unsynchdata = $this->Unsynchronise($tagstring);
1607         if (strlen($unsynchdata) != strlen($tagstring)) {
1608           // unsynchronisation needed
1609           $TagUnsynchronisation = true;
1610           $tagstring = $unsynchdata;
1611         }
1612       }
1613 
1614       while ($this->paddedlength < (strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion))) {
1615         $this->paddedlength += 1024;
1616       }
1617 
1618       $footer = false; // ID3v2 footers not yet supported in getID3()
1619       if (!$footer && ($this->paddedlength > (strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion)))) {
1620         // pad up to $paddedlength bytes if unpadded tag is shorter than $paddedlength
1621         // "Furthermore it MUST NOT have any padding when a tag footer is added to the tag."
1622         if (($this->paddedlength - strlen($tagstring) - getid3_id3v2::ID3v2HeaderLength($this->majorversion)) > 0) {
1623           $tagstring .= str_repeat("\x00", $this->paddedlength - strlen($tagstring) - getid3_id3v2::ID3v2HeaderLength($this->majorversion));
1624         }
1625       }
1626       if ($this->id3v2_use_unsynchronisation && (substr($tagstring, strlen($tagstring) - 1, 1) == "\xFF")) {
1627         // special unsynchronisation case:
1628         // if last byte == $FF then appended a $00
1629         $TagUnsynchronisation = true;
1630         $tagstring .= "\x00";
1631       }
1632 
1633       $tagheader  = 'ID3';
1634       $tagheader .= chr($this->majorversion);
1635       $tagheader .= chr($this->minorversion);
1636       $tagheader .= $this->GenerateID3v2TagFlags(array('unsynchronisation'=>$TagUnsynchronisation));
1637       $tagheader .= getid3_lib::BigEndian2String(strlen($tagstring), 4, true);
1638 
1639       return $tagheader.$tagstring;
1640     }
1641     $this->errors[] = 'tag_data is not an array in GenerateID3v2Tag()';
1642     return false;
1643   }
1644 
1645   public function ID3v2IsValidPriceString($pricestring) {
1646     if (getid3_id3v2::LanguageLookup(substr($pricestring, 0, 3), true) == '') {
1647       return false;
1648     } elseif (!$this->IsANumber(substr($pricestring, 3), true)) {
1649       return false;
1650     }
1651     return true;
1652   }
1653 
1654   public function ID3v2FrameFlagsLookupTagAlter($framename) {
1655     // unfinished
1656     switch ($framename) {
1657       case 'RGAD':
1658         $allow = true;
1659       default:
1660         $allow = false;
1661         break;
1662     }
1663     return $allow;
1664   }
1665 
1666   public function ID3v2FrameFlagsLookupFileAlter($framename) {
1667     // unfinished
1668     switch ($framename) {
1669       case 'RGAD':
1670         return false;
1671         break;
1672 
1673       default:
1674         return false;
1675         break;
1676     }
1677   }
1678 
1679   public function ID3v2IsValidETCOevent($eventid) {
1680     if (($eventid < 0) || ($eventid > 0xFF)) {
1681       // outside range of 1 byte
1682       return false;
1683     } elseif (($eventid >= 0xF0) && ($eventid <= 0xFC)) {
1684       // reserved for future use
1685       return false;
1686     } elseif (($eventid >= 0x17) && ($eventid <= 0xDF)) {
1687       // reserved for future use
1688       return false;
1689     } elseif (($eventid >= 0x0E) && ($eventid <= 0x16) && ($this->majorversion == 2)) {
1690       // not defined in ID3v2.2
1691       return false;
1692     } elseif (($eventid >= 0x15) && ($eventid <= 0x16) && ($this->majorversion == 3)) {
1693       // not defined in ID3v2.3
1694       return false;
1695     }
1696     return true;
1697   }
1698 
1699   public function ID3v2IsValidSYLTtype($contenttype) {
1700     if (($contenttype >= 0) && ($contenttype <= 8) && ($this->majorversion == 4)) {
1701       return true;
1702     } elseif (($contenttype >= 0) && ($contenttype <= 6) && ($this->majorversion == 3)) {
1703       return true;
1704     }
1705     return false;
1706   }
1707 
1708   public function ID3v2IsValidRVA2channeltype($channeltype) {
1709     if (($channeltype >= 0) && ($channeltype <= 8) && ($this->majorversion == 4)) {
1710       return true;
1711     }
1712     return false;
1713   }
1714 
1715   public function ID3v2IsValidAPICpicturetype($picturetype) {
1716     if (($picturetype >= 0) && ($picturetype <= 0x14) && ($this->majorversion >= 2) && ($this->majorversion <= 4)) {
1717       return true;
1718     }
1719     return false;
1720   }
1721 
1722   public function ID3v2IsValidAPICimageformat($imageformat) {
1723     if ($imageformat == '-->') {
1724       return true;
1725     } elseif ($this->majorversion == 2) {
1726       if ((strlen($imageformat) == 3) && ($imageformat == strtoupper($imageformat))) {
1727         return true;
1728       }
1729     } elseif (($this->majorversion == 3) || ($this->majorversion == 4)) {
1730       if ($this->IsValidMIMEstring($imageformat)) {
1731         return true;
1732       }
1733     }
1734     return false;
1735   }
1736 
1737   public function ID3v2IsValidCOMRreceivedAs($receivedas) {
1738     if (($this->majorversion >= 3) && ($receivedas >= 0) && ($receivedas <= 8)) {
1739       return true;
1740     }
1741     return false;
1742   }
1743 
1744   public function ID3v2IsValidRGADname($RGADname) {
1745     if (($RGADname >= 0) && ($RGADname <= 2)) {
1746       return true;
1747     }
1748     return false;
1749   }
1750 
1751   public function ID3v2IsValidRGADoriginator($RGADoriginator) {
1752     if (($RGADoriginator >= 0) && ($RGADoriginator <= 3)) {
1753       return true;
1754     }
1755     return false;
1756   }
1757 
1758   public function ID3v2IsValidTextEncoding($textencodingbyte) {
1759     static $ID3v2IsValidTextEncoding_cache = array(
1760       2 => array(true, true),
1761       3 => array(true, true),
1762       4 => array(true, true, true, true));
1763     return isset($ID3v2IsValidTextEncoding_cache[$this->majorversion][$textencodingbyte]);
1764   }
1765 
1766   public function Unsynchronise($data) {
1767     // Whenever a false synchronisation is found within the tag, one zeroed
1768     // byte is inserted after the first false synchronisation byte. The
1769     // format of a correct sync that should be altered by ID3 encoders is as
1770     // follows:
1771     //      %11111111 111xxxxx
1772     // And should be replaced with:
1773     //      %11111111 00000000 111xxxxx
1774     // This has the side effect that all $FF 00 combinations have to be
1775     // altered, so they won't be affected by the decoding process. Therefore
1776     // all the $FF 00 combinations have to be replaced with the $FF 00 00
1777     // combination during the unsynchronisation.
1778 
1779     $data = str_replace("\xFF\x00", "\xFF\x00\x00", $data);
1780     $unsyncheddata = '';
1781     $datalength = strlen($data);
1782     for ($i = 0; $i < $datalength; $i++) {
1783       $thischar = $data{$i};
1784       $unsyncheddata .= $thischar;
1785       if ($thischar == "\xFF") {
1786         $nextchar = ord($data{$i + 1});
1787         if (($nextchar & 0xE0) == 0xE0) {
1788           // previous byte = 11111111, this byte = 111?????
1789           $unsyncheddata .= "\x00";
1790         }
1791       }
1792     }
1793     return $unsyncheddata;
1794   }
1795 
1796   public function is_hash($var) {
1797     // written by dev-nullØchristophe*vg
1798     // taken from http://www.php.net/manual/en/function.array-merge-recursive.php
1799     if (is_array($var)) {
1800       $keys = array_keys($var);
1801       $all_num = true;
1802       for ($i = 0; $i < count($keys); $i++) {
1803         if (is_string($keys[$i])) {
1804           return true;
1805         }
1806       }
1807     }
1808     return false;
1809   }
1810 
1811   public function array_join_merge($arr1, $arr2) {
1812     // written by dev-nullØchristophe*vg
1813     // taken from http://www.php.net/manual/en/function.array-merge-recursive.php
1814     if (is_array($arr1) && is_array($arr2)) {
1815       // the same -> merge
1816       $new_array = array();
1817 
1818       if ($this->is_hash($arr1) && $this->is_hash($arr2)) {
1819         // hashes -> merge based on keys
1820         $keys = array_merge(array_keys($arr1), array_keys($arr2));
1821         foreach ($keys as $key) {
1822           $new_array[$key] = $this->array_join_merge((isset($arr1[$key]) ? $arr1[$key] : ''), (isset($arr2[$key]) ? $arr2[$key] : ''));
1823         }
1824       } else {
1825         // two real arrays -> merge
1826         $new_array = array_reverse(array_unique(array_reverse(array_merge($arr1, $arr2))));
1827       }
1828       return $new_array;
1829     } else {
1830       // not the same ... take new one if defined, else the old one stays
1831       return $arr2 ? $arr2 : $arr1;
1832     }
1833   }
1834 
1835   public function IsValidMIMEstring($mimestring) {
1836     if ((strlen($mimestring) >= 3) && (strpos($mimestring, '/') > 0) && (strpos($mimestring, '/') < (strlen($mimestring) - 1))) {
1837       return true;
1838     }
1839     return false;
1840   }
1841 
1842   public function IsWithinBitRange($number, $maxbits, $signed=false) {
1843     if ($signed) {
1844       if (($number > (0 - pow(2, $maxbits - 1))) && ($number <= pow(2, $maxbits - 1))) {
1845         return true;
1846       }
1847     } else {
1848       if (($number >= 0) && ($number <= pow(2, $maxbits))) {
1849         return true;
1850       }
1851     }
1852     return false;
1853   }
1854 
1855   public function safe_parse_url($url) {
1856     $parts = @parse_url($url);
1857     $parts['scheme'] = (isset($parts['scheme']) ? $parts['scheme'] : '');
1858     $parts['host']   = (isset($parts['host'])   ? $parts['host']   : '');
1859     $parts['user']   = (isset($parts['user'])   ? $parts['user']   : '');
1860     $parts['pass']   = (isset($parts['pass'])   ? $parts['pass']   : '');
1861     $parts['path']   = (isset($parts['path'])   ? $parts['path']   : '');
1862     $parts['query']  = (isset($parts['query'])  ? $parts['query']  : '');
1863     return $parts;
1864   }
1865 
1866   public function IsValidURL($url, $allowUserPass=false) {
1867     if ($url == '') {
1868       return false;
1869     }
1870     if ($allowUserPass !== true) {
1871       if (strstr($url, '@')) {
1872         // in the format http://user:pass@example.com  or http://user@example.com
1873         // but could easily be somebody incorrectly entering an email address in place of a URL
1874         return false;
1875       }
1876     }
1877     if ($parts = $this->safe_parse_url($url)) {
1878       if (($parts['scheme'] != 'http') && ($parts['scheme'] != 'https') && ($parts['scheme'] != 'ftp') && ($parts['scheme'] != 'gopher')) {
1879         return false;
1880       } 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'])) {
1881         return false;
1882       } elseif (!preg_match('#^([[:alnum:]-]|[\\_])*$#i', $parts['user'], $regs)) {
1883         return false;
1884       } elseif (!preg_match('#^([[:alnum:]-]|[\\_])*$#i', $parts['pass'], $regs)) {
1885         return false;
1886       } elseif (!preg_match('#^[[:alnum:]/_\\.@~-]*$#i', $parts['path'], $regs)) {
1887         return false;
1888       } elseif (!empty($parts['query']) && !preg_match('#^[[:alnum:]?&=+:;_()%\\#/,\\.-]*$#i', $parts['query'], $regs)) {
1889         return false;
1890       } else {
1891         return true;
1892       }
1893     }
1894     return false;
1895   }
1896 
1897   public static function ID3v2ShortFrameNameLookup($majorversion, $long_description) {
1898     $long_description = str_replace(' ', '_', strtolower(trim($long_description)));
1899     static $ID3v2ShortFrameNameLookup = array();
1900     if (empty($ID3v2ShortFrameNameLookup)) {
1901 
1902       // The following are unique to ID3v2.2
1903       $ID3v2ShortFrameNameLookup[2]['comment']                                          = 'COM';
1904       $ID3v2ShortFrameNameLookup[2]['album']                                            = 'TAL';
1905       $ID3v2ShortFrameNameLookup[2]['beats_per_minute']                                 = 'TBP';
1906       $ID3v2ShortFrameNameLookup[2]['composer']                                         = 'TCM';
1907       $ID3v2ShortFrameNameLookup[2]['genre']                                            = 'TCO';
1908       $ID3v2ShortFrameNameLookup[2]['itunescompilation']                                = 'TCP';
1909       $ID3v2ShortFrameNameLookup[2]['copyright']                                        = 'TCR';
1910       $ID3v2ShortFrameNameLookup[2]['encoded_by']                                       = 'TEN';
1911       $ID3v2ShortFrameNameLookup[2]['language']                                         = 'TLA';
1912       $ID3v2ShortFrameNameLookup[2]['length']                                           = 'TLE';
1913       $ID3v2ShortFrameNameLookup[2]['original_artist']                                  = 'TOA';
1914       $ID3v2ShortFrameNameLookup[2]['original_filename']                                = 'TOF';
1915       $ID3v2ShortFrameNameLookup[2]['original_lyricist']                                = 'TOL';
1916       $ID3v2ShortFrameNameLookup[2]['original_album_title']                             = 'TOT';
1917       $ID3v2ShortFrameNameLookup[2]['artist']                                           = 'TP1';
1918       $ID3v2ShortFrameNameLookup[2]['band']                                             = 'TP2';
1919       $ID3v2ShortFrameNameLookup[2]['conductor']                                        = 'TP3';
1920       $ID3v2ShortFrameNameLookup[2]['remixer']                                          = 'TP4';
1921       $ID3v2ShortFrameNameLookup[2]['publisher']                                        = 'TPB';
1922       $ID3v2ShortFrameNameLookup[2]['isrc']                                             = 'TRC';
1923       $ID3v2ShortFrameNameLookup[2]['tracknumber']                                      = 'TRK';
1924       $ID3v2ShortFrameNameLookup[2]['size']                                             = 'TSI';
1925       $ID3v2ShortFrameNameLookup[2]['encoder_settings']                                 = 'TSS';
1926       $ID3v2ShortFrameNameLookup[2]['description']                                      = 'TT1';
1927       $ID3v2ShortFrameNameLookup[2]['title']                                            = 'TT2';
1928       $ID3v2ShortFrameNameLookup[2]['subtitle']                                         = 'TT3';
1929       $ID3v2ShortFrameNameLookup[2]['lyricist']                                         = 'TXT';
1930       $ID3v2ShortFrameNameLookup[2]['user_text']                                        = 'TXX';
1931       $ID3v2ShortFrameNameLookup[2]['year']                                             = 'TYE';
1932       $ID3v2ShortFrameNameLookup[2]['unique_file_identifier']                           = 'UFI';
1933       $ID3v2ShortFrameNameLookup[2]['unsynchronised_lyrics']                            = 'ULT';
1934       $ID3v2ShortFrameNameLookup[2]['url_file']                                         = 'WAF';
1935       $ID3v2ShortFrameNameLookup[2]['url_artist']                                       = 'WAR';
1936       $ID3v2ShortFrameNameLookup[2]['url_source']                                       = 'WAS';
1937       $ID3v2ShortFrameNameLookup[2]['copyright_information']                            = 'WCP';
1938       $ID3v2ShortFrameNameLookup[2]['url_publisher']                                    = 'WPB';
1939       $ID3v2ShortFrameNameLookup[2]['url_user']                                         = 'WXX';
1940 
1941       // The following are common to ID3v2.3 and ID3v2.4
1942       $ID3v2ShortFrameNameLookup[3]['audio_encryption']                                 = 'AENC';
1943       $ID3v2ShortFrameNameLookup[3]['attached_picture']                                 = 'APIC';
1944       $ID3v2ShortFrameNameLookup[3]['comment']                                          = 'COMM';
1945       $ID3v2ShortFrameNameLookup[3]['commercial']                                       = 'COMR';
1946       $ID3v2ShortFrameNameLookup[3]['encryption_method_registration']                   = 'ENCR';
1947       $ID3v2ShortFrameNameLookup[3]['event_timing_codes']                               = 'ETCO';
1948       $ID3v2ShortFrameNameLookup[3]['general_encapsulated_object']                      = 'GEOB';
1949       $ID3v2ShortFrameNameLookup[3]['group_identification_registration']                = 'GRID';
1950       $ID3v2ShortFrameNameLookup[3]['linked_information']                               = 'LINK';
1951       $ID3v2ShortFrameNameLookup[3]['music_cd_identifier']                              = 'MCDI';
1952       $ID3v2ShortFrameNameLookup[3]['mpeg_location_lookup_table']                       = 'MLLT';
1953       $ID3v2ShortFrameNameLookup[3]['ownership']                                        = 'OWNE';
1954       $ID3v2ShortFrameNameLookup[3]['play_counter']                                     = 'PCNT';
1955       $ID3v2ShortFrameNameLookup[3]['popularimeter']                                    = 'POPM';
1956       $ID3v2ShortFrameNameLookup[3]['position_synchronisation']                         = 'POSS';
1957       $ID3v2ShortFrameNameLookup[3]['private']                                          = 'PRIV';
1958       $ID3v2ShortFrameNameLookup[3]['recommended_buffer_size']                          = 'RBUF';
1959       $ID3v2ShortFrameNameLookup[3]['reverb']                                           = 'RVRB';
1960       $ID3v2ShortFrameNameLookup[3]['synchronised_lyrics']                              = 'SYLT';
1961       $ID3v2ShortFrameNameLookup[3]['synchronised_tempo_codes']                         = 'SYTC';
1962       $ID3v2ShortFrameNameLookup[3]['album']                                            = 'TALB';
1963       $ID3v2ShortFrameNameLookup[3]['beats_per_minute']                                 = 'TBPM';
1964       $ID3v2ShortFrameNameLookup[3]['itunescompilation']                                = 'TCMP';
1965       $ID3v2ShortFrameNameLookup[3]['composer']                                         = 'TCOM';
1966       $ID3v2ShortFrameNameLookup[3]['genre']                                            = 'TCON';
1967       $ID3v2ShortFrameNameLookup[3]['copyright']                                        = 'TCOP';
1968       $ID3v2ShortFrameNameLookup[3]['playlist_delay']                                   = 'TDLY';
1969       $ID3v2ShortFrameNameLookup[3]['encoded_by']                                       = 'TENC';
1970       $ID3v2ShortFrameNameLookup[3]['lyricist']                                         = 'TEXT';
1971       $ID3v2ShortFrameNameLookup[3]['file_type']                                        = 'TFLT';
1972       $ID3v2ShortFrameNameLookup[3]['content_group_description']                        = 'TIT1';
1973       $ID3v2ShortFrameNameLookup[3]['title']                                            = 'TIT2';
1974       $ID3v2ShortFrameNameLookup[3]['subtitle']                                         = 'TIT3';
1975       $ID3v2ShortFrameNameLookup[3]['initial_key']                                      = 'TKEY';
1976       $ID3v2ShortFrameNameLookup[3]['language']                                         = 'TLAN';
1977       $ID3v2ShortFrameNameLookup[3]['length']                                           = 'TLEN';
1978       $ID3v2ShortFrameNameLookup[3]['media_type']                                       = 'TMED';
1979       $ID3v2ShortFrameNameLookup[3]['original_album_title']                             = 'TOAL';
1980       $ID3v2ShortFrameNameLookup[3]['original_filename']                                = 'TOFN';
1981       $ID3v2ShortFrameNameLookup[3]['original_lyricist']                                = 'TOLY';
1982       $ID3v2ShortFrameNameLookup[3]['original_artist']                                  = 'TOPE';
1983       $ID3v2ShortFrameNameLookup[3]['file_owner']                                       = 'TOWN';
1984       $ID3v2ShortFrameNameLookup[3]['artist']                                           = 'TPE1';
1985       $ID3v2ShortFrameNameLookup[3]['band']                                             = 'TPE2';
1986       $ID3v2ShortFrameNameLookup[3]['conductor']                                        = 'TPE3';
1987       $ID3v2ShortFrameNameLookup[3]['remixer']                                          = 'TPE4';
1988       $ID3v2ShortFrameNameLookup[3]['part_of_a_set']                                    = 'TPOS';
1989       $ID3v2ShortFrameNameLookup[3]['publisher']                                        = 'TPUB';
1990       $ID3v2ShortFrameNameLookup[3]['tracknumber']                                      = 'TRCK';
1991       $ID3v2ShortFrameNameLookup[3]['internet_radio_station_name']                      = 'TRSN';
1992       $ID3v2ShortFrameNameLookup[3]['internet_radio_station_owner']                     = 'TRSO';
1993       $ID3v2ShortFrameNameLookup[3]['isrc']                                             = 'TSRC';
1994       $ID3v2ShortFrameNameLookup[3]['encoder_settings']                                 = 'TSSE';
1995       $ID3v2ShortFrameNameLookup[3]['user_text']                                        = 'TXXX';
1996       $ID3v2ShortFrameNameLookup[3]['unique_file_identifier']                           = 'UFID';
1997       $ID3v2ShortFrameNameLookup[3]['terms_of_use']                                     = 'USER';
1998       $ID3v2ShortFrameNameLookup[3]['unsynchronised_lyrics']                            = 'USLT';
1999       $ID3v2ShortFrameNameLookup[3]['commercial']                                       = 'WCOM';
2000       $ID3v2ShortFrameNameLookup[3]['copyright_information']                            = 'WCOP';
2001       $ID3v2ShortFrameNameLookup[3]['url_file']                                         = 'WOAF';
2002       $ID3v2ShortFrameNameLookup[3]['url_artist']                                       = 'WOAR';
2003       $ID3v2ShortFrameNameLookup[3]['url_source']                                       = 'WOAS';
2004       $ID3v2ShortFrameNameLookup[3]['url_station']                                      = 'WORS';
2005       $ID3v2ShortFrameNameLookup[3]['payment']                                          = 'WPAY';
2006       $ID3v2ShortFrameNameLookup[3]['url_publisher']                                    = 'WPUB';
2007       $ID3v2ShortFrameNameLookup[3]['url_user']                                         = 'WXXX';
2008 
2009       // The above are common to ID3v2.3 and ID3v2.4
2010       // so copy them to ID3v2.4 before adding specifics for 2.3 and 2.4
2011       $ID3v2ShortFrameNameLookup[4] = $ID3v2ShortFrameNameLookup[3];
2012 
2013       // The following are unique to ID3v2.3
2014       $ID3v2ShortFrameNameLookup[3]['equalisation']                                     = 'EQUA';
2015       $ID3v2ShortFrameNameLookup[3]['involved_people_list']                             = 'IPLS';
2016       $ID3v2ShortFrameNameLookup[3]['relative_volume_adjustment']                       = 'RVAD';
2017       $ID3v2ShortFrameNameLookup[3]['date']                                             = 'TDAT';
2018       $ID3v2ShortFrameNameLookup[3]['time']                                             = 'TIME';
2019       $ID3v2ShortFrameNameLookup[3]['original_release_year']                            = 'TORY';
2020       $ID3v2ShortFrameNameLookup[3]['recording_dates']                                  = 'TRDA';
2021       $ID3v2ShortFrameNameLookup[3]['size']                                             = 'TSIZ';
2022       $ID3v2ShortFrameNameLookup[3]['year']                                             = 'TYER';
2023 
2024 
2025       // The following are unique to ID3v2.4
2026       $ID3v2ShortFrameNameLookup[4]['audio_seek_point_index']                           = 'ASPI';
2027       $ID3v2ShortFrameNameLookup[4]['equalisation']                                     = 'EQU2';
2028       $ID3v2ShortFrameNameLookup[4]['relative_volume_adjustment']                       = 'RVA2';
2029       $ID3v2ShortFrameNameLookup[4]['seek']                                             = 'SEEK';
2030       $ID3v2ShortFrameNameLookup[4]['signature']                                        = 'SIGN';
2031       $ID3v2ShortFrameNameLookup[4]['encoding_time']                                    = 'TDEN';
2032       $ID3v2ShortFrameNameLookup[4]['original_release_time']                            = 'TDOR';
2033       $ID3v2ShortFrameNameLookup[4]['recording_time']                                   = 'TDRC';
2034       $ID3v2ShortFrameNameLookup[4]['release_time']                                     = 'TDRL';
2035       $ID3v2ShortFrameNameLookup[4]['tagging_time']                                     = 'TDTG';
2036       $ID3v2ShortFrameNameLookup[4]['involved_people_list']                             = 'TIPL';
2037       $ID3v2ShortFrameNameLookup[4]['musician_credits_list']                            = 'TMCL';
2038       $ID3v2ShortFrameNameLookup[4]['mood']                                             = 'TMOO';
2039       $ID3v2ShortFrameNameLookup[4]['produced_notice']                                  = 'TPRO';
2040       $ID3v2ShortFrameNameLookup[4]['album_sort_order']                                 = 'TSOA';
2041       $ID3v2ShortFrameNameLookup[4]['performer_sort_order']                             = 'TSOP';
2042       $ID3v2ShortFrameNameLookup[4]['title_sort_order']                                 = 'TSOT';
2043       $ID3v2ShortFrameNameLookup[4]['set_subtitle']                                     = 'TSST';
2044     }
2045     return (isset($ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)]) ? $ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)] : '');
2046 
2047   }
2048 
2049 }
2050