Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  •    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   * Class that holds a tree of availability conditions.
      19   *
      20   * @package core_availability
      21   * @copyright 2014 The Open University
      22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      23   */
      24  
      25  namespace core_availability;
      26  
      27  defined('MOODLE_INTERNAL') || die();
      28  
      29  /**
      30   * Class that holds a tree of availability conditions.
      31   *
      32   * The structure of this tree in JSON input data is:
      33   *
      34   * { op:'&', c:[] }
      35   *
      36   * where 'op' is one of the OP_xx constants and 'c' is an array of children.
      37   *
      38   * At the root level one of the following additional values must be included:
      39   *
      40   * op '|' or '!&'
      41   *   show:true
      42   *   Boolean value controlling whether a failed match causes the item to
      43   *   display to students with information, or be completely hidden.
      44   * op '&' or '!|'
      45   *   showc:[]
      46   *   Array of same length as c with booleans corresponding to each child; you
      47   *   can make it be hidden or shown depending on which one they fail. (Anything
      48   *   with false takes precedence.)
      49   *
      50   * @package core_availability
      51   * @copyright 2014 The Open University
      52   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      53   */
      54  class tree extends tree_node {
      55      /** @var int Operator: AND */
      56      const OP_AND = '&';
      57      /** @var int Operator: OR */
      58      const OP_OR = '|';
      59      /** @var int Operator: NOT(AND) */
      60      const OP_NOT_AND = '!&';
      61      /** @var int Operator: NOT(OR) */
      62      const OP_NOT_OR = '!|';
      63  
      64      /** @var bool True if this tree is at root level */
      65      protected $root;
      66  
      67      /** @var string Operator type (OP_xx constant) */
      68      protected $op;
      69  
      70      /** @var tree_node[] Children in this branch (may be empty array if needed) */
      71      protected $children;
      72  
      73      /**
      74       * Array of 'show information or hide completely' options for each child.
      75       * This array is only set for the root tree if it is in AND or NOT OR mode,
      76       * otherwise it is null.
      77       *
      78       * @var bool[]
      79       */
      80      protected $showchildren;
      81  
      82      /**
      83       * Single 'show information or hide completely' option for tree. This option
      84       * is only set for the root tree if it is in OR or NOT AND mode, otherwise
      85       * it is true.
      86       *
      87       * @var bool
      88       */
      89      protected $show;
      90  
      91      /**
      92       * Display a representation of this tree (used for debugging).
      93       *
      94       * @return string Text representation of tree
      95       */
      96      public function __toString() {
      97          $result = '';
      98          if ($this->root && is_null($this->showchildren)) {
      99              $result .= $this->show ? '+' : '-';
     100          }
     101          $result .= $this->op . '(';
     102          $first = true;
     103          foreach ($this->children as $index => $child) {
     104              if ($first) {
     105                  $first = false;
     106              } else {
     107                  $result .= ',';
     108              }
     109              if (!is_null($this->showchildren)) {
     110                  $result .= $this->showchildren[$index] ? '+' : '-';
     111              }
     112              $result .= (string)$child;
     113          }
     114          $result .= ')';
     115          return $result;
     116      }
     117  
     118      /**
     119       * Decodes availability structure.
     120       *
     121       * This function also validates the retrieved data as follows:
     122       * 1. Data that does not meet the API-defined structure causes a
     123       *    coding_exception (this should be impossible unless there is
     124       *    a system bug or somebody manually hacks the database).
     125       * 2. Data that meets the structure but cannot be implemented (e.g.
     126       *    reference to missing plugin or to module that doesn't exist) is
     127       *    either silently discarded (if $lax is true) or causes a
     128       *    coding_exception (if $lax is false).
     129       *
     130       * @see decode_availability
     131       * @param \stdClass $structure Structure (decoded from JSON)
     132       * @param boolean $lax If true, throw exceptions only for invalid structure
     133       * @param boolean $root If true, this is the root tree
     134       * @return tree Availability tree
     135       * @throws \coding_exception If data is not valid structure
     136       */
     137      public function __construct($structure, $lax = false, $root = true) {
     138          $this->root = $root;
     139  
     140          // Check object.
     141          if (!is_object($structure)) {
     142              throw new \coding_exception('Invalid availability structure (not object)');
     143          }
     144  
     145          // Extract operator.
     146          if (!isset($structure->op)) {
     147              throw new \coding_exception('Invalid availability structure (missing ->op)');
     148          }
     149          $this->op = $structure->op;
     150          if (!in_array($this->op, array(self::OP_AND, self::OP_OR,
     151                  self::OP_NOT_AND, self::OP_NOT_OR), true)) {
     152              throw new \coding_exception('Invalid availability structure (unknown ->op)');
     153          }
     154  
     155          // For root tree, get show options.
     156          $this->show = true;
     157          $this->showchildren = null;
     158          if ($root) {
     159              if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
     160                  // Per-child show options.
     161                  if (!isset($structure->showc)) {
     162                      throw new \coding_exception(
     163                              'Invalid availability structure (missing ->showc)');
     164                  }
     165                  if (!is_array($structure->showc)) {
     166                      throw new \coding_exception(
     167                              'Invalid availability structure (->showc not array)');
     168                  }
     169                  foreach ($structure->showc as $value) {
     170                      if (!is_bool($value)) {
     171                          throw new \coding_exception(
     172                                  'Invalid availability structure (->showc value not bool)');
     173                      }
     174                  }
     175                  // Set it empty now - add corresponding ones later.
     176                  $this->showchildren = array();
     177              } else {
     178                  // Entire tree show option. (Note: This is because when you use
     179                  // OR mode, say you have A OR B, the user does not meet conditions
     180                  // for either A or B. A is set to 'show' and B is set to 'hide'.
     181                  // But they don't have either, so how do we know which one to do?
     182                  // There might as well be only one value.)
     183                  if (!isset($structure->show)) {
     184                      throw new \coding_exception(
     185                              'Invalid availability structure (missing ->show)');
     186                  }
     187                  if (!is_bool($structure->show)) {
     188                      throw new \coding_exception(
     189                              'Invalid availability structure (->show not bool)');
     190                  }
     191                  $this->show = $structure->show;
     192              }
     193          }
     194  
     195          // Get list of enabled plugins.
     196          $pluginmanager = \core_plugin_manager::instance();
     197          $enabled = $pluginmanager->get_enabled_plugins('availability');
     198  
     199          // For unit tests, also allow the mock plugin type (even though it
     200          // isn't configured in the code as a proper plugin).
     201          if (PHPUNIT_TEST) {
     202              $enabled['mock'] = true;
     203          }
     204  
     205          // Get children.
     206          if (!isset($structure->c)) {
     207              throw new \coding_exception('Invalid availability structure (missing ->c)');
     208          }
     209          if (!is_array($structure->c)) {
     210              throw new \coding_exception('Invalid availability structure (->c not array)');
     211          }
     212          if (is_array($this->showchildren) && count($structure->showc) != count($structure->c)) {
     213              throw new \coding_exception('Invalid availability structure (->c, ->showc mismatch)');
     214          }
     215          $this->children = array();
     216          foreach ($structure->c as $index => $child) {
     217              if (!is_object($child)) {
     218                  throw new \coding_exception('Invalid availability structure (child not object)');
     219              }
     220  
     221              // First see if it's a condition. These have a defined type.
     222              if (isset($child->type)) {
     223                  // Look for a plugin of this type.
     224                  $classname = '\availability_' . $child->type . '\condition';
     225                  if (!array_key_exists($child->type, $enabled)) {
     226                      if ($lax) {
     227                          // On load of existing settings, ignore if class
     228                          // doesn't exist.
     229                          continue;
     230                      } else {
     231                          throw new \coding_exception('Unknown condition type: ' . $child->type);
     232                      }
     233                  }
     234                  $this->children[] = new $classname($child);
     235              } else {
     236                  // Not a condition. Must be a subtree.
     237                  $this->children[] = new tree($child, $lax, false);
     238              }
     239              if (!is_null($this->showchildren)) {
     240                  $this->showchildren[] = $structure->showc[$index];
     241              }
     242          }
     243      }
     244  
     245      public function check_available($not, info $info, $grabthelot, $userid) {
     246          // If there are no children in this group, we just treat it as available.
     247          $information = '';
     248          if (!$this->children) {
     249              return new result(true);
     250          }
     251  
     252          // Get logic flags from operator.
     253          list($innernot, $andoperator) = $this->get_logic_flags($not);
     254  
     255          if ($andoperator) {
     256              $allow = true;
     257          } else {
     258              $allow = false;
     259          }
     260          $failedchildren = array();
     261          $totallyhide = !$this->show;
     262          foreach ($this->children as $index => $child) {
     263              // Check available and get info.
     264              $childresult = $child->check_available(
     265                      $innernot, $info, $grabthelot, $userid);
     266              $childyes = $childresult->is_available();
     267              if (!$childyes) {
     268                  $failedchildren[] = $childresult;
     269                  if (!is_null($this->showchildren) && !$this->showchildren[$index]) {
     270                      $totallyhide = true;
     271                  }
     272              }
     273  
     274              if ($andoperator && !$childyes) {
     275                  $allow = false;
     276                  // Do not exit loop at this point, as we will still include other info.
     277              } else if (!$andoperator && $childyes) {
     278                  // Exit loop since we are going to allow access (from this tree at least).
     279                  $allow = true;
     280                  break;
     281              }
     282          }
     283  
     284          if ($allow) {
     285              return new result(true);
     286          } else if ($totallyhide) {
     287              return new result(false);
     288          } else {
     289              return new result(false, $this, $failedchildren);
     290          }
     291      }
     292  
     293      public function is_applied_to_user_lists() {
     294          return true;
     295      }
     296  
     297      /**
     298       * Tests against a user list. Users who cannot access the activity due to
     299       * availability restrictions will be removed from the list.
     300       *
     301       * This test ONLY includes conditions which are marked as being applied to
     302       * user lists. For example, group conditions are included but date
     303       * conditions are not included.
     304       *
     305       * The function operates reasonably efficiently i.e. should not do per-user
     306       * database queries. It is however likely to be fairly slow.
     307       *
     308       * @param array $users Array of userid => object
     309       * @param bool $not If tree's parent indicates it's being checked negatively
     310       * @param info $info Info about current context
     311       * @param capability_checker $checker Capability checker
     312       * @return array Filtered version of input array
     313       */
     314      public function filter_user_list(array $users, $not, info $info,
     315              capability_checker $checker) {
     316          // Get logic flags from operator.
     317          list($innernot, $andoperator) = $this->get_logic_flags($not);
     318  
     319          if ($andoperator) {
     320              // For AND, start with the whole result and whittle it down.
     321              $result = $users;
     322          } else {
     323              // For OR, start with nothing.
     324              $result = array();
     325              $anyconditions = false;
     326          }
     327  
     328          // Loop through all valid children.
     329          foreach ($this->children as $index => $child) {
     330              if (!$child->is_applied_to_user_lists()) {
     331                  if ($andoperator) {
     332                      continue;
     333                  } else {
     334                      // OR condition with one option that doesn't restrict user
     335                      // lists = everyone is allowed.
     336                      $anyconditions = false;
     337                      break;
     338                  }
     339              }
     340              $childresult = $child->filter_user_list($users, $innernot, $info, $checker);
     341              if ($andoperator) {
     342                  $result = array_intersect_key($result, $childresult);
     343              } else {
     344                  // Combine results into array.
     345                  foreach ($childresult as $id => $user) {
     346                      $result[$id] = $user;
     347                  }
     348                  $anyconditions = true;
     349              }
     350          }
     351  
     352          // For OR operator, if there were no conditions just return input.
     353          if (!$andoperator && !$anyconditions) {
     354              return $users;
     355          } else {
     356              return $result;
     357          }
     358      }
     359  
     360      public function get_user_list_sql($not, info $info, $onlyactive) {
     361          global $DB;
     362          // Get logic flags from operator.
     363          list($innernot, $andoperator) = $this->get_logic_flags($not);
     364  
     365          // Loop through all valid children, getting SQL for each.
     366          $childresults = array();
     367          foreach ($this->children as $index => $child) {
     368              if (!$child->is_applied_to_user_lists()) {
     369                  if ($andoperator) {
     370                      continue;
     371                  } else {
     372                      // OR condition with one option that doesn't restrict user
     373                      // lists = everyone is allowed.
     374                      $childresults = array();
     375                      break;
     376                  }
     377              }
     378              $childresult = $child->get_user_list_sql($innernot, $info, $onlyactive);
     379              if ($childresult[0]) {
     380                  $childresults[] = $childresult;
     381              } else if (!$andoperator) {
     382                  // When using OR operator, if any part doesn't have restrictions,
     383                  // then nor does the whole thing.
     384                  return array('', array());
     385              }
     386          }
     387  
     388          // If there are no conditions, return null.
     389          if (!$childresults) {
     390              return array('', array());
     391          }
     392          // If there is a single condition, return it.
     393          if (count($childresults) === 1) {
     394              return $childresults[0];
     395          }
     396  
     397          // Combine results using INTERSECT or UNION.
     398          $outsql = null;
     399          $subsql = array();
     400          $outparams = array();
     401          foreach ($childresults as $childresult) {
     402              $subsql[] = $childresult[0];
     403              $outparams = array_merge($outparams, $childresult[1]);
     404          }
     405          if ($andoperator) {
     406              $outsql = $DB->sql_intersect($subsql, 'id');
     407          } else {
     408              $outsql = '(' . join(') UNION (', $subsql) . ')';
     409          }
     410          return array($outsql, $outparams);
     411      }
     412  
     413      public function is_available_for_all($not = false) {
     414          // Get logic flags.
     415          list($innernot, $andoperator) = $this->get_logic_flags($not);
     416  
     417          // No children = always available.
     418          if (!$this->children) {
     419              return true;
     420          }
     421  
     422          // Check children.
     423          foreach ($this->children as $child) {
     424              $innerall = $child->is_available_for_all($innernot);
     425              if ($andoperator) {
     426                  // When there is an AND operator, then any child that results
     427                  // in unavailable status would cause the whole thing to be
     428                  // unavailable.
     429                  if (!$innerall) {
     430                      return false;
     431                  }
     432              } else {
     433                  // When there is an OR operator, then any child which must only
     434                  // be available means the whole thing must be available.
     435                  if ($innerall) {
     436                      return true;
     437                  }
     438              }
     439          }
     440  
     441          // If we get to here then for an AND operator that means everything must
     442          // be available. From OR it means that everything must be possibly
     443          // not available.
     444          return $andoperator;
     445      }
     446  
     447      /**
     448       * Gets full information about this tree (including all children) as HTML
     449       * for display to staff.
     450       *
     451       * @param info $info Information about location of condition tree
     452       * @throws \coding_exception If you call on a non-root tree
     453       * @return string HTML data (empty string if none)
     454       */
     455      public function get_full_information(info $info) {
     456          if (!$this->root) {
     457              throw new \coding_exception('Only supported on root item');
     458          }
     459          return $this->get_full_information_recursive(false, $info, null, true);
     460      }
     461  
     462      /**
     463       * Gets information about this tree corresponding to the given result
     464       * object. (In other words, only conditions which the student actually
     465       * fails will be shown - and nothing if display is turned off.)
     466       *
     467       * @param info $info Information about location of condition tree
     468       * @param result $result Result object
     469       * @throws \coding_exception If you call on a non-root tree
     470       * @return string HTML data (empty string if none)
     471       */
     472      public function get_result_information(info $info, result $result) {
     473          if (!$this->root) {
     474              throw new \coding_exception('Only supported on root item');
     475          }
     476          return $this->get_full_information_recursive(false, $info, $result, true);
     477      }
     478  
     479      /**
     480       * Gets information about this tree (including all or selected children) as
     481       * HTML for display to staff or student.
     482       *
     483       * @param bool $not True if there is a NOT in effect
     484       * @param info $info Information about location of condition tree
     485       * @param result $result Result object if this is a student display, else null
     486       * @param bool $root True if this is the root item
     487       * @param bool $hidden Staff display; true if this tree has show=false (from parent)
     488       * @return string|renderable Information to render
     489       */
     490      protected function get_full_information_recursive(
     491              $not, info $info, result $result = null, $root, $hidden = false) {
     492          // Get list of children - either full list, or those which are shown.
     493          $children = $this->children;
     494          $staff = true;
     495          if ($result) {
     496              $children = $result->filter_nodes($children);
     497              $staff = false;
     498          }
     499  
     500          // If no children, return empty string.
     501          if (!$children) {
     502              return '';
     503          }
     504  
     505          list($innernot, $andoperator) = $this->get_logic_flags($not);
     506  
     507          // If there is only one child, don't bother displaying this tree
     508          // (AND and OR makes no difference). Recurse to the child if a tree,
     509          // otherwise display directly.
     510          if (count ($children) === 1) {
     511              $child = reset($children);
     512              if ($this->root && is_null($result)) {
     513                  if (is_null($this->showchildren)) {
     514                      $childhidden = !$this->show;
     515                  } else {
     516                      $childhidden = !$this->showchildren[0];
     517                  }
     518              } else {
     519                  $childhidden = $hidden;
     520              }
     521              if ($child instanceof tree) {
     522                  return $child->get_full_information_recursive(
     523                          $innernot, $info, $result, $root, $childhidden);
     524              } else {
     525                  if ($root) {
     526                      $result = $child->get_standalone_description($staff, $innernot, $info);
     527                  } else {
     528                      $result = $child->get_description($staff, $innernot, $info);
     529                  }
     530                  if ($childhidden) {
     531                      $result .= ' ' . get_string('hidden_marker', 'availability');
     532                  }
     533                  return $result;
     534              }
     535          }
     536  
     537          // Multiple children, so prepare child messages (recursive).
     538          $items = array();
     539          $index = 0;
     540          foreach ($children as $child) {
     541              // Work out if this node is hidden (staff view only).
     542              $childhidden = $this->root && is_null($result) &&
     543                      !is_null($this->showchildren) && !$this->showchildren[$index];
     544              if ($child instanceof tree) {
     545                  $items[] = $child->get_full_information_recursive(
     546                          $innernot, $info, $result, false, $childhidden);
     547              } else {
     548                  $childdescription = $child->get_description($staff, $innernot, $info);
     549                  if ($childhidden) {
     550                      $childdescription .= ' ' . get_string('hidden_marker', 'availability');
     551                  }
     552                  $items[] = $childdescription;
     553              }
     554              $index++;
     555          }
     556  
     557          // If showing output to staff, and root is set to hide completely,
     558          // then include this information in the message.
     559          if ($this->root) {
     560              $treehidden = !$this->show && is_null($result);
     561          } else {
     562              $treehidden = $hidden;
     563          }
     564  
     565          // Format output for display.
     566          return new \core_availability_multiple_messages($root, $andoperator, $treehidden, $items);
     567      }
     568  
     569      /**
     570       * Converts the operator for the tree into two flags used for computing
     571       * the result.
     572       *
     573       * The 2 flags are $innernot (whether to set $not when calling for children)
     574       * and $andoperator (whether to use AND or OR operator to combine children).
     575       *
     576       * @param bool $not Not flag passed to this tree
     577       * @return array Array of the 2 flags ($innernot, $andoperator)
     578       */
     579      public function get_logic_flags($not) {
     580          // Work out which type of logic to use for the group.
     581          switch($this->op) {
     582              case self::OP_AND:
     583              case self::OP_OR:
     584                  $negative = false;
     585                  break;
     586              case self::OP_NOT_AND:
     587              case self::OP_NOT_OR:
     588                  $negative = true;
     589                  break;
     590              default:
     591                  throw new \coding_exception('Unknown operator');
     592          }
     593          switch($this->op) {
     594              case self::OP_AND:
     595              case self::OP_NOT_AND:
     596                  $andoperator = true;
     597                  break;
     598              case self::OP_OR:
     599              case self::OP_NOT_OR:
     600                  $andoperator = false;
     601                  break;
     602              default:
     603                  throw new \coding_exception('Unknown operator');
     604          }
     605  
     606          // Select NOT (or not) for children. It flips if this is a 'not' group.
     607          $innernot = $negative ? !$not : $not;
     608  
     609          // Select operator to use for this group. If flips for negative, because:
     610          // NOT (a AND b) = (NOT a) OR (NOT b)
     611          // NOT (a OR b) = (NOT a) AND (NOT b).
     612          if ($innernot) {
     613              $andoperator = !$andoperator;
     614          }
     615          return array($innernot, $andoperator);
     616      }
     617  
     618      public function save() {
     619          $result = new \stdClass();
     620          $result->op = $this->op;
     621          // Only root tree has the 'show' options.
     622          if ($this->root) {
     623              if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
     624                  $result->showc = $this->showchildren;
     625              } else {
     626                  $result->show = $this->show;
     627              }
     628          }
     629          $result->c = array();
     630          foreach ($this->children as $child) {
     631              $result->c[] = $child->save();
     632          }
     633          return $result;
     634      }
     635  
     636      /**
     637       * Checks whether this tree is empty (contains no children).
     638       *
     639       * @return boolean True if empty
     640       */
     641      public function is_empty() {
     642          return count($this->children) === 0;
     643      }
     644  
     645      /**
     646       * Recursively gets all children of a particular class (you can use a base
     647       * class to get all conditions, or a specific class).
     648       *
     649       * @param string $classname Full class name e.g. core_availability\condition
     650       * @return array Array of nodes of that type (flattened, not a tree any more)
     651       */
     652      public function get_all_children($classname) {
     653          $result = array();
     654          $this->recursive_get_all_children($classname, $result);
     655          return $result;
     656      }
     657  
     658      /**
     659       * Internal function that implements get_all_children efficiently.
     660       *
     661       * @param string $classname Full class name e.g. core_availability\condition
     662       * @param array $result Output array of nodes
     663       */
     664      protected function recursive_get_all_children($classname, array &$result) {
     665          foreach ($this->children as $child) {
     666              if (is_a($child, $classname)) {
     667                  $result[] = $child;
     668              }
     669              if ($child instanceof tree) {
     670                  $child->recursive_get_all_children($classname, $result);
     671              }
     672          }
     673      }
     674  
     675      public function update_after_restore($restoreid, $courseid,
     676              \base_logger $logger, $name) {
     677          $changed = false;
     678          foreach ($this->children as $index => $child) {
     679              if ($child->include_after_restore($restoreid, $courseid, $logger, $name,
     680                      info::get_restore_task($restoreid))) {
     681                  $thischanged = $child->update_after_restore($restoreid, $courseid,
     682                          $logger, $name);
     683                  $changed = $changed || $thischanged;
     684              } else {
     685                  unset($this->children[$index]);
     686                  unset($this->showchildren[$index]);
     687                  $this->showchildren = array_values($this->showchildren);
     688                  $changed = true;
     689              }
     690          }
     691          return $changed;
     692      }
     693  
     694      public function update_dependency_id($table, $oldid, $newid) {
     695          $changed = false;
     696          foreach ($this->children as $child) {
     697              $thischanged = $child->update_dependency_id($table, $oldid, $newid);
     698              $changed = $changed || $thischanged;
     699          }
     700          return $changed;
     701      }
     702  
     703      /**
     704       * Returns a JSON object which corresponds to a tree.
     705       *
     706       * Intended for unit testing, as normally the JSON values are constructed
     707       * by JavaScript code.
     708       *
     709       * This function generates 'nested' (i.e. not root-level) trees.
     710       *
     711       * @param array $children Array of JSON objects from component children
     712       * @param string $op Operator (tree::OP_xx)
     713       * @return stdClass JSON object
     714       * @throws coding_exception If you get parameters wrong
     715       */
     716      public static function get_nested_json(array $children, $op = self::OP_AND) {
     717  
     718          // Check $op and work out its type.
     719          switch($op) {
     720              case self::OP_AND:
     721              case self::OP_NOT_OR:
     722              case self::OP_OR:
     723              case self::OP_NOT_AND:
     724                  break;
     725              default:
     726                  throw new \coding_exception('Invalid $op');
     727          }
     728  
     729          // Do simple tree.
     730          $result = new \stdClass();
     731          $result->op = $op;
     732          $result->c = $children;
     733          return $result;
     734      }
     735  
     736      /**
     737       * Returns a JSON object which corresponds to a tree at root level.
     738       *
     739       * Intended for unit testing, as normally the JSON values are constructed
     740       * by JavaScript code.
     741       *
     742       * The $show parameter can be a boolean for all OP_xx options. For OP_AND
     743       * and OP_NOT_OR where you have individual show options, you can specify
     744       * a boolean (same for all) or an array.
     745       *
     746       * @param array $children Array of JSON objects from component children
     747       * @param string $op Operator (tree::OP_xx)
     748       * @param bool|array $show Whether 'show' option is turned on (see above)
     749       * @return stdClass JSON object ready for encoding
     750       * @throws coding_exception If you get parameters wrong
     751       */
     752      public static function get_root_json(array $children, $op = self::OP_AND, $show = true) {
     753  
     754          // Get the basic object.
     755          $result = self::get_nested_json($children, $op);
     756  
     757          // Check $op type.
     758          switch($op) {
     759              case self::OP_AND:
     760              case self::OP_NOT_OR:
     761                  $multishow = true;
     762                  break;
     763              case self::OP_OR:
     764              case self::OP_NOT_AND:
     765                  $multishow = false;
     766                  break;
     767          }
     768  
     769          // Add show options depending on operator.
     770          if ($multishow) {
     771              if (is_bool($show)) {
     772                  $result->showc = array_pad(array(), count($result->c), $show);
     773              } else if (is_array($show)) {
     774                  // The JSON will break if anything isn't an actual bool, so check.
     775                  foreach ($show as $item) {
     776                      if (!is_bool($item)) {
     777                          throw new \coding_exception('$show array members must be bool');
     778                      }
     779                  }
     780                  // Check the size matches.
     781                  if (count($show) != count($result->c)) {
     782                      throw new \coding_exception('$show array size does not match $children');
     783                  }
     784                  $result->showc = $show;
     785              } else {
     786                  throw new \coding_exception('$show must be bool or array');
     787              }
     788          } else {
     789              if (!is_bool($show)) {
     790                  throw new \coding_exception('For this operator, $show must be bool');
     791              }
     792              $result->show = $show;
     793          }
     794  
     795          return $result;
     796      }
     797  }