write.real.php 11 KB

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