1<?php 2/*********************************************** 3* File : wbxmlencoder.php 4* Project : Z-Push 5* Descr : WBXMLEncoder encodes to Wap Binary XML 6* 7* Created : 01.10.2007 8* 9* Copyright 2007 - 2016 Zarafa Deutschland GmbH 10* 11* This program is free software: you can redistribute it and/or modify 12* it under the terms of the GNU Affero General Public License, version 3, 13* as published by the Free Software Foundation. 14* 15* This program is distributed in the hope that it will be useful, 16* but WITHOUT ANY WARRANTY; without even the implied warranty of 17* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18* GNU Affero General Public License for more details. 19* 20* You should have received a copy of the GNU Affero General Public License 21* along with this program. If not, see <http://www.gnu.org/licenses/>. 22* 23* Consult LICENSE file for details 24************************************************/ 25 26class WBXMLEncoder extends WBXMLDefs { 27 private $_dtd; 28 private $_out; 29 30 private $_tagcp = 0; 31 32 private $log = false; 33 private $logStack = array(); 34 35 // We use a delayed output mechanism in which we only output a tag when it actually has something 36 // in it. This can cause entire XML trees to disappear if they don't have output data in them; Ie 37 // calling 'startTag' 10 times, and then 'endTag' will cause 0 bytes of output apart from the header. 38 39 // Only when content() is called do we output the current stack of tags 40 41 private $_stack; 42 43 private $multipart; // the content is multipart 44 private $bodyparts; 45 46 public function __construct($output, $multipart = false) { 47 $this->log = ZLog::IsWbxmlDebugEnabled(); 48 49 $this->_out = $output; 50 51 // reverse-map the DTD 52 foreach($this->dtd["namespaces"] as $nsid => $nsname) { 53 $this->_dtd["namespaces"][$nsname] = $nsid; 54 } 55 56 foreach($this->dtd["codes"] as $cp => $value) { 57 $this->_dtd["codes"][$cp] = array(); 58 foreach($this->dtd["codes"][$cp] as $tagid => $tagname) { 59 $this->_dtd["codes"][$cp][$tagname] = $tagid; 60 } 61 } 62 $this->_stack = array(); 63 $this->multipart = $multipart; 64 $this->bodyparts = array(); 65 } 66 67 /** 68 * Puts the WBXML header on the stream 69 * 70 * @access public 71 * @return 72 */ 73 public function startWBXML() { 74 if ($this->multipart) { 75 header("Content-Type: application/vnd.ms-sync.multipart"); 76 ZLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->startWBXML() type: vnd.ms-sync.multipart"); 77 } 78 else { 79 header("Content-Type: application/vnd.ms-sync.wbxml"); 80 ZLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->startWBXML() type: vnd.ms-sync.wbxml"); 81 } 82 83 $this->outByte(0x03); // WBXML 1.3 84 $this->outMBUInt(0x01); // Public ID 1 85 $this->outMBUInt(106); // UTF-8 86 $this->outMBUInt(0x00); // string table length (0) 87 } 88 89 /** 90 * Puts a StartTag on the output stack 91 * 92 * @param $tag 93 * @param $attributes 94 * @param $nocontent 95 * 96 * @access public 97 * @return 98 */ 99 public function startTag($tag, $attributes = false, $nocontent = false) { 100 $stackelem = array(); 101 102 if(!$nocontent) { 103 $stackelem['tag'] = $tag; 104 $stackelem['nocontent'] = $nocontent; 105 $stackelem['sent'] = false; 106 107 array_push($this->_stack, $stackelem); 108 109 // If 'nocontent' is specified, then apparently the user wants to force 110 // output of an empty tag, and we therefore output the stack here 111 } else { 112 $this->_outputStack(); 113 $this->_startTag($tag, $nocontent); 114 } 115 } 116 117 /** 118 * Puts an EndTag on the stack 119 * 120 * @access public 121 * @return 122 */ 123 public function endTag() { 124 $stackelem = array_pop($this->_stack); 125 126 // Only output end tags for items that have had a start tag sent 127 if($stackelem['sent']) { 128 $this->_endTag(); 129 130 if(count($this->_stack) == 0) 131 ZLog::Write(LOGLEVEL_DEBUG, "WBXMLEncoder->endTag() WBXML output completed"); 132 133 if(count($this->_stack) == 0 && $this->multipart == true) { 134 $this->processMultipart(); 135 } 136 if(count($this->_stack) == 0) 137 $this->writeLog(); 138 } 139 } 140 141 /** 142 * Puts content on the output stack. 143 * 144 * @param string $content 145 * 146 * @access public 147 * @return 148 */ 149 public function content($content) { 150 // We need to filter out any \0 chars because it's the string terminator in WBXML. We currently 151 // cannot send \0 characters within the XML content anywhere. 152 $content = str_replace("\0","",$content); 153 154 if("x" . $content == "x") 155 return; 156 $this->_outputStack(); 157 $this->_content($content); 158 } 159 160 /** 161 * Puts content of a stream on the output stack AND closes it. 162 * 163 * @param resource $stream 164 * @param boolean $asBase64 if true, the data will be encoded as base64, default: false 165 * @param boolean $opaque if true, output the opaque data, default: false 166 * 167 * @access public 168 * @return 169 */ 170 public function contentStream($stream, $asBase64 = false, $opaque = false) { 171 // Do not append filters to opaque data as it might contain null char 172 if (!$asBase64 && !$opaque) { 173 stream_filter_register('replacenullchar', 'ReplaceNullcharFilter'); 174 $rnc_filter = stream_filter_append($stream, 'replacenullchar'); 175 } 176 177 $this->_outputStack(); 178 $this->_contentStream($stream, $asBase64, $opaque); 179 180 if (!$asBase64 && !$opaque) { 181 stream_filter_remove($rnc_filter); 182 } 183 184 fclose($stream); 185 } 186 187 /** 188 * Gets the value of multipart 189 * 190 * @access public 191 * @return boolean 192 */ 193 public function getMultipart() { 194 return $this->multipart; 195 } 196 197 /** 198 * Adds a bodypart 199 * 200 * @param Stream $bp 201 * 202 * @access public 203 * @return void 204 */ 205 public function addBodypartStream($bp) { 206 if (!is_resource($bp)) 207 throw new WBXMLException("WBXMLEncoder->addBodypartStream(): trying to add a ".gettype($bp)." instead of a stream"); 208 if ($this->multipart) 209 $this->bodyparts[] = $bp; 210 } 211 212 /** 213 * Gets the number of bodyparts 214 * 215 * @access public 216 * @return int 217 */ 218 public function getBodypartsCount() { 219 return count($this->bodyparts); 220 } 221 222 /**---------------------------------------------------------------------------------------------------------- 223 * Private WBXMLEncoder stuff 224 */ 225 226 /** 227 * Output any tags on the stack that haven't been output yet 228 * 229 * @access private 230 * @return 231 */ 232 private function _outputStack() { 233 for($i=0;$i<count($this->_stack);$i++) { 234 if(!$this->_stack[$i]['sent']) { 235 $this->_startTag($this->_stack[$i]['tag'], $this->_stack[$i]['nocontent']); 236 $this->_stack[$i]['sent'] = true; 237 } 238 } 239 } 240 241 /** 242 * Outputs an actual start tag 243 * 244 * @access private 245 * @return 246 */ 247 private function _startTag($tag, $nocontent = false) { 248 if ($this->log) 249 $this->logStartTag($tag, $nocontent); 250 251 $mapping = $this->getMapping($tag); 252 253 if(!$mapping) 254 return false; 255 256 if($this->_tagcp != $mapping["cp"]) { 257 $this->outSwitchPage($mapping["cp"]); 258 $this->_tagcp = $mapping["cp"]; 259 } 260 261 $code = $mapping["code"]; 262 263 if(!isset($nocontent) || !$nocontent) 264 $code |= 0x40; 265 266 $this->outByte($code); 267 } 268 269 /** 270 * Outputs actual data. 271 * 272 * @access private 273 * @param string $content 274 * @return 275 */ 276 private function _content($content) { 277 if ($this->log) 278 $this->logContent($content); 279 $this->outByte(self::WBXML_STR_I); 280 $this->outTermStr($content); 281 } 282 283 /** 284 * Outputs actual data coming from a stream, optionally encoded as base64. 285 * 286 * @access private 287 * @param resource $stream 288 * @param boolean $asBase64 289 * @return 290 */ 291 private function _contentStream($stream, $asBase64, $opaque) { 292 $stat = fstat($stream); 293 // write full stream, including the finalizing terminator to the output stream (stuff outTermStr() would do) 294 if ($opaque) { 295 $this->outByte(self::WBXML_OPAQUE); 296 $this->outMBUInt($stat['size']); 297 } 298 else { 299 $this->outByte(self::WBXML_STR_I); 300 } 301 302 if ($asBase64) { 303 $out_filter = stream_filter_append($this->_out, 'convert.base64-encode'); 304 } 305 $written = stream_copy_to_stream($stream, $this->_out); 306 if ($asBase64) { 307 stream_filter_remove($out_filter); 308 } 309 if (!$opaque) { 310 fwrite($this->_out, chr(0)); 311 } 312 313 if ($this->log) { 314 // data is out, do some logging 315 $this->logContent(sprintf("<<< written %d of %d bytes of %s data >>>", $written, $stat['size'], $asBase64 ? "base64 encoded":"plain")); 316 } 317 } 318 319 /** 320 * Outputs an actual end tag 321 * 322 * @access private 323 * @return 324 */ 325 private function _endTag() { 326 if ($this->log) 327 $this->logEndTag(); 328 $this->outByte(self::WBXML_END); 329 } 330 331 /** 332 * Outputs a byte 333 * 334 * @param $byte 335 * 336 * @access private 337 * @return 338 */ 339 private function outByte($byte) { 340 fwrite($this->_out, chr($byte)); 341 } 342 343 /** 344 * Output the multibyte integers to the stream. 345 * 346 * A multi-byte integer consists of a series of octets, 347 * where the most significant bit is the continuation flag 348 * and the remaining seven bits are a scalar value. 349 * The octets are arranged in a big-endian order, 350 * eg, the most significant seven bits are transmitted first. 351 * 352 * @see https://www.w3.org/1999/06/NOTE-wbxml-19990624/#_Toc443384895 353 * 354 * @param int $uint 355 * 356 * @access private 357 * @return void 358 */ 359 private function outMBUInt($uint) { 360 if ($uint == 0x0) { 361 return $this->outByte($uint); 362 } 363 364 $out = ''; 365 366 for ($i = 0; $uint != 0; $i++) { 367 $byte = $uint & 0x7f; 368 $uint = $uint >> 7; 369 if ($i == 0) { 370 $out = chr($byte) . $out; 371 } 372 else { 373 $out = chr($byte | 0x80) . $out; 374 } 375 } 376 fwrite($this->_out, $out); 377 } 378 379 /** 380 * Outputs content with string terminator 381 * 382 * @param $content 383 * 384 * @access private 385 * @return 386 */ 387 private function outTermStr($content) { 388 fwrite($this->_out, $content); 389 fwrite($this->_out, chr(0)); 390 } 391 392 /** 393 * Switches the codepage 394 * 395 * @param $page 396 * 397 * @access private 398 * @return 399 */ 400 private function outSwitchPage($page) { 401 $this->outByte(self::WBXML_SWITCH_PAGE); 402 $this->outByte($page); 403 } 404 405 /** 406 * Get the mapping for a tag 407 * 408 * @param $tag 409 * 410 * @access private 411 * @return array 412 */ 413 private function getMapping($tag) { 414 $mapping = array(); 415 416 $split = $this->splitTag($tag); 417 418 if(isset($split["ns"])) { 419 $cp = $this->_dtd["namespaces"][$split["ns"]]; 420 } 421 else { 422 $cp = 0; 423 } 424 425 $code = $this->_dtd["codes"][$cp][$split["tag"]]; 426 427 $mapping["cp"] = $cp; 428 $mapping["code"] = $code; 429 430 return $mapping; 431 } 432 433 /** 434 * Split a tag from a the fulltag (namespace + tag) 435 * 436 * @param $fulltag 437 * 438 * @access private 439 * @return array keys: 'ns' (namespace), 'tag' (tag) 440 */ 441 private function splitTag($fulltag) { 442 $ns = false; 443 $pos = strpos($fulltag, chr(58)); // chr(58) == ':' 444 445 if($pos) { 446 $ns = substr($fulltag, 0, $pos); 447 $tag = substr($fulltag, $pos+1); 448 } 449 else { 450 $tag = $fulltag; 451 } 452 453 $ret = array(); 454 if($ns) 455 $ret["ns"] = $ns; 456 $ret["tag"] = $tag; 457 458 return $ret; 459 } 460 461 /** 462 * Logs a StartTag to ZLog 463 * 464 * @param $tag 465 * @param $nocontent 466 * 467 * @access private 468 * @return 469 */ 470 private function logStartTag($tag, $nocontent) { 471 $spaces = str_repeat(" ", count($this->logStack)); 472 if($nocontent) 473 ZLog::Write(LOGLEVEL_WBXML,"O " . $spaces . " <$tag/>"); 474 else { 475 array_push($this->logStack, $tag); 476 ZLog::Write(LOGLEVEL_WBXML,"O " . $spaces . " <$tag>"); 477 } 478 } 479 480 /** 481 * Logs a EndTag to ZLog 482 * 483 * @access private 484 * @return 485 */ 486 private function logEndTag() { 487 $spaces = str_repeat(" ", count($this->logStack)); 488 $tag = array_pop($this->logStack); 489 ZLog::Write(LOGLEVEL_WBXML,"O " . $spaces . "</$tag>"); 490 } 491 492 /** 493 * Logs content to ZLog 494 * 495 * @param string $content 496 * 497 * @access private 498 * @return 499 */ 500 private function logContent($content) { 501 $spaces = str_repeat(" ", count($this->logStack)); 502 ZLog::Write(LOGLEVEL_WBXML,"O " . $spaces . $content); 503 } 504 505 /** 506 * Processes the multipart response 507 * 508 * @access private 509 * @return void 510 */ 511 private function processMultipart() { 512 ZLog::Write(LOGLEVEL_DEBUG, sprintf("WBXMLEncoder->processMultipart() with %d parts to be processed", $this->getBodypartsCount())); 513 $len = ob_get_length(); 514 $buffer = ob_get_clean(); 515 $nrBodyparts = $this->getBodypartsCount(); 516 $blockstart = (($nrBodyparts + 1) * 2) * 4 + 4; 517 518 fwrite($this->_out, pack("iii", ($nrBodyparts + 1), $blockstart, $len)); 519 520 foreach ($this->bodyparts as $i=>$bp) { 521 $blockstart = $blockstart + $len; 522 $len = fstat($bp); 523 $len = (isset($len['size'])) ? $len['size'] : 0; 524 if ($len == 0) { 525 ZLog::Write(LOGLEVEL_WARN, sprintf("WBXMLEncoder->processMultipart(): the length of the body part at position %d is 0", $i)); 526 } 527 fwrite($this->_out, pack("ii", $blockstart, $len)); 528 } 529 530 fwrite($this->_out, $buffer); 531 532 foreach($this->bodyparts as $bp) { 533 stream_copy_to_stream($bp, $this->_out); 534 fclose($bp); 535 } 536 } 537 538 /** 539 * Writes the sent WBXML data to the log if it is not bigger than 512K. 540 * 541 * @access private 542 * @return void 543 */ 544 private function writeLog() { 545 if (ob_get_length() === false) { 546 $data = "output buffer disabled"; 547 } elseif (ob_get_length() < 524288) { 548 $data = base64_encode(ob_get_contents()); 549 } else { 550 $data = "more than 512K of data"; 551 } 552 ZLog::Write(LOGLEVEL_WBXML, "WBXML-OUT: ". $data, false); 553 } 554} 555