[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Utility class. 19 * 20 * @package core 21 * @category phpunit 22 * @copyright 2012 Petr Skoda {@link http://skodak.org} 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 require_once (__DIR__.'/../../testing/classes/util.php'); 27 28 /** 29 * Collection of utility methods. 30 * 31 * @package core 32 * @category phpunit 33 * @copyright 2012 Petr Skoda {@link http://skodak.org} 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class phpunit_util extends testing_util { 37 /** 38 * @var int last value of db writes counter, used for db resetting 39 */ 40 public static $lastdbwrites = null; 41 42 /** @var array An array of original globals, restored after each test */ 43 protected static $globals = array(); 44 45 /** @var array list of debugging messages triggered during the last test execution */ 46 protected static $debuggings = array(); 47 48 /** @var phpunit_message_sink alternative target for moodle messaging */ 49 protected static $messagesink = null; 50 51 /** @var phpunit_phpmailer_sink alternative target for phpmailer messaging */ 52 protected static $phpmailersink = null; 53 54 /** @var phpunit_message_sink alternative target for moodle messaging */ 55 protected static $eventsink = null; 56 57 /** 58 * @var array Files to skip when resetting dataroot folder 59 */ 60 protected static $datarootskiponreset = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess'); 61 62 /** 63 * @var array Files to skip when dropping dataroot folder 64 */ 65 protected static $datarootskipondrop = array('.', '..', 'lock', 'webrunner.xml'); 66 67 /** 68 * Load global $CFG; 69 * @internal 70 * @static 71 * @return void 72 */ 73 public static function initialise_cfg() { 74 global $DB; 75 $dbhash = false; 76 try { 77 $dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest')); 78 } catch (Exception $e) { 79 // not installed yet 80 initialise_cfg(); 81 return; 82 } 83 if ($dbhash !== core_component::get_all_versions_hash()) { 84 // do not set CFG - the only way forward is to drop and reinstall 85 return; 86 } 87 // standard CFG init 88 initialise_cfg(); 89 } 90 91 /** 92 * Reset contents of all database tables to initial values, reset caches, etc. 93 * 94 * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! 95 * 96 * @static 97 * @param bool $detectchanges 98 * true - changes in global state and database are reported as errors 99 * false - no errors reported 100 * null - only critical problems are reported as errors 101 * @return void 102 */ 103 public static function reset_all_data($detectchanges = false) { 104 global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION; 105 106 // Stop any message redirection. 107 phpunit_util::stop_message_redirection(); 108 109 // Stop any message redirection. 110 phpunit_util::stop_phpmailer_redirection(); 111 112 // Stop any message redirection. 113 phpunit_util::stop_event_redirection(); 114 115 // We used to call gc_collect_cycles here to ensure desctructors were called between tests. 116 // This accounted for 25% of the total time running phpunit - so we removed it. 117 118 // Show any unhandled debugging messages, the runbare() could already reset it. 119 self::display_debugging_messages(); 120 self::reset_debugging(); 121 122 // reset global $DB in case somebody mocked it 123 $DB = self::get_global_backup('DB'); 124 125 if ($DB->is_transaction_started()) { 126 // we can not reset inside transaction 127 $DB->force_transaction_rollback(); 128 } 129 130 $resetdb = self::reset_database(); 131 $warnings = array(); 132 133 if ($detectchanges === true) { 134 if ($resetdb) { 135 $warnings[] = 'Warning: unexpected database modification, resetting DB state'; 136 } 137 138 $oldcfg = self::get_global_backup('CFG'); 139 $oldsite = self::get_global_backup('SITE'); 140 foreach($CFG as $k=>$v) { 141 if (!property_exists($oldcfg, $k)) { 142 $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value'; 143 } else if ($oldcfg->$k !== $CFG->$k) { 144 $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value'; 145 } 146 unset($oldcfg->$k); 147 148 } 149 if ($oldcfg) { 150 foreach($oldcfg as $k=>$v) { 151 $warnings[] = 'Warning: unexpected removal of $CFG->'.$k; 152 } 153 } 154 155 if ($USER->id != 0) { 156 $warnings[] = 'Warning: unexpected change of $USER'; 157 } 158 159 if ($COURSE->id != $oldsite->id) { 160 $warnings[] = 'Warning: unexpected change of $COURSE'; 161 } 162 163 } 164 165 if (ini_get('max_execution_time') != 0) { 166 // This is special warning for all resets because we do not want any 167 // libraries to mess with timeouts unintentionally. 168 // Our PHPUnit integration is not supposed to change it either. 169 170 if ($detectchanges !== false) { 171 $warnings[] = 'Warning: max_execution_time was changed to '.ini_get('max_execution_time'); 172 } 173 set_time_limit(0); 174 } 175 176 // restore original globals 177 $_SERVER = self::get_global_backup('_SERVER'); 178 $CFG = self::get_global_backup('CFG'); 179 $SITE = self::get_global_backup('SITE'); 180 $_GET = array(); 181 $_POST = array(); 182 $_FILES = array(); 183 $_REQUEST = array(); 184 $COURSE = $SITE; 185 186 // reinitialise following globals 187 $OUTPUT = new bootstrap_renderer(); 188 $PAGE = new moodle_page(); 189 $FULLME = null; 190 $ME = null; 191 $SCRIPT = null; 192 193 // Empty sessison and set fresh new not-logged-in user. 194 \core\session\manager::init_empty_session(); 195 196 // reset all static caches 197 \core\event\manager::phpunit_reset(); 198 accesslib_clear_all_caches(true); 199 get_string_manager()->reset_caches(true); 200 reset_text_filters_cache(true); 201 events_get_handlers('reset'); 202 core_text::reset_caches(); 203 get_message_processors(false, true); 204 filter_manager::reset_caches(); 205 // Reset internal users. 206 core_user::reset_internal_users(); 207 208 //TODO MDL-25290: add more resets here and probably refactor them to new core function 209 210 // Reset course and module caches. 211 if (class_exists('format_base')) { 212 // If file containing class is not loaded, there is no cache there anyway. 213 format_base::reset_course_cache(0); 214 } 215 get_fast_modinfo(0, 0, true); 216 217 // Reset other singletons. 218 if (class_exists('core_plugin_manager')) { 219 core_plugin_manager::reset_caches(true); 220 } 221 if (class_exists('\core\update\checker')) { 222 \core\update\checker::reset_caches(true); 223 } 224 if (class_exists('\core\update\deployer')) { 225 \core\update\deployer::reset_caches(true); 226 } 227 228 // purge dataroot directory 229 self::reset_dataroot(); 230 231 // restore original config once more in case resetting of caches changed CFG 232 $CFG = self::get_global_backup('CFG'); 233 234 // inform data generator 235 self::get_data_generator()->reset(); 236 237 // fix PHP settings 238 error_reporting($CFG->debug); 239 240 // verify db writes just in case something goes wrong in reset 241 if (self::$lastdbwrites != $DB->perf_get_writes()) { 242 error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); 243 self::$lastdbwrites = $DB->perf_get_writes(); 244 } 245 246 if ($warnings) { 247 $warnings = implode("\n", $warnings); 248 trigger_error($warnings, E_USER_WARNING); 249 } 250 } 251 252 /** 253 * Reset all database tables to default values. 254 * @static 255 * @return bool true if reset done, false if skipped 256 */ 257 public static function reset_database() { 258 global $DB; 259 260 if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) { 261 return false; 262 } 263 264 if (!parent::reset_database()) { 265 return false; 266 } 267 268 self::$lastdbwrites = $DB->perf_get_writes(); 269 270 return true; 271 } 272 273 /** 274 * Called during bootstrap only! 275 * @internal 276 * @static 277 * @return void 278 */ 279 public static function bootstrap_init() { 280 global $CFG, $SITE, $DB; 281 282 // backup the globals 283 self::$globals['_SERVER'] = $_SERVER; 284 self::$globals['CFG'] = clone($CFG); 285 self::$globals['SITE'] = clone($SITE); 286 self::$globals['DB'] = $DB; 287 288 // refresh data in all tables, clear caches, etc. 289 phpunit_util::reset_all_data(); 290 } 291 292 /** 293 * Print some Moodle related info to console. 294 * @internal 295 * @static 296 * @return void 297 */ 298 public static function bootstrap_moodle_info() { 299 echo self::get_site_info(); 300 } 301 302 /** 303 * Returns original state of global variable. 304 * @static 305 * @param string $name 306 * @return mixed 307 */ 308 public static function get_global_backup($name) { 309 if ($name === 'DB') { 310 // no cloning of database object, 311 // we just need the original reference, not original state 312 return self::$globals['DB']; 313 } 314 if (isset(self::$globals[$name])) { 315 if (is_object(self::$globals[$name])) { 316 $return = clone(self::$globals[$name]); 317 return $return; 318 } else { 319 return self::$globals[$name]; 320 } 321 } 322 return null; 323 } 324 325 /** 326 * Is this site initialised to run unit tests? 327 * 328 * @static 329 * @return int array errorcode=>message, 0 means ok 330 */ 331 public static function testing_ready_problem() { 332 global $DB; 333 334 if (!self::is_test_site()) { 335 // dataroot was verified in bootstrap, so it must be DB 336 return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix'); 337 } 338 339 $tables = $DB->get_tables(false); 340 if (empty($tables)) { 341 return array(PHPUNIT_EXITCODE_INSTALL, ''); 342 } 343 344 if (!self::is_test_data_updated()) { 345 return array(PHPUNIT_EXITCODE_REINSTALL, ''); 346 } 347 348 return array(0, ''); 349 } 350 351 /** 352 * Drop all test site data. 353 * 354 * Note: To be used from CLI scripts only. 355 * 356 * @static 357 * @param bool $displayprogress if true, this method will echo progress information. 358 * @return void may terminate execution with exit code 359 */ 360 public static function drop_site($displayprogress = false) { 361 global $DB, $CFG; 362 363 if (!self::is_test_site()) { 364 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!'); 365 } 366 367 // Purge dataroot 368 if ($displayprogress) { 369 echo "Purging dataroot:\n"; 370 } 371 372 self::reset_dataroot(); 373 testing_initdataroot($CFG->dataroot, 'phpunit'); 374 self::drop_dataroot(); 375 376 // drop all tables 377 self::drop_database($displayprogress); 378 } 379 380 /** 381 * Perform a fresh test site installation 382 * 383 * Note: To be used from CLI scripts only. 384 * 385 * @static 386 * @return void may terminate execution with exit code 387 */ 388 public static function install_site() { 389 global $DB, $CFG; 390 391 if (!self::is_test_site()) { 392 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!'); 393 } 394 395 if ($DB->get_tables()) { 396 list($errorcode, $message) = phpunit_util::testing_ready_problem(); 397 if ($errorcode) { 398 phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised'); 399 } else { 400 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised'); 401 } 402 } 403 404 $options = array(); 405 $options['adminpass'] = 'admin'; 406 $options['shortname'] = 'phpunit'; 407 $options['fullname'] = 'PHPUnit test site'; 408 409 install_cli_database($options, false); 410 411 // Set the admin email address. 412 $DB->set_field('user', 'email', '[email protected]', array('username' => 'admin')); 413 414 // Disable all logging for performance and sanity reasons. 415 set_config('enabled_stores', '', 'tool_log'); 416 417 // We need to keep the installed dataroot filedir files. 418 // So each time we reset the dataroot before running a test, the default files are still installed. 419 self::save_original_data_files(); 420 421 // install timezone info 422 $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone'); 423 update_timezone_records($timezones); 424 425 // Store version hash in the database and in a file. 426 self::store_versions_hash(); 427 428 // Store database data and structure. 429 self::store_database_state(); 430 } 431 432 /** 433 * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files using defaults from /phpunit.xml.dist 434 * @static 435 * @return bool true means main config file created, false means only dataroot file created 436 */ 437 public static function build_config_file() { 438 global $CFG; 439 440 $template = ' 441 <testsuite name="@component@_testsuite"> 442 <directory suffix="_test.php">@dir@</directory> 443 </testsuite>'; 444 $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); 445 446 $suites = ''; 447 448 $plugintypes = core_component::get_plugin_types(); 449 ksort($plugintypes); 450 foreach ($plugintypes as $type=>$unused) { 451 $plugs = core_component::get_plugin_list($type); 452 ksort($plugs); 453 foreach ($plugs as $plug=>$fullplug) { 454 if (!file_exists("$fullplug/tests/")) { 455 continue; 456 } 457 $dir = substr($fullplug, strlen($CFG->dirroot)+1); 458 $dir .= '/tests'; 459 $component = $type.'_'.$plug; 460 461 $suite = str_replace('@component@', $component, $template); 462 $suite = str_replace('@dir@', $dir, $suite); 463 464 $suites .= $suite; 465 } 466 } 467 // Start a sequence between 100000 and 199000 to ensure each call to init produces 468 // different ids in the database. This reduces the risk that hard coded values will 469 // end up being placed in phpunit or behat test code. 470 $sequencestart = 100000 + mt_rand(0, 99) * 1000; 471 472 $data = preg_replace('|<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', $suites, $data, 1); 473 $data = str_replace( 474 '<const name="PHPUNIT_SEQUENCE_START" value=""/>', 475 '<const name="PHPUNIT_SEQUENCE_START" value="' . $sequencestart . '"/>', 476 $data); 477 478 $result = false; 479 if (is_writable($CFG->dirroot)) { 480 if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) { 481 testing_fix_file_permissions("$CFG->dirroot/phpunit.xml"); 482 } 483 } 484 485 // relink - it seems that xml:base does not work in phpunit xml files, remove this nasty hack if you find a way to set xml base for relative refs 486 $data = str_replace('lib/phpunit/', $CFG->dirroot.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'phpunit'.DIRECTORY_SEPARATOR, $data); 487 $data = preg_replace('|<directory suffix="_test.php">([^<]+)</directory>|', 488 '<directory suffix="_test.php">'.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1</directory>', 489 $data); 490 file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data); 491 testing_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml"); 492 493 return (bool)$result; 494 } 495 496 /** 497 * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist 498 * 499 * @static 500 * @return void, stops if can not write files 501 */ 502 public static function build_component_config_files() { 503 global $CFG; 504 505 $template = ' 506 <testsuites> 507 <testsuite name="@component@"> 508 <directory suffix="_test.php">.</directory> 509 </testsuite> 510 </testsuites>'; 511 512 // Start a sequence between 100000 and 199000 to ensure each call to init produces 513 // different ids in the database. This reduces the risk that hard coded values will 514 // end up being placed in phpunit or behat test code. 515 $sequencestart = 100000 + mt_rand(0, 99) * 1000; 516 517 // Use the upstream file as source for the distributed configurations 518 $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); 519 $ftemplate = preg_replace('|<!--All core suites.*</testsuites>|s', '<!--@component_suite@-->', $ftemplate); 520 521 // Gets all the components with tests 522 $components = tests_finder::get_components_with_tests('phpunit'); 523 524 // Create the corresponding phpunit.xml file for each component 525 foreach ($components as $cname => $cpath) { 526 // Calculate the component suite 527 $ctemplate = $template; 528 $ctemplate = str_replace('@component@', $cname, $ctemplate); 529 530 // Apply it to the file template 531 $fcontents = str_replace('<!--@component_suite@-->', $ctemplate, $ftemplate); 532 $fcontents = str_replace( 533 '<const name="PHPUNIT_SEQUENCE_START" value=""/>', 534 '<const name="PHPUNIT_SEQUENCE_START" value="' . $sequencestart . '"/>', 535 $fcontents); 536 537 // fix link to schema 538 $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/'); 539 $fcontents = str_replace('lib/phpunit/', str_repeat('../', $level).'lib/phpunit/', $fcontents); 540 541 // Write the file 542 $result = false; 543 if (is_writable($cpath)) { 544 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) { 545 testing_fix_file_permissions("$cpath/phpunit.xml"); 546 } 547 } 548 // Problems writing file, throw error 549 if (!$result) { 550 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, "Can not create $cpath/phpunit.xml configuration file, verify dir permissions"); 551 } 552 } 553 } 554 555 /** 556 * To be called from debugging() only. 557 * @param string $message 558 * @param int $level 559 * @param string $from 560 */ 561 public static function debugging_triggered($message, $level, $from) { 562 // Store only if debugging triggered from actual test, 563 // we need normal debugging outside of tests to find problems in our phpunit integration. 564 $backtrace = debug_backtrace(); 565 566 foreach ($backtrace as $bt) { 567 $intest = false; 568 if (isset($bt['object']) and is_object($bt['object'])) { 569 if ($bt['object'] instanceof PHPUnit_Framework_TestCase) { 570 if (strpos($bt['function'], 'test') === 0) { 571 $intest = true; 572 break; 573 } 574 } 575 } 576 } 577 if (!$intest) { 578 return false; 579 } 580 581 $debug = new stdClass(); 582 $debug->message = $message; 583 $debug->level = $level; 584 $debug->from = $from; 585 586 self::$debuggings[] = $debug; 587 588 return true; 589 } 590 591 /** 592 * Resets the list of debugging messages. 593 */ 594 public static function reset_debugging() { 595 self::$debuggings = array(); 596 set_debugging(DEBUG_DEVELOPER); 597 } 598 599 /** 600 * Returns all debugging messages triggered during test. 601 * @return array with instances having message, level and stacktrace property. 602 */ 603 public static function get_debugging_messages() { 604 return self::$debuggings; 605 } 606 607 /** 608 * Prints out any debug messages accumulated during test execution. 609 * @return bool false if no debug messages, true if debug triggered 610 */ 611 public static function display_debugging_messages() { 612 if (empty(self::$debuggings)) { 613 return false; 614 } 615 foreach(self::$debuggings as $debug) { 616 echo 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n"; 617 } 618 619 return true; 620 } 621 622 /** 623 * Start message redirection. 624 * 625 * Note: Do not call directly from tests, 626 * use $sink = $this->redirectMessages() instead. 627 * 628 * @return phpunit_message_sink 629 */ 630 public static function start_message_redirection() { 631 if (self::$messagesink) { 632 self::stop_message_redirection(); 633 } 634 self::$messagesink = new phpunit_message_sink(); 635 return self::$messagesink; 636 } 637 638 /** 639 * End message redirection. 640 * 641 * Note: Do not call directly from tests, 642 * use $sink->close() instead. 643 */ 644 public static function stop_message_redirection() { 645 self::$messagesink = null; 646 } 647 648 /** 649 * Are messages redirected to some sink? 650 * 651 * Note: to be called from messagelib.php only! 652 * 653 * @return bool 654 */ 655 public static function is_redirecting_messages() { 656 return !empty(self::$messagesink); 657 } 658 659 /** 660 * To be called from messagelib.php only! 661 * 662 * @param stdClass $message record from message_read table 663 * @return bool true means send message, false means message "sent" to sink. 664 */ 665 public static function message_sent($message) { 666 if (self::$messagesink) { 667 self::$messagesink->add_message($message); 668 } 669 } 670 671 /** 672 * Start phpmailer redirection. 673 * 674 * Note: Do not call directly from tests, 675 * use $sink = $this->redirectEmails() instead. 676 * 677 * @return phpunit_phpmailer_sink 678 */ 679 public static function start_phpmailer_redirection() { 680 if (self::$phpmailersink) { 681 self::stop_phpmailer_redirection(); 682 } 683 self::$phpmailersink = new phpunit_phpmailer_sink(); 684 return self::$phpmailersink; 685 } 686 687 /** 688 * End phpmailer redirection. 689 * 690 * Note: Do not call directly from tests, 691 * use $sink->close() instead. 692 */ 693 public static function stop_phpmailer_redirection() { 694 self::$phpmailersink = null; 695 } 696 697 /** 698 * Are messages for phpmailer redirected to some sink? 699 * 700 * Note: to be called from moodle_phpmailer.php only! 701 * 702 * @return bool 703 */ 704 public static function is_redirecting_phpmailer() { 705 return !empty(self::$phpmailersink); 706 } 707 708 /** 709 * To be called from messagelib.php only! 710 * 711 * @param stdClass $message record from message_read table 712 * @return bool true means send message, false means message "sent" to sink. 713 */ 714 public static function phpmailer_sent($message) { 715 if (self::$phpmailersink) { 716 self::$phpmailersink->add_message($message); 717 } 718 } 719 720 /** 721 * Start event redirection. 722 * 723 * @private 724 * Note: Do not call directly from tests, 725 * use $sink = $this->redirectEvents() instead. 726 * 727 * @return phpunit_event_sink 728 */ 729 public static function start_event_redirection() { 730 if (self::$eventsink) { 731 self::stop_event_redirection(); 732 } 733 self::$eventsink = new phpunit_event_sink(); 734 return self::$eventsink; 735 } 736 737 /** 738 * End event redirection. 739 * 740 * @private 741 * Note: Do not call directly from tests, 742 * use $sink->close() instead. 743 */ 744 public static function stop_event_redirection() { 745 self::$eventsink = null; 746 } 747 748 /** 749 * Are events redirected to some sink? 750 * 751 * Note: to be called from \core\event\base only! 752 * 753 * @private 754 * @return bool 755 */ 756 public static function is_redirecting_events() { 757 return !empty(self::$eventsink); 758 } 759 760 /** 761 * To be called from \core\event\base only! 762 * 763 * @private 764 * @param \core\event\base $event record from event_read table 765 * @return bool true means send event, false means event "sent" to sink. 766 */ 767 public static function event_triggered(\core\event\base $event) { 768 if (self::$eventsink) { 769 self::$eventsink->add_event($event); 770 } 771 } 772 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |