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