Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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 all global functions to do with manipulating portfolios.
  19   *
  20   * Everything else that is logically namespaced by class is in its own file
  21   * in lib/portfolio/ directory.
  22   *
  23   * Major Contributors
  24   *     - Penny Leach <penny@catalyst.net.nz>
  25   *
  26   * @package core_portfolio
  27   * @category portfolio
  28   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  29   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  
  32  defined('MOODLE_INTERNAL') || die();
  33  
  34  // require some of the sublibraries first.
  35  // this is not an exhaustive list, the others are pulled in as they're needed
  36  // so we don't have to always include everything unnecessarily for performance
  37  
  38  // very lightweight list of constants. always needed and no further dependencies
  39  require_once($CFG->libdir . '/portfolio/constants.php');
  40  // a couple of exception deinitions. always needed and no further dependencies
  41  require_once($CFG->libdir . '/portfolio/exceptions.php');  // exception classes used by portfolio code
  42  // The base class for the caller classes. We always need this because we're either drawing a button,
  43  // in which case the button needs to know the calling class definition, which requires the base class,
  44  // or we're exporting, in which case we need the caller class anyway.
  45  require_once($CFG->libdir . '/portfolio/caller.php');
  46  
  47  // the other dependencies are included on demand:
  48  // libdir/portfolio/formats.php  - the classes for the export formats
  49  // libdir/portfolio/forms.php    - all portfolio form classes (requires formslib)
  50  // libdir/portfolio/plugin.php   - the base class for the export plugins
  51  // libdir/portfolio/exporter.php - the exporter class
  52  
  53  
  54  /**
  55   * Use this to add a portfolio button or icon or form to a page.
  56   *
  57   * These class methods do not check permissions. the caller must check permissions first.
  58   * Later, during the export process, the caller class is instantiated and the check_permissions method is called
  59   * If you are exporting a single file, you should always call set_format_by_file($file)
  60   * This class can be used like this:
  61   * <code>
  62   * $button = new portfolio_add_button();
  63   * $button->set_callback_options('name_of_caller_class', array('id' => 6), 'yourcomponent'); eg. mod_forum
  64   * $button->render(PORTFOLIO_ADD_FULL_FORM, get_string('addeverythingtoportfolio', 'yourcomponent'));
  65   * </code>
  66   * or like this:
  67   * <code>
  68   * $button = new portfolio_add_button(array('callbackclass' => 'name_of_caller_class', 'callbackargs' => array('id' => 6), 'callbackcomponent' => 'yourcomponent')); eg. mod_forum
  69   * $somehtml .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
  70   * </code>
  71   *{@link http://docs.moodle.org/dev/Adding_a_Portfolio_Button_to_a_page} for more information
  72   *
  73   * @package core_portfolio
  74   * @category portfolio
  75   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  76   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  77   */
  78  class portfolio_add_button {
  79  
  80      /** @var string the name of the callback functions */
  81      private $callbackclass;
  82  
  83      /** @var array can be an array of arguments to pass back to the callback functions (passed by reference)*/
  84      private $callbackargs;
  85  
  86      /** @var string caller file */
  87      private $callbackcomponent;
  88  
  89      /** @var array array of more specific formats (eg based on mime detection) */
  90      private $formats;
  91  
  92      /** @var array array of portfolio instances */
  93      private $instances;
  94  
  95      /** @var stored_file for single-file exports */
  96      private $file;
  97  
  98      /** @var string for writing specific types of files*/
  99      private $intendedmimetype;
 100  
 101      /**
 102       * Constructor. Either pass the options here or set them using the helper methods.
 103       * Generally the code will be clearer if you use the helper methods.
 104       *
 105       * @param array $options keyed array of options:
 106       *                       key 'callbackclass': name of the caller class (eg forum_portfolio_caller')
 107       *                       key 'callbackargs': the array of callback arguments your caller class wants passed to it in the constructor
 108       *                       key 'callbackcomponent': the file containing the class definition of your caller class.
 109       *                       See set_callback_options for more information on these three.
 110       *                       key 'formats': an array of PORTFOLIO_FORMATS this caller will support
 111       *                       See set_formats or set_format_by_file for more information on this.
 112       */
 113      public function __construct($options=null) {
 114          global $SESSION, $CFG;
 115  
 116          if (empty($CFG->enableportfolios)) {
 117              debugging('Building portfolio add button while portfolios is disabled. This code can be optimised.', DEBUG_DEVELOPER);
 118          }
 119  
 120          $cache = cache::make('core', 'portfolio_add_button_portfolio_instances');
 121          $instances = $cache->get('instances');
 122          if ($instances === false) {
 123              $instances = portfolio_instances();
 124              $cache->set('instances', $instances);
 125          }
 126  
 127          $this->instances = $instances;
 128          if (empty($options)) {
 129              return true;
 130          }
 131          $constructoroptions = array('callbackclass', 'callbackargs', 'callbackcomponent');
 132          foreach ((array)$options as $key => $value) {
 133              if (!in_array($key, $constructoroptions)) {
 134                  throw new portfolio_button_exception('invalidbuttonproperty', 'portfolio', $key);
 135              }
 136          }
 137  
 138          $this->set_callback_options($options['callbackclass'], $options['callbackargs'], $options['callbackcomponent']);
 139      }
 140  
 141      /**
 142       * Function to set the callback options
 143       *
 144       * @param string $class Name of the class containing the callback functions
 145       *      activity components should ALWAYS use their name_portfolio_caller
 146       *      other locations must use something unique
 147       * @param array $argarray This can be an array or hash of arguments to pass
 148       *      back to the callback functions (passed by reference)
 149       *      these MUST be primatives to be added as hidden form fields.
 150       *      and the values get cleaned to PARAM_ALPHAEXT or PARAM_FLOAT or PARAM_PATH
 151       * @param string $component This is the name of the component in Moodle, eg 'mod_forum'
 152       */
 153      public function set_callback_options($class, array $argarray, $component) {
 154          global $CFG;
 155  
 156          // Require the base class first before any other files.
 157          require_once($CFG->libdir . '/portfolio/caller.php');
 158  
 159          // Include any potential callback files and check for errors.
 160          portfolio_include_callback_file($component, $class);
 161  
 162          // This will throw exceptions but should not actually do anything other than verify callbackargs.
 163          $test = new $class($argarray);
 164          unset($test);
 165  
 166          $this->callbackcomponent = $component;
 167          $this->callbackclass = $class;
 168          $this->callbackargs = $argarray;
 169      }
 170  
 171      /**
 172       * Sets the available export formats for this content.
 173       * This function will also poll the static function in the caller class
 174       * and make sure we're not overriding a format that has nothing to do with mimetypes.
 175       * Eg: if you pass IMAGE here but the caller can export LEAP2A it will keep LEAP2A as well.
 176       * @see portfolio_most_specific_formats for more information
 177       * @see portfolio_format_from_mimetype
 178       *
 179       * @param array $formats if the calling code knows better than the static method on the calling class (base_supported_formats).
 180       *                       Eg: if it's going to be a single file, or if you know it's HTML, you can pass it here instead.
 181       *                       This is almost always the case so it should be use all the times
 182       *                       portfolio_format_from_mimetype for how to get the appropriate formats to pass here for uploaded files.
 183       *                       or just call set_format_by_file instead
 184       */
 185      public function set_formats($formats=null) {
 186          if (is_string($formats)) {
 187              $formats = array($formats);
 188          }
 189          if (empty($formats)) {
 190              $formats = array();
 191          }
 192          if (empty($this->callbackclass)) {
 193              throw new portfolio_button_exception('noclassbeforeformats', 'portfolio');
 194          }
 195          $callerformats = call_user_func(array($this->callbackclass, 'base_supported_formats'));
 196          $this->formats = portfolio_most_specific_formats($formats, $callerformats);
 197      }
 198  
 199      /**
 200       * Reset formats to the default,
 201       * which is usually what base_supported_formats returns
 202       */
 203      public function reset_formats() {
 204          $this->set_formats();
 205      }
 206  
 207  
 208      /**
 209       * If we already know we have exactly one file,
 210       * bypass set_formats and just pass the file
 211       * so we can detect the formats by mimetype.
 212       *
 213       * @param stored_file $file file to set the format from
 214       * @param array $extraformats any additional formats other than by mimetype
 215       *                            eg leap2a etc
 216       */
 217      public function set_format_by_file(stored_file $file, $extraformats=null) {
 218          $this->file = $file;
 219          $fileformat = portfolio_format_from_mimetype($file->get_mimetype());
 220          if (is_string($extraformats)) {
 221              $extraformats = array($extraformats);
 222          } else if (!is_array($extraformats)) {
 223              $extraformats = array();
 224          }
 225          $this->set_formats(array_merge(array($fileformat), $extraformats));
 226      }
 227  
 228      /**
 229       * Correllary this is use to set_format_by_file, but it is also used when there is no stored_file and
 230       * when we're writing out a new type of file (like csv or pdf)
 231       *
 232       * @param string $extn the file extension we intend to generate
 233       * @param array  $extraformats any additional formats other than by mimetype
 234       *                             eg leap2a etc
 235       */
 236      public function set_format_by_intended_file($extn, $extraformats=null) {
 237          $mimetype = mimeinfo('type', 'something. ' . $extn);
 238          $fileformat = portfolio_format_from_mimetype($mimetype);
 239          $this->intendedmimetype = $fileformat;
 240          if (is_string($extraformats)) {
 241              $extraformats = array($extraformats);
 242          } else if (!is_array($extraformats)) {
 243              $extraformats = array();
 244          }
 245          $this->set_formats(array_merge(array($fileformat), $extraformats));
 246      }
 247  
 248      /**
 249       * Echo the form/button/icon/text link to the page
 250       *
 251       * @param int $format format to display the button or form or icon or link.
 252       *                    See constants PORTFOLIO_ADD_XXX for more info.
 253       *                    optional, defaults to PORTFOLIO_ADD_FULL_FORM
 254       * @param string $addstr string to use for the button or icon alt text or link text.
 255       *                       this is whole string, not key. optional, defaults to 'Export to portfolio';
 256       */
 257      public function render($format=null, $addstr=null) {
 258          echo $this->to_html($format, $addstr);
 259      }
 260  
 261      /**
 262       * Returns the form/button/icon/text link as html
 263       *
 264       * @param int $format format to display the button or form or icon or link.
 265       *                    See constants PORTFOLIO_ADD_XXX for more info.
 266       *                    Optional, defaults to PORTFOLIO_ADD_FULL_FORM
 267       * @param string $addstr string to use for the button or icon alt text or link text.
 268       *                       This is whole string, not key.  optional, defaults to 'Add to portfolio';
 269       * @return void|string|moodle_url
 270       */
 271      public function to_html($format=null, $addstr=null) {
 272          global $CFG, $COURSE, $OUTPUT, $USER;
 273          if (!$this->is_renderable()) {
 274              return;
 275          }
 276          if (empty($this->callbackclass) || empty($this->callbackcomponent)) {
 277              throw new portfolio_button_exception('mustsetcallbackoptions', 'portfolio');
 278          }
 279          if (empty($this->formats)) {
 280              // use the caller defaults
 281              $this->set_formats();
 282          }
 283          $url = new moodle_url('/portfolio/add.php');
 284          foreach ($this->callbackargs as $key => $value) {
 285              if (!empty($value) && !is_string($value) && !is_numeric($value)) {
 286                  $a = new stdClass();
 287                  $a->key = $key;
 288                  $a->value = print_r($value, true);
 289                  debugging(get_string('nonprimative', 'portfolio', $a));
 290                  return;
 291              }
 292              $url->param('ca_' . $key, $value);
 293          }
 294          $url->param('sesskey', sesskey());
 295          $url->param('callbackcomponent', $this->callbackcomponent);
 296          $url->param('callbackclass', $this->callbackclass);
 297          $url->param('course', (!empty($COURSE)) ? $COURSE->id : 0);
 298          $url->param('callerformats', implode(',', $this->formats));
 299          $mimetype = null;
 300          if ($this->file instanceof stored_file) {
 301              $mimetype = $this->file->get_mimetype();
 302          } else if ($this->intendedmimetype) {
 303              $mimetype = $this->intendedmimetype;
 304          }
 305          $selectoutput = '';
 306          if (count($this->instances) == 1) {
 307              $tmp = array_values($this->instances);
 308              $instance = $tmp[0];
 309  
 310              $formats = portfolio_supported_formats_intersect($this->formats, $instance->supported_formats());
 311              if (count($formats) == 0) {
 312                  // bail. no common formats.
 313                  //debugging(get_string('nocommonformats', 'portfolio', (object)array('location' => $this->callbackclass, 'formats' => implode(',', $this->formats))));
 314                  return;
 315              }
 316              if ($error = portfolio_instance_sanity_check($instance)) {
 317                  // bail, plugin is misconfigured
 318                  //debugging(get_string('instancemisconfigured', 'portfolio', get_string($error[$instance->get('id')], 'portfolio_' . $instance->get('plugin'))));
 319                  return;
 320              }
 321              if (!$instance->allows_multiple_exports() && $already = portfolio_existing_exports($USER->id, $instance->get('plugin'))) {
 322                  //debugging(get_string('singleinstancenomultiallowed', 'portfolio'));
 323                  return;
 324              }
 325              if ($mimetype&& !$instance->file_mime_check($mimetype)) {
 326                  // bail, we have a specific file or mimetype and this plugin doesn't support it
 327                  //debugging(get_string('mimecheckfail', 'portfolio', (object)array('plugin' => $instance->get('plugin'), 'mimetype' => $mimetype)));
 328                  return;
 329              }
 330              $url->param('instance', $instance->get('id'));
 331          }
 332          else {
 333              if (!$selectoutput = portfolio_instance_select($this->instances, $this->formats, $this->callbackclass, $mimetype, 'instance', true)) {
 334                  return;
 335              }
 336          }
 337          // If we just want a moodle_url to redirect to, do it now.
 338          if ($format == PORTFOLIO_ADD_MOODLE_URL) {
 339              return $url;
 340          }
 341  
 342          // if we just want a url to redirect to, do it now
 343          if ($format == PORTFOLIO_ADD_FAKE_URL) {
 344              return $url->out(false);
 345          }
 346  
 347          if (empty($addstr)) {
 348              $addstr = get_string('addtoportfolio', 'portfolio');
 349          }
 350          if (empty($format)) {
 351              $format = PORTFOLIO_ADD_FULL_FORM;
 352          }
 353  
 354          $formoutput = '<form method="post" action="' . $CFG->wwwroot . '/portfolio/add.php" id="portfolio-add-button">' . "\n";
 355          $formoutput .= html_writer::input_hidden_params($url);
 356          $linkoutput = '';
 357  
 358          switch ($format) {
 359              case PORTFOLIO_ADD_FULL_FORM:
 360                  $formoutput .= $selectoutput;
 361                  $formoutput .= "\n" . '<input type="submit" class="btn btn-secondary" value="' . $addstr .'" />';
 362                  $formoutput .= "\n" . '</form>';
 363              break;
 364              case PORTFOLIO_ADD_ICON_FORM:
 365                  $formoutput .= $selectoutput;
 366                  $formoutput .= "\n" . '<button class="portfolio-add-icon">' . $OUTPUT->pix_icon('t/portfolioadd', $addstr) . '</button>';
 367                  $formoutput .= "\n" . '</form>';
 368              break;
 369              case PORTFOLIO_ADD_ICON_LINK:
 370                  $linkoutput = $OUTPUT->action_icon($url, new pix_icon('t/portfolioadd', $addstr, '',
 371                      array('class' => 'portfolio-add-icon smallicon')));
 372              break;
 373              case PORTFOLIO_ADD_TEXT_LINK:
 374                  $linkoutput = html_writer::link($url, $addstr, array('class' => 'portfolio-add-link',
 375                      'title' => $addstr));
 376              break;
 377              default:
 378                  debugging(get_string('invalidaddformat', 'portfolio', $format));
 379          }
 380          $output = (in_array($format, array(PORTFOLIO_ADD_FULL_FORM, PORTFOLIO_ADD_ICON_FORM)) ? $formoutput : $linkoutput);
 381          return $output;
 382      }
 383  
 384      /**
 385       * Perform some internal checks.
 386       * These are not errors, just situations
 387       * where it's not appropriate to add the button
 388       *
 389       * @return bool
 390       */
 391      private function is_renderable() {
 392          global $CFG;
 393          if (empty($CFG->enableportfolios)) {
 394              return false;
 395          }
 396          if (defined('PORTFOLIO_INTERNAL')) {
 397              // something somewhere has detected a risk of this being called during inside the preparation
 398              // eg forum_print_attachments
 399              return false;
 400          }
 401          if (empty($this->instances) || count($this->instances) == 0) {
 402              return false;
 403          }
 404          return true;
 405      }
 406  
 407      /**
 408       * Getter for $format property
 409       *
 410       * @return array
 411       */
 412      public function get_formats() {
 413          return $this->formats;
 414      }
 415  
 416      /**
 417       * Getter for $callbackargs property
 418       *
 419       * @return array
 420       */
 421      public function get_callbackargs() {
 422          return $this->callbackargs;
 423      }
 424  
 425      /**
 426       * Getter for $callbackcomponent property
 427       *
 428       * @return string
 429       */
 430      public function get_callbackcomponent() {
 431          return $this->callbackcomponent;
 432      }
 433  
 434      /**
 435       * Getter for $callbackclass property
 436       *
 437       * @return string
 438       */
 439      public function get_callbackclass() {
 440          return $this->callbackclass;
 441      }
 442  }
 443  
 444  /**
 445   * Returns a drop menu with a list of available instances.
 446   *
 447   * @param array          $instances      array of portfolio plugin instance objects - the instances to put in the menu
 448   * @param array          $callerformats  array of PORTFOLIO_FORMAT_XXX constants - the formats the caller supports (this is used to filter plugins)
 449   * @param string         $callbackclass  the callback class name - used for debugging only for when there are no common formats
 450   * @param string         $mimetype       if we already know we have exactly one file, or are going to write one, pass it here to do mime filtering.
 451   * @param string         $selectname     the name of the select element. Optional, defaults to instance.
 452   * @param bool           $return         whether to print or return the output. Optional, defaults to print.
 453   * @param bool           $returnarray    if returning, whether to return the HTML or the array of options. Optional, defaults to HTML.
 454   * @return void|array|string the html, from <select> to </select> inclusive.
 455   */
 456  function portfolio_instance_select($instances, $callerformats, $callbackclass, $mimetype=null, $selectname='instance', $return=false, $returnarray=false) {
 457      global $CFG, $USER;
 458  
 459      if (empty($CFG->enableportfolios)) {
 460          return;
 461      }
 462  
 463      $insane = portfolio_instance_sanity_check();
 464      $pinsane = portfolio_plugin_sanity_check();
 465  
 466      $count = 0;
 467      $selectoutput = "\n" . '<label class="accesshide" for="instanceid">' . get_string('plugin', 'portfolio') . '</label>';
 468      $selectoutput .= "\n" . '<select id="instanceid" name="' . $selectname . '" class="custom-select">' . "\n";
 469      $existingexports = portfolio_existing_exports_by_plugin($USER->id);
 470      foreach ($instances as $instance) {
 471          $formats = portfolio_supported_formats_intersect($callerformats, $instance->supported_formats());
 472          if (count($formats) == 0) {
 473              // bail. no common formats.
 474              continue;
 475          }
 476          if (array_key_exists($instance->get('id'), $insane)) {
 477              // bail, plugin is misconfigured
 478              //debugging(get_string('instanceismisconfigured', 'portfolio', get_string($insane[$instance->get('id')], 'portfolio_' . $instance->get('plugin'))));
 479              continue;
 480          } else if (array_key_exists($instance->get('plugin'), $pinsane)) {
 481              // bail, plugin is misconfigured
 482              //debugging(get_string('pluginismisconfigured', 'portfolio', get_string($pinsane[$instance->get('plugin')], 'portfolio_' . $instance->get('plugin'))));
 483              continue;
 484          }
 485          if (!$instance->allows_multiple_exports() && in_array($instance->get('plugin'), $existingexports)) {
 486              // bail, already exporting something with this plugin and it doesn't support multiple exports
 487              continue;
 488          }
 489          if ($mimetype && !$instance->file_mime_check($mimetype)) {
 490              //debugging(get_string('mimecheckfail', 'portfolio', (object)array('plugin' => $instance->get('plugin'), 'mimetype' => $mimetype())));
 491              // bail, we have a specific file and this plugin doesn't support it
 492              continue;
 493          }
 494          $count++;
 495          $selectoutput .= "\n" . '<option value="' . $instance->get('id') . '">' . $instance->get('name') . '</option>' . "\n";
 496          $options[$instance->get('id')] = $instance->get('name');
 497      }
 498      if (empty($count)) {
 499          // bail. no common formats.
 500          //debugging(get_string('nocommonformats', 'portfolio', (object)array('location' => $callbackclass, 'formats' => implode(',', $callerformats))));
 501          return;
 502      }
 503      $selectoutput .= "\n" . "</select>\n";
 504      if (!empty($returnarray)) {
 505          return $options;
 506      }
 507      if (!empty($return)) {
 508          return $selectoutput;
 509      }
 510      echo $selectoutput;
 511  }
 512  
 513  /**
 514   * Return all portfolio instances
 515   *
 516   * @todo MDL-15768 - check capabilities here
 517   * @param bool $visibleonly Don't include hidden instances. Defaults to true and will be overridden to true if the next parameter is true
 518   * @param bool $useronly    Check the visibility preferences and permissions of the logged in user. Defaults to true.
 519   * @return array of portfolio instances (full objects, not just database records)
 520   */
 521  function portfolio_instances($visibleonly=true, $useronly=true) {
 522  
 523      global $DB, $USER;
 524  
 525      $values = array();
 526      $sql = 'SELECT * FROM {portfolio_instance}';
 527  
 528      if ($visibleonly || $useronly) {
 529          $values[] = 1;
 530          $sql .= ' WHERE visible = ?';
 531      }
 532      if ($useronly) {
 533          $sql .= ' AND id NOT IN (
 534                  SELECT instance FROM {portfolio_instance_user}
 535                  WHERE userid = ? AND name = ? AND ' . $DB->sql_compare_text('value') . ' = ?
 536              )';
 537          $values = array_merge($values, array($USER->id, 'visible', 0));
 538      }
 539      $sql .= ' ORDER BY name';
 540  
 541      $instances = array();
 542      foreach ($DB->get_records_sql($sql, $values) as $instance) {
 543          $instances[$instance->id] = portfolio_instance($instance->id, $instance);
 544      }
 545      return $instances;
 546  }
 547  
 548  /**
 549   * Return whether there are visible instances in portfolio.
 550   *
 551   * @return bool true when there are some visible instances.
 552   */
 553  function portfolio_has_visible_instances() {
 554      global $DB;
 555      return $DB->record_exists('portfolio_instance', array('visible' => 1));
 556  }
 557  
 558  /**
 559   * Supported formats currently in use.
 560   * Canonical place for a list of all formats
 561   * that portfolio plugins and callers
 562   * can use for exporting content
 563   *
 564   * @return array keyed array of all the available export formats (constant => classname)
 565   */
 566  function portfolio_supported_formats() {
 567      return array(
 568          PORTFOLIO_FORMAT_FILE         => 'portfolio_format_file',
 569          PORTFOLIO_FORMAT_IMAGE        => 'portfolio_format_image',
 570          PORTFOLIO_FORMAT_RICHHTML     => 'portfolio_format_richhtml',
 571          PORTFOLIO_FORMAT_PLAINHTML    => 'portfolio_format_plainhtml',
 572          PORTFOLIO_FORMAT_TEXT         => 'portfolio_format_text',
 573          PORTFOLIO_FORMAT_VIDEO        => 'portfolio_format_video',
 574          PORTFOLIO_FORMAT_PDF          => 'portfolio_format_pdf',
 575          PORTFOLIO_FORMAT_DOCUMENT     => 'portfolio_format_document',
 576          PORTFOLIO_FORMAT_SPREADSHEET  => 'portfolio_format_spreadsheet',
 577          PORTFOLIO_FORMAT_PRESENTATION => 'portfolio_format_presentation',
 578          /*PORTFOLIO_FORMAT_MBKP, */ // later
 579          PORTFOLIO_FORMAT_LEAP2A       => 'portfolio_format_leap2a',
 580          PORTFOLIO_FORMAT_RICH         => 'portfolio_format_rich',
 581      );
 582  }
 583  
 584  /**
 585   * Deduce export format from file mimetype
 586   * This function returns the revelant portfolio export format
 587   * which is used to determine which portfolio plugins can be used
 588   * for exporting this content
 589   * according to the given mime type
 590   * this only works when exporting exactly <b>one</b> file, or generating a new one
 591   * (like a pdf or csv export)
 592   *
 593   * @param string $mimetype (usually $file->get_mimetype())
 594   * @return string the format constant (see PORTFOLIO_FORMAT_XXX constants)
 595   */
 596  function portfolio_format_from_mimetype($mimetype) {
 597      global $CFG;
 598      static $alreadymatched;
 599      if (empty($alreadymatched)) {
 600          $alreadymatched = array();
 601      }
 602      if (array_key_exists($mimetype, $alreadymatched)) {
 603          return $alreadymatched[$mimetype];
 604      }
 605      $allformats = portfolio_supported_formats();
 606      require_once($CFG->libdir . '/portfolio/formats.php');
 607      foreach ($allformats as $format => $classname) {
 608          $supportedmimetypes = call_user_func(array($classname, 'mimetypes'));
 609          if (!is_array($supportedmimetypes)) {
 610              debugging("one of the portfolio format classes, $classname, said it supported something funny for mimetypes, should have been array...");
 611              debugging(print_r($supportedmimetypes, true));
 612              continue;
 613          }
 614          if (in_array($mimetype, $supportedmimetypes)) {
 615              $alreadymatched[$mimetype] = $format;
 616              return $format;
 617          }
 618      }
 619      return PORTFOLIO_FORMAT_FILE; // base case for files...
 620  }
 621  
 622  /**
 623   * Intersection of plugin formats and caller formats.
 624   * Walks both the caller formats and portfolio plugin formats
 625   * and looks for matches (walking the hierarchy as well)
 626   * and returns the intersection
 627   *
 628   * @param array $callerformats formats the caller supports
 629   * @param array $pluginformats formats the portfolio plugin supports
 630   * @return array
 631   */
 632  function portfolio_supported_formats_intersect($callerformats, $pluginformats) {
 633      global $CFG;
 634      $allformats = portfolio_supported_formats();
 635      $intersection = array();
 636      foreach ($callerformats as $cf) {
 637          if (!array_key_exists($cf, $allformats)) {
 638              if (!portfolio_format_is_abstract($cf)) {
 639                  debugging(get_string('invalidformat', 'portfolio', $cf));
 640              }
 641              continue;
 642          }
 643          require_once($CFG->libdir . '/portfolio/formats.php');
 644          $cfobj = new $allformats[$cf]();
 645          foreach ($pluginformats as $p => $pf) {
 646              if (!array_key_exists($pf, $allformats)) {
 647                  if (!portfolio_format_is_abstract($pf)) {
 648                      debugging(get_string('invalidformat', 'portfolio', $pf));
 649                  }
 650                  unset($pluginformats[$p]); // to avoid the same warning over and over
 651                  continue;
 652              }
 653              if ($cfobj instanceof $allformats[$pf]) {
 654                  $intersection[] = $cf;
 655              }
 656          }
 657      }
 658      return $intersection;
 659  }
 660  
 661  /**
 662   * Tiny helper to figure out whether a portfolio format is abstract
 663   *
 664   * @param string $format the format to test
 665   * @return bool
 666   */
 667  function portfolio_format_is_abstract($format) {
 668      if (class_exists($format)) {
 669          $class = $format;
 670      } else if (class_exists('portfolio_format_' . $format)) {
 671          $class = 'portfolio_format_' . $format;
 672      } else {
 673          $allformats = portfolio_supported_formats();
 674          if (array_key_exists($format, $allformats)) {
 675              $class = $allformats[$format];
 676          }
 677      }
 678      if (empty($class)) {
 679          return true; // it may as well be, we can't instantiate it :)
 680      }
 681      $rc = new ReflectionClass($class);
 682      return $rc->isAbstract();
 683  }
 684  
 685  /**
 686   * Return the combination of the two arrays of formats with duplicates in terms of specificity removed
 687   * and also removes conflicting formats.
 688   * Use case: a module is exporting a single file, so the general formats would be FILE and MBKP
 689   *           while the specific formats would be the specific subclass of FILE based on mime (say IMAGE)
 690   *           and this function would return IMAGE and MBKP
 691   *
 692   * @param array $specificformats array of more specific formats (eg based on mime detection)
 693   * @param array $generalformats  array of more general formats (usually more supported)
 694   * @return array merged formats with dups removed
 695   */
 696  function portfolio_most_specific_formats($specificformats, $generalformats) {
 697      global $CFG;
 698      $allformats = portfolio_supported_formats();
 699      if (empty($specificformats)) {
 700          return $generalformats;
 701      } else if (empty($generalformats)) {
 702          return $specificformats;
 703      }
 704      $removedformats = array();
 705      foreach ($specificformats as $k => $f) {
 706          // look for something less specific and remove it, ie outside of the inheritance tree of the current formats.
 707          if (!array_key_exists($f, $allformats)) {
 708              if (!portfolio_format_is_abstract($f)) {
 709                  throw new portfolio_button_exception('invalidformat', 'portfolio', $f);
 710              }
 711          }
 712          if (in_array($f, $removedformats)) {
 713              // already been removed from the general list
 714              //debugging("skipping $f because it was already removed");
 715              unset($specificformats[$k]);
 716          }
 717          require_once($CFG->libdir . '/portfolio/formats.php');
 718          $fobj = new $allformats[$f];
 719          foreach ($generalformats as $key => $cf) {
 720              if (in_array($cf, $removedformats)) {
 721                  //debugging("skipping $cf because it was already removed");
 722                  continue;
 723              }
 724              $cfclass = $allformats[$cf];
 725              $cfobj = new $allformats[$cf];
 726              if ($fobj instanceof $cfclass && $cfclass != get_class($fobj)) {
 727                  //debugging("unsetting $key $cf because it's not specific enough ($f is better)");
 728                  unset($generalformats[$key]);
 729                  $removedformats[] = $cf;
 730                  continue;
 731              }
 732              // check for conflicts
 733              if ($fobj->conflicts($cf)) {
 734                  //debugging("unsetting $key $cf because it conflicts with $f");
 735                  unset($generalformats[$key]);
 736                  $removedformats[] = $cf;
 737                  continue;
 738              }
 739              if ($cfobj->conflicts($f)) {
 740                  //debugging("unsetting $key $cf because it reverse-conflicts with $f");
 741                  $removedformats[] = $cf;
 742                  unset($generalformats[$key]);
 743                  continue;
 744              }
 745          }
 746          //debugging('inside loop');
 747          //print_object($generalformats);
 748      }
 749  
 750      //debugging('final formats');
 751      $finalformats =  array_unique(array_merge(array_values($specificformats), array_values($generalformats)));
 752      //print_object($finalformats);
 753      return $finalformats;
 754  }
 755  
 756  /**
 757   * Helper function to return a format object from the constant
 758   *
 759   * @param string $name the constant PORTFOLIO_FORMAT_XXX
 760   * @return portfolio_format
 761   */
 762  function portfolio_format_object($name) {
 763      global $CFG;
 764      require_once($CFG->libdir . '/portfolio/formats.php');
 765      $formats = portfolio_supported_formats();
 766      return new $formats[$name];
 767  }
 768  
 769  /**
 770   * Helper function to return an instance of a plugin (with config loaded)
 771   *
 772   * @param int   $instanceid id of instance
 773   * @param object $record database row that corresponds to this instance
 774   *                       this is passed to avoid unnecessary lookups
 775   *                       Optional, and the record will be retrieved if null.
 776   * @return object of portfolio_plugin_XXX
 777   */
 778  function portfolio_instance($instanceid, $record=null) {
 779      global $DB, $CFG;
 780  
 781      if ($record) {
 782          $instance  = $record;
 783      } else {
 784          if (!$instance = $DB->get_record('portfolio_instance', array('id' => $instanceid))) {
 785              throw new portfolio_exception('invalidinstance', 'portfolio');
 786          }
 787      }
 788      require_once($CFG->libdir . '/portfolio/plugin.php');
 789      require_once($CFG->dirroot . '/portfolio/'. $instance->plugin . '/lib.php');
 790      $classname = 'portfolio_plugin_' . $instance->plugin;
 791      return new $classname($instanceid, $instance);
 792  }
 793  
 794  /**
 795   * Helper function to call a static function on a portfolio plugin class.
 796   * This will figure out the classname and require the right file and call the function.
 797   * You can send a variable number of arguments to this function after the first two
 798   * and they will be passed on to the function you wish to call.
 799   *
 800   * @param string $plugin   name of plugin
 801   * @param string $function function to call
 802   * @return mixed
 803   */
 804  function portfolio_static_function($plugin, $function) {
 805      global $CFG;
 806  
 807      $pname = null;
 808      if (is_object($plugin) || is_array($plugin)) {
 809          $plugin = (object)$plugin;
 810          $pname = $plugin->name;
 811      } else {
 812          $pname = $plugin;
 813      }
 814  
 815      $args = func_get_args();
 816      if (count($args) <= 2) {
 817          $args = array();
 818      }
 819      else {
 820          array_shift($args);
 821          array_shift($args);
 822      }
 823  
 824      require_once($CFG->libdir . '/portfolio/plugin.php');
 825      require_once($CFG->dirroot . '/portfolio/' . $plugin .  '/lib.php');
 826      return call_user_func_array(array('portfolio_plugin_' . $plugin, $function), $args);
 827  }
 828  
 829  /**
 830   * Helper function to check all the plugins for sanity and set any insane ones to invisible.
 831   *
 832   * @param array $plugins array of supported plugin types
 833   * @return array array of insane instances (keys= id, values = reasons (keys for plugin lang)
 834   */
 835  function portfolio_plugin_sanity_check($plugins=null) {
 836      global $DB;
 837      if (is_string($plugins)) {
 838          $plugins = array($plugins);
 839      } else if (empty($plugins)) {
 840          $plugins = core_component::get_plugin_list('portfolio');
 841          $plugins = array_keys($plugins);
 842      }
 843  
 844      $insane = array();
 845      foreach ($plugins as $plugin) {
 846          if ($result = portfolio_static_function($plugin, 'plugin_sanity_check')) {
 847              $insane[$plugin] = $result;
 848          }
 849      }
 850      if (empty($insane)) {
 851          return array();
 852      }
 853      list($where, $params) = $DB->get_in_or_equal(array_keys($insane));
 854      $where = ' plugin ' . $where;
 855      $DB->set_field_select('portfolio_instance', 'visible', 0, $where, $params);
 856      return $insane;
 857  }
 858  
 859  /**
 860   * Helper function to check all the instances for sanity and set any insane ones to invisible.
 861   *
 862   * @param array $instances array of plugin instances
 863   * @return array array of insane instances (keys= id, values = reasons (keys for plugin lang)
 864   */
 865  function portfolio_instance_sanity_check($instances=null) {
 866      global $DB;
 867      if (empty($instances)) {
 868          $instances = portfolio_instances(false);
 869      } else if (!is_array($instances)) {
 870          $instances = array($instances);
 871      }
 872  
 873      $insane = array();
 874      foreach ($instances as $instance) {
 875          if (is_object($instance) && !($instance instanceof portfolio_plugin_base)) {
 876              $instance = portfolio_instance($instance->id, $instance);
 877          } else if (is_numeric($instance)) {
 878              $instance = portfolio_instance($instance);
 879          }
 880          if (!($instance instanceof portfolio_plugin_base)) {
 881              debugging('something weird passed to portfolio_instance_sanity_check, not subclass or id');
 882              continue;
 883          }
 884          if ($result = $instance->instance_sanity_check()) {
 885              $insane[$instance->get('id')] = $result;
 886          }
 887      }
 888      if (empty($insane)) {
 889          return array();
 890      }
 891      list ($where, $params) = $DB->get_in_or_equal(array_keys($insane));
 892      $where = ' id ' . $where;
 893      $DB->set_field_select('portfolio_instance', 'visible', 0, $where, $params);
 894      portfolio_insane_notify_admins($insane, true);
 895      return $insane;
 896  }
 897  
 898  /**
 899   * Helper function to display a table of plugins (or instances) and reasons for disabling
 900   *
 901   * @param array $insane array of portfolio plugin
 902   * @param array $instances if reporting instances rather than whole plugins, pass the array (key = id, value = object) here
 903   * @param bool $return option to deliver the report in html format or print it out directly to the page.
 904   * @return void|string of portfolio report in html table format
 905   */
 906  function portfolio_report_insane($insane, $instances=false, $return=false) {
 907      global $OUTPUT;
 908      if (empty($insane)) {
 909          return;
 910      }
 911  
 912      static $pluginstr;
 913      if (empty($pluginstr)) {
 914          $pluginstr = get_string('plugin', 'portfolio');
 915      }
 916      if ($instances) {
 917          $headerstr = get_string('someinstancesdisabled', 'portfolio');
 918      } else {
 919          $headerstr = get_string('somepluginsdisabled', 'portfolio');
 920      }
 921  
 922      $output = $OUTPUT->notification($headerstr, 'notifyproblem');
 923      $table = new html_table();
 924      $table->head = array($pluginstr, '');
 925      $table->data = array();
 926      foreach ($insane as $plugin => $reason) {
 927          if ($instances) {
 928              $instance = $instances[$plugin];
 929              $plugin   = $instance->get('plugin');
 930              $name     = $instance->get('name');
 931          } else {
 932              $name = $plugin;
 933          }
 934          $table->data[] = array($name, get_string($reason, 'portfolio_' . $plugin));
 935      }
 936      $output .= html_writer::table($table);
 937      $output .= '<br /><br /><br />';
 938  
 939      if ($return) {
 940          return $output;
 941      }
 942      echo $output;
 943  }
 944  
 945  /**
 946   * Helper function to rethrow a caught portfolio_exception as an export exception.
 947   * Used because when a portfolio_export exception is thrown the export is cancelled
 948   * throws portfolio_export_exceptiog
 949   *
 950   * @param portfolio_exporter $exporter  current exporter object
 951   * @param object             $exception exception to rethrow
 952   */
 953  function portfolio_export_rethrow_exception($exporter, $exception) {
 954      throw new portfolio_export_exception($exporter, $exception->errorcode, $exception->module, $exception->link, $exception->a);
 955  }
 956  
 957  /**
 958   * Try and determine expected_time for purely file based exports
 959   * or exports that might include large file attachments.
 960   *
 961   * @param stored_file|array $totest - either an array of stored_file objects or a single stored_file object
 962   * @return string PORTFOLIO_TIME_XXX
 963   */
 964  function portfolio_expected_time_file($totest) {
 965      global $CFG;
 966      if ($totest instanceof stored_file) {
 967          $totest = array($totest);
 968      }
 969      $size = 0;
 970      foreach ($totest as $file) {
 971          if (!($file instanceof stored_file)) {
 972              debugging('something weird passed to portfolio_expected_time_file - not stored_file object');
 973              debugging(print_r($file, true));
 974              continue;
 975          }
 976          $size += $file->get_filesize();
 977      }
 978  
 979      $fileinfo = portfolio_filesize_info();
 980  
 981      $moderate = $high = 0; // avoid warnings
 982  
 983      foreach (array('moderate', 'high') as $setting) {
 984          $settingname = 'portfolio_' . $setting . '_filesize_threshold';
 985          if (empty($CFG->{$settingname}) || !array_key_exists($CFG->{$settingname}, $fileinfo['options'])) {
 986              debugging("weird or unset admin value for $settingname, using default instead");
 987              $$setting = $fileinfo[$setting];
 988          } else {
 989              $$setting = $CFG->{$settingname};
 990          }
 991      }
 992  
 993      if ($size < $moderate) {
 994          return PORTFOLIO_TIME_LOW;
 995      } else if ($size < $high) {
 996          return PORTFOLIO_TIME_MODERATE;
 997      }
 998      return PORTFOLIO_TIME_HIGH;
 999  }
1000  
1001  
1002  /**
1003   * The default filesizes and threshold information for file based transfers.
1004   * This shouldn't need to be used outside the admin pages and the portfolio code
1005   *
1006   * @return array
1007   */
1008  function portfolio_filesize_info() {
1009      $filesizes = array();
1010      $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152, 5242880, 10485760, 20971520, 52428800);
1011      foreach ($sizelist as $size) {
1012          $filesizes[$size] = display_size($size);
1013      }
1014      return array(
1015          'options' => $filesizes,
1016          'moderate' => 1048576,
1017          'high'     => 5242880,
1018      );
1019  }
1020  
1021  /**
1022   * Try and determine expected_time for purely database based exports
1023   * or exports that might include large parts of a database.
1024   *
1025   * @param int $recordcount number of records trying to export
1026   * @return string PORTFOLIO_TIME_XXX
1027   */
1028  function portfolio_expected_time_db($recordcount) {
1029      global $CFG;
1030  
1031      if (empty($CFG->portfolio_moderate_db_threshold)) {
1032          set_config('portfolio_moderate_db_threshold', 20);
1033      }
1034      if (empty($CFG->portfolio_high_db_threshold)) {
1035          set_config('portfolio_high_db_threshold', 50);
1036      }
1037      if ($recordcount < $CFG->portfolio_moderate_db_threshold) {
1038          return PORTFOLIO_TIME_LOW;
1039      } else if ($recordcount < $CFG->portfolio_high_db_threshold) {
1040          return PORTFOLIO_TIME_MODERATE;
1041      }
1042      return PORTFOLIO_TIME_HIGH;
1043  }
1044  
1045  /**
1046   * Function to send portfolio report to admins
1047   *
1048   * @param array $insane array of insane plugins
1049   * @param array $instances (optional) if reporting instances rather than whole plugins
1050   */
1051  function portfolio_insane_notify_admins($insane, $instances=false) {
1052  
1053      global $CFG;
1054  
1055      if (defined('ADMIN_EDITING_PORTFOLIO')) {
1056          return true;
1057      }
1058  
1059      $admins = get_admins();
1060  
1061      if (empty($admins)) {
1062          return;
1063      }
1064      if ($instances) {
1065          $instances = portfolio_instances(false, false);
1066      }
1067  
1068      $site = get_site();
1069  
1070      $a = new StdClass;
1071      $a->sitename = format_string($site->fullname, true, array('context' => context_course::instance(SITEID)));
1072      $a->fixurl   = "$CFG->wwwroot/$CFG->admin/settings.php?section=manageportfolios";
1073      $a->htmllist = portfolio_report_insane($insane, $instances, true);
1074      $a->textlist = '';
1075  
1076      foreach ($insane as $k => $reason) {
1077          if ($instances) {
1078              $a->textlist = $instances[$k]->get('name') . ': ' . $reason . "\n";
1079          } else {
1080              $a->textlist = $k . ': ' . $reason . "\n";
1081          }
1082      }
1083  
1084      $subject   = get_string('insanesubject', 'portfolio');
1085      $plainbody = get_string('insanebody', 'portfolio', $a);
1086      $htmlbody  = get_string('insanebodyhtml', 'portfolio', $a);
1087      $smallbody = get_string('insanebodysmall', 'portfolio', $a);
1088  
1089      foreach ($admins as $admin) {
1090          $eventdata = new \core\message\message();
1091          $eventdata->courseid = SITEID;
1092          $eventdata->modulename = 'portfolio';
1093          $eventdata->component = 'portfolio';
1094          $eventdata->name = 'notices';
1095          $eventdata->userfrom = get_admin();
1096          $eventdata->userto = $admin;
1097          $eventdata->subject = $subject;
1098          $eventdata->fullmessage = $plainbody;
1099          $eventdata->fullmessageformat = FORMAT_PLAIN;
1100          $eventdata->fullmessagehtml = $htmlbody;
1101          $eventdata->smallmessage = $smallbody;
1102          message_send($eventdata);
1103      }
1104  }
1105  
1106  /**
1107   * Setup page export
1108   *
1109   * @param moodle_page $PAGE global variable from page object
1110   * @param portfolio_caller_base $caller plugin type caller
1111   */
1112  function portfolio_export_pagesetup($PAGE, $caller) {
1113      // set up the context so that build_navigation works nice
1114      $caller->set_context($PAGE);
1115  
1116      list($extranav, $cm) = $caller->get_navigation();
1117  
1118      // and now we know the course for sure and maybe the cm, call require_login with it
1119      require_login($PAGE->course, false, $cm);
1120  
1121      foreach ($extranav as $navitem) {
1122          $PAGE->navbar->add($navitem['name']);
1123      }
1124      $PAGE->navbar->add(get_string('exporting', 'portfolio'));
1125  }
1126  
1127  /**
1128   * Get export type id
1129   *
1130   * @param string $type plugin type
1131   * @param int $userid the user to check for
1132   * @return mixed|bool
1133   */
1134  function portfolio_export_type_to_id($type, $userid) {
1135      global $DB;
1136      $sql = 'SELECT t.id FROM {portfolio_tempdata} t JOIN {portfolio_instance} i ON t.instance = i.id WHERE t.userid = ? AND i.plugin = ?';
1137      return $DB->get_field_sql($sql, array($userid, $type));
1138  }
1139  
1140  /**
1141   * Return a list of current exports for the given user.
1142   * This will not go through and call rewaken_object, because it's heavy.
1143   * It's really just used to figure out what exports are currently happening.
1144   * This is useful for plugins that don't support multiple exports per session
1145   *
1146   * @param int $userid the user to check for
1147   * @param string $type (optional) the portfolio plugin to filter by
1148   * @return array
1149   */
1150  function portfolio_existing_exports($userid, $type=null) {
1151      global $DB;
1152      $sql = 'SELECT t.*,t.instance,i.plugin,i.name FROM {portfolio_tempdata} t JOIN {portfolio_instance} i ON t.instance = i.id WHERE t.userid = ? ';
1153      $values = array($userid);
1154      if ($type) {
1155          $sql .= ' AND i.plugin = ?';
1156          $values[] = $type;
1157      }
1158      return $DB->get_records_sql($sql, $values);
1159  }
1160  
1161  /**
1162   * Return an array of existing exports by type for a given user.
1163   * This is much more lightweight than existing_exports because it only returns the types, rather than the whole serialised data
1164   * so can be used for checking availability of multiple plugins at the same time.
1165   * @see existing_exports
1166   *
1167   * @param int $userid the user to check for
1168   * @return array
1169   */
1170  function portfolio_existing_exports_by_plugin($userid) {
1171      global $DB;
1172      $sql = 'SELECT t.id,i.plugin FROM {portfolio_tempdata} t JOIN {portfolio_instance} i ON t.instance = i.id WHERE t.userid = ? ';
1173      $values = array($userid);
1174      return $DB->get_records_sql_menu($sql, $values);
1175  }
1176  
1177  /**
1178   * Return default common options for {@link format_text()} when preparing a content to be exported.
1179   * It is important not to apply filters and not to clean the HTML in format_text()
1180   *
1181   * @return stdClass
1182   */
1183  function portfolio_format_text_options() {
1184  
1185      $options                = new stdClass();
1186      $options->para          = false;
1187      $options->newlines      = true;
1188      $options->filter        = false;
1189      $options->noclean       = true;
1190      $options->overflowdiv   = false;
1191  
1192      return $options;
1193  }
1194  
1195  /**
1196   * callback function from {@link portfolio_rewrite_pluginfile_urls}
1197   * looks through preg_replace matches and replaces content with whatever the active portfolio export format says
1198   *
1199   * @param int $contextid module context id
1200   * @param string $component module name (eg:mod_assignment)
1201   * @param string $filearea normal file_area arguments
1202   * @param int $itemid component item id
1203   * @param portfolio_format $format exporter format type
1204   * @param array $options extra options to pass through to the file_output function in the format (optional)
1205   * @param array $matches internal matching
1206   * @return object|array|string
1207   */
1208  function portfolio_rewrite_pluginfile_url_callback($contextid, $component, $filearea, $itemid, $format, $options, $matches) {
1209      $matches = $matches[0]; // No internal matching.
1210  
1211      // Loads the HTML.
1212      $dom = new DomDocument();
1213      if (!$dom->loadHTML($matches)) {
1214          return $matches;
1215      }
1216  
1217      // Navigates to the node.
1218      $xpath = new DOMXPath($dom);
1219      $nodes = $xpath->query('/html/body/child::*');
1220      if (empty($nodes) || count($nodes) > 1) {
1221          // Unexpected sequence, none or too many nodes.
1222          return $matches;
1223      }
1224      $dom = $nodes->item(0);
1225  
1226      $attributes = array();
1227      foreach ($dom->attributes as $attr => $node) {
1228          $attributes[$attr] = $node->value;
1229      }
1230      // now figure out the file
1231      $fs = get_file_storage();
1232      $key = 'href';
1233      if (!array_key_exists('href', $attributes) && array_key_exists('src', $attributes)) {
1234          $key = 'src';
1235      }
1236      if (!array_key_exists($key, $attributes)) {
1237          debugging('Couldn\'t find an attribute to use that contains @@PLUGINFILE@@ in portfolio_rewrite_pluginfile');
1238          return $matches;
1239      }
1240      $filename = substr($attributes[$key], strpos($attributes[$key], '@@PLUGINFILE@@') + strlen('@@PLUGINFILE@@'));
1241      $filepath = '/';
1242      if (strpos($filename, '/') !== 0) {
1243          $bits = explode('/', $filename);
1244          $filename = array_pop($bits);
1245          $filepath = implode('/', $bits);
1246      }
1247      if (!$file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, urldecode($filename))) {
1248          debugging("Couldn't find a file from the embedded path info context $contextid component $component filearea $filearea itemid $itemid filepath $filepath name $filename");
1249          return $matches;
1250      }
1251      if (empty($options)) {
1252          $options = array();
1253      }
1254      $options['attributes'] = $attributes;
1255      return $format->file_output($file, $options);
1256  }
1257  
1258  /**
1259   * Function to require any potential callback files, throwing exceptions
1260   * if an issue occurs.
1261   *
1262   * @param string $component This is the name of the component in Moodle, eg 'mod_forum'
1263   * @param string $class Name of the class containing the callback functions
1264   *     activity components should ALWAYS use their name_portfolio_caller
1265   *     other locations must use something unique
1266   */
1267  function portfolio_include_callback_file($component, $class = null) {
1268      global $CFG;
1269      require_once($CFG->libdir . '/adminlib.php');
1270  
1271      // It's possible that they are passing a file path rather than passing a component.
1272      // We want to try and convert this to a component name, eg. mod_forum.
1273      $pos = strrpos($component, '/');
1274      if ($pos !== false) {
1275          // Get rid of the first slash (if it exists).
1276          $component = ltrim($component, '/');
1277          // Get a list of valid plugin types.
1278          $plugintypes = core_component::get_plugin_types();
1279          // Assume it is not valid for now.
1280          $isvalid = false;
1281          // Go through the plugin types.
1282          foreach ($plugintypes as $type => $path) {
1283              // Getting the path relative to the dirroot.
1284              $path = preg_replace('|^' . preg_quote($CFG->dirroot, '|') . '/|', '', $path);
1285              if (strrpos($component, $path) === 0) {
1286                  // Found the plugin type.
1287                  $isvalid = true;
1288                  $plugintype = $type;
1289                  $pluginpath = $path;
1290              }
1291          }
1292          // Throw exception if not a valid component.
1293          if (!$isvalid) {
1294              throw new coding_exception('Somehow a non-valid plugin path was passed, could be a hackz0r attempt, exiting.');
1295          }
1296          // Remove the file name.
1297          $component = trim(substr($component, 0, $pos), '/');
1298          // Replace the path with the type.
1299          $component = str_replace($pluginpath, $plugintype, $component);
1300          // Ok, replace '/' with '_'.
1301          $component = str_replace('/', '_', $component);
1302          // Place a debug message saying the third parameter should be changed.
1303          debugging('The third parameter sent to the function set_callback_options should be the component name, not a file path, please update this.', DEBUG_DEVELOPER);
1304      }
1305  
1306      // Check that it is a valid component.
1307      if (!get_component_version($component)) {
1308          throw new portfolio_button_exception('nocallbackcomponent', 'portfolio', '', $component);
1309      }
1310  
1311      // Obtain the component's location.
1312      if (!$componentloc = core_component::get_component_directory($component)) {
1313          throw new portfolio_button_exception('nocallbackcomponent', 'portfolio', '', $component);
1314      }
1315  
1316      // Check if the component contains the necessary file for the portfolio plugin.
1317      // These are locallib.php, portfoliolib.php and portfolio_callback.php.
1318      $filefound = false;
1319      if (file_exists($componentloc . '/locallib.php')) {
1320          $filefound = true;
1321          require_once($componentloc . '/locallib.php');
1322      }
1323      if (file_exists($componentloc . '/portfoliolib.php')) {
1324          $filefound = true;
1325          debugging('Please standardise your plugin by renaming your portfolio callback file to locallib.php, or if that file already exists moving the portfolio functionality there.', DEBUG_DEVELOPER);
1326          require_once ($componentloc . '/portfoliolib.php');
1327      }
1328      if (file_exists($componentloc . '/portfolio_callback.php')) {
1329          $filefound = true;
1330          debugging('Please standardise your plugin by renaming your portfolio callback file to locallib.php, or if that file already exists moving the portfolio functionality there.', DEBUG_DEVELOPER);
1331          require_once($componentloc . '/portfolio_callback.php');
1332      }
1333  
1334      // Ensure that we found a file we can use, if not throw an exception.
1335      if (!$filefound) {
1336          throw new portfolio_button_exception('nocallbackfile', 'portfolio', '', $component);
1337      }
1338  
1339      if (!is_null($class)) {
1340          // If class is specified, check it exists and extends portfolio_caller_base.
1341          if (!class_exists($class) || !is_subclass_of($class, 'portfolio_caller_base')) {
1342              throw new portfolio_button_exception('nocallbackclass', 'portfolio', '', $class);
1343          }
1344      }
1345  }
1346  
1347  /**
1348   * Go through all the @@PLUGINFILE@@ matches in some text,
1349   * extract the file information and pass it back to the portfolio export format
1350   * to regenerate the html to output
1351   *
1352   * @param string $text the text to search through
1353   * @param int $contextid normal file_area arguments
1354   * @param string $component module name
1355   * @param string $filearea normal file_area arguments
1356   * @param int $itemid normal file_area arguments
1357   * @param portfolio_format $format the portfolio export format
1358   * @param array $options additional options to be included in the plugin file url (optional)
1359   * @return mixed
1360   */
1361  function portfolio_rewrite_pluginfile_urls($text, $contextid, $component, $filearea, $itemid, $format, $options=null) {
1362      $patterns = array(
1363          '(<(a|A)[^<]*?href="@@PLUGINFILE@@/[^>]*?>.*?</(a|A)>)',
1364          '(<(img|IMG)\s[^<]*?src="@@PLUGINFILE@@/[^>]*?/?>)',
1365      );
1366      $pattern = '~' . implode('|', $patterns) . '~';
1367      $callback = partial('portfolio_rewrite_pluginfile_url_callback', $contextid, $component, $filearea, $itemid, $format, $options);
1368      return preg_replace_callback($pattern, $callback, $text);
1369  }
1370  // this function has to go last, because the regexp screws up syntax highlighting in some editors
1371