[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/blocks/ -> moodleblock.class.php (source)

   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   * This file contains the parent class for moodle blocks, block_base.
  19   *
  20   * @package    core_block
  21   * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  22   */
  23  
  24  /// Constants
  25  
  26  /**
  27   * Block type of list. Contents of block should be set as an associative array in the content object as items ($this->content->items). Optionally include footer text in $this->content->footer.
  28   */
  29  define('BLOCK_TYPE_LIST',    1);
  30  
  31  /**
  32   * Block type of text. Contents of block should be set to standard html text in the content object as items ($this->content->text). Optionally include footer text in $this->content->footer.
  33   */
  34  define('BLOCK_TYPE_TEXT',    2);
  35  /**
  36   * Block type of tree. $this->content->items is a list of tree_item objects and $this->content->footer is a string.
  37   */
  38  define('BLOCK_TYPE_TREE',    3);
  39  
  40  /**
  41   * Class for describing a moodle block, all Moodle blocks derive from this class
  42   *
  43   * @author Jon Papaioannou
  44   * @package core_block
  45   */
  46  class block_base {
  47  
  48      /**
  49       * Internal var for storing/caching translated strings
  50       * @var string $str
  51       */
  52      var $str;
  53  
  54      /**
  55       * The title of the block to be displayed in the block title area.
  56       * @var string $title
  57       */
  58      var $title         = NULL;
  59  
  60      /**
  61       * The name of the block to be displayed in the block title area if the title is empty.
  62       * @var string arialabel
  63       */
  64      var $arialabel         = NULL;
  65  
  66      /**
  67       * The type of content that this block creates. Currently support options - BLOCK_TYPE_LIST, BLOCK_TYPE_TEXT
  68       * @var int $content_type
  69       */
  70      var $content_type  = BLOCK_TYPE_TEXT;
  71  
  72      /**
  73       * An object to contain the information to be displayed in the block.
  74       * @var stdObject $content
  75       */
  76      var $content       = NULL;
  77  
  78      /**
  79       * The initialized instance of this block object.
  80       * @var block $instance
  81       */
  82      var $instance      = NULL;
  83  
  84      /**
  85       * The page that this block is appearing on.
  86       * @var moodle_page
  87       */
  88      public $page       = NULL;
  89  
  90      /**
  91       * This blocks's context.
  92       * @var stdClass
  93       */
  94      public $context    = NULL;
  95  
  96      /**
  97       * An object containing the instance configuration information for the current instance of this block.
  98       * @var stdObject $config
  99       */
 100      var $config        = NULL;
 101  
 102      /**
 103       * How often the cronjob should run, 0 if not at all.
 104       * @var int $cron
 105       */
 106  
 107      var $cron          = NULL;
 108  
 109  /// Class Functions
 110  
 111      /**
 112       * Fake constructor to keep PHP5 happy
 113       *
 114       */
 115      function __construct() {
 116          $this->init();
 117      }
 118  
 119      /**
 120       * Function that can be overridden to do extra cleanup before
 121       * the database tables are deleted. (Called once per block, not per instance!)
 122       */
 123      function before_delete() {
 124      }
 125  
 126      /**
 127       * Returns the block name, as present in the class name,
 128       * the database, the block directory, etc etc.
 129       *
 130       * @return string
 131       */
 132      function name() {
 133          // Returns the block name, as present in the class name,
 134          // the database, the block directory, etc etc.
 135          static $myname;
 136          if ($myname === NULL) {
 137              $myname = strtolower(get_class($this));
 138              $myname = substr($myname, strpos($myname, '_') + 1);
 139          }
 140          return $myname;
 141      }
 142  
 143      /**
 144       * Parent class version of this function simply returns NULL
 145       * This should be implemented by the derived class to return
 146       * the content object.
 147       *
 148       * @return stdObject
 149       */
 150      function get_content() {
 151          // This should be implemented by the derived class.
 152          return NULL;
 153      }
 154  
 155      /**
 156       * Returns the class $title var value.
 157       *
 158       * Intentionally doesn't check if a title is set.
 159       * This is already done in {@link _self_test()}
 160       *
 161       * @return string $this->title
 162       */
 163      function get_title() {
 164          // Intentionally doesn't check if a title is set. This is already done in _self_test()
 165          return $this->title;
 166      }
 167  
 168      /**
 169       * Returns the class $content_type var value.
 170       *
 171       * Intentionally doesn't check if content_type is set.
 172       * This is already done in {@link _self_test()}
 173       *
 174       * @return string $this->content_type
 175       */
 176      function get_content_type() {
 177          // Intentionally doesn't check if a content_type is set. This is already done in _self_test()
 178          return $this->content_type;
 179      }
 180  
 181      /**
 182       * Returns true or false, depending on whether this block has any content to display
 183       * and whether the user has permission to view the block
 184       *
 185       * @return boolean
 186       */
 187      function is_empty() {
 188          if ( !has_capability('moodle/block:view', $this->context) ) {
 189              return true;
 190          }
 191  
 192          $this->get_content();
 193          return(empty($this->content->text) && empty($this->content->footer));
 194      }
 195  
 196      /**
 197       * First sets the current value of $this->content to NULL
 198       * then calls the block's {@link get_content()} function
 199       * to set its value back.
 200       *
 201       * @return stdObject
 202       */
 203      function refresh_content() {
 204          // Nothing special here, depends on content()
 205          $this->content = NULL;
 206          return $this->get_content();
 207      }
 208  
 209      /**
 210       * Return a block_contents object representing the full contents of this block.
 211       *
 212       * This internally calls ->get_content(), and then adds the editing controls etc.
 213       *
 214       * You probably should not override this method, but instead override
 215       * {@link html_attributes()}, {@link formatted_contents()} or {@link get_content()},
 216       * {@link hide_header()}, {@link (get_edit_controls)}, etc.
 217       *
 218       * @return block_contents a representation of the block, for rendering.
 219       * @since Moodle 2.0.
 220       */
 221      public function get_content_for_output($output) {
 222          global $CFG;
 223  
 224          $bc = new block_contents($this->html_attributes());
 225          $bc->attributes['data-block'] = $this->name();
 226          $bc->blockinstanceid = $this->instance->id;
 227          $bc->blockpositionid = $this->instance->blockpositionid;
 228  
 229          if ($this->instance->visible) {
 230              $bc->content = $this->formatted_contents($output);
 231              if (!empty($this->content->footer)) {
 232                  $bc->footer = $this->content->footer;
 233              }
 234          } else {
 235              $bc->add_class('invisible');
 236          }
 237  
 238          if (!$this->hide_header()) {
 239              $bc->title = $this->title;
 240          }
 241  
 242          if (empty($bc->title)) {
 243              $bc->arialabel = new lang_string('pluginname', get_class($this));
 244              $this->arialabel = $bc->arialabel;
 245          }
 246  
 247          if ($this->page->user_is_editing()) {
 248              $bc->controls = $this->page->blocks->edit_controls($this);
 249          } else {
 250              // we must not use is_empty on hidden blocks
 251              if ($this->is_empty() && !$bc->controls) {
 252                  return null;
 253              }
 254          }
 255  
 256          if (empty($CFG->allowuserblockhiding)
 257                  || (empty($bc->content) && empty($bc->footer))
 258                  || !$this->instance_can_be_collapsed()) {
 259              $bc->collapsible = block_contents::NOT_HIDEABLE;
 260          } else if (get_user_preferences('block' . $bc->blockinstanceid . 'hidden', false)) {
 261              $bc->collapsible = block_contents::HIDDEN;
 262          } else {
 263              $bc->collapsible = block_contents::VISIBLE;
 264          }
 265  
 266          if ($this->instance_can_be_docked() && !$this->hide_header()) {
 267              $bc->dockable = true;
 268          }
 269  
 270          $bc->annotation = ''; // TODO MDL-19398 need to work out what to say here.
 271  
 272          return $bc;
 273      }
 274  
 275      /**
 276       * Convert the contents of the block to HTML.
 277       *
 278       * This is used by block base classes like block_list to convert the structured
 279       * $this->content->list and $this->content->icons arrays to HTML. So, in most
 280       * blocks, you probaby want to override the {@link get_contents()} method,
 281       * which generates that structured representation of the contents.
 282       *
 283       * @param $output The core_renderer to use when generating the output.
 284       * @return string the HTML that should appearn in the body of the block.
 285       * @since Moodle 2.0.
 286       */
 287      protected function formatted_contents($output) {
 288          $this->get_content();
 289          $this->get_required_javascript();
 290          if (!empty($this->content->text)) {
 291              return $this->content->text;
 292          } else {
 293              return '';
 294          }
 295      }
 296  
 297      /**
 298       * Tests if this block has been implemented correctly.
 299       * Also, $errors isn't used right now
 300       *
 301       * @return boolean
 302       */
 303  
 304      function _self_test() {
 305          // Tests if this block has been implemented correctly.
 306          // Also, $errors isn't used right now
 307          $errors = array();
 308  
 309          $correct = true;
 310          if ($this->get_title() === NULL) {
 311              $errors[] = 'title_not_set';
 312              $correct = false;
 313          }
 314          if (!in_array($this->get_content_type(), array(BLOCK_TYPE_LIST, BLOCK_TYPE_TEXT, BLOCK_TYPE_TREE))) {
 315              $errors[] = 'invalid_content_type';
 316              $correct = false;
 317          }
 318          //following selftest was not working when roles&capabilities were used from block
 319  /*        if ($this->get_content() === NULL) {
 320              $errors[] = 'content_not_set';
 321              $correct = false;
 322          }*/
 323          $formats = $this->applicable_formats();
 324          if (empty($formats) || array_sum($formats) === 0) {
 325              $errors[] = 'no_formats';
 326              $correct = false;
 327          }
 328  
 329          $width = $this->preferred_width();
 330          if (!is_int($width) || $width <= 0) {
 331              $errors[] = 'invalid_width';
 332              $correct = false;
 333          }
 334          return $correct;
 335      }
 336  
 337      /**
 338       * Subclasses should override this and return true if the
 339       * subclass block has a settings.php file.
 340       *
 341       * @return boolean
 342       */
 343      function has_config() {
 344          return false;
 345      }
 346  
 347      /**
 348       * Default behavior: save all variables as $CFG properties
 349       * You don't need to override this if you 're satisfied with the above
 350       *
 351       * @param array $data
 352       * @return boolean
 353       */
 354      function config_save($data) {
 355          foreach ($data as $name => $value) {
 356              set_config($name, $value);
 357          }
 358          return true;
 359      }
 360  
 361      /**
 362       * Which page types this block may appear on.
 363       *
 364       * The information returned here is processed by the
 365       * {@link blocks_name_allowed_in_format()} function. Look there if you need
 366       * to know exactly how this works.
 367       *
 368       * Default case: everything except mod and tag.
 369       *
 370       * @return array page-type prefix => true/false.
 371       */
 372      function applicable_formats() {
 373          // Default case: the block can be used in courses and site index, but not in activities
 374          return array('all' => true, 'mod' => false, 'tag' => false);
 375      }
 376  
 377  
 378      /**
 379       * Default return is false - header will be shown
 380       * @return boolean
 381       */
 382      function hide_header() {
 383          return false;
 384      }
 385  
 386      /**
 387       * Return any HTML attributes that you want added to the outer <div> that
 388       * of the block when it is output.
 389       *
 390       * Because of the way certain JS events are wired it is a good idea to ensure
 391       * that the default values here still get set.
 392       * I found the easiest way to do this and still set anything you want is to
 393       * override it within your block in the following way
 394       *
 395       * <code php>
 396       * function html_attributes() {
 397       *    $attributes = parent::html_attributes();
 398       *    $attributes['class'] .= ' mynewclass';
 399       *    return $attributes;
 400       * }
 401       * </code>
 402       *
 403       * @return array attribute name => value.
 404       */
 405      function html_attributes() {
 406          $attributes = array(
 407              'id' => 'inst' . $this->instance->id,
 408              'class' => 'block_' . $this->name(). '  block',
 409              'role' => $this->get_aria_role()
 410          );
 411          if ($this->hide_header()) {
 412              $attributes['class'] .= ' no-header';
 413          }
 414          if ($this->instance_can_be_docked() && get_user_preferences('docked_block_instance_'.$this->instance->id, 0)) {
 415              $attributes['class'] .= ' dock_on_load';
 416          }
 417          return $attributes;
 418      }
 419  
 420      /**
 421       * Set up a particular instance of this class given data from the block_insances
 422       * table and the current page. (See {@link block_manager::load_blocks()}.)
 423       *
 424       * @param stdClass $instance data from block_insances, block_positions, etc.
 425       * @param moodle_page $the page this block is on.
 426       */
 427      function _load_instance($instance, $page) {
 428          if (!empty($instance->configdata)) {
 429              $this->config = unserialize(base64_decode($instance->configdata));
 430          }
 431          $this->instance = $instance;
 432          $this->context = context_block::instance($instance->id);
 433          $this->page = $page;
 434          $this->specialization();
 435      }
 436  
 437      /**
 438       * Allows the block to load any JS it requires into the page.
 439       *
 440       * By default this function simply permits the user to dock the block if it is dockable.
 441       */
 442      function get_required_javascript() {
 443          if ($this->instance_can_be_docked() && !$this->hide_header()) {
 444              user_preference_allow_ajax_update('docked_block_instance_'.$this->instance->id, PARAM_INT);
 445          }
 446      }
 447  
 448      /**
 449       * This function is called on your subclass right after an instance is loaded
 450       * Use this function to act on instance data just after it's loaded and before anything else is done
 451       * For instance: if your block will have different title's depending on location (site, course, blog, etc)
 452       */
 453      function specialization() {
 454          // Just to make sure that this method exists.
 455      }
 456  
 457      /**
 458       * Is each block of this type going to have instance-specific configuration?
 459       * Normally, this setting is controlled by {@link instance_allow_multiple()}: if multiple
 460       * instances are allowed, then each will surely need its own configuration. However, in some
 461       * cases it may be necessary to provide instance configuration to blocks that do not want to
 462       * allow multiple instances. In that case, make this function return true.
 463       * I stress again that this makes a difference ONLY if {@link instance_allow_multiple()} returns false.
 464       * @return boolean
 465       */
 466      function instance_allow_config() {
 467          return false;
 468      }
 469  
 470      /**
 471       * Are you going to allow multiple instances of each block?
 472       * If yes, then it is assumed that the block WILL USE per-instance configuration
 473       * @return boolean
 474       */
 475      function instance_allow_multiple() {
 476          // Are you going to allow multiple instances of each block?
 477          // If yes, then it is assumed that the block WILL USE per-instance configuration
 478          return false;
 479      }
 480  
 481      /**
 482       * Serialize and store config data
 483       */
 484      function instance_config_save($data, $nolongerused = false) {
 485          global $DB;
 486          $DB->set_field('block_instances', 'configdata', base64_encode(serialize($data)),
 487                  array('id' => $this->instance->id));
 488      }
 489  
 490      /**
 491       * Replace the instance's configuration data with those currently in $this->config;
 492       */
 493      function instance_config_commit($nolongerused = false) {
 494          global $DB;
 495          $this->instance_config_save($this->config);
 496      }
 497  
 498      /**
 499       * Do any additional initialization you may need at the time a new block instance is created
 500       * @return boolean
 501       */
 502      function instance_create() {
 503          return true;
 504      }
 505  
 506      /**
 507       * Delete everything related to this instance if you have been using persistent storage other than the configdata field.
 508       * @return boolean
 509       */
 510      function instance_delete() {
 511          return true;
 512      }
 513  
 514      /**
 515       * Allows the block class to have a say in the user's ability to edit (i.e., configure) blocks of this type.
 516       * The framework has first say in whether this will be allowed (e.g., no editing allowed unless in edit mode)
 517       * but if the framework does allow it, the block can still decide to refuse.
 518       * @return boolean
 519       */
 520      function user_can_edit() {
 521          global $USER;
 522  
 523          if (has_capability('moodle/block:edit', $this->context)) {
 524              return true;
 525          }
 526  
 527          // The blocks in My Moodle are a special case.  We want them to inherit from the user context.
 528          if (!empty($USER->id)
 529              && $this->instance->parentcontextid == $this->page->context->id   // Block belongs to this page
 530              && $this->page->context->contextlevel == CONTEXT_USER             // Page belongs to a user
 531              && $this->page->context->instanceid == $USER->id) {               // Page belongs to this user
 532              return has_capability('moodle/my:manageblocks', $this->page->context);
 533          }
 534  
 535          return false;
 536      }
 537  
 538      /**
 539       * Allows the block class to have a say in the user's ability to create new instances of this block.
 540       * The framework has first say in whether this will be allowed (e.g., no adding allowed unless in edit mode)
 541       * but if the framework does allow it, the block can still decide to refuse.
 542       * This function has access to the complete page object, the creation related to which is being determined.
 543       *
 544       * @param moodle_page $page
 545       * @return boolean
 546       */
 547      function user_can_addto($page) {
 548          global $USER;
 549  
 550          // The blocks in My Moodle are a special case and use a different capability.
 551          if (!empty($USER->id)
 552              && $page->context->contextlevel == CONTEXT_USER // Page belongs to a user
 553              && $page->context->instanceid == $USER->id // Page belongs to this user
 554              && $page->pagetype == 'my-index') { // Ensure we are on the My Moodle page
 555              $capability = 'block/' . $this->name() . ':myaddinstance';
 556              return $this->has_add_block_capability($page, $capability)
 557                      && has_capability('moodle/my:manageblocks', $page->context);
 558          }
 559  
 560          $capability = 'block/' . $this->name() . ':addinstance';
 561          if ($this->has_add_block_capability($page, $capability)
 562                  && has_capability('moodle/block:edit', $page->context)) {
 563              return true;
 564          }
 565  
 566          return false;
 567      }
 568  
 569      /**
 570       * Returns true if the user can add a block to a page.
 571       *
 572       * @param moodle_page $page
 573       * @param string $capability the capability to check
 574       * @return boolean true if user can add a block, false otherwise.
 575       */
 576      private function has_add_block_capability($page, $capability) {
 577          // Check if the capability exists.
 578          if (!get_capability_info($capability)) {
 579              // Debug warning that the capability does not exist, but no more than once per page.
 580              static $warned = array();
 581              if (!isset($warned[$this->name()])) {
 582                  debugging('The block ' .$this->name() . ' does not define the standard capability ' .
 583                          $capability , DEBUG_DEVELOPER);
 584                  $warned[$this->name()] = 1;
 585              }
 586              // If the capability does not exist, the block can always be added.
 587              return true;
 588          } else {
 589              return has_capability($capability, $page->context);
 590          }
 591      }
 592  
 593      static function get_extra_capabilities() {
 594          return array('moodle/block:view', 'moodle/block:edit');
 595      }
 596  
 597      // Methods deprecated in Moodle 2.0 ========================================
 598  
 599      /**
 600       * Default case: the block wants to be 180 pixels wide
 601       * @deprecated since Moodle 2.0.
 602       * @return int
 603       */
 604      function preferred_width() {
 605          return 180;
 606      }
 607  
 608      /**
 609       * Can be overridden by the block to prevent the block from being dockable.
 610       *
 611       * @return bool
 612       */
 613      public function instance_can_be_docked() {
 614          global $CFG;
 615          return (!empty($CFG->allowblockstodock) && $this->page->theme->enable_dock);
 616      }
 617  
 618      /**
 619       * If overridden and set to false by the block it will not be hidable when
 620       * editing is turned on.
 621       *
 622       * @return bool
 623       */
 624      public function instance_can_be_hidden() {
 625          return true;
 626      }
 627  
 628      /**
 629       * If overridden and set to false by the block it will not be collapsible.
 630       *
 631       * @return bool
 632       */
 633      public function instance_can_be_collapsed() {
 634          return true;
 635      }
 636  
 637      /** @callback callback functions for comments api */
 638      public static function comment_template($options) {
 639          $ret = <<<EOD
 640  <div class="comment-userpicture">___picture___</div>
 641  <div class="comment-content">
 642      ___name___ - <span>___time___</span>
 643      <div>___content___</div>
 644  </div>
 645  EOD;
 646          return $ret;
 647      }
 648      public static function comment_permissions($options) {
 649          return array('view'=>true, 'post'=>true);
 650      }
 651      public static function comment_url($options) {
 652          return null;
 653      }
 654      public static function comment_display($comments, $options) {
 655          return $comments;
 656      }
 657      public static function comment_add(&$comments, $options) {
 658          return true;
 659      }
 660  
 661      /**
 662       * Returns the aria role attribute that best describes this block.
 663       *
 664       * Region is the default, but this should be overridden by a block is there is a region child, or even better
 665       * a landmark child.
 666       *
 667       * Options are as follows:
 668       *    - landmark
 669       *      - application
 670       *      - banner
 671       *      - complementary
 672       *      - contentinfo
 673       *      - form
 674       *      - main
 675       *      - navigation
 676       *      - search
 677       *
 678       * @return string
 679       */
 680      public function get_aria_role() {
 681          return 'complementary';
 682      }
 683  }
 684  
 685  /**
 686   * Specialized class for displaying a block with a list of icons/text labels
 687   *
 688   * The get_content method should set $this->content->items and (optionally)
 689   * $this->content->icons, instead of $this->content->text.
 690   *
 691   * @author Jon Papaioannou
 692   * @package core_block
 693   */
 694  
 695  class block_list extends block_base {
 696      var $content_type  = BLOCK_TYPE_LIST;
 697  
 698      function is_empty() {
 699          if ( !has_capability('moodle/block:view', $this->context) ) {
 700              return true;
 701          }
 702  
 703          $this->get_content();
 704          return (empty($this->content->items) && empty($this->content->footer));
 705      }
 706  
 707      protected function formatted_contents($output) {
 708          $this->get_content();
 709          $this->get_required_javascript();
 710          if (!empty($this->content->items)) {
 711              return $output->list_block_contents($this->content->icons, $this->content->items);
 712          } else {
 713              return '';
 714          }
 715      }
 716  
 717      function html_attributes() {
 718          $attributes = parent::html_attributes();
 719          $attributes['class'] .= ' list_block';
 720          return $attributes;
 721      }
 722  
 723  }
 724  
 725  /**
 726   * Specialized class for displaying a tree menu.
 727   *
 728   * The {@link get_content()} method involves setting the content of
 729   * <code>$this->content->items</code> with an array of {@link tree_item}
 730   * objects (these are the top-level nodes). The {@link tree_item::children}
 731   * property may contain more tree_item objects, and so on. The tree_item class
 732   * itself is abstract and not intended for use, use one of it's subclasses.
 733   *
 734   * Unlike {@link block_list}, the icons are specified as part of the items,
 735   * not in a separate array.
 736   *
 737   * @author Alan Trick
 738   * @package core_block
 739   * @internal this extends block_list so we get is_empty() for free
 740   */
 741  class block_tree extends block_list {
 742  
 743      /**
 744       * @var int specifies the manner in which contents should be added to this
 745       * block type. In this case <code>$this->content->items</code> is used with
 746       * {@link tree_item}s.
 747       */
 748      public $content_type = BLOCK_TYPE_TREE;
 749  
 750      /**
 751       * Make the formatted HTML ouput.
 752       *
 753       * Also adds the required javascript call to the page output.
 754       *
 755       * @param core_renderer $output
 756       * @return string HTML
 757       */
 758      protected function formatted_contents($output) {
 759          // based of code in admin_tree
 760          global $PAGE; // TODO change this when there is a proper way for blocks to get stuff into head.
 761          static $eventattached;
 762          if ($eventattached===null) {
 763              $eventattached = true;
 764          }
 765          if (!$this->content) {
 766              $this->content = new stdClass;
 767              $this->content->items = array();
 768          }
 769          $this->get_required_javascript();
 770          $this->get_content();
 771          $content = $output->tree_block_contents($this->content->items,array('class'=>'block_tree list'));
 772          if (isset($this->id) && !is_numeric($this->id)) {
 773              $content = $output->box($content, 'block_tree_box', $this->id);
 774          }
 775          return $content;
 776      }
 777  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1