Differences Between: [Versions 39 and 310]
1 <?php 2 3 namespace BirknerAlex\XMPPHP; 4 5 /** 6 * XMPPHP: The PHP XMPP Library 7 * Copyright (C) 2008 Nathanael C. Fritz 8 * This file is part of SleekXMPP. 9 * 10 * XMPPHP is free software; you can redistribute it and/or modify 11 * it under the terms of the GNU General Public License as published by 12 * the Free Software Foundation; either version 2 of the License, or 13 * (at your option) any later version. 14 * 15 * XMPPHP 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 General Public License for more details. 19 * 20 * You should have received a copy of the GNU General Public License 21 * along with XMPPHP; if not, write to the Free Software 22 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23 * 24 * @category xmpphp 25 * @package XMPPHP 26 * @author Nathanael C. Fritz <JID: fritzy@netflint.net> 27 * @author Stephan Wentz <JID: stephan@jabber.wentz.it> 28 * @author Michael Garvin <JID: gar@netflint.net> 29 * @author Alexander Birkner (https://github.com/BirknerAlex) 30 * @copyright 2008 Nathanael C. Fritz 31 */ 32 33 /** 34 * XMPPHP Main Class 35 * 36 * @category xmpphp 37 * @package XMPPHP 38 * @author Nathanael C. Fritz <JID: fritzy@netflint.net> 39 * @author Stephan Wentz <JID: stephan@jabber.wentz.it> 40 * @author Michael Garvin <JID: gar@netflint.net> 41 * @copyright 2008 Nathanael C. Fritz 42 * @version $Id$ 43 */ 44 class XMLStream { 45 /** 46 * @var resource 47 */ 48 protected $socket; 49 /** 50 * @var resource 51 */ 52 protected $parser; 53 /** 54 * @var string 55 */ 56 protected $buffer; 57 /** 58 * @var integer 59 */ 60 protected $xml_depth = 0; 61 /** 62 * @var string 63 */ 64 protected $host; 65 /** 66 * @var integer 67 */ 68 protected $port; 69 /** 70 * @var string 71 */ 72 protected $stream_start = '<stream>'; 73 /** 74 * @var string 75 */ 76 protected $stream_end = '</stream>'; 77 /** 78 * @var boolean 79 */ 80 protected $disconnected = true; 81 /** 82 * @var boolean 83 */ 84 protected $sent_disconnect = false; 85 /** 86 * @var array 87 */ 88 protected $ns_map = array(); 89 /** 90 * @var array 91 */ 92 protected $current_ns = array(); 93 /** 94 * @var array 95 */ 96 protected $xmlobj = null; 97 /** 98 * @var array 99 */ 100 protected $nshandlers = array(); 101 /** 102 * @var array 103 */ 104 protected $xpathhandlers = array(); 105 /** 106 * @var array 107 */ 108 protected $idhandlers = array(); 109 /** 110 * @var array 111 */ 112 protected $eventhandlers = array(); 113 /** 114 * @var integer 115 */ 116 protected $lastid = 0; 117 /** 118 * @var string 119 */ 120 protected $default_ns; 121 /** 122 * @var string[] 123 */ 124 protected $until = array(); 125 /** 126 * @var int[] 127 */ 128 protected $until_count = array(); 129 /** 130 * @var array 131 */ 132 protected $until_happened = false; 133 /** 134 * @var array 135 */ 136 protected $until_payload = array(); 137 /** 138 * @var Log 139 */ 140 protected $log; 141 /** 142 * @var boolean 143 */ 144 protected $reconnect = true; 145 /** 146 * @var boolean 147 */ 148 protected $been_reset = false; 149 /** 150 * @var boolean 151 */ 152 protected $is_server; 153 /** 154 * @var float 155 */ 156 protected $last_send = 0; 157 /** 158 * @var boolean 159 */ 160 protected $use_ssl = false; 161 /** 162 * @var integer 163 */ 164 protected $reconnectTimeout = 30; 165 166 /** 167 * Constructor 168 * 169 * @param string $host 170 * @param string $port 171 * @param boolean $printlog 172 * @param string $loglevel 173 * @param boolean $is_server 174 */ 175 public function __construct($host = null, $port = null, $printlog = false, $loglevel = null, $is_server = false) { 176 $this->reconnect = !$is_server; 177 $this->is_server = $is_server; 178 $this->host = $host; 179 $this->port = $port; 180 $this->setupParser(); 181 $this->log = new Log($printlog, $loglevel); 182 } 183 184 /** 185 * Destructor 186 * Cleanup connection 187 */ 188 public function __destruct() { 189 if(!$this->disconnected && $this->socket) { 190 $this->disconnect(); 191 } 192 } 193 194 /** 195 * Return the log instance 196 * 197 * @return Log 198 */ 199 public function getLog() { 200 return $this->log; 201 } 202 203 /** 204 * Get next ID 205 * 206 * @return integer 207 */ 208 public function getId() { 209 $this->lastid++; 210 return $this->lastid; 211 } 212 213 /** 214 * Set SSL 215 * 216 * @return integer 217 */ 218 public function useSSL($use=true) { 219 $this->use_ssl = $use; 220 } 221 222 /** 223 * Add ID Handler 224 * 225 * @param integer $id 226 * @param string $pointer 227 * @param string $obj 228 */ 229 public function addIdHandler($id, $pointer, $obj = null) { 230 $this->idhandlers[$id] = array($pointer, $obj); 231 } 232 233 /** 234 * Add Handler 235 * 236 * @param string $name 237 * @param string $ns 238 * @param string $pointer 239 * @param string $obj 240 * @param integer $depth 241 */ 242 public function addHandler($name, $ns, $pointer, $obj = null, $depth = 1) { 243 #TODO deprication warning 244 $this->nshandlers[] = array($name,$ns,$pointer,$obj, $depth); 245 } 246 247 /** 248 * Add XPath Handler 249 * 250 * @param string $xpath 251 * @param string $pointer 252 * @param 253 */ 254 public function addXPathHandler($xpath, $pointer, $obj = null) { 255 if (preg_match_all("/\(?{[^\}]+}\)?(\/?)[^\/]+/", $xpath, $regs)) { 256 $ns_tags = $regs[0]; 257 } else { 258 $ns_tags = array($xpath); 259 } 260 foreach($ns_tags as $ns_tag) { 261 list($l, $r) = explode('}', $ns_tag); 262 if ($r != null) { 263 $xpart = array(substr($l, 1), $r); 264 } else { 265 $xpart = array(null, $l); 266 } 267 $xpath_array[] = $xpart; 268 } 269 $this->xpathhandlers[] = array($xpath_array, $pointer, $obj); 270 } 271 272 /** 273 * Add Event Handler 274 * 275 * @param integer $id 276 * @param string $pointer 277 * @param string $obj 278 */ 279 public function addEventHandler($name, $pointer, $obj) { 280 $this->eventhandlers[] = array($name, $pointer, $obj); 281 } 282 283 /** 284 * Connect to XMPP Host 285 * 286 * @param integer $timeout Timeout in seconds 287 * @param boolean $persistent 288 * @param boolean $sendinit Send XMPP starting sequence after connect 289 * automatically 290 * 291 * @throws Exception When the connection fails 292 */ 293 public function connect($timeout = 30, $persistent = false, $sendinit = true) { 294 $this->sent_disconnect = false; 295 $starttime = time(); 296 297 do { 298 $this->disconnected = false; 299 $this->sent_disconnect = false; 300 if($persistent) { 301 $conflag = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT; 302 } else { 303 $conflag = STREAM_CLIENT_CONNECT; 304 } 305 $conntype = 'tcp'; 306 if($this->use_ssl) $conntype = 'ssl'; 307 $this->log->log("Connecting to $conntype://{$this->host}:{$this->port}"); 308 try { 309 $this->socket = @stream_socket_client("$conntype://{$this->host}:{$this->port}", $errno, $errstr, $timeout, $conflag); 310 } catch (Exception $e) { 311 throw new Exception($e->getMessage()); 312 } 313 if(!$this->socket) { 314 $this->log->log("Could not connect.", Log::LEVEL_ERROR); 315 $this->disconnected = true; 316 # Take it easy for a few seconds 317 sleep(min($timeout, 5)); 318 } 319 } while (!$this->socket && (time() - $starttime) < $timeout); 320 321 if ($this->socket) { 322 stream_set_blocking($this->socket, 1); 323 if($sendinit) $this->send($this->stream_start); 324 } else { 325 throw new Exception("Could not connect before timeout."); 326 } 327 } 328 329 /** 330 * Reconnect XMPP Host 331 * 332 * @throws Exception When the connection fails 333 * @uses $reconnectTimeout 334 * @see setReconnectTimeout() 335 */ 336 public function doReconnect() { 337 if(!$this->is_server) { 338 $this->log->log("Reconnecting ($this->reconnectTimeout)...", Log::LEVEL_WARNING); 339 $this->connect($this->reconnectTimeout, false, false); 340 $this->reset(); 341 $this->event('reconnect'); 342 } 343 } 344 345 public function setReconnectTimeout($timeout) { 346 $this->reconnectTimeout = $timeout; 347 } 348 349 /** 350 * Disconnect from XMPP Host 351 */ 352 public function disconnect() { 353 $this->log->log("Disconnecting...", Log::LEVEL_VERBOSE); 354 if(false == (bool) $this->socket) { 355 return; 356 } 357 $this->reconnect = false; 358 $this->send($this->stream_end); 359 $this->sent_disconnect = true; 360 $this->processUntil('end_stream', 5); 361 $this->disconnected = true; 362 } 363 364 /** 365 * Are we are disconnected? 366 * 367 * @return boolean 368 */ 369 public function isDisconnected() { 370 return $this->disconnected; 371 } 372 373 /** 374 * Checks if the given string is closed with the same tag as it is 375 * opened. We try to be as fast as possible here. 376 * 377 * @param string $buff Read buffer of __process() 378 * 379 * @return boolean true if the buffer seems to be complete 380 */ 381 protected function bufferComplete($buff) 382 { 383 if (substr($buff, -1) != '>') { 384 return false; 385 } 386 //we always have a space since the namespace needs to be 387 //declared. could be a tab, though 388 $start = substr( 389 $buff, 1, 390 min(strpos($buff, '>', 2), strpos($buff, ' ', 2)) - 1 391 ); 392 $stop = substr($buff, -strlen($start) - 3); 393 394 if ($start == '?xml') { 395 //starting with an xml tag. this means a stream is being 396 // opened, which is not much of data, so no fear it's 397 // not complete 398 return true; 399 } 400 if (substr($stop, -2) == '/>') { 401 //one tag, i.e. <success /> 402 return true; 403 } 404 if ('</' . $start . '>' == $stop) { 405 return true; 406 } 407 408 return false; 409 } 410 411 /** 412 * Core reading tool 413 * 414 * @param mixed $maximum Limit when to return 415 * - 0: only read if data is immediately ready 416 * - NULL: wait forever and ever 417 * - integer: process for this amount of microseconds 418 * @param boolean $return_when_received Immediately return when data have been 419 * received 420 * 421 * @return boolean True when all goes well, false when something fails 422 */ 423 private function __process($maximum = 5, $return_when_received = false) 424 { 425 $remaining = $maximum; 426 427 do { 428 $starttime = (microtime(true) * 1000000); 429 $read = array($this->socket); 430 $write = array(); 431 $except = array(); 432 if (is_null($maximum)) { 433 $secs = NULL; 434 $usecs = NULL; 435 } else if ($maximum == 0) { 436 $secs = 0; 437 $usecs = 0; 438 } else { 439 $usecs = $remaining % 1000000; 440 $secs = floor(($remaining - $usecs) / 1000000); 441 } 442 $updated = @stream_select($read, $write, $except, $secs, $usecs); 443 if ($updated === false) { 444 $this->log->log("Error on stream_select()", Log::LEVEL_VERBOSE); 445 if ($this->reconnect) { 446 $this->doReconnect(); 447 } else { 448 fclose($this->socket); 449 $this->socket = NULL; 450 return false; 451 } 452 } else if ($updated > 0) { 453 $buff = ''; 454 do { 455 if ($buff != '') { 456 //disable blocking for now because fread() will 457 // block until the 4k are full if we already 458 // read a part of the packet 459 stream_set_blocking($this->socket, 0); 460 } 461 $part = fread($this->socket, 4096); 462 stream_set_blocking($this->socket, 1); 463 464 if (!$part && feof($this->socket)) { 465 if($this->reconnect) { 466 $this->doReconnect(); 467 } else { 468 fclose($this->socket); 469 $this->socket = NULL; 470 return false; 471 } 472 } 473 $this->log->log("RECV: $part", Log::LEVEL_VERBOSE); 474 $buff .= $part; 475 } while (!$this->bufferComplete($buff)); 476 477 xml_parse($this->parser, $buff, false); 478 if ($return_when_received) { 479 return true; 480 } 481 } else { 482 # $updated == 0 means no changes during timeout. 483 } 484 $endtime = (microtime(true)*1000000); 485 $time_past = $endtime - $starttime; 486 $remaining = $remaining - $time_past; 487 } while (is_null($maximum) || $remaining > 0); 488 return true; 489 } 490 491 /** 492 * Process 493 * 494 * @return string 495 */ 496 public function process() { 497 $this->__process(NULL); 498 } 499 500 /** 501 * Process until a timeout occurs 502 * 503 * @param integer $timeout Time in seconds 504 * 505 * @return string 506 * 507 * @see __process() 508 */ 509 public function processTime($timeout=NULL) { 510 if (is_null($timeout)) { 511 return $this->__process(NULL); 512 } else { 513 return $this->__process($timeout * 1000000); 514 } 515 } 516 517 /** 518 * Process until a specified event or a timeout occurs 519 * 520 * @param string|array $event Event name or array of event names 521 * @param integer $timeout Timeout in seconds 522 * 523 * @return array Payload 524 */ 525 public function processUntil($event, $timeout = -1) 526 { 527 if ($this->disconnected) { 528 throw new Exception('You need to connect first'); 529 } 530 531 $start = time(); 532 if (!is_array($event)) { 533 $event = array($event); 534 } 535 536 $this->until[] = $event; 537 end($this->until); 538 $event_key = key($this->until); 539 reset($this->until); 540 541 $this->until_count[$event_key] = 0; 542 $updated = ''; 543 while (!$this->disconnected 544 && $this->until_count[$event_key] < 1 545 && ($timeout == -1 || time() - $start < $timeout) 546 ) { 547 $maximum = $timeout == -1 548 ? NULL 549 : ($timeout - (time() - $start)) * 1000000; 550 $ret = $this->__process($maximum, true); 551 if (!$ret) { 552 break; 553 } 554 } 555 556 if (array_key_exists($event_key, $this->until_payload)) { 557 $payload = $this->until_payload[$event_key]; 558 unset($this->until_payload[$event_key]); 559 unset($this->until_count[$event_key]); 560 unset($this->until[$event_key]); 561 } else { 562 $payload = array(); 563 } 564 565 return $payload; 566 } 567 568 /** 569 * Obsolete? 570 */ 571 public function Xapply_socket($socket) { 572 $this->socket = $socket; 573 } 574 575 /** 576 * XML start callback 577 * 578 * @see xml_set_element_handler 579 * 580 * @param resource $parser 581 * @param string $name 582 */ 583 public function startXML($parser, $name, $attr) { 584 if($this->been_reset) { 585 $this->been_reset = false; 586 $this->xml_depth = 0; 587 } 588 $this->xml_depth++; 589 if(array_key_exists('XMLNS', $attr)) { 590 $this->current_ns[$this->xml_depth] = $attr['XMLNS']; 591 } else { 592 $this->current_ns[$this->xml_depth] = $this->current_ns[$this->xml_depth - 1]; 593 if(!$this->current_ns[$this->xml_depth]) $this->current_ns[$this->xml_depth] = $this->default_ns; 594 } 595 $ns = $this->current_ns[$this->xml_depth]; 596 foreach($attr as $key => $value) { 597 if(strstr($key, ":")) { 598 $key = explode(':', $key); 599 $key = $key[1]; 600 $this->ns_map[$key] = $value; 601 } 602 } 603 if(!strstr($name, ":") === false) 604 { 605 $name = explode(':', $name); 606 $ns = $this->ns_map[$name[0]]; 607 $name = $name[1]; 608 } 609 $obj = new XMLObj($name, $ns, $attr); 610 if($this->xml_depth > 1) { 611 $this->xmlobj[$this->xml_depth - 1]->subs[] = $obj; 612 } 613 $this->xmlobj[$this->xml_depth] = $obj; 614 } 615 616 /** 617 * XML end callback 618 * 619 * @see xml_set_element_handler 620 * 621 * @param resource $parser 622 * @param string $name 623 */ 624 public function endXML($parser, $name) { 625 #$this->log->log("Ending $name", Log::LEVEL_DEBUG); 626 #print "$name\n"; 627 if($this->been_reset) { 628 $this->been_reset = false; 629 $this->xml_depth = 0; 630 } 631 $this->xml_depth--; 632 if($this->xml_depth == 1) { 633 #clean-up old objects 634 #$found = false; #FIXME This didn't appear to be in use --Gar 635 foreach($this->xpathhandlers as $handler) { 636 if (is_array($this->xmlobj) && array_key_exists(2, $this->xmlobj)) { 637 $searchxml = $this->xmlobj[2]; 638 $nstag = array_shift($handler[0]); 639 if (($nstag[0] == null or $searchxml->ns == $nstag[0]) and ($nstag[1] == "*" or $nstag[1] == $searchxml->name)) { 640 foreach($handler[0] as $nstag) { 641 if ($searchxml !== null and $searchxml->hasSub($nstag[1], $ns=$nstag[0])) { 642 $searchxml = $searchxml->sub($nstag[1], $ns=$nstag[0]); 643 } else { 644 $searchxml = null; 645 break; 646 } 647 } 648 if ($searchxml !== null) { 649 if($handler[2] === null) $handler[2] = $this; 650 $this->log->log("Calling {$handler[1]}", Log::LEVEL_DEBUG); 651 $handler[2]->{$handler[1]}($this->xmlobj[2]); 652 } 653 } 654 } 655 } 656 foreach($this->nshandlers as $handler) { 657 if($handler[4] != 1 and array_key_exists(2, $this->xmlobj) and $this->xmlobj[2]->hasSub($handler[0])) { 658 $searchxml = $this->xmlobj[2]->sub($handler[0]); 659 } elseif(is_array($this->xmlobj) and array_key_exists(2, $this->xmlobj)) { 660 $searchxml = $this->xmlobj[2]; 661 } 662 if($searchxml !== null and $searchxml->name == $handler[0] and ($searchxml->ns == $handler[1] or (!$handler[1] and $searchxml->ns == $this->default_ns))) { 663 if($handler[3] === null) $handler[3] = $this; 664 $this->log->log("Calling {$handler[2]}", Log::LEVEL_DEBUG); 665 $handler[3]->{$handler[2]}($this->xmlobj[2]); 666 } 667 } 668 foreach($this->idhandlers as $id => $handler) { 669 if(array_key_exists('id', $this->xmlobj[2]->attrs) and $this->xmlobj[2]->attrs['id'] == $id) { 670 if($handler[1] === null) $handler[1] = $this; 671 $handler[1]->{$handler[0]}($this->xmlobj[2]); 672 #id handlers are only used once 673 unset($this->idhandlers[$id]); 674 break; 675 } 676 } 677 if(is_array($this->xmlobj)) { 678 $this->xmlobj = array_slice($this->xmlobj, 0, 1); 679 if(isset($this->xmlobj[0]) && $this->xmlobj[0] instanceof XMLObj) { 680 $this->xmlobj[0]->subs = null; 681 } 682 } 683 unset($this->xmlobj[2]); 684 } 685 if($this->xml_depth == 0 and !$this->been_reset) { 686 if(!$this->disconnected) { 687 if(!$this->sent_disconnect) { 688 $this->send($this->stream_end); 689 } 690 $this->disconnected = true; 691 $this->sent_disconnect = true; 692 fclose($this->socket); 693 if($this->reconnect) { 694 $this->doReconnect(); 695 } 696 } 697 $this->event('end_stream'); 698 } 699 } 700 701 /** 702 * XML character callback 703 * @see xml_set_character_data_handler 704 * 705 * @param resource $parser 706 * @param string $data 707 */ 708 public function charXML($parser, $data) { 709 if(array_key_exists($this->xml_depth, $this->xmlobj)) { 710 $this->xmlobj[$this->xml_depth]->data .= $data; 711 } 712 } 713 714 /** 715 * Event? 716 * 717 * @param string $name 718 * @param string $payload 719 */ 720 public function event($name, $payload = null) { 721 $this->log->log("EVENT: $name", Log::LEVEL_DEBUG); 722 foreach($this->eventhandlers as $handler) { 723 if($name == $handler[0]) { 724 if($handler[2] === null) { 725 $handler[2] = $this; 726 } 727 $handler[2]->{$handler[1]}($payload); 728 } 729 } 730 731 foreach($this->until as $key => $until) { 732 if(is_array($until)) { 733 if(in_array($name, $until)) { 734 $this->until_payload[$key][] = array($name, $payload); 735 if(!isset($this->until_count[$key])) { 736 $this->until_count[$key] = 0; 737 } 738 $this->until_count[$key] += 1; 739 #$this->until[$key] = false; 740 } 741 } 742 } 743 } 744 745 /** 746 * Read from socket 747 */ 748 public function read() { 749 $buff = @fread($this->socket, 1024); 750 if(!$buff) { 751 if($this->reconnect) { 752 $this->doReconnect(); 753 } else { 754 fclose($this->socket); 755 return false; 756 } 757 } 758 $this->log->log("RECV: $buff", Log::LEVEL_VERBOSE); 759 xml_parse($this->parser, $buff, false); 760 } 761 762 /** 763 * Send to socket 764 * 765 * @param string $msg 766 */ 767 public function send($msg, $timeout=NULL) { 768 769 if (is_null($timeout)) { 770 $secs = NULL; 771 $usecs = NULL; 772 } else if ($timeout == 0) { 773 $secs = 0; 774 $usecs = 0; 775 } else { 776 $maximum = $timeout * 1000000; 777 $usecs = $maximum % 1000000; 778 $secs = floor(($maximum - $usecs) / 1000000); 779 } 780 781 $read = array(); 782 $write = array($this->socket); 783 $except = array(); 784 785 $select = @stream_select($read, $write, $except, $secs, $usecs); 786 787 if($select === False) { 788 $this->log->log("ERROR sending message; reconnecting."); 789 $this->doReconnect(); 790 # TODO: retry send here 791 return false; 792 } elseif ($select > 0) { 793 $this->log->log("Socket is ready; send it.", Log::LEVEL_VERBOSE); 794 } else { 795 $this->log->log("Socket is not ready; break.", Log::LEVEL_ERROR); 796 return false; 797 } 798 799 $sentbytes = @fwrite($this->socket, $msg); 800 $this->log->log("SENT: " . mb_substr($msg, 0, $sentbytes, '8bit'), Log::LEVEL_VERBOSE); 801 if($sentbytes === FALSE) { 802 $this->log->log("ERROR sending message; reconnecting.", Log::LEVEL_ERROR); 803 $this->doReconnect(); 804 return false; 805 } 806 $this->log->log("Successfully sent $sentbytes bytes.", Log::LEVEL_VERBOSE); 807 return $sentbytes; 808 } 809 810 public function time() { 811 list($usec, $sec) = explode(" ", microtime()); 812 return (float)$sec + (float)$usec; 813 } 814 815 /** 816 * Reset connection 817 */ 818 public function reset() { 819 $this->xml_depth = 0; 820 unset($this->xmlobj); 821 $this->xmlobj = array(); 822 $this->setupParser(); 823 if(!$this->is_server) { 824 $this->send($this->stream_start); 825 } 826 $this->been_reset = true; 827 } 828 829 /** 830 * Setup the XML parser 831 */ 832 public function setupParser() { 833 $this->parser = xml_parser_create('UTF-8'); 834 xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1); 835 xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, 'UTF-8'); 836 xml_set_object($this->parser, $this); 837 xml_set_element_handler($this->parser, 'startXML', 'endXML'); 838 xml_set_character_data_handler($this->parser, 'charXML'); 839 } 840 841 public function readyToProcess() { 842 $read = array($this->socket); 843 $write = array(); 844 $except = array(); 845 $updated = @stream_select($read, $write, $except, 0); 846 return (($updated !== false) && ($updated > 0)); 847 } 848 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body