[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Copyright 2008-2014 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file COPYING for license information (LGPL). If you 6 * did not receive this file, see http://www.horde.org/licenses/lgpl21. 7 * 8 * @category Horde 9 * @copyright 2008-2014 Horde LLC 10 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 11 * @package Imap_Client 12 */ 13 14 /** 15 * Abstraction of the IMAP4rev1 search criteria (see RFC 3501 [6.4.4]). 16 * Allows translation between abstracted search criteria and a generated IMAP 17 * search criteria string suitable for sending to a remote IMAP server. 18 * 19 * @author Michael Slusarz <slusarz@horde.org> 20 * @category Horde 21 * @copyright 2008-2014 Horde LLC 22 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 23 * @package Imap_Client 24 */ 25 class Horde_Imap_Client_Search_Query implements Serializable 26 { 27 /** 28 * Serialized version. 29 */ 30 const VERSION = 3; 31 32 /** 33 * Constants for dateSearch() 34 */ 35 const DATE_BEFORE = 'BEFORE'; 36 const DATE_ON = 'ON'; 37 const DATE_SINCE = 'SINCE'; 38 39 /** 40 * Constants for intervalSearch() 41 */ 42 const INTERVAL_OLDER = 'OLDER'; 43 const INTERVAL_YOUNGER = 'YOUNGER'; 44 45 /** 46 * The charset of the search strings. All text strings must be in 47 * this charset. By default, this is 'US-ASCII' (see RFC 3501 [6.4.4]). 48 * 49 * @var string 50 */ 51 protected $_charset = null; 52 53 /** 54 * The list of search params. 55 * 56 * @var array 57 */ 58 protected $_search = array(); 59 60 /** 61 * String representation: The IMAP search string. 62 */ 63 public function __toString() 64 { 65 try { 66 $res = $this->build(null); 67 return $res['query']->escape(); 68 } catch (Exception $e) { 69 return ''; 70 } 71 } 72 73 /** 74 * Sets the charset of the search text. 75 * 76 * @param string $charset The charset to use for the search. 77 * @param boolean $convert Convert existing text values? 78 * 79 * @throws Horde_Imap_Client_Exception_SearchCharset 80 */ 81 public function charset($charset, $convert = true) 82 { 83 $oldcharset = $this->_charset; 84 $this->_charset = strtoupper($charset); 85 86 if (!$convert || ($oldcharset == $this->_charset)) { 87 return; 88 } 89 90 foreach (array('header', 'text') as $item) { 91 if (isset($this->_search[$item])) { 92 foreach ($this->_search[$item] as $key => $val) { 93 $new_val = Horde_String::convertCharset($val['text'], $oldcharset, $this->_charset); 94 if (Horde_String::convertCharset($new_val, $this->_charset, $oldcharset) != $val['text']) { 95 throw new Horde_Imap_Client_Exception_SearchCharset($this->_charset); 96 } 97 $this->_search[$item][$key]['text'] = $new_val; 98 } 99 } 100 } 101 } 102 103 /** 104 * Builds an IMAP4rev1 compliant search string. 105 * 106 * @param array $exts The list of extensions supported by the server. 107 * This determines whether certain criteria can be 108 * used, and determines whether workarounds are used 109 * for other criteria. In the format returned by 110 * Horde_Imap_Client_Base::capability(). If this value 111 * is null, all extensions are assumed to be 112 * available. 113 * 114 * @return array An array with these elements: 115 * - charset: (string) The charset of the search string. If null, no 116 * text strings appear in query. 117 * - exts: (array) The list of IMAP extensions used to create the 118 * string. 119 * - query: (Horde_Imap_Client_Data_Format_List) The IMAP search 120 * command. 121 * 122 * @throws Horde_Imap_Client_Exception_NoSupportExtension 123 */ 124 public function build($exts = array()) 125 { 126 $temp = array( 127 'cmds' => new Horde_Imap_Client_Data_Format_List(), 128 'exts' => $exts, 129 'exts_used' => array() 130 ); 131 $cmds = &$temp['cmds']; 132 $charset = null; 133 $exts_used = &$temp['exts_used']; 134 $ptr = &$this->_search; 135 136 if (isset($ptr['new'])) { 137 $this->_addFuzzy(!empty($ptr['newfuzzy']), $temp); 138 if ($ptr['new']) { 139 $cmds->add('NEW'); 140 unset($ptr['flag']['UNSEEN']); 141 } else { 142 $cmds->add('OLD'); 143 } 144 unset($ptr['flag']['RECENT']); 145 } 146 147 if (!empty($ptr['flag'])) { 148 foreach ($ptr['flag'] as $key => $val) { 149 $this->_addFuzzy(!empty($val['fuzzy']), $temp); 150 151 $tmp = ''; 152 if (empty($val['set'])) { 153 // This is a 'NOT' search. All system flags but \Recent 154 // have 'UN' equivalents. 155 if ($key == 'RECENT') { 156 $cmds->add('NOT'); 157 } else { 158 $tmp = 'UN'; 159 } 160 } 161 162 if ($val['type'] == 'keyword') { 163 $cmds->add(array( 164 $tmp . 'KEYWORD', 165 $key 166 )); 167 } else { 168 $cmds->add($tmp . $key); 169 } 170 } 171 } 172 173 if (!empty($ptr['header'])) { 174 /* The list of 'system' headers that have a specific search 175 * query. */ 176 $systemheaders = array( 177 'BCC', 'CC', 'FROM', 'SUBJECT', 'TO' 178 ); 179 180 foreach ($ptr['header'] as $val) { 181 $this->_addFuzzy(!empty($val['fuzzy']), $temp); 182 183 if (!empty($val['not'])) { 184 $cmds->add('NOT'); 185 } 186 187 if (in_array($val['header'], $systemheaders)) { 188 $cmds->add($val['header']); 189 } else { 190 $cmds->add(array( 191 'HEADER', 192 new Horde_Imap_Client_Data_Format_Astring($val['header']) 193 )); 194 } 195 $cmds->add(new Horde_Imap_Client_Data_Format_Astring(isset($val['text']) ? $val['text'] : '')); 196 $charset = is_null($this->_charset) 197 ? 'US-ASCII' 198 : $this->_charset; 199 } 200 } 201 202 if (!empty($ptr['text'])) { 203 foreach ($ptr['text'] as $val) { 204 $this->_addFuzzy(!empty($val['fuzzy']), $temp); 205 206 if (!empty($val['not'])) { 207 $cmds->add('NOT'); 208 } 209 $cmds->add(array( 210 $val['type'], 211 new Horde_Imap_Client_Data_Format_Astring($val['text']) 212 )); 213 if (is_null($charset)) { 214 $charset = is_null($this->_charset) 215 ? 'US-ASCII' 216 : $this->_charset; 217 } 218 } 219 } 220 221 if (!empty($ptr['size'])) { 222 foreach ($ptr['size'] as $key => $val) { 223 $this->_addFuzzy(!empty($val['fuzzy']), $temp); 224 if (!empty($val['not'])) { 225 $cmds->add('NOT'); 226 } 227 $cmds->add(array( 228 $key, 229 new Horde_Imap_Client_Data_Format_Number($val['size']) 230 )); 231 } 232 } 233 234 if (isset($ptr['ids']) && 235 (count($ptr['ids']['ids']) || $ptr['ids']['ids']->special)) { 236 $this->_addFuzzy(!empty($ptr['ids']['fuzzy']), $temp); 237 if (!empty($ptr['ids']['not'])) { 238 $cmds->add('NOT'); 239 } 240 if (!$ptr['ids']['ids']->sequence) { 241 $cmds->add('UID'); 242 } 243 $cmds->add(strval($ptr['ids']['ids'])); 244 } 245 246 if (!empty($ptr['date'])) { 247 foreach ($ptr['date'] as $val) { 248 $this->_addFuzzy(!empty($val['fuzzy']), $temp); 249 250 if (!empty($val['not'])) { 251 $cmds->add('NOT'); 252 } 253 254 if (empty($val['header'])) { 255 $cmds->add($val['range']); 256 } else { 257 $cmds->add('SENT' . $val['range']); 258 } 259 $cmds->add($val['date']); 260 } 261 } 262 263 if (!empty($ptr['within'])) { 264 if (is_null($exts) || isset($exts['WITHIN'])) { 265 $exts_used[] = 'WITHIN'; 266 } 267 268 foreach ($ptr['within'] as $key => $val) { 269 $this->_addFuzzy(!empty($val['fuzzy']), $temp); 270 if (!empty($val['not'])) { 271 $cmds->add('NOT'); 272 } 273 274 if (is_null($exts) || isset($exts['WITHIN'])) { 275 $cmds->add(array( 276 $key, 277 new Horde_Imap_Client_Data_Format_Number($val['interval']) 278 )); 279 } else { 280 // This workaround is only accurate to within 1 day, due 281 // to limitations with the IMAP4rev1 search commands. 282 $cmds->add(array( 283 ($key == self::INTERVAL_OLDER) ? self::DATE_BEFORE : self::DATE_SINCE, 284 new Horde_Imap_Client_Data_Format_Date('now -' . $val['interval'] . ' seconds') 285 )); 286 } 287 } 288 } 289 290 if (!empty($ptr['modseq'])) { 291 if (!is_null($exts) && !isset($exts['CONDSTORE'])) { 292 throw new Horde_Imap_Client_Exception_NoSupportExtension('CONDSTORE'); 293 } 294 295 $exts_used[] = 'CONDSTORE'; 296 297 $this->_addFuzzy(!empty($ptr['modseq']['fuzzy']), $temp); 298 299 if (!empty($ptr['modseq']['not'])) { 300 $cmds->add('NOT'); 301 } 302 $cmds->add('MODSEQ'); 303 if (isset($ptr['modseq']['name'])) { 304 $cmds->add(array( 305 new Horde_Imap_Client_Data_Format_String($ptr['modseq']['name']), 306 $ptr['modseq']['type'] 307 )); 308 } 309 $cmds->add(new Horde_Imap_Client_Data_Format_Number($ptr['modseq']['value'])); 310 } 311 312 if (isset($ptr['prevsearch'])) { 313 if (!is_null($exts) && !isset($exts['SEARCHRES'])) { 314 throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES'); 315 } 316 317 $exts_used[] = 'SEARCHRES'; 318 319 $this->_addFuzzy(!empty($ptr['prevsearchfuzzy']), $temp); 320 321 if (!$ptr['prevsearch']) { 322 $cmds->add('NOT'); 323 } 324 $cmds->add('$'); 325 } 326 327 // Add AND'ed queries 328 if (!empty($ptr['and'])) { 329 foreach ($ptr['and'] as $val) { 330 $ret = $val->build(); 331 if ($ret['charset'] != 'US-ASCII') { 332 $charset = $ret['charset']; 333 } 334 $exts_used = array_merge($exts_used, $ret['exts']); 335 $cmds->add($ret['query'], true); 336 } 337 } 338 339 // Add OR'ed queries 340 if (!empty($ptr['or'])) { 341 foreach ($ptr['or'] as $val) { 342 $ret = $val->build(); 343 344 if ($ret['charset'] != 'US-ASCII') { 345 $charset = $ret['charset']; 346 } 347 $exts_used = array_merge($exts_used, $ret['exts']); 348 349 // First OR'd query 350 if (count($cmds)) { 351 $new_cmds = new Horde_Imap_Client_Data_Format_List(); 352 $new_cmds->add(array( 353 'OR', 354 $ret['query'], 355 $cmds 356 )); 357 $cmds = $new_cmds; 358 } else { 359 $cmds = $ret['query']; 360 } 361 } 362 } 363 364 // Default search is 'ALL' 365 if (!count($cmds)) { 366 $cmds->add('ALL'); 367 } 368 369 return array( 370 'charset' => $charset, 371 'exts' => array_keys(array_flip($exts_used)), 372 'query' => $cmds 373 ); 374 } 375 376 /** 377 * Adds fuzzy modifier to search keys. 378 * 379 * @param boolean $add Add the fuzzy modifier? 380 * @param array $temp Temporary build data. 381 * 382 * @throws Horde_Imap_Client_Exception_NoSupport_Extension 383 */ 384 protected function _addFuzzy($add, &$temp) 385 { 386 if ($add) { 387 if (!isset($temp['exts']['SEARCH']) || 388 !in_array('FUZZY', $temp['exts']['SEARCH'])) { 389 throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCH=FUZZY'); 390 } 391 $temp['cmds']->add('FUZZY'); 392 $temp['exts_used'][] = 'SEARCH=FUZZY'; 393 } 394 } 395 396 /** 397 * Search for a flag/keywords. 398 * 399 * @param string $name The flag or keyword name. 400 * @param boolean $set If true, search for messages that have the flag 401 * set. If false, search for messages that do not 402 * have the flag set. 403 * @param array $opts Additional options: 404 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 405 * MUST support RFC 6203. 406 */ 407 public function flag($name, $set = true, array $opts = array()) 408 { 409 $name = strtoupper(ltrim($name, '\\')); 410 if (!isset($this->_search['flag'])) { 411 $this->_search['flag'] = array(); 412 } 413 414 /* The list of defined system flags (see RFC 3501 [2.3.2]). */ 415 $systemflags = array( 416 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'RECENT', 'SEEN' 417 ); 418 419 $this->_search['flag'][$name] = array_filter(array( 420 'fuzzy' => !empty($opts['fuzzy']), 421 'set' => $set, 422 'type' => in_array($name, $systemflags) ? 'flag' : 'keyword' 423 )); 424 } 425 426 /** 427 * Determines if flags are a part of the search. 428 * 429 * @return boolean True if search query involves flags. 430 */ 431 public function flagSearch() 432 { 433 return !empty($this->_search['flag']); 434 } 435 436 /** 437 * Search for either new messages (messages that have the '\Recent' flag 438 * but not the '\Seen' flag) or old messages (messages that do not have 439 * the '\Recent' flag). If new messages are searched, this will clear 440 * any '\Recent' or '\Unseen' flag searches. If old messages are searched, 441 * this will clear any '\Recent' flag search. 442 * 443 * @param boolean $newmsgs If true, searches for new messages. Else, 444 * search for old messages. 445 * @param array $opts Additional options: 446 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 447 * MUST support RFC 6203. 448 */ 449 public function newMsgs($newmsgs = true, array $opts = array()) 450 { 451 $this->_search['new'] = $newmsgs; 452 if (!empty($opts['fuzzy'])) { 453 $this->_search['newfuzzy'] = true; 454 } 455 } 456 457 /** 458 * Search for text in the header of a message. 459 * 460 * @param string $header The header field. 461 * @param string $text The search text. 462 * @param boolean $not If true, do a 'NOT' search of $text. 463 * @param array $opts Additional options: 464 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 465 * MUST support RFC 6203. 466 */ 467 public function headerText($header, $text, $not = false, 468 array $opts = array()) 469 { 470 if (!isset($this->_search['header'])) { 471 $this->_search['header'] = array(); 472 } 473 $this->_search['header'][] = array_filter(array( 474 'fuzzy' => !empty($opts['fuzzy']), 475 'header' => strtoupper($header), 476 'text' => $text, 477 'not' => $not 478 )); 479 } 480 481 /** 482 * Search for text in either the entire message, or just the body. 483 * 484 * @param string $text The search text. 485 * @param string $bodyonly If true, only search in the body of the 486 * message. If false, also search in the headers. 487 * @param boolean $not If true, do a 'NOT' search of $text. 488 * @param array $opts Additional options: 489 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 490 * MUST support RFC 6203. 491 */ 492 public function text($text, $bodyonly = true, $not = false, 493 array $opts = array()) 494 { 495 if (!isset($this->_search['text'])) { 496 $this->_search['text'] = array(); 497 } 498 499 $this->_search['text'][] = array_filter(array( 500 'fuzzy' => !empty($opts['fuzzy']), 501 'not' => $not, 502 'text' => $text, 503 'type' => $bodyonly ? 'BODY' : 'TEXT' 504 )); 505 } 506 507 /** 508 * Search for messages smaller/larger than a certain size. 509 * 510 * @todo: Remove $not for 3.0 511 * 512 * @param integer $size The size (in bytes). 513 * @param boolean $larger Search for messages larger than $size? 514 * @param boolean $not If true, do a 'NOT' search of $text. 515 * @param array $opts Additional options: 516 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 517 * MUST support RFC 6203. 518 */ 519 public function size($size, $larger = false, $not = false, 520 array $opts = array()) 521 { 522 if (!isset($this->_search['size'])) { 523 $this->_search['size'] = array(); 524 } 525 $this->_search['size'][$larger ? 'LARGER' : 'SMALLER'] = array_filter(array( 526 'fuzzy' => !empty($opts['fuzzy']), 527 'not' => $not, 528 'size' => (float)$size 529 )); 530 } 531 532 /** 533 * Search for messages within a given UID range. Only one message range 534 * can be specified per query. 535 * 536 * @param Horde_Imap_Client_Ids $ids The list of UIDs to search. 537 * @param boolean $not If true, do a 'NOT' search of the 538 * UIDs. 539 * @param array $opts Additional options: 540 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 541 * MUST support RFC 6203. 542 */ 543 public function ids(Horde_Imap_Client_Ids $ids, $not = false, 544 array $opts = array()) 545 { 546 if (!$ids->isEmpty()) { 547 $this->_search['ids'] = array_filter(array( 548 'fuzzy' => !empty($opts['fuzzy']), 549 'ids' => $ids, 550 'not' => $not 551 )); 552 } 553 } 554 555 /** 556 * Search for messages within a date range. 557 * 558 * @param mixed $date DateTime or Horde_Date object. 559 * @param string $range Either: 560 * - Horde_Imap_Client_Search_Query::DATE_BEFORE 561 * - Horde_Imap_Client_Search_Query::DATE_ON 562 * - Horde_Imap_Client_Search_Query::DATE_SINCE 563 * @param boolean $header If true, search using the date in the message 564 * headers. If false, search using the internal 565 * IMAP date (usually arrival time). 566 * @param boolean $not If true, do a 'NOT' search of the range. 567 * @param array $opts Additional options: 568 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 569 * MUST support RFC 6203. 570 */ 571 public function dateSearch($date, $range, $header = true, $not = false, 572 array $opts = array()) 573 { 574 if (!isset($this->_search['date'])) { 575 $this->_search['date'] = array(); 576 } 577 578 // We should really be storing the raw DateTime object as data, 579 // but all versions of the query object have converted at this stage. 580 $ob = new Horde_Imap_Client_Data_Format_Date($date); 581 582 $this->_search['date'][] = array_filter(array( 583 'date' => $ob->escape(), 584 'fuzzy' => !empty($opts['fuzzy']), 585 'header' => $header, 586 'range' => $range, 587 'not' => $not 588 )); 589 } 590 591 /** 592 * Search for messages within a given interval. Only one interval of each 593 * type can be specified per search query. If the IMAP server supports 594 * the WITHIN extension (RFC 5032), it will be used. Otherwise, the 595 * search query will be dynamically created using IMAP4rev1 search 596 * terms. 597 * 598 * @param integer $interval Seconds from the present. 599 * @param string $range Either: 600 * - Horde_Imap_Client_Search_Query::INTERVAL_OLDER 601 * - Horde_Imap_Client_Search_Query::INTERVAL_YOUNGER 602 * @param boolean $not If true, do a 'NOT' search. 603 * @param array $opts Additional options: 604 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 605 * MUST support RFC 6203. 606 */ 607 public function intervalSearch($interval, $range, $not = false, 608 array $opts = array()) 609 { 610 if (!isset($this->_search['within'])) { 611 $this->_search['within'] = array(); 612 } 613 $this->_search['within'][$range] = array( 614 'fuzzy' => !empty($opts['fuzzy']), 615 'interval' => $interval, 616 'not' => $not 617 ); 618 } 619 620 /** 621 * AND queries - the contents of this query will be AND'ed (in its 622 * entirety) with the contents of EACH of the queries passed in. All 623 * AND'd queries must share the same charset as this query. 624 * 625 * @param mixed $queries A query, or an array of queries, to AND with the 626 * current query. 627 */ 628 public function andSearch($queries) 629 { 630 if (!isset($this->_search['and'])) { 631 $this->_search['and'] = array(); 632 } 633 634 if ($queries instanceof Horde_Imap_Client_Search_Query) { 635 $queries = array($queries); 636 } 637 638 $this->_search['and'] = array_merge($this->_search['and'], $queries); 639 } 640 641 /** 642 * OR a query - the contents of this query will be OR'ed (in its entirety) 643 * with the contents of EACH of the queries passed in. All OR'd queries 644 * must share the same charset as this query. All contents of any single 645 * query will be AND'ed together. 646 * 647 * @param mixed $queries A query, or an array of queries, to OR with the 648 * current query. 649 */ 650 public function orSearch($queries) 651 { 652 if (!isset($this->_search['or'])) { 653 $this->_search['or'] = array(); 654 } 655 656 if ($queries instanceof Horde_Imap_Client_Search_Query) { 657 $queries = array($queries); 658 } 659 660 $this->_search['or'] = array_merge($this->_search['or'], $queries); 661 } 662 663 /** 664 * Search for messages modified since a specific moment. The IMAP server 665 * must support the CONDSTORE extension (RFC 7162) for this query to be 666 * used. 667 * 668 * @param integer $value The mod-sequence value. 669 * @param string $name The entry-name string. 670 * @param string $type Either 'shared', 'priv', or 'all'. Defaults to 671 * 'all' 672 * @param boolean $not If true, do a 'NOT' search. 673 * @param array $opts Additional options: 674 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 675 * MUST support RFC 6203. 676 */ 677 public function modseq($value, $name = null, $type = null, $not = false, 678 array $opts = array()) 679 { 680 if (!is_null($type)) { 681 $type = strtolower($type); 682 if (!in_array($type, array('shared', 'priv', 'all'))) { 683 $type = 'all'; 684 } 685 } 686 687 $this->_search['modseq'] = array_filter(array( 688 'fuzzy' => !empty($opts['fuzzy']), 689 'name' => $name, 690 'not' => $not, 691 'type' => (!is_null($name) && is_null($type)) ? 'all' : $type, 692 'value' => $value 693 )); 694 } 695 696 /** 697 * Use the results from the previous SEARCH command. The IMAP server must 698 * support the SEARCHRES extension (RFC 5182) for this query to be used. 699 * 700 * @param boolean $not If true, don't match the previous query. 701 * @param array $opts Additional options: 702 * - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server 703 * MUST support RFC 6203. 704 */ 705 public function previousSearch($not = false, array $opts = array()) 706 { 707 $this->_search['prevsearch'] = $not; 708 if (!empty($opts['fuzzy'])) { 709 $this->_search['prevsearchfuzzy'] = true; 710 } 711 } 712 713 /* Serializable methods. */ 714 715 /** 716 * Serialization. 717 * 718 * @return string Serialized data. 719 */ 720 public function serialize() 721 { 722 $data = array( 723 // Serialized data ID. 724 self::VERSION, 725 $this->_search 726 ); 727 728 if (!is_null($this->_charset)) { 729 $data[] = $this->_charset; 730 } 731 732 return serialize($data); 733 } 734 735 /** 736 * Unserialization. 737 * 738 * @param string $data Serialized data. 739 * 740 * @throws Exception 741 */ 742 public function unserialize($data) 743 { 744 $data = @unserialize($data); 745 if (!is_array($data) || 746 !isset($data[0]) || 747 ($data[0] != self::VERSION)) { 748 throw new Exception('Cache version change'); 749 } 750 751 $this->_search = $data[1]; 752 if (isset($data[2])) { 753 $this->_charset = $data[2]; 754 } 755 } 756 757 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |