1<?php 2 3///////////////////////////////////////////////////////////////// 4/// getID3() by James Heinrich <info@getid3.org> // 5// available at https://github.com/JamesHeinrich/getID3 // 6// or https://www.getid3.org // 7// or http://getid3.sourceforge.net // 8// see readme.txt for more details // 9///////////////////////////////////////////////////////////////// 10// // 11// write.real.php // 12// module for writing RealAudio/RealVideo tags // 13// dependencies: module.tag.real.php // 14// /// 15///////////////////////////////////////////////////////////////// 16 17class getid3_write_real 18{ 19 /** 20 * @var string 21 */ 22 public $filename; 23 24 /** 25 * @var array 26 */ 27 public $tag_data = array(); 28 29 /** 30 * Read buffer size in bytes. 31 * 32 * @var int 33 */ 34 public $fread_buffer_size = 32768; 35 36 /** 37 * Any non-critical errors will be stored here. 38 * 39 * @var array 40 */ 41 public $warnings = array(); 42 43 /** 44 * Any critical errors will be stored here. 45 * 46 * @var array 47 */ 48 public $errors = array(); 49 50 /** 51 * Minimum length of CONT tag in bytes. 52 * 53 * @var int 54 */ 55 public $paddedlength = 512; 56 57 public function __construct() { 58 } 59 60 /** 61 * @return bool 62 */ 63 public function WriteReal() { 64 // File MUST be writeable - CHMOD(646) at least 65 if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'r+b'))) { 66 67 // Initialize getID3 engine 68 $getID3 = new getID3; 69 $OldThisFileInfo = $getID3->analyze($this->filename); 70 if (empty($OldThisFileInfo['real']['chunks']) && !empty($OldThisFileInfo['real']['old_ra_header'])) { 71 $this->errors[] = 'Cannot write Real tags on old-style file format'; 72 fclose($fp_source); 73 return false; 74 } 75 76 if (empty($OldThisFileInfo['real']['chunks'])) { 77 $this->errors[] = 'Cannot write Real tags because cannot find DATA chunk in file'; 78 fclose($fp_source); 79 return false; 80 } 81 $oldChunkInfo = array(); 82 foreach ($OldThisFileInfo['real']['chunks'] as $chunknumber => $chunkarray) { 83 $oldChunkInfo[$chunkarray['name']] = $chunkarray; 84 } 85 if (!empty($oldChunkInfo['CONT']['length'])) { 86 $this->paddedlength = max($oldChunkInfo['CONT']['length'], $this->paddedlength); 87 } 88 89 $new_CONT_tag_data = $this->GenerateCONTchunk(); 90 $new_PROP_tag_data = $this->GeneratePROPchunk($OldThisFileInfo['real']['chunks'], $new_CONT_tag_data); 91 $new__RMF_tag_data = $this->GenerateRMFchunk($OldThisFileInfo['real']['chunks']); 92 93 if (isset($oldChunkInfo['.RMF']['length']) && ($oldChunkInfo['.RMF']['length'] == strlen($new__RMF_tag_data))) { 94 fseek($fp_source, $oldChunkInfo['.RMF']['offset']); 95 fwrite($fp_source, $new__RMF_tag_data); 96 } else { 97 $this->errors[] = 'new .RMF tag ('.strlen($new__RMF_tag_data).' bytes) different length than old .RMF tag ('.$oldChunkInfo['.RMF']['length'].' bytes)'; 98 fclose($fp_source); 99 return false; 100 } 101 102 if (isset($oldChunkInfo['PROP']['length']) && ($oldChunkInfo['PROP']['length'] == strlen($new_PROP_tag_data))) { 103 fseek($fp_source, $oldChunkInfo['PROP']['offset']); 104 fwrite($fp_source, $new_PROP_tag_data); 105 } else { 106 $this->errors[] = 'new PROP tag ('.strlen($new_PROP_tag_data).' bytes) different length than old PROP tag ('.$oldChunkInfo['PROP']['length'].' bytes)'; 107 fclose($fp_source); 108 return false; 109 } 110 111 if (isset($oldChunkInfo['CONT']['length']) && ($oldChunkInfo['CONT']['length'] == strlen($new_CONT_tag_data))) { 112 113 // new data length is same as old data length - just overwrite 114 fseek($fp_source, $oldChunkInfo['CONT']['offset']); 115 fwrite($fp_source, $new_CONT_tag_data); 116 fclose($fp_source); 117 return true; 118 119 } else { 120 121 if (empty($oldChunkInfo['CONT'])) { 122 // no existing CONT chunk 123 $BeforeOffset = $oldChunkInfo['DATA']['offset']; 124 $AfterOffset = $oldChunkInfo['DATA']['offset']; 125 } else { 126 // new data is longer than old data 127 $BeforeOffset = $oldChunkInfo['CONT']['offset']; 128 $AfterOffset = $oldChunkInfo['CONT']['offset'] + $oldChunkInfo['CONT']['length']; 129 } 130 if ($tempfilename = tempnam(GETID3_TEMP_DIR, 'getID3')) { 131 if (getID3::is_writable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) { 132 133 rewind($fp_source); 134 fwrite($fp_temp, fread($fp_source, $BeforeOffset)); 135 fwrite($fp_temp, $new_CONT_tag_data); 136 fseek($fp_source, $AfterOffset); 137 while ($buffer = fread($fp_source, $this->fread_buffer_size)) { 138 fwrite($fp_temp, $buffer, strlen($buffer)); 139 } 140 fclose($fp_temp); 141 142 if (copy($tempfilename, $this->filename)) { 143 unlink($tempfilename); 144 fclose($fp_source); 145 return true; 146 } 147 unlink($tempfilename); 148 $this->errors[] = 'FAILED: copy('.$tempfilename.', '.$this->filename.')'; 149 150 } else { 151 $this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")'; 152 } 153 } 154 fclose($fp_source); 155 return false; 156 157 } 158 159 } 160 $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")'; 161 return false; 162 } 163 164 /** 165 * @param array $chunks 166 * 167 * @return string 168 */ 169 public function GenerateRMFchunk(&$chunks) { 170 $oldCONTexists = false; 171 $chunkNameKeys = array(); 172 foreach ($chunks as $key => $chunk) { 173 $chunkNameKeys[$chunk['name']] = $key; 174 if ($chunk['name'] == 'CONT') { 175 $oldCONTexists = true; 176 } 177 } 178 $newHeadersCount = $chunks[$chunkNameKeys['.RMF']]['headers_count'] + ($oldCONTexists ? 0 : 1); 179 180 $RMFchunk = "\x00\x00"; // object version 181 $RMFchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['.RMF']]['file_version'], 4); 182 $RMFchunk .= getid3_lib::BigEndian2String($newHeadersCount, 4); 183 184 $RMFchunk = '.RMF'.getid3_lib::BigEndian2String(strlen($RMFchunk) + 8, 4).$RMFchunk; // .RMF chunk identifier + chunk length 185 return $RMFchunk; 186 } 187 188 /** 189 * @param array $chunks 190 * @param string $new_CONT_tag_data 191 * 192 * @return string 193 */ 194 public function GeneratePROPchunk(&$chunks, &$new_CONT_tag_data) { 195 $old_CONT_length = 0; 196 $old_DATA_offset = 0; 197 $old_INDX_offset = 0; 198 $chunkNameKeys = array(); 199 foreach ($chunks as $key => $chunk) { 200 $chunkNameKeys[$chunk['name']] = $key; 201 if ($chunk['name'] == 'CONT') { 202 $old_CONT_length = $chunk['length']; 203 } elseif ($chunk['name'] == 'DATA') { 204 if (!$old_DATA_offset) { 205 $old_DATA_offset = $chunk['offset']; 206 } 207 } elseif ($chunk['name'] == 'INDX') { 208 if (!$old_INDX_offset) { 209 $old_INDX_offset = $chunk['offset']; 210 } 211 } 212 } 213 $CONTdelta = strlen($new_CONT_tag_data) - $old_CONT_length; 214 215 $PROPchunk = "\x00\x00"; // object version 216 $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['max_bit_rate'], 4); 217 $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['avg_bit_rate'], 4); 218 $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['max_packet_size'], 4); 219 $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['avg_packet_size'], 4); 220 $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['num_packets'], 4); 221 $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['duration'], 4); 222 $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['preroll'], 4); 223 $PROPchunk .= getid3_lib::BigEndian2String(max(0, $old_INDX_offset + $CONTdelta), 4); 224 $PROPchunk .= getid3_lib::BigEndian2String(max(0, $old_DATA_offset + $CONTdelta), 4); 225 $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['num_streams'], 2); 226 $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['flags_raw'], 2); 227 228 $PROPchunk = 'PROP'.getid3_lib::BigEndian2String(strlen($PROPchunk) + 8, 4).$PROPchunk; // PROP chunk identifier + chunk length 229 return $PROPchunk; 230 } 231 232 /** 233 * @return string 234 */ 235 public function GenerateCONTchunk() { 236 foreach ($this->tag_data as $key => $value) { 237 // limit each value to 0xFFFF bytes 238 $this->tag_data[$key] = substr($value, 0, 65535); 239 } 240 241 $CONTchunk = "\x00\x00"; // object version 242 243 $CONTchunk .= getid3_lib::BigEndian2String((!empty($this->tag_data['title']) ? strlen($this->tag_data['title']) : 0), 2); 244 $CONTchunk .= (!empty($this->tag_data['title']) ? strlen($this->tag_data['title']) : ''); 245 246 $CONTchunk .= getid3_lib::BigEndian2String((!empty($this->tag_data['artist']) ? strlen($this->tag_data['artist']) : 0), 2); 247 $CONTchunk .= (!empty($this->tag_data['artist']) ? strlen($this->tag_data['artist']) : ''); 248 249 $CONTchunk .= getid3_lib::BigEndian2String((!empty($this->tag_data['copyright']) ? strlen($this->tag_data['copyright']) : 0), 2); 250 $CONTchunk .= (!empty($this->tag_data['copyright']) ? strlen($this->tag_data['copyright']) : ''); 251 252 $CONTchunk .= getid3_lib::BigEndian2String((!empty($this->tag_data['comment']) ? strlen($this->tag_data['comment']) : 0), 2); 253 $CONTchunk .= (!empty($this->tag_data['comment']) ? strlen($this->tag_data['comment']) : ''); 254 255 if ($this->paddedlength > (strlen($CONTchunk) + 8)) { 256 $CONTchunk .= str_repeat("\x00", $this->paddedlength - strlen($CONTchunk) - 8); 257 } 258 259 $CONTchunk = 'CONT'.getid3_lib::BigEndian2String(strlen($CONTchunk) + 8, 4).$CONTchunk; // CONT chunk identifier + chunk length 260 261 return $CONTchunk; 262 } 263 264 /** 265 * @return bool 266 */ 267 public function RemoveReal() { 268 // File MUST be writeable - CHMOD(646) at least 269 if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'r+b'))) { 270 271 // Initialize getID3 engine 272 $getID3 = new getID3; 273 $OldThisFileInfo = $getID3->analyze($this->filename); 274 if (empty($OldThisFileInfo['real']['chunks']) && !empty($OldThisFileInfo['real']['old_ra_header'])) { 275 $this->errors[] = 'Cannot remove Real tags from old-style file format'; 276 fclose($fp_source); 277 return false; 278 } 279 280 if (empty($OldThisFileInfo['real']['chunks'])) { 281 $this->errors[] = 'Cannot remove Real tags because cannot find DATA chunk in file'; 282 fclose($fp_source); 283 return false; 284 } 285 foreach ($OldThisFileInfo['real']['chunks'] as $chunknumber => $chunkarray) { 286 $oldChunkInfo[$chunkarray['name']] = $chunkarray; 287 } 288 289 if (empty($oldChunkInfo['CONT'])) { 290 // no existing CONT chunk 291 fclose($fp_source); 292 return true; 293 } 294 295 $BeforeOffset = $oldChunkInfo['CONT']['offset']; 296 $AfterOffset = $oldChunkInfo['CONT']['offset'] + $oldChunkInfo['CONT']['length']; 297 if ($tempfilename = tempnam(GETID3_TEMP_DIR, 'getID3')) { 298 if (getID3::is_writable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) { 299 300 rewind($fp_source); 301 fwrite($fp_temp, fread($fp_source, $BeforeOffset)); 302 fseek($fp_source, $AfterOffset); 303 while ($buffer = fread($fp_source, $this->fread_buffer_size)) { 304 fwrite($fp_temp, $buffer, strlen($buffer)); 305 } 306 fclose($fp_temp); 307 308 if (copy($tempfilename, $this->filename)) { 309 unlink($tempfilename); 310 fclose($fp_source); 311 return true; 312 } 313 unlink($tempfilename); 314 $this->errors[] = 'FAILED: copy('.$tempfilename.', '.$this->filename.')'; 315 316 } else { 317 $this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")'; 318 } 319 } 320 fclose($fp_source); 321 return false; 322 } 323 $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")'; 324 return false; 325 } 326 327} 328