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.apetag.php                                            //
12// module for writing APE tags                                 //
13// dependencies: module.tag.apetag.php                         //
14//                                                            ///
15/////////////////////////////////////////////////////////////////
16
17if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
18	exit;
19}
20getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.apetag.php', __FILE__, true);
21
22class getid3_write_apetag
23{
24	/**
25	 * @var string
26	 */
27	public $filename;
28
29	/**
30	 * @var array
31	 */
32	public $tag_data;
33
34	/**
35	 * ReplayGain / MP3gain tags will be copied from old tag even if not passed in data.
36	 *
37	 * @var bool
38	 */
39	public $always_preserve_replaygain = true;
40
41	/**
42	 * Any non-critical errors will be stored here.
43	 *
44	 * @var array
45	 */
46	public $warnings                   = array();
47
48	/**
49	 * Any critical errors will be stored here.
50	 *
51	 * @var array
52	 */
53	public $errors                     = array();
54
55	public function __construct() {
56	}
57
58	/**
59	 * @return bool
60	 */
61	public function WriteAPEtag() {
62		// NOTE: All data passed to this function must be UTF-8 format
63
64		$getID3 = new getID3;
65		$ThisFileInfo = $getID3->analyze($this->filename);
66
67		if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['lyrics3']['tag_offset_end'])) {
68			if ($ThisFileInfo['ape']['tag_offset_start'] >= $ThisFileInfo['lyrics3']['tag_offset_end']) {
69				// Current APE tag between Lyrics3 and ID3v1/EOF
70				// This break Lyrics3 functionality
71				if (!$this->DeleteAPEtag()) {
72					return false;
73				}
74				$ThisFileInfo = $getID3->analyze($this->filename);
75			}
76		}
77
78		if ($this->always_preserve_replaygain) {
79			$ReplayGainTagsToPreserve = array('mp3gain_minmax', 'mp3gain_album_minmax', 'mp3gain_undo', 'replaygain_track_peak', 'replaygain_track_gain', 'replaygain_album_peak', 'replaygain_album_gain');
80			foreach ($ReplayGainTagsToPreserve as $rg_key) {
81				if (isset($ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0]) && !isset($this->tag_data[strtoupper($rg_key)][0])) {
82					$this->tag_data[strtoupper($rg_key)][0] = $ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0];
83				}
84			}
85		}
86
87		if ($APEtag = $this->GenerateAPEtag()) {
88			if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) {
89				$oldignoreuserabort = ignore_user_abort(true);
90				flock($fp, LOCK_EX);
91
92				$PostAPEdataOffset = $ThisFileInfo['avdataend'];
93				if (isset($ThisFileInfo['ape']['tag_offset_end'])) {
94					$PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['ape']['tag_offset_end']);
95				}
96				if (isset($ThisFileInfo['lyrics3']['tag_offset_start'])) {
97					$PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['lyrics3']['tag_offset_start']);
98				}
99				fseek($fp, $PostAPEdataOffset);
100				$PostAPEdata = '';
101				if ($ThisFileInfo['filesize'] > $PostAPEdataOffset) {
102					$PostAPEdata = fread($fp, $ThisFileInfo['filesize'] - $PostAPEdataOffset);
103				}
104
105				fseek($fp, $PostAPEdataOffset);
106				if (isset($ThisFileInfo['ape']['tag_offset_start'])) {
107					fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
108				}
109				ftruncate($fp, ftell($fp));
110				fwrite($fp, $APEtag, strlen($APEtag));
111				if (!empty($PostAPEdata)) {
112					fwrite($fp, $PostAPEdata, strlen($PostAPEdata));
113				}
114				flock($fp, LOCK_UN);
115				fclose($fp);
116				ignore_user_abort($oldignoreuserabort);
117				return true;
118			}
119		}
120		return false;
121	}
122
123	/**
124	 * @return bool
125	 */
126	public function DeleteAPEtag() {
127		$getID3 = new getID3;
128		$ThisFileInfo = $getID3->analyze($this->filename);
129		if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['ape']['tag_offset_end'])) {
130			if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) {
131
132				flock($fp, LOCK_EX);
133				$oldignoreuserabort = ignore_user_abort(true);
134
135				fseek($fp, $ThisFileInfo['ape']['tag_offset_end']);
136				$DataAfterAPE = '';
137				if ($ThisFileInfo['filesize'] > $ThisFileInfo['ape']['tag_offset_end']) {
138					$DataAfterAPE = fread($fp, $ThisFileInfo['filesize'] - $ThisFileInfo['ape']['tag_offset_end']);
139				}
140
141				ftruncate($fp, $ThisFileInfo['ape']['tag_offset_start']);
142				fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
143
144				if (!empty($DataAfterAPE)) {
145					fwrite($fp, $DataAfterAPE, strlen($DataAfterAPE));
146				}
147
148				flock($fp, LOCK_UN);
149				fclose($fp);
150				ignore_user_abort($oldignoreuserabort);
151
152				return true;
153			}
154			return false;
155		}
156		return true;
157	}
158
159	/**
160	 * @return string|false
161	 */
162	public function GenerateAPEtag() {
163		// NOTE: All data passed to this function must be UTF-8 format
164
165		$items = array();
166		if (!is_array($this->tag_data)) {
167			return false;
168		}
169		foreach ($this->tag_data as $key => $arrayofvalues) {
170			if (!is_array($arrayofvalues)) {
171				return false;
172			}
173
174			$valuestring = '';
175			foreach ($arrayofvalues as $value) {
176				$valuestring .= str_replace("\x00", '', $value)."\x00";
177			}
178			$valuestring = rtrim($valuestring, "\x00");
179
180			// Length of the assigned value in bytes
181			$tagitem  = getid3_lib::LittleEndian2String(strlen($valuestring), 4);
182
183			//$tagitem .= $this->GenerateAPEtagFlags(true, true, false, 0, false);
184			$tagitem .= "\x00\x00\x00\x00";
185
186			$tagitem .= $this->CleanAPEtagItemKey($key)."\x00";
187			$tagitem .= $valuestring;
188
189			$items[] = $tagitem;
190
191		}
192
193		return $this->GenerateAPEtagHeaderFooter($items, true).implode('', $items).$this->GenerateAPEtagHeaderFooter($items, false);
194	}
195
196	/**
197	 * @param array $items
198	 * @param bool  $isheader
199	 *
200	 * @return string
201	 */
202	public function GenerateAPEtagHeaderFooter(&$items, $isheader=false) {
203		$tagdatalength = 0;
204		foreach ($items as $itemdata) {
205			$tagdatalength += strlen($itemdata);
206		}
207
208		$APEheader  = 'APETAGEX';
209		$APEheader .= getid3_lib::LittleEndian2String(2000, 4);
210		$APEheader .= getid3_lib::LittleEndian2String(32 + $tagdatalength, 4);
211		$APEheader .= getid3_lib::LittleEndian2String(count($items), 4);
212		$APEheader .= $this->GenerateAPEtagFlags(true, true, $isheader, 0, false);
213		$APEheader .= str_repeat("\x00", 8);
214
215		return $APEheader;
216	}
217
218	/**
219	 * @param bool $header
220	 * @param bool $footer
221	 * @param bool $isheader
222	 * @param int  $encodingid
223	 * @param bool $readonly
224	 *
225	 * @return string
226	 */
227	public function GenerateAPEtagFlags($header=true, $footer=true, $isheader=false, $encodingid=0, $readonly=false) {
228		$APEtagFlags = array_fill(0, 4, 0);
229		if ($header) {
230			$APEtagFlags[0] |= 0x80; // Tag contains a header
231		}
232		if (!$footer) {
233			$APEtagFlags[0] |= 0x40; // Tag contains no footer
234		}
235		if ($isheader) {
236			$APEtagFlags[0] |= 0x20; // This is the header, not the footer
237		}
238
239		// 0: Item contains text information coded in UTF-8
240		// 1: Item contains binary information °)
241		// 2: Item is a locator of external stored information °°)
242		// 3: reserved
243		$APEtagFlags[3] |= ($encodingid << 1);
244
245		if ($readonly) {
246			$APEtagFlags[3] |= 0x01; // Tag or Item is Read Only
247		}
248
249		return chr($APEtagFlags[3]).chr($APEtagFlags[2]).chr($APEtagFlags[1]).chr($APEtagFlags[0]);
250	}
251
252	/**
253	 * @param string $itemkey
254	 *
255	 * @return string
256	 */
257	public function CleanAPEtagItemKey($itemkey) {
258		$itemkey = preg_replace("#[^\x20-\x7E]#i", '', $itemkey);
259
260		// http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html
261		switch (strtoupper($itemkey)) {
262			case 'EAN/UPC':
263			case 'ISBN':
264			case 'LC':
265			case 'ISRC':
266				$itemkey = strtoupper($itemkey);
267				break;
268
269			default:
270				$itemkey = ucwords($itemkey);
271				break;
272		}
273		return $itemkey;
274
275	}
276
277}
278