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