Merge branch 'MDL-67850-seb-integration-3' of https://github.com/catalyst/moodle
authorJake Dallimore <jake@moodle.com>
Thu, 30 Apr 2020 01:13:14 +0000 (09:13 +0800)
committerJake Dallimore <jake@moodle.com>
Thu, 30 Apr 2020 01:13:14 +0000 (09:13 +0800)
82 files changed:
.eslintignore
.stylelintignore
admin/settings/development.php
lang/en/admin.php
lib/classes/component.php
lib/classes/plugin_manager.php
lib/db/upgrade.php
lib/plist/LICENSE [new file with mode: 0644]
lib/plist/README.md [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFPropertyList.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFType.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFTypeDetector.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/IOException.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/PListException.php [new file with mode: 0644]
lib/plist/readme_moodle.txt [new file with mode: 0644]
lib/thirdpartylibs.xml
lib/upgrade.txt
mod/quiz/accessrule/safebrowser/rule.php [deleted file]
mod/quiz/accessrule/safebrowser/tests/rule_test.php [deleted file]
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/src/managetemplates.js [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/access_manager.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/config_key.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/access_prevented.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_created.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_deleted.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_disabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_enabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_updated.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/helper.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/hideif_rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/link_generator.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/form/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/table/template_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/privacy/provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/property_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/quiz_settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/settings_provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template_controller.php [new file with mode: 0644]
mod/quiz/accessrule/seb/config.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/access.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/caches.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/install.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/install.xml [new file with mode: 0644]
mod/quiz/accessrule/seb/db/upgrade.php [moved from mod/quiz/accessrule/safebrowser/lang/en/quizaccess_safebrowser.php with 53% similarity]
mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php [new file with mode: 0644]
mod/quiz/accessrule/seb/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/behat/edit_form.feature [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/generator/behat_quizaccess_seb_generator.php [moved from mod/quiz/accessrule/safebrowser/classes/privacy/provider.php with 53% similarity]
mod/quiz/accessrule/seb/tests/generator/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/access_manager_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/backup_restore_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/base.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/config_key_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/event_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/helper_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/hideif_rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/link_generator_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/privacy_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/property_list_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/quiz_settings_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/JSON_unencrypted_mac_001.txt [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/encrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencryptedwithoutoriginator.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_mac_001.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_win_223.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/settings_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/template_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/version.php [moved from mod/quiz/accessrule/safebrowser/version.php with 68% similarity]
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
version.php

index 2ef8a38..b9c0b6a 100644 (file)
@@ -65,6 +65,7 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
+lib/plist/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index f00e696..5d9e5c1 100644 (file)
@@ -66,6 +66,7 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
+lib/plist/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index f8a5ceb..46206fc 100644 (file)
@@ -10,7 +10,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp = new admin_settingpage('experimentalsettings', new lang_string('experimentalsettings', 'admin'));
     //TODO: Re-enable cc-import once re-implemented in 2.0.x
     //$temp->add(new admin_setting_configcheckbox('enableimsccimport', new lang_string('enable_cc_import', 'imscc'), new lang_string('enable_cc_import_description', 'imscc'), 0));
-    $temp->add(new admin_setting_configcheckbox('enablesafebrowserintegration', new lang_string('enablesafebrowserintegration', 'admin'), new lang_string('configenablesafebrowserintegration', 'admin'), 0));
 
     $temp->add(new admin_setting_configcheckbox('dndallowtextandlinks', new lang_string('dndallowtextandlinks', 'admin'), new lang_string('configdndallowtextandlinks', 'admin'), 0));
 
index dc2d684..9370d23 100644 (file)
@@ -236,7 +236,6 @@ $string['configenablemobilewebservice'] = 'Enable mobile service for the officia
 $string['configenablerssfeeds'] = 'If enabled, RSS feeds are generated by various features across the site, such as blogs, forums, database activities and glossaries. Note that RSS feeds also need to be enabled for the particular activity modules.';
 $string['configenablerssfeedsdisabled'] = 'It is not available because RSS feeds are disabled in all the Site. To enable them, go to the Variables settings under Admin Configuration.';
 $string['configenablerssfeedsdisabled2'] = 'RSS feeds are currently disabled at site level. They may be enabled in Advanced features in the Site administration.';
-$string['configenablesafebrowserintegration'] = 'This adds the choice \'Require Safe Exam Browser\' to the \'Browser security\' field on the quiz settings form. See https://www.safeexambrowser.org/ for more information.';
 $string['configenablestats'] = 'If you choose \'yes\' here, Moodle\'s cronjob will process the logs and gather some statistics.  Depending on the amount of traffic on your site, this can take awhile. If you enable this, you will be able to see some interesting graphs and statistics about each of your courses, or on a sitewide basis.';
 $string['configenabletrusttext'] = 'By default Moodle will always thoroughly clean text that comes from users to remove any possible bad scripts, media etc that could be a security risk.  The Trusted Content system is a way of giving particular users that you trust the ability to include these advanced features in their content without interference.  To enable this system, you need to first enable this setting, and then grant the Trusted Content permission to a specific Moodle role.  Texts created or uploaded by such users will be marked as trusted and will not be cleaned before display.';
 $string['configenablewebservices'] = 'Web services enable other systems, such as the Moodle app, to log in to the site and perform operations. For extra security, the setting should be disabled if you are not using the app, or an external tool/service that requires integration via web services.';
@@ -545,7 +544,6 @@ $string['enablemoodlenet'] = 'Enable integration with MoodleNet instances';
 $string['enablemoodlenet_desc'] = 'If enabled, and provided the MoodleNet plugin is installed, users can import content from MoodleNet into this site.';
 $string['enablerecordcache'] = 'Enable record cache';
 $string['enablerssfeeds'] = 'Enable RSS feeds';
-$string['enablesafebrowserintegration'] = 'Enable Safe Exam Browser integration';
 $string['enablesearchareas'] = 'Enable search areas';
 $string['enablestats'] = 'Enable statistics';
 $string['enabletrusttext'] = 'Enable trusted content';
index f898c36..2a6eae8 100644 (file)
@@ -86,6 +86,7 @@ class core_component {
     protected static $psr0namespaces = array(
         'Horde' => 'lib/horde/framework/Horde',
         'Mustache' => 'lib/mustache/src/Mustache',
+        'CFPropertyList' => 'lib/plist/classes/CFPropertyList',
     );
     /** @var array associative array of PRS-4 namespaces and corresponding paths. */
     protected static $psr4namespaces = array(
index 61536ec..962bc3e 100644 (file)
@@ -1960,7 +1960,7 @@ class core_plugin_manager {
 
             'quizaccess' => array(
                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate',
-                'password', 'safebrowser', 'securewindow', 'timelimit'
+                'password', 'seb', 'securewindow', 'timelimit'
             ),
 
             'report' => array(
index f71d4d4..0b5f9e4 100644 (file)
@@ -2306,5 +2306,13 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2020041700.01);
     }
 
+    if ($oldversion < 2020042800.01) {
+        // Delete obsolete config value.
+        unset_config('enablesafebrowserintegration');
+        // Clean up config of the old plugin.
+        unset_all_config_for_plugin('quizaccess_safebrowser');
+
+        upgrade_main_savepoint(true, 2020042800.01);
+    }
     return true;
 }
diff --git a/lib/plist/LICENSE b/lib/plist/LICENSE
new file mode 100644 (file)
index 0000000..5e63f61
--- /dev/null
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2009 Christian Kruse, Rodney Rehm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/lib/plist/README.md b/lib/plist/README.md
new file mode 100644 (file)
index 0000000..7eaf4f8
--- /dev/null
@@ -0,0 +1,39 @@
+# CFPropertyList
+
+The PHP implementation of Apple's PropertyList can handle XML PropertyLists as well as binary PropertyLists. It offers functionality to easily convert data between worlds, e.g. recalculating timestamps from unix epoch to apple epoch and vice versa. A feature to automagically create (guess) the plist structure from a normal PHP data structure will help you dump your data to plist in no time.
+
+Note: CFPropertylist was originally hosted on [Google Code](http://code.google.com/p/cfpropertylist/)
+
+## Choose Your Favorite Operating System
+
+CFPropertyList does not rely on any "Apple proprietary" components, like plutil. CFPropertyList runs on any Operating System with PHP and some standard extensions installed.
+
+Although you might want to deliver data to your iPhone application, you might want to run those server side services on your standard Linux (or even Windows) environment, rather than buying an expensive Apple Server. With CFPropertyList you now have the power to provide data from your favorite Operating System.
+
+## Requirements And Limitations
+
+* requires PHP5.3 (as of CFPropertyList 2.0)
+* requires either [MBString](http://php.net/mbstring) or [Iconv](http://php.net/iconv)
+* requires either [BC](http://php.net/bc) or [GMP](http://php.net/gmp) or [phpseclib](http://phpseclib.sourceforge.net/) (see BigIntegerBug for an explanation) - as of CFPropertyList 1.0.1
+
+## Authors
+
+- Rodney Rehm <rodney.rehm@medialize.de>
+- Christian Kruse <cjk@wwwtech.de>
+- PSR-0 changes by Jarvis Badgley <https://github.com/ChiperSoft/CFPropertyList>
+
+## License
+
+CFPropertyList is published under the [MIT License](http://www.opensource.org/licenses/mit-license.php).
+
+## Installation
+
+see [Composer / Packagist](http://packagist.org/packages/rodneyrehm/plist).
+
+## Related
+
+* [man(5) plist](http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html)
+* [CFBinaryPList.c](http://www.opensource.apple.com/source/CF/CF-476.15/CFBinaryPList.c)
+* [CFPropertyList in Ruby](http://rubyforge.org/projects/cfpropertylist/)
+* [CFPropertyList in Python](https://github.com/bencochran/CFPropertyList)
+* [plist on Wikipedia](http://en.wikipedia.org/wiki/Plist)
diff --git a/lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php b/lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php
new file mode 100644 (file)
index 0000000..34cb708
--- /dev/null
@@ -0,0 +1,1051 @@
+<?php
+/**
+ * CFPropertyList
+ * {@link http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html Property Lists}
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @version $Id$
+ */
+namespace CFPropertyList;
+
+/**
+ * Facility for reading and writing binary PropertyLists. Ported from {@link http://www.opensource.apple.com/source/CF/CF-476.15/CFBinaryPList.c CFBinaryPList.c}.
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @example example-read-02.php Read a Binary PropertyList
+ * @example example-read-03.php Read a PropertyList without knowing the type
+ */
+abstract class CFBinaryPropertyList {
+  /**
+   * Content of the plist (unparsed string)
+   * @var string
+   */
+  protected $content = NULL;
+
+  /**
+   * position in the (unparsed) string
+   * @var integer
+   */
+  protected $pos = 0;
+
+  /**
+   * Table containing uniqued objects
+   * @var array
+   */
+  protected $uniqueTable = Array();
+
+  /**
+   * Number of objects in file
+   * @var integer
+   */
+  protected $countObjects = 0;
+
+  /**
+   * The length of all strings in the file (byte length, not character length)
+   * @var integer
+   */
+  protected $stringSize = 0;
+
+  /**
+   * The length of all ints in file (byte length)
+   * @var integer
+   */
+  protected $intSize = 0;
+
+  /**
+   * The length of misc objects (i.e. not integer and not string) in file
+   * @var integer
+   */
+  protected $miscSize = 0;
+
+  /**
+   * Number of object references in file (needed to calculate reference byte length)
+   * @var integer
+   */
+  protected $objectRefs = 0;
+
+  /**
+   * Number of objects written during save phase; needed to calculate the size of the object table
+   * @var integer
+   */
+  protected $writtenObjectCount = 0;
+
+  /**
+   * Table containing all objects in the file
+   */
+  protected $objectTable = Array();
+
+  /**
+   * The size of object references
+   */
+  protected $objectRefSize = 0;
+
+  /**
+   * The „offsets” (i.e. the different entries) in the file
+   */
+  protected $offsets = Array();
+
+  /**
+   * Read a „null type” (filler byte, true, false, 0 byte)
+   * @param $length The byte itself
+   * @return the byte value (e.g. CFBoolean(true), CFBoolean(false), 0 or 15)
+   * @throws PListException on encountering an unknown null type
+   */
+  protected function readBinaryNullType($length) {
+    switch($length) {
+      case 0: return 0; // null type
+      case 8: return new CFBoolean(false);
+      case 9: return new CFBoolean(true);
+      case 15: return 15; // fill type
+    }
+
+    throw new PListException("unknown null type: $length");
+  }
+
+  /**
+   * Create an 64 bit integer using bcmath or gmp
+   * @param int $hi The higher word
+   * @param int $lo The lower word
+   * @return mixed The integer (as int if possible, as string if not possible)
+   * @throws PListException if neither gmp nor bc available
+   */
+  protected static function make64Int($hi,$lo) {
+    // on x64, we can just use int
+    if(PHP_INT_SIZE > 4) return (((int)$hi)<<32) | ((int)$lo);
+
+    // lower word has to be unsigned since we don't use bitwise or, we use bcadd/gmp_add
+    $lo = sprintf("%u", $lo);
+
+    // use GMP or bcmath if possible
+    if(function_exists("gmp_mul")) return gmp_strval(gmp_add(gmp_mul($hi, "4294967296"), $lo));
+
+    if(function_exists("bcmul")) return bcadd(bcmul($hi,"4294967296"), $lo);
+
+    if(class_exists('Math_BigInteger')) {
+      $bi = new \Math_BigInteger($hi);
+      return $bi->multiply(new \Math_BigInteger("4294967296"))->add(new \Math_BigInteger($lo))->toString();
+    }
+
+    throw new PListException("either gmp or bc has to be installed, or the Math_BigInteger has to be available!");
+  }
+
+  /**
+   * Read an integer value
+   * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
+   * @return CFNumber The integer value
+   * @throws PListException if integer val is invalid
+   * @throws IOException if read error occurs
+   * @uses make64Int() to overcome PHP's big integer problems
+   */
+  protected function readBinaryInt($length) {
+    if($length > 3) throw new PListException("Integer greater than 8 bytes: $length");
+
+    $nbytes = 1 << $length;
+
+    $val = null;
+    if(strlen($buff = substr($this->content, $this->pos, $nbytes)) != $nbytes) throw IOException::readError("");
+    $this->pos += $nbytes;
+
+    switch($length) {
+      case 0:
+        $val = unpack("C", $buff);
+        $val = $val[1];
+        break;
+      case 1:
+        $val = unpack("n", $buff);
+        $val = $val[1];
+        break;
+      case 2:
+        $val = unpack("N", $buff);
+        $val = $val[1];
+        break;
+      case 3:
+        $words = unpack("Nhighword/Nlowword",$buff);
+        //$val = $words['highword'] << 32 | $words['lowword'];
+        $val = self::make64Int($words['highword'],$words['lowword']);
+        break;
+    }
+
+    return new CFNumber($val);
+  }
+
+  /**
+   * Read a real value
+   * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
+   * @return CFNumber The real value
+   * @throws PListException if real val is invalid
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryReal($length) {
+    if($length > 3) throw new PListException("Real greater than 8 bytes: $length");
+
+    $nbytes = 1 << $length;
+    $val = null;
+    if(strlen($buff = substr($this->content,$this->pos, $nbytes)) != $nbytes) throw IOException::readError("");
+    $this->pos += $nbytes;
+
+    switch($length) {
+      case 0: // 1 byte float? must be an error
+      case 1: // 2 byte float? must be an error
+        $x = $length + 1;
+        throw new PListException("got {$x} byte float, must be an error!");
+      case 2:
+        $val = unpack("f", strrev($buff));
+        $val = $val[1];
+        break;
+      case 3:
+        $val = unpack("d", strrev($buff));
+        $val = $val[1];
+        break;
+    }
+
+    return new CFNumber($val);
+  }
+
+  /**
+   * Read a date value
+   * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
+   * @return CFDate The date value
+   * @throws PListException if date val is invalid
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryDate($length) {
+    if($length > 3) throw new PListException("Date greater than 8 bytes: $length");
+
+    $nbytes = 1 << $length;
+    $val = null;
+    if(strlen($buff = substr($this->content, $this->pos, $nbytes)) != $nbytes) throw IOException::readError("");
+    $this->pos += $nbytes;
+
+    switch($length) {
+      case 0: // 1 byte CFDate is an error
+      case 1: // 2 byte CFDate is an error
+        $x = $length + 1;
+        throw new PListException("{$x} byte CFdate, error");
+
+      case 2:
+        $val = unpack("f", strrev($buff));
+        $val = $val[1];
+        break;
+      case 3:
+        $val = unpack("d", strrev($buff));
+        $val = $val[1];
+        break;
+    }
+
+    return new CFDate($val,CFDate::TIMESTAMP_APPLE);
+  }
+
+  /**
+   * Read a data value
+   * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
+   * @return CFData The data value
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryData($length) {
+    if($length == 0) $buff = "";
+    else {
+      $buff = substr($this->content, $this->pos, $length);
+      if(strlen($buff) != $length) throw IOException::readError("");
+      $this->pos += $length;
+    }
+
+    return new CFData($buff,false);
+  }
+
+  /**
+   * Read a string value, usually coded as utf8
+   * @param integer $length The length (in bytes) of the string value
+   * @return CFString The string value, utf8 encoded
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryString($length) {
+    if($length == 0) $buff = "";
+    else {
+      if(strlen($buff = substr($this->content, $this->pos, $length)) != $length) throw IOException::readError("");
+      $this->pos += $length;
+    }
+
+    if(!isset($this->uniqueTable[$buff])) $this->uniqueTable[$buff] = true;
+    return new CFString($buff);
+  }
+
+  /**
+   * Convert the given string from one charset to another.
+   * Trying to use MBString, Iconv, Recode - in that particular order.
+   * @param string $string the string to convert
+   * @param string $fromCharset the charset the given string is currently encoded in
+   * @param string $toCharset the charset to convert to, defaults to UTF-8
+   * @return string the converted string
+   * @throws PListException on neither MBString, Iconv, Recode being available
+   */
+  public static function convertCharset($string, $fromCharset, $toCharset='UTF-8') {
+    if(function_exists('mb_convert_encoding')) return mb_convert_encoding($string, $toCharset, $fromCharset);
+    if(function_exists('iconv')) return iconv($fromCharset, $toCharset, $string);
+    if(function_exists('recode_string')) return recode_string($fromCharset .'..'. $toCharset, $string);
+
+    throw new PListException('neither iconv nor mbstring supported. how are we supposed to work on strings here?');
+  }
+
+  /**
+   * Count characters considering character set
+   * Trying to use MBString, Iconv - in that particular order.
+   * @param string $string the string to convert
+   * @param string $charset the charset the given string is currently encoded in
+   * @return integer The number of characters in that string
+   * @throws PListException on neither MBString, Iconv being available
+   */
+  public static function charsetStrlen($string,$charset="UTF-8") {
+    if(function_exists('mb_strlen')) return mb_strlen($string, $charset);
+    if(function_exists('iconv_strlen')) return iconv_strlen($string,$charset);
+
+    throw new PListException('neither iconv nor mbstring supported. how are we supposed to work on strings here?');
+  }
+
+  /**
+   * Read a unicode string value, coded as UTF-16BE
+   * @param integer $length The length (in bytes) of the string value
+   * @return CFString The string value, utf8 encoded
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryUnicodeString($length) {
+    /* The problem is: we get the length of the string IN CHARACTERS;
+       since a char in UTF-16 can be 16 or 32 bit long, we don't really know
+       how long the string is in bytes */
+    if(strlen($buff = substr($this->content, $this->pos, 2*$length)) != 2*$length) throw IOException::readError("");
+    $this->pos += 2 * $length;
+
+    if(!isset($this->uniqueTable[$buff])) $this->uniqueTable[$buff] = true;
+    return new CFString(self::convertCharset($buff, "UTF-16BE", "UTF-8"));
+  }
+
+  /**
+   * Read an array value, including contained objects
+   * @param integer $length The number of contained objects
+   * @return CFArray The array value, including the objects
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryArray($length) {
+    $ary = new CFArray();
+
+    // first: read object refs
+    if($length != 0) {
+      if(strlen($buff = substr($this->content, $this->pos, $length * $this->objectRefSize)) != $length * $this->objectRefSize) throw IOException::readError("");
+      $this->pos += $length * $this->objectRefSize;
+
+      $objects = self::unpackWithSize($this->objectRefSize, $buff);
+
+      // now: read objects
+      for($i=0;$i<$length;++$i) {
+        $object = $this->readBinaryObjectAt($objects[$i+1]+1);
+        $ary->add($object);
+      }
+    }
+
+    return $ary;
+  }
+
+  /**
+   * Read a dictionary value, including contained objects
+   * @param integer $length The number of contained objects
+   * @return CFDictionary The dictionary value, including the objects
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryDict($length) {
+    $dict = new CFDictionary();
+
+    // first: read keys
+    if($length != 0) {
+      if(strlen($buff = substr($this->content, $this->pos, $length * $this->objectRefSize)) != $length * $this->objectRefSize) throw IOException::readError("");
+      $this->pos += $length * $this->objectRefSize;
+      $keys = self::unpackWithSize($this->objectRefSize, $buff);
+
+      // second: read object refs
+      if(strlen($buff = substr($this->content, $this->pos, $length * $this->objectRefSize)) != $length * $this->objectRefSize) throw IOException::readError("");
+      $this->pos += $length * $this->objectRefSize;
+      $objects = self::unpackWithSize($this->objectRefSize, $buff);
+
+      // read real keys and objects
+      for($i=0;$i<$length;++$i) {
+        $key = $this->readBinaryObjectAt($keys[$i+1]+1);
+        $object = $this->readBinaryObjectAt($objects[$i+1]+1);
+        $dict->add($key->getValue(),$object);
+      }
+    }
+
+    return $dict;
+  }
+
+  /**
+   * Read an object type byte, decode it and delegate to the correct reader function
+   * @return mixed The value of the delegate reader, so any of the CFType subclasses
+   * @throws IOException if read error occurs
+   */
+  function readBinaryObject() {
+    // first: read the marker byte
+    if(strlen($buff = substr($this->content,$this->pos,1)) != 1) throw IOException::readError("");
+    $this->pos++;
+
+    $object_length = unpack("C*", $buff);
+    $object_length = $object_length[1]  & 0xF;
+    $buff = unpack("H*", $buff);
+    $buff = $buff[1];
+
+    $object_type = substr($buff, 0, 1);
+    if($object_type != "0" && $object_length == 15) {
+      $object_length = $this->readBinaryObject($this->objectRefSize);
+      $object_length = $object_length->getValue();
+    }
+
+    $retval = null;
+    switch($object_type) {
+      case '0': // null, false, true, fillbyte
+        $retval = $this->readBinaryNullType($object_length);
+        break;
+      case '1': // integer
+        $retval = $this->readBinaryInt($object_length);
+        break;
+      case '2': // real
+        $retval = $this->readBinaryReal($object_length);
+        break;
+      case '3': // date
+        $retval = $this->readBinaryDate($object_length);
+        break;
+      case '4': // data
+        $retval = $this->readBinaryData($object_length);
+        break;
+      case '5': // byte string, usually utf8 encoded
+        $retval = $this->readBinaryString($object_length);
+        break;
+      case '6': // unicode string (utf16be)
+        $retval = $this->readBinaryUnicodeString($object_length);
+        break;
+      case '8':
+        $num = $this->readBinaryInt($object_length);
+        $retval = new CFUid($num->getValue());
+        break;
+      case 'a': // array
+        $retval = $this->readBinaryArray($object_length);
+        break;
+      case 'd': // dictionary
+        $retval = $this->readBinaryDict($object_length);
+        break;
+    }
+
+    return $retval;
+  }
+
+  /**
+   * Read an object type byte at position $pos, decode it and delegate to the correct reader function
+   * @param integer $pos The table position in the offsets table
+   * @return mixed The value of the delegate reader, so any of the CFType subclasses
+   */
+  function readBinaryObjectAt($pos) {
+    $this->pos = $this->offsets[$pos];
+    return $this->readBinaryObject();
+  }
+
+  /**
+   * Parse a binary plist string
+   * @return void
+   * @throws IOException if read error occurs
+   */
+  public function parseBinaryString() {
+    $this->uniqueTable = Array();
+    $this->countObjects = 0;
+    $this->stringSize = 0;
+    $this->intSize = 0;
+    $this->miscSize = 0;
+    $this->objectRefs = 0;
+
+    $this->writtenObjectCount = 0;
+    $this->objectTable = Array();
+    $this->objectRefSize = 0;
+
+    $this->offsets = Array();
+
+    // first, we read the trailer: 32 byte from the end
+    $buff = substr($this->content,-32);
+
+    if(strlen($buff) < 32) {
+      throw new PListException('Error in PList format: content is less than at least necessary 32 bytes!');
+    }
+
+    $infos = unpack("x6/Coffset_size/Cobject_ref_size/x4/Nnumber_of_objects/x4/Ntop_object/x4/Ntable_offset",$buff);
+
+    // after that, get the offset table
+    $coded_offset_table = substr($this->content,$infos['table_offset'],$infos['number_of_objects'] * $infos['offset_size']);
+    if(strlen($coded_offset_table) != $infos['number_of_objects'] * $infos['offset_size']) throw IOException::readError("");
+    $this->countObjects = $infos['number_of_objects'];
+
+    // decode offset table
+    $formats = Array("","C*","n*",NULL,"N*");
+    if($infos['offset_size'] == 3) { # since PHP does not support parenthesis in pack/unpack expressions,
+                                     # "(H6)*" does not work and we have to work round this by repeating the
+                                     # expression as often as it fits in the string
+      $this->offsets = array(NULL);
+      while($coded_offset_table) {
+        $str = unpack("H6",$coded_offset_table);
+        $this->offsets[] = hexdec($str[1]);
+        $coded_offset_table = substr($coded_offset_table,3);
+      }
+    }
+    else $this->offsets = unpack($formats[$infos['offset_size']],$coded_offset_table);
+
+    $this->uniqueTable = Array();
+    $this->objectRefSize = $infos['object_ref_size'];
+
+    $top = $this->readBinaryObjectAt($infos['top_object']+1);
+    $this->add($top);
+  }
+
+  /**
+   * Read a binary plist stream
+   * @param resource $stream The stream to read
+   * @return void
+   * @throws IOException if read error occurs
+   */
+  function readBinaryStream($stream) {
+    if(($str = stream_get_contents($stream)) === false || empty($str)) {
+      throw new PListException("Error reading stream!");
+    }
+
+    $this->parseBinary($str);
+  }
+
+  /**
+   * parse a binary plist string
+   * @param string $content The stream to read, defaults to {@link $this->content}
+   * @return void
+   * @throws IOException if read error occurs
+   */
+  function parseBinary($content=NULL) {
+    if($content !== NULL) {
+      $this->content = $content;
+    }
+
+    if(empty($this->content)) {
+      throw new PListException("Content may not be empty!");
+    }
+
+    if(substr($this->content,0,8) != 'bplist00') {
+      throw new PListException("Invalid binary string!");
+    }
+
+    $this->pos = 0;
+
+    $this->parseBinaryString();
+  }
+
+  /**
+   * Read a binary plist file
+   * @param string $file The file to read
+   * @return void
+   * @throws IOException if read error occurs
+   */
+  function readBinary($file) {
+    if(!($fd = fopen($file,"rb"))) {
+      throw new IOException("Could not open file {$file}!");
+    }
+
+    $this->readBinaryStream($fd);
+    fclose($fd);
+  }
+
+  /**
+   * calculate the bytes needed for a size integer value
+   * @param integer $int The integer value to calculate
+   * @return integer The number of bytes needed
+   */
+  public static function bytesSizeInt($int) {
+    $nbytes = 0;
+
+    if($int > 0xE) $nbytes += 2; // 2 size-bytes
+    if($int > 0xFF) $nbytes += 1; // 3 size-bytes
+    if($int > 0xFFFF) $nbytes += 2; // 5 size-bytes
+
+    return $nbytes;
+  }
+
+  /**
+   * Calculate the byte needed for a „normal” integer value
+   * @param integer $int The integer value
+   * @return integer The number of bytes needed + 1 (because of the „marker byte”)
+   */
+  public static function bytesInt($int) {
+    $nbytes = 1;
+
+    if($int > 0xFF) $nbytes += 1; // 2 byte integer
+    if($int > 0xFFFF) $nbytes += 2; // 4 byte integer
+    if($int > 0xFFFFFFFF) $nbytes += 4; // 8 byte integer
+    if($int < 0) $nbytes += 7; // 8 byte integer (since it is signed)
+
+    return $nbytes + 1; // one „marker” byte
+  }
+
+  /**
+   * „pack” a value (i.e. write the binary representation as big endian to a string) with the specified size
+   * @param integer $nbytes The number of bytes to pack
+   * @param integer $int The integer value to pack
+   * @return string The packed value as string
+   */
+  public static function packItWithSize($nbytes,$int) {
+    $formats = Array("C", "n", "N", "N");
+    $format = $formats[$nbytes-1];
+
+    if($nbytes == 3) return substr(pack($format, $int), -3);
+    return pack($format, $int);
+  }
+
+  /**
+   * „unpack” multiple values of the specified size (i.e. get the integers from their binary representation) from a string
+   * @param integer $nbytes The number of bytes of each value to unpack
+   * @param integer $buff The string packed with integer values
+   * @return array The unpacked integers
+   */
+  public static function unpackWithSize($nbytes,$buff) {
+    $formats = Array("C*", "n*", "N*", "N*");
+    $format = $formats[$nbytes-1];
+
+    if($nbytes == 3) $buff = "\0" . implode("\0", str_split($buff, 3));
+    return unpack($format, $buff);
+  }
+
+  /**
+   * Calculate the bytes needed to save the number of objects
+   * @param integer $count_objects The number of objects
+   * @return integer The number of bytes
+   */
+  public static function bytesNeeded($count_objects) {
+    $nbytes = 0;
+
+    while($count_objects >= 1) {
+      $nbytes++;
+      $count_objects /= 256;
+    }
+
+    return $nbytes;
+  }
+
+  /**
+   * Code an integer to byte representation
+   * @param integer $int The integer value
+   * @return string The packed byte value
+   */
+  public static function intBytes($int) {
+    $intbytes = "";
+
+    if($int > 0xFFFF) $intbytes = "\x12".pack("N", $int); // 4 byte integer
+    elseif($int > 0xFF) $intbytes = "\x11".pack("n", $int); // 2 byte integer
+    else $intbytes = "\x10".pack("C", $int); // 8 byte integer
+
+    return $intbytes;
+  }
+
+  /**
+   * Code an type byte, consisting of the type marker and the length of the type
+   * @param string $type The type byte value (i.e. "d" for dictionaries)
+   * @param integer $type_len The length of the type
+   * @return string The packed type byte value
+   */
+  public static function typeBytes($type,$type_len) {
+    $optional_int = "";
+
+    if($type_len < 15) $type .= sprintf("%x", $type_len);
+    else {
+      $type .= "f";
+      $optional_int = self::intBytes($type_len);
+    }
+
+    return pack("H*", $type).$optional_int;
+  }
+
+  /**
+   * Count number of objects and create a unique table for strings
+   * @param $value The value to count and unique
+   * @return void
+   */
+  protected function uniqueAndCountValues($value) {
+    // no uniquing for other types than CFString and CFData
+    if($value instanceof CFNumber) {
+      $val = $value->getValue();
+      if(intval($val) == $val && !is_float($val) && strpos($val,'.') === false) $this->intSize += self::bytesInt($val);
+      else $this->miscSize += 9; // 9 bytes (8 + marker byte) for real
+      $this->countObjects++;
+      return;
+    }
+    elseif($value instanceof CFDate) {
+      $this->miscSize += 9; // since date in plist is real, we need 9 byte (8 + marker byte)
+      $this->countObjects++;
+      return;
+    }
+    elseif($value instanceof CFBoolean) {
+      $this->countObjects++;
+      $this->miscSize += 1;
+      return;
+    }
+    elseif($value instanceof CFArray) {
+      $cnt = 0;
+      foreach($value as $v) {
+        ++$cnt;
+        $this->uniqueAndCountValues($v);
+        $this->objectRefs++; // each array member is a ref
+      }
+
+      $this->countObjects++;
+      $this->intSize += self::bytesSizeInt($cnt);
+      $this->miscSize++; // marker byte for array
+      return;
+    }
+    elseif($value instanceof CFDictionary) {
+      $cnt = 0;
+      foreach($value as $k => $v) {
+        ++$cnt;
+        if(!isset($this->uniqueTable[$k])) {
+          $this->uniqueTable[$k] = 0;
+          $len = self::binaryStrlen($k);
+          $this->stringSize += $len + 1;
+          $this->intSize += self::bytesSizeInt(self::charsetStrlen($k,'UTF-8'));
+        }
+
+        $this->objectRefs += 2; // both, key and value, are refs
+        $this->uniqueTable[$k]++;
+        $this->uniqueAndCountValues($v);
+      }
+
+      $this->countObjects++;
+      $this->miscSize++; // marker byte for dict
+      $this->intSize += self::bytesSizeInt($cnt);
+      return;
+    }
+    elseif($value instanceOf CFData) {
+      $val = $value->getValue();
+      $len = strlen($val);
+      $this->intSize += self::bytesSizeInt($len);
+      $this->miscSize += $len + 1;
+      $this->countObjects++;
+      return;
+    }
+    else $val = $value->getValue();
+
+    if(!isset($this->uniqueTable[$val])) {
+      $this->uniqueTable[$val] = 0;
+      $len = self::binaryStrlen($val);
+      $this->stringSize += $len + 1;
+      $this->intSize += self::bytesSizeInt(self::charsetStrlen($val,'UTF-8'));
+    }
+    $this->uniqueTable[$val]++;
+  }
+
+  /**
+   * Convert CFPropertyList to binary format; since we have to count our objects we simply unique CFDictionary and CFArray
+   * @return string The binary plist content
+   */
+  public function toBinary() {
+    $this->uniqueTable = Array();
+    $this->countObjects = 0;
+    $this->stringSize = 0;
+    $this->intSize = 0;
+    $this->miscSize = 0;
+    $this->objectRefs = 0;
+
+    $this->writtenObjectCount = 0;
+    $this->objectTable = Array();
+    $this->objectRefSize = 0;
+
+    $this->offsets = Array();
+
+    $binary_str = "bplist00";
+    $value = $this->getValue(true);
+    $this->uniqueAndCountValues($value);
+
+    $this->countObjects += count($this->uniqueTable);
+    $this->objectRefSize = self::bytesNeeded($this->countObjects);
+    $file_size = $this->stringSize + $this->intSize + $this->miscSize + $this->objectRefs * $this->objectRefSize + 40;
+    $offset_size = self::bytesNeeded($file_size);
+    $table_offset = $file_size - 32;
+
+    $this->objectTable = Array();
+    $this->writtenObjectCount = 0;
+    $this->uniqueTable = Array(); // we needed it to calculate several values
+    $value->toBinary($this);
+
+    $object_offset = 8;
+    $offsets = Array();
+
+    for($i=0;$i<count($this->objectTable);++$i) {
+      $binary_str .= $this->objectTable[$i];
+      $offsets[$i] = $object_offset;
+      $object_offset += strlen($this->objectTable[$i]);
+    }
+
+    for($i=0;$i<count($offsets);++$i) {
+      $binary_str .= self::packItWithSize($offset_size, $offsets[$i]);
+    }
+
+
+    $binary_str .= pack("x6CC", $offset_size, $this->objectRefSize);
+    $binary_str .= pack("x4N", $this->countObjects);
+    $binary_str .= pack("x4N", 0);
+    $binary_str .= pack("x4N", $table_offset);
+
+    return $binary_str;
+  }
+
+  /**
+   * Counts the number of bytes the string will have when coded; utf-16be if non-ascii characters are present.
+   * @param string $val The string value
+   * @return integer The length of the coded string in bytes
+   */
+  protected static function binaryStrlen($val) {
+    for($i=0;$i<strlen($val);++$i) {
+      if(ord($val{$i}) >= 128) {
+        $val = self::convertCharset($val, 'UTF-8', 'UTF-16BE');
+        return strlen($val);
+      }
+    }
+
+    return strlen($val);
+  }
+
+  /**
+   * Uniques and transforms a string value to binary format and adds it to the object table
+   * @param string $val The string value
+   * @return integer The position in the object table
+   */
+  public function stringToBinary($val) {
+    $saved_object_count = -1;
+
+    if(!isset($this->uniqueTable[$val])) {
+      $saved_object_count = $this->writtenObjectCount++;
+      $this->uniqueTable[$val] = $saved_object_count;
+      $utf16 = false;
+
+      for($i=0;$i<strlen($val);++$i) {
+        if(ord($val{$i}) >= 128) {
+          $utf16 = true;
+          break;
+        }
+      }
+
+      if($utf16) {
+        $bdata = self::typeBytes("6", mb_strlen($val,'UTF-8')); // 6 is 0110, unicode string (utf16be)
+        $val = self::convertCharset($val, 'UTF-8', 'UTF-16BE');
+        $this->objectTable[$saved_object_count] = $bdata.$val;
+      }
+      else {
+        $bdata = self::typeBytes("5", strlen($val)); // 5 is 0101 which is an ASCII string (seems to be ASCII encoded)
+        $this->objectTable[$saved_object_count] = $bdata.$val;
+      }
+    }
+    else $saved_object_count = $this->uniqueTable[$val];
+
+    return $saved_object_count;
+  }
+
+  /**
+   * Codes an integer to binary format
+   * @param integer $value The integer value
+   * @return string the coded integer
+   */
+  protected function intToBinary($value) {
+    $nbytes = 0;
+    if($value > 0xFF) $nbytes = 1; // 1 byte integer
+    if($value > 0xFFFF) $nbytes += 1; // 4 byte integer
+    if($value > 0xFFFFFFFF) $nbytes += 1; // 8 byte integer
+    if($value < 0) $nbytes = 3; // 8 byte integer, since signed
+
+    $bdata = self::typeBytes("1", $nbytes); // 1 is 0001, type indicator for integer
+    $buff = "";
+
+    if($nbytes < 3) {
+      if($nbytes == 0) $fmt = "C";
+      elseif($nbytes == 1) $fmt = "n";
+      else $fmt = "N";
+
+      $buff = pack($fmt, $value);
+    }
+    else {
+      if(PHP_INT_SIZE > 4) {
+        // 64 bit signed integer; we need the higher and the lower 32 bit of the value
+        $high_word = $value >> 32;
+        $low_word = $value & 0xFFFFFFFF;
+      }
+      else {
+        // since PHP can only handle 32bit signed, we can only get 32bit signed values at this point - values above 0x7FFFFFFF are
+        // floats. So we ignore the existance of 64bit on non-64bit-machines
+        if($value < 0) $high_word = 0xFFFFFFFF;
+        else $high_word = 0;
+        $low_word = $value;
+      }
+      $buff = pack("N", $high_word).pack("N", $low_word);
+    }
+
+    return $bdata.$buff;
+  }
+
+  /**
+   * Codes a real value to binary format
+   * @param float $val The real value
+   * @return string The coded real
+   */
+  protected function realToBinary($val) {
+    $bdata = self::typeBytes("2", 3); // 2 is 0010, type indicator for reals
+    return $bdata.strrev(pack("d", (float)$val));
+  }
+
+  public function uidToBinary($value) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $val = "";
+
+    $nbytes = 0;
+    if($value > 0xFF) $nbytes = 1; // 1 byte integer
+    if($value > 0xFFFF) $nbytes += 1; // 4 byte integer
+    if($value > 0xFFFFFFFF) $nbytes += 1; // 8 byte integer
+    if($value < 0) $nbytes = 3; // 8 byte integer, since signed
+
+    $bdata = self::typeBytes("1000", $nbytes); // 1 is 0001, type indicator for integer
+    $buff = "";
+
+    if($nbytes < 3) {
+      if($nbytes == 0) $fmt = "C";
+      elseif($nbytes == 1) $fmt = "n";
+      else $fmt = "N";
+
+      $buff = pack($fmt, $value);
+    }
+    else {
+      if(PHP_INT_SIZE > 4) {
+        // 64 bit signed integer; we need the higher and the lower 32 bit of the value
+        $high_word = $value >> 32;
+        $low_word = $value & 0xFFFFFFFF;
+      }
+      else {
+        // since PHP can only handle 32bit signed, we can only get 32bit signed values at this point - values above 0x7FFFFFFF are
+        // floats. So we ignore the existance of 64bit on non-64bit-machines
+        if($value < 0) $high_word = 0xFFFFFFFF;
+        else $high_word = 0;
+        $low_word = $value;
+      }
+      $buff = pack("N", $high_word).pack("N", $low_word);
+    }
+
+    $val = $bdata.$buff;
+
+    $this->objectTable[$saved_object_count] = $val;
+    return $saved_object_count;
+  }
+
+  /**
+   * Converts a numeric value to binary and adds it to the object table
+   * @param numeric $value The numeric value
+   * @return integer The position in the object table
+   */
+  public function numToBinary($value) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $val = "";
+    if(intval($value) == $value && !is_float($value) && strpos($value,'.') === false) $val = $this->intToBinary($value);
+    else $val = $this->realToBinary($value);
+
+    $this->objectTable[$saved_object_count] = $val;
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert date value (apple format) to binary and adds it to the object table
+   * @param integer $value The date value
+   * @return integer The position of the coded value in the object table
+   */
+  public function dateToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $hour = gmdate("H",$val);
+    $min = gmdate("i",$val);
+    $sec = gmdate("s",$val);
+    $mday = gmdate("j",$val);
+    $mon = gmdate("n",$val);
+    $year = gmdate("Y",$val);
+
+    $val = gmmktime($hour,$min,$sec,$mon,$mday,$year) - CFDate::DATE_DIFF_APPLE_UNIX; // CFDate is a real, number of seconds since 01/01/2001 00:00:00 GMT
+
+    $bdata = self::typeBytes("3", 3); // 3 is 0011, type indicator for date
+    $this->objectTable[$saved_object_count] = $bdata.strrev(pack("d", $val));
+
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert a bool value to binary and add it to the object table
+   * @param bool $val The boolean value
+   * @return integer The position in the object table
+   */
+  public function boolToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+    $this->objectTable[$saved_object_count] = $val ? "\x9" : "\x8"; // 0x9 is 1001, type indicator for true; 0x8 is 1000, type indicator for false
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert data value to binary format and add it to the object table
+   * @param string $val The data value
+   * @return integer The position in the object table
+   */
+  public function dataToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $bdata = self::typeBytes("4", strlen($val)); // a is 1000, type indicator for data
+    $this->objectTable[$saved_object_count] = $bdata.$val;
+
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert array to binary format and add it to the object table
+   * @param CFArray $val The array to convert
+   * @return integer The position in the object table
+   */
+  public function arrayToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $bdata = self::typeBytes("a", count($val->getValue())); // a is 1010, type indicator for arrays
+
+    foreach($val as $v) {
+      $bval = $v->toBinary($this);
+      $bdata .= self::packItWithSize($this->objectRefSize, $bval);
+    }
+
+    $this->objectTable[$saved_object_count] = $bdata;
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert dictionary to binary format and add it to the object table
+   * @param CFDictionary $val The dict to convert
+   * @return integer The position in the object table
+   */
+  public function dictToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+    $bdata = self::typeBytes("d", count($val->getValue())); // d=1101, type indicator for dictionary
+
+    foreach($val as $k => $v) {
+      $str = new CFString($k);
+      $key = $str->toBinary($this);
+      $bdata .= self::packItWithSize($this->objectRefSize, $key);
+    }
+
+    foreach($val as $k => $v) {
+      $bval = $v->toBinary($this);
+      $bdata .= self::packItWithSize($this->objectRefSize, $bval);
+    }
+
+    $this->objectTable[$saved_object_count] = $bdata;
+    return $saved_object_count;
+  }
+
+}
+
+# eof
diff --git a/lib/plist/classes/CFPropertyList/CFPropertyList.php b/lib/plist/classes/CFPropertyList/CFPropertyList.php
new file mode 100644 (file)
index 0000000..0e07b7c
--- /dev/null
@@ -0,0 +1,608 @@
+<?php
+/**
+ * CFPropertyList
+ * {@link http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html Property Lists}
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @version $Id$
+ * @example example-read-01.php Read an XML PropertyList
+ * @example example-read-02.php Read a Binary PropertyList
+ * @example example-read-03.php Read a PropertyList without knowing the type
+ * @example example-create-01.php Using the CFPropertyList API
+ * @example example-create-02.php Using {@link CFTypeDetector}
+ * @example example-create-03.php Using {@link CFTypeDetector} with {@link CFDate} and {@link CFData}
+ * @example example-modify-01.php Read, modify and save a PropertyList
+ */
+
+namespace CFPropertyList;
+use \Iterator, \DOMDocument, \DOMException, DOMImplementation, DOMNode;
+
+/**
+ * Require IOException, PListException, CFType and CFBinaryPropertyList
+ */
+require_once(__DIR__.'/IOException.php');
+require_once(__DIR__.'/PListException.php');
+require_once(__DIR__.'/CFType.php');
+require_once(__DIR__.'/CFBinaryPropertyList.php');
+require_once(__DIR__.'/CFTypeDetector.php');
+
+/**
+ * Property List
+ * Interface for handling reading, editing and saving Property Lists as defined by Apple.
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @example example-read-01.php Read an XML PropertyList
+ * @example example-read-02.php Read a Binary PropertyList
+ * @example example-read-03.php Read a PropertyList without knowing the type
+ * @example example-create-01.php Using the CFPropertyList API
+ * @example example-create-02.php Using {@link CFTypeDetector}
+ * @example example-create-03.php Using {@link CFTypeDetector} with {@link CFDate} and {@link CFData}
+ * @example example-create-04.php Using and extended {@link CFTypeDetector}
+ */
+class CFPropertyList extends CFBinaryPropertyList implements Iterator {
+  /**
+   * Format constant for binary format
+   * @var integer
+   */
+  const FORMAT_BINARY = 1;
+
+  /**
+   * Format constant for xml format
+   * @var integer
+   */
+  const FORMAT_XML = 2;
+
+  /**
+   * Format constant for automatic format recognizing
+   * @var integer
+   */
+  const FORMAT_AUTO = 0;
+
+  /**
+   * Path of PropertyList
+   * @var string
+   */
+  protected $file = null;
+  
+  /**
+   * Detected format of PropertyList
+   * @var integer
+   */
+  protected $detectedFormat = null;
+
+  /**
+   * Path of PropertyList
+   * @var integer
+   */
+  protected $format = null;
+
+  /**
+   * CFType nodes
+   * @var array
+   */
+  protected $value = array();
+
+  /**
+   * Position of iterator {@link http://php.net/manual/en/class.iterator.php}
+   * @var integer
+   */
+  protected $iteratorPosition = 0;
+
+  /**
+   * List of Keys for numerical iterator access {@link http://php.net/manual/en/class.iterator.php}
+   * @var array
+   */
+  protected $iteratorKeys = null;
+
+  /**
+   * List of NodeNames to ClassNames for resolving plist-files
+   * @var array
+   */
+  protected static $types = array(
+    'string'  => 'CFString',
+    'real'    => 'CFNumber',
+    'integer' => 'CFNumber',
+    'date'    => 'CFDate',
+    'true'    => 'CFBoolean',
+    'false'   => 'CFBoolean',
+    'data'    => 'CFData',
+    'array'   => 'CFArray',
+    'dict'    => 'CFDictionary'
+ );
+
+
+  /**
+   * Create new CFPropertyList.
+   * If a path to a PropertyList is specified, it is loaded automatically.
+   * @param string $file Path of PropertyList
+   * @param integer $format he format of the property list, see {@link FORMAT_XML}, {@link FORMAT_BINARY} and {@link FORMAT_AUTO}, defaults to {@link FORMAT_AUTO}
+   * @throws IOException if file could not be read by {@link load()}
+   * @uses $file for storing the current file, if specified
+   * @uses load() for loading the plist-file
+   */
+  public function __construct($file=null,$format=self::FORMAT_AUTO) {
+    $this->file = $file;
+    $this->format = $format;
+    $this->detectedFormat = $format;
+    if($this->file) $this->load();
+  }
+
+  /**
+   * Load an XML PropertyList.
+   * @param string $file Path of PropertyList, defaults to {@link $file}
+   * @return void
+   * @throws IOException if file could not be read
+   * @throws DOMException if XML-file could not be read properly
+   * @uses load() to actually load the file
+   */
+  public function loadXML($file=null) {
+    $this->load($file,CFPropertyList::FORMAT_XML);
+  }
+
+  /**
+   * Load an XML PropertyList.
+   * @param resource $stream A stream containing the xml document.
+   * @return void
+   * @throws IOException if stream could not be read
+   * @throws DOMException if XML-stream could not be read properly
+   */
+  public function loadXMLStream($stream) {
+    if(($contents = stream_get_contents($stream)) === FALSE) throw IOException::notReadable('<stream>');
+    $this->parse($contents,CFPropertyList::FORMAT_XML);
+  }
+
+  /**
+   * Load an binary PropertyList.
+   * @param string $file Path of PropertyList, defaults to {@link $file}
+   * @return void
+   * @throws IOException if file could not be read
+   * @throws PListException if binary plist-file could not be read properly
+   * @uses load() to actually load the file
+   */
+  public function loadBinary($file=null) {
+    $this->load($file,CFPropertyList::FORMAT_BINARY);
+  }
+
+  /**
+   * Load an binary PropertyList.
+   * @param stream $stream Stream containing the PropertyList
+   * @return void
+   * @throws IOException if file could not be read
+   * @throws PListException if binary plist-file could not be read properly
+   * @uses parse() to actually load the file
+   */
+  public function loadBinaryStream($stream) {
+    if(($contents = stream_get_contents($stream)) === FALSE) throw IOException::notReadable('<stream>');
+    $this->parse($contents,CFPropertyList::FORMAT_BINARY);
+  }
+
+  /**
+   * Load a plist file.
+   * Load and import a plist file.
+   * @param string $file Path of PropertyList, defaults to {@link $file}
+   * @param integer $format The format of the property list, see {@link FORMAT_XML}, {@link FORMAT_BINARY} and {@link FORMAT_AUTO}, defaults to {@link $format}
+   * @return void
+   * @throws PListException if file format version is not 00
+   * @throws IOException if file could not be read
+   * @throws DOMException if plist file could not be parsed properly
+   * @uses $file if argument $file was not specified
+   * @uses $value reset to empty array
+   * @uses import() for importing the values
+   */
+  public function load($file=null,$format=null) {
+    $file = $file ? $file : $this->file;
+    $format = $format !== null ? $format : $this->format;
+    $this->value = array();
+
+    if(!is_readable($file)) throw IOException::notReadable($file);
+
+    switch($format) {
+      case CFPropertyList::FORMAT_BINARY:
+        $this->readBinary($file);
+        break;
+      case CFPropertyList::FORMAT_AUTO: // what we now do is ugly, but neccessary to recognize the file format
+        $fd = fopen($file,"rb");
+        if(($magic_number = fread($fd,8)) === false) throw IOException::notReadable($file);
+        fclose($fd);
+
+        $filetype = substr($magic_number,0,6);
+        $version  = substr($magic_number,-2);
+
+        if($filetype == "bplist") {
+          if($version != "00") throw new PListException("Wrong file format version! Expected 00, got $version!");
+          $this->detectedFormat = CFPropertyList::FORMAT_BINARY;
+          $this->readBinary($file);
+          break;
+        }
+        $this->detectedFormat = CFPropertyList::FORMAT_XML;
+        // else: xml format, break not neccessary
+      case CFPropertyList::FORMAT_XML:
+        $doc = new DOMDocument();
+        if(!$doc->load($file)) throw new DOMException();
+        $this->import($doc->documentElement, $this);
+        break;
+    }
+  }
+
+  /**
+   * Parse a plist string.
+   * Parse and import a plist string.
+   * @param string $str String containing the PropertyList, defaults to {@link $content}
+   * @param integer $format The format of the property list, see {@link FORMAT_XML}, {@link FORMAT_BINARY} and {@link FORMAT_AUTO}, defaults to {@link $format}
+   * @return void
+   * @throws PListException if file format version is not 00
+   * @throws IOException if file could not be read
+   * @throws DOMException if plist file could not be parsed properly
+   * @uses $content if argument $str was not specified
+   * @uses $value reset to empty array
+   * @uses import() for importing the values
+   */
+  public function parse($str=NULL,$format=NULL) {
+    $format = $format !== null ? $format : $this->format;
+    $str = $str !== null ? $str : $this->content;
+    $this->value = array();
+
+    switch($format) {
+      case CFPropertyList::FORMAT_BINARY:
+        $this->parseBinary($str);
+        break;
+      case CFPropertyList::FORMAT_AUTO: // what we now do is ugly, but neccessary to recognize the file format
+        if(($magic_number = substr($str,0,8)) === false) throw IOException::notReadable("<string>");
+
+        $filetype = substr($magic_number,0,6);
+        $version  = substr($magic_number,-2);
+
+        if($filetype == "bplist") {
+          if($version != "00") throw new PListException("Wrong file format version! Expected 00, got $version!");
+          $this->detectedFormat = CFPropertyList::FORMAT_BINARY;
+          $this->parseBinary($str);
+          break;
+        }
+        $this->detectedFormat = CFPropertyList::FORMAT_XML;
+        // else: xml format, break not neccessary
+      case CFPropertyList::FORMAT_XML:
+        $doc = new DOMDocument();
+        if(!$doc->loadXML($str)) throw new DOMException();
+        $this->import($doc->documentElement, $this);
+        break;
+    }
+  }
+
+  /**
+   * Convert a DOMNode into a CFType.
+   * @param DOMNode $node Node to import children of
+   * @param CFDictionary|CFArray|CFPropertyList $parent
+   * @return void
+   */
+  protected function import(DOMNode $node, $parent) {
+    // abort if there are no children
+    if(!$node->childNodes->length) return;
+
+    foreach($node->childNodes as $n) {
+      // skip if we can't handle the element
+      if(!isset(self::$types[$n->nodeName])) continue;
+
+      $class = 'CFPropertyList\\'.self::$types[$n->nodeName];
+      $key = null;
+
+      // find previous <key> if possible
+      $ps = $n->previousSibling;
+      while($ps && $ps->nodeName == '#text' && $ps->previousSibling) $ps = $ps->previousSibling;
+
+      // read <key> if possible
+      if($ps && $ps->nodeName == 'key') $key = $ps->firstChild->nodeValue;
+
+      switch($n->nodeName) {
+        case 'date':
+          $value = new $class(CFDate::dateValue($n->nodeValue));
+          break;
+        case 'data':
+          $value = new $class($n->nodeValue,true);
+          break;
+        case 'string':
+          $value = new $class($n->nodeValue);
+          break;
+
+        case 'real':
+        case 'integer':
+          $value = new $class($n->nodeName == 'real' ? floatval($n->nodeValue) : intval($n->nodeValue));
+          break;
+
+        case 'true':
+        case 'false':
+          $value = new $class($n->nodeName == 'true');
+          break;
+
+        case 'array':
+        case 'dict':
+          $value = new $class();
+          $this->import($n, $value);
+
+          if($value instanceof CFDictionary) {
+            $hsh = $value->getValue();
+            if(isset($hsh['CF$UID']) && count($hsh) == 1) {
+              $value = new CFUid($hsh['CF$UID']->getValue());
+            }
+          }
+
+          break;
+      }
+
+      // Dictionaries need a key
+      if($parent instanceof CFDictionary) $parent->add($key, $value);
+      // others don't
+      else $parent->add($value);
+    }
+  }
+
+  /**
+   * Convert CFPropertyList to XML and save to file.
+   * @param string $file Path of PropertyList, defaults to {@link $file}
+   * @return void
+   * @throws IOException if file could not be read
+   * @uses $file if $file was not specified
+   */
+  public function saveXML($file) {
+    $this->save($file,CFPropertyList::FORMAT_XML);
+  }
+
+  /**
+   * Convert CFPropertyList to binary format (bplist00) and save to file.
+   * @param string $file Path of PropertyList, defaults to {@link $file}
+   * @return void
+   * @throws IOException if file could not be read
+   * @uses $file if $file was not specified
+   */
+  public function saveBinary($file) {
+    $this->save($file,CFPropertyList::FORMAT_BINARY);
+  }
+
+  /**
+   * Convert CFPropertyList to XML or binary and save to file.
+   * @param string $file Path of PropertyList, defaults to {@link $file}
+   * @param string $format Format of PropertyList, defaults to {@link $format}
+   * @return void
+   * @throws IOException if file could not be read
+   * @throws PListException if evaluated $format is neither {@link FORMAT_XML} nor {@link FORMAL_BINARY}
+   * @uses $file if $file was not specified
+   * @uses $format if $format was not specified
+   */
+  public function save($file=null,$format=null) {
+    $file = $file ? $file : $this->file;
+    $format = $format ? $format : $this->format;
+    if($format == self::FORMAT_AUTO)$format = $this->detectedFormat;
+
+    if( !in_array( $format, array( self::FORMAT_BINARY, self::FORMAT_XML ) ) )
+      throw new PListException( "format {$format} is not supported, use CFPropertyList::FORMAT_BINARY or CFPropertyList::FORMAT_XML" );
+
+    if(!file_exists($file)) {
+      // dirname("file.xml") == "" and is treated as the current working directory
+      if(!is_writable(dirname($file))) throw IOException::notWritable($file);
+    }
+    else if(!is_writable($file)) throw IOException::notWritable($file);
+
+    $content = $format == self::FORMAT_BINARY ? $this->toBinary() : $this->toXML();
+
+    $fh = fopen($file, 'wb');
+    fwrite($fh,$content);
+    fclose($fh);
+  }
+
+  /**
+   * Convert CFPropertyList to XML
+   * @param bool $formatted Print plist formatted (i.e. with newlines and whitespace indention) if true; defaults to false
+   * @return string The XML content
+   */
+  public function toXML($formatted=false) {
+    $domimpl = new DOMImplementation();
+    // <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+    $dtd = $domimpl->createDocumentType('plist', '-//Apple//DTD PLIST 1.0//EN', 'http://www.apple.com/DTDs/PropertyList-1.0.dtd');
+    $doc = $domimpl->createDocument(null, "plist", $dtd);
+    $doc->encoding = "UTF-8";
+
+    // format output
+    if($formatted) {
+      $doc->formatOutput = true;
+      $doc->preserveWhiteSpace = true;
+    }
+
+    // get documentElement and set attribs
+    $plist = $doc->documentElement;
+    $plist->setAttribute('version', '1.0');
+
+    // add PropertyList's children
+    $plist->appendChild($this->getValue(true)->toXML($doc));
+
+    return $doc->saveXML();
+  }
+
+
+  /************************************************************************************************
+   *    M A N I P U L A T I O N
+   ************************************************************************************************/
+
+  /**
+   * Add CFType to collection.
+   * @param CFType $value CFType to add to collection
+   * @return void
+   * @uses $value for adding $value
+   */
+  public function add(CFType $value=null) {
+    // anything but CFType is null, null is an empty string - sad but true
+    if( !$value )
+      $value = new CFString();
+
+    $this->value[] = $value;
+  }
+
+  /**
+   * Get CFType from collection.
+   * @param integer $key Key of CFType to retrieve from collection
+   * @return CFType CFType found at $key, null else
+   * @uses $value for retrieving CFType of $key
+   */
+  public function get($key) {
+    if(isset($this->value[$key])) return $this->value[$key];
+    return null;
+  }
+
+  /**
+   * Generic getter (magic)
+   *
+   * @param integer $key Key of CFType to retrieve from collection
+   * @return CFType CFType found at $key, null else
+   * @author Sean Coates <sean@php.net>
+   * @link http://php.net/oop5.overloading
+   */
+  public function __get($key) {
+    return $this->get($key);
+  }
+
+  /**
+   * Remove CFType from collection.
+   * @param integer $key Key of CFType to removes from collection
+   * @return CFType removed CFType, null else
+   * @uses $value for removing CFType of $key
+   */
+  public function del($key) {
+    if(isset($this->value[$key])) {
+      $t = $this->value[$key];
+      unset($this->value[$key]);
+      return $t;
+    }
+
+    return null;
+  }
+
+  /**
+   * Empty the collection
+   * @return array the removed CFTypes
+   * @uses $value for removing CFType of $key
+   */
+  public function purge() {
+    $t = $this->value;
+    $this->value = array();
+    return $t;
+  }
+
+  /**
+   * Get first (and only) child, or complete collection.
+   * @param string $cftype if set to true returned value will be CFArray instead of an array in case of a collection
+   * @return CFType|array CFType or list of CFTypes known to the PropertyList
+   * @uses $value for retrieving CFTypes
+   */
+  public function getValue($cftype=false) {
+    if(count($this->value) === 1) {
+      $t = array_values( $this->value );
+      return $t[0];
+       }
+    if($cftype) {
+      $t = new CFArray();
+      foreach( $this->value as $value ) {
+        if( $value instanceof CFType ) $t->add($value);
+      }
+      return $t;
+    }
+    return $this->value;
+  }
+
+  /**
+   * Create CFType-structure from guessing the data-types.
+   * The functionality has been moved to the more flexible {@link CFTypeDetector} facility.
+   * @param mixed $value Value to convert to CFType
+   * @param array $options Configuration for casting values [autoDictionary, suppressExceptions, objectToArrayMethod, castNumericStrings]
+   * @return CFType CFType based on guessed type
+   * @uses CFTypeDetector for actual type detection
+   * @deprecated
+   */
+  public static function guess($value, $options=array()) {
+    static $t = null;
+    if( $t === null )
+      $t = new CFTypeDetector( $options );
+
+    return $t->toCFType( $value );
+  }
+
+
+  /************************************************************************************************
+   *    S E R I A L I Z I N G
+   ************************************************************************************************/
+
+  /**
+   * Get PropertyList as array.
+   * @return mixed primitive value of first (and only) CFType, or array of primitive values of collection
+   * @uses $value for retrieving CFTypes
+   */
+  public function toArray() {
+    $a = array();
+    foreach($this->value as $value) $a[] = $value->toArray();
+    if(count($a) === 1) return $a[0];
+
+    return $a;
+  }
+
+
+  /************************************************************************************************
+   *    I T E R A T O R   I N T E R F A C E
+   ************************************************************************************************/
+
+  /**
+   * Rewind {@link $iteratorPosition} to first position (being 0)
+   * @link http://php.net/manual/en/iterator.rewind.php
+   * @return void
+   * @uses $iteratorPosition set to 0
+   * @uses $iteratorKeys store keys of {@link $value}
+   */
+  public function rewind() {
+    $this->iteratorPosition = 0;
+    $this->iteratorKeys = array_keys($this->value);
+  }
+
+  /**
+   * Get Iterator's current {@link CFType} identified by {@link $iteratorPosition}
+   * @link http://php.net/manual/en/iterator.current.php
+   * @return CFType current Item
+   * @uses $iteratorPosition identify current key
+   * @uses $iteratorKeys identify current value
+   */
+  public function current() {
+    return $this->value[$this->iteratorKeys[$this->iteratorPosition]];
+  }
+
+  /**
+   * Get Iterator's current key identified by {@link $iteratorPosition}
+   * @link http://php.net/manual/en/iterator.key.php
+   * @return string key of the current Item
+   * @uses $iteratorPosition identify current key
+   * @uses $iteratorKeys identify current value
+   */
+  public function key() {
+    return $this->iteratorKeys[$this->iteratorPosition];
+  }
+
+  /**
+   * Increment {@link $iteratorPosition} to address next {@see CFType}
+   * @link http://php.net/manual/en/iterator.next.php
+   * @return void
+   * @uses $iteratorPosition increment by 1
+   */
+  public function next() {
+    $this->iteratorPosition++;
+  }
+
+  /**
+   * Test if {@link $iteratorPosition} addresses a valid element of {@link $value}
+   * @link http://php.net/manual/en/iterator.valid.php
+   * @return boolean true if current position is valid, false else
+   * @uses $iteratorPosition test if within {@link $iteratorKeys}
+   * @uses $iteratorPosition test if within {@link $value}
+   */
+  public function valid() {
+    return isset($this->iteratorKeys[$this->iteratorPosition]) && isset($this->value[$this->iteratorKeys[$this->iteratorPosition]]);
+  }
+
+}
+
+# eof
diff --git a/lib/plist/classes/CFPropertyList/CFType.php b/lib/plist/classes/CFPropertyList/CFType.php
new file mode 100644 (file)
index 0000000..ba2e57a
--- /dev/null
@@ -0,0 +1,757 @@
+<?php
+/**
+ * Data-Types for CFPropertyList as defined by Apple.
+ * {@link http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html Property Lists}
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @subpackage plist.types
+ * @version $Id$
+ */
+namespace CFPropertyList;
+use \DOMDocument, \Iterator, \ArrayAccess;
+
+/**
+ * Base-Class of all CFTypes used by CFPropertyList
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @subpackage plist.types
+ * @version $Id$
+ * @example example-create-01.php Using the CFPropertyList API
+ * @example example-create-02.php Using CFPropertyList::guess()
+ * @example example-create-03.php Using CFPropertyList::guess() with {@link CFDate} and {@link CFData}
+ */
+abstract class CFType {
+  /**
+   * CFType nodes
+   * @var array
+   */
+  protected $value = null;
+
+  /**
+   * Create new CFType.
+   * @param mixed $value Value of CFType
+   */
+  public function __construct($value=null) {
+    $this->setValue($value);
+  }
+
+  /************************************************************************************************
+   *    M A G I C   P R O P E R T I E S
+   ************************************************************************************************/
+
+  /**
+   * Get the CFType's value
+   * @return mixed CFType's value
+   */
+  public function getValue() {
+    return $this->value;
+  }
+
+  /**
+   * Set the CFType's value
+   * @return void
+   */
+  public function setValue($value) {
+    $this->value = $value;
+  }
+
+  /************************************************************************************************
+   *    S E R I A L I Z I N G
+   ************************************************************************************************/
+
+  /**
+   * Get XML-Node.
+   * @param DOMDocument $doc DOMDocument to create DOMNode in
+   * @param string $nodeName Name of element to create
+   * @return DOMNode Node created based on CType
+   * @uses $value as nodeValue
+   */
+  public function toXML(DOMDocument $doc, $nodeName) {
+    $node = $doc->createElement($nodeName);
+    $text = $doc->createTextNode($this->value);
+    $node->appendChild($text);
+    return $node;
+  }
+
+  /**
+   * convert value to binary representation
+   * @param CFBinaryPropertyList The binary property list object
+   * @return The offset in the object table
+   */
+  public abstract function toBinary(CFBinaryPropertyList &$bplist);
+
+  /**
+   * Get CFType's value.
+   * @return mixed primitive value
+   * @uses $value for retrieving primitive of CFType
+   */
+  public function toArray() {
+    return $this->getValue();
+  }
+
+}
+
+/**
+ * String Type of CFPropertyList
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @subpackage plist.types
+ */
+class CFString extends CFType {
+  /**
+   * Get XML-Node.
+   * @param DOMDocument $doc DOMDocument to create DOMNode in
+   * @param string $nodeName For compatibility reasons; just ignore it
+   * @return DOMNode &lt;string&gt;-Element
+   */
+  public function toXML(DOMDocument $doc,$nodeName="") {
+    return parent::toXML($doc, 'string');
+  }
+
+  /**
+   * convert value to binary representation
+   * @param CFBinaryPropertyList The binary property list object
+   * @return The offset in the object table
+   */
+  public function toBinary(CFBinaryPropertyList &$bplist) {
+    return $bplist->stringToBinary($this->value);
+  }
+}
+
+class CFUid extends CFType {
+  public
+  function toXML(DOMDocument $doc,$nodeName="") {
+    $obj = new CFDictionary(array('CF$UID' => new CFNumber($this->value)));
+    return $obj->toXml($doc);
+  }
+
+  public
+  function toBinary(CFBinaryPropertyList &$bplist) {
+    return $bplist->uidToBinary($this->value);
+  }
+}
+
+/**
+ * Number Type of CFPropertyList
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @subpackage plist.types
+ */
+class CFNumber extends CFType {
+  /**
+   * Get XML-Node.
+   * Returns &lt;real&gt; if $value is a float, &lt;integer&gt; if $value is an integer.
+   * @param DOMDocument $doc DOMDocument to create DOMNode in
+   * @param string $nodeName For compatibility reasons; just ignore it
+   * @return DOMNode &lt;real&gt; or &lt;integer&gt;-Element
+   */
+  public function toXML(DOMDocument $doc,$nodeName="") {
+    $ret = 'real';
+    if(intval($this->value) == $this->value && !is_float($this->value) && strpos($this->value,'.') === false) {
+      $this->value = intval($this->value);
+      $ret = 'integer';
+    }
+    return parent::toXML($doc, $ret);
+  }
+
+  /**
+   * convert value to binary representation
+   * @param CFBinaryPropertyList The binary property list object
+   * @return The offset in the object table
+   */
+  public function toBinary(CFBinaryPropertyList &$bplist) {
+    return $bplist->numToBinary($this->value);
+  }
+}
+
+/**
+ * Date Type of CFPropertyList
+ * Note: CFDate uses Unix timestamp (epoch) to store dates internally
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @subpackage plist.types
+ */
+class CFDate extends CFType {
+  const TIMESTAMP_APPLE = 0;
+  const TIMESTAMP_UNIX  = 1;
+  const DATE_DIFF_APPLE_UNIX = 978307200;
+
+  /**
+   * Create new Date CFType.
+   * @param integer $value timestamp to set
+   * @param integer $format format the timestamp is specified in, use {@link TIMESTAMP_APPLE} or {@link TIMESTAMP_UNIX}, defaults to {@link TIMESTAMP_APPLE}
+   * @uses setValue() to convert the timestamp
+   */
+  function __construct($value,$format=CFDate::TIMESTAMP_UNIX) {
+    $this->setValue($value,$format);
+  }
+
+  /**
+   * Set the Date CFType's value.
+   * @param integer $value timestamp to set
+   * @param integer $format format the timestamp is specified in, use {@link TIMESTAMP_APPLE} or {@link TIMESTAMP_UNIX}, defaults to {@link TIMESTAMP_UNIX}
+   * @return void
+   * @uses TIMESTAMP_APPLE to determine timestamp type
+   * @uses TIMESTAMP_UNIX to determine timestamp type
+   * @uses DATE_DIFF_APPLE_UNIX to convert Apple-timestamp to Unix-timestamp
+   */
+  function setValue($value,$format=CFDate::TIMESTAMP_UNIX) {
+    if($format == CFDate::TIMESTAMP_UNIX) $this->value = $value;
+    else $this->value = $value + CFDate::DATE_DIFF_APPLE_UNIX;
+  }
+
+  /**
+   * Get the Date CFType's value.
+   * @param integer $format format the timestamp is specified in, use {@link TIMESTAMP_APPLE} or {@link TIMESTAMP_UNIX}, defaults to {@link TIMESTAMP_UNIX}
+   * @return integer Unix timestamp
+   * @uses TIMESTAMP_APPLE to determine timestamp type
+   * @uses TIMESTAMP_UNIX to determine timestamp type
+   * @uses DATE_DIFF_APPLE_UNIX to convert Unix-timestamp to Apple-timestamp
+   */
+  function getValue($format=CFDate::TIMESTAMP_UNIX) {
+    if($format == CFDate::TIMESTAMP_UNIX) return $this->value;
+    else return $this->value - CFDate::DATE_DIFF_APPLE_UNIX;
+  }
+
+  /**
+   * Get XML-Node.
+   * @param DOMDocument $doc DOMDocument to create DOMNode in
+   * @param string $nodeName For compatibility reasons; just ignore it
+   * @return DOMNode &lt;date&gt;-Element
+   */
+  public function toXML(DOMDocument $doc,$nodeName="") {
+    $text = $doc->createTextNode(gmdate("Y-m-d\TH:i:s\Z",$this->getValue()));
+    $node = $doc->createElement("date");
+    $node->appendChild($text);
+    return $node;
+  }
+
+  /**
+   * convert value to binary representation
+   * @param CFBinaryPropertyList The binary property list object
+   * @return The offset in the object table
+   */
+  public function toBinary(CFBinaryPropertyList &$bplist) {
+    return $bplist->dateToBinary($this->value);
+  }
+
+  /**
+   * Create a UNIX timestamp from a PList date string
+   * @param string $val The date string (e.g. "2009-05-13T20:23:43Z")
+   * @return integer The UNIX timestamp
+   * @throws PListException when encountering an unknown date string format
+   */
+  public static function dateValue($val) {
+    //2009-05-13T20:23:43Z
+    if(!preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z/',$val,$matches)) throw new PListException("Unknown date format: $val");
+    return gmmktime($matches[4],$matches[5],$matches[6],$matches[2],$matches[3],$matches[1]);
+  }
+}
+
+/**
+ * Boolean Type of CFPropertyList
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @subpackage plist.types
+ */
+class CFBoolean extends CFType {
+  /**
+   * Get XML-Node.
+   * Returns &lt;true&gt; if $value is a true, &lt;false&gt; if $value is false.
+   * @param DOMDocument $doc DOMDocument to create DOMNode in
+   * @param string $nodeName For compatibility reasons; just ignore it
+   * @return DOMNode &lt;true&gt; or &lt;false&gt;-Element
+   */
+  public function toXML(DOMDocument $doc,$nodeName="") {
+    return $doc->createElement($this->value ? 'true' : 'false');
+  }
+
+  /**
+   * convert value to binary representation
+   * @param CFBinaryPropertyList The binary property list object
+   * @return The offset in the object table
+   */
+  public function toBinary(CFBinaryPropertyList &$bplist) {
+    return $bplist->boolToBinary($this->value);
+  }
+
+}
+
+/**
+ * Data Type of CFPropertyList
+ * Note: Binary data is base64-encoded.
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @subpackage plist.types
+ */
+class CFData extends CFType {
+  /**
+   * Create new Data CFType
+   * @param string $value data to be contained by new object
+   * @param boolean $already_coded if true $value will not be base64-encoded, defaults to false
+   */
+  public function __construct($value=null,$already_coded=false) {
+    if($already_coded) $this->value = $value;
+    else $this->setValue($value);
+  }
+
+  /**
+   * Set the CFType's value and base64-encode it.
+   * <b>Note:</b> looks like base64_encode has troubles with UTF-8 encoded strings
+   * @return void
+   */
+  public function setValue($value) {
+    //if(function_exists('mb_check_encoding') && mb_check_encoding($value, 'UTF-8')) $value = utf8_decode($value);
+    $this->value = base64_encode($value);
+  }
+
+  /**
+   * Get base64 encoded data
+   * @return string The base64 encoded data value
+   */
+  public function getCodedValue() {
+    return $this->value;
+  }
+
+  /**
+   * Get the base64-decoded CFType's value.
+   * @return mixed CFType's value
+   */
+  public function getValue() {
+    return base64_decode($this->value);
+  }
+
+  /**
+   * Get XML-Node.
+   * @param DOMDocument $doc DOMDocument to create DOMNode in
+   * @param string $nodeName For compatibility reasons; just ignore it
+   * @return DOMNode &lt;data&gt;-Element
+   */
+  public function toXML(DOMDocument $doc,$nodeName="") {
+    return parent::toXML($doc, 'data');
+  }
+
+  /**
+   * convert value to binary representation
+   * @param CFBinaryPropertyList The binary property list object
+   * @return The offset in the object table
+   */
+  public function toBinary(CFBinaryPropertyList &$bplist) {
+    return $bplist->dataToBinary($this->getValue());
+  }
+}
+
+/**
+ * Array Type of CFPropertyList
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @subpackage plist.types
+ */
+class CFArray extends CFType implements Iterator, ArrayAccess {
+  /**
+   * Position of iterator {@link http://php.net/manual/en/class.iterator.php}
+   * @var integer
+   */
+  protected $iteratorPosition = 0;
+
+
+  /**
+   * Create new CFType.
+   * @param array $value Value of CFType
+   */
+  public function __construct($value=array()) {
+    $this->value = $value;
+  }
+
+  /**
+   * Set the CFType's value
+   * <b>Note:</b> this dummy does nothing
+   * @return void
+   */
+  public function setValue($value) {
+  }
+
+  /**
+   * Add CFType to collection.
+   * @param CFType $value CFType to add to collection, defaults to null which results in an empty {@link CFString}
+   * @return void
+   * @uses $value for adding $value
+   */
+  public function add(CFType $value=null) {
+    // anything but CFType is null, null is an empty string - sad but true
+    if( !$value )
+      $value = new CFString();
+
+    $this->value[] = $value;
+  }
+
+  /**
+   * Get CFType from collection.
+   * @param integer $key Key of CFType to retrieve from collection
+   * @return CFType CFType found at $key, null else
+   * @uses $value for retrieving CFType of $key
+   */
+  public function get($key) {
+    if(isset($this->value[$key])) return $this->value[$key];
+    return null;
+  }
+
+  /**
+   * Remove CFType from collection.
+   * @param integer $key Key of CFType to removes from collection
+   * @return CFType removed CFType, null else
+   * @uses $value for removing CFType of $key
+   */
+  public function del($key) {
+    if(isset($this->value[$key])) unset($this->value[$key]);
+  }
+
+
+  /************************************************************************************************
+   *    S E R I A L I Z I N G
+   ************************************************************************************************/
+
+  /**
+   * Get XML-Node.
+   * @param DOMDocument $doc DOMDocument to create DOMNode in
+   * @param string $nodeName For compatibility reasons; just ignore it
+   * @return DOMNode &lt;array&gt;-Element
+   */
+  public function toXML(DOMDocument $doc,$nodeName="") {
+    $node = $doc->createElement('array');
+
+    foreach($this->value as $value) $node->appendChild($value->toXML($doc));
+    return $node;
+  }
+
+  /**
+   * convert value to binary representation
+   * @param CFBinaryPropertyList The binary property list object
+   * @return The offset in the object table
+   */
+  public function toBinary(CFBinaryPropertyList &$bplist) {
+    return $bplist->arrayToBinary($this);
+  }
+
+  /**
+   * Get CFType's value.
+   * @return array primitive value
+   * @uses $value for retrieving primitive of CFType
+   */
+  public function toArray() {
+    $a = array();
+    foreach($this->value as $value) $a[] = $value->toArray();
+    return $a;
+  }
+
+
+  /************************************************************************************************
+   *    I T E R A T O R   I N T E R F A C E
+   ************************************************************************************************/
+
+  /**
+   * Rewind {@link $iteratorPosition} to first position (being 0)
+   * @link http://php.net/manual/en/iterator.rewind.php
+   * @return void
+   * @uses $iteratorPosition set to 0
+   */
+  public function rewind() {
+    $this->iteratorPosition = 0;
+  }
+
+  /**
+   * Get Iterator's current {@link CFType} identified by {@link $iteratorPosition}
+   * @link http://php.net/manual/en/iterator.current.php
+   * @return CFType current Item
+   * @uses $iteratorPosition identify current key
+   */
+  public function current() {
+    return $this->value[$this->iteratorPosition];
+  }
+
+  /**
+   * Get Iterator's current key identified by {@link $iteratorPosition}
+   * @link http://php.net/manual/en/iterator.key.php
+   * @return string key of the current Item
+   * @uses $iteratorPosition identify current key
+   */
+  public function key() {
+    return $this->iteratorPosition;
+  }
+
+  /**
+   * Increment {@link $iteratorPosition} to address next {@see CFType}
+   * @link http://php.net/manual/en/iterator.next.php
+   * @return void
+   * @uses $iteratorPosition increment by 1
+   */
+  public function next() {
+    $this->iteratorPosition++;
+  }
+
+  /**
+   * Test if {@link $iteratorPosition} addresses a valid element of {@link $value}
+   * @link http://php.net/manual/en/iterator.valid.php
+   * @return boolean true if current position is valid, false else
+   * @uses $iteratorPosition test if within {@link $iteratorKeys}
+   * @uses $iteratorPosition test if within {@link $value}
+   */
+  public function valid() {
+    return isset($this->value[$this->iteratorPosition]);
+  }
+
+  /************************************************************************************************
+   *    ArrayAccess   I N T E R F A C E
+   ************************************************************************************************/
+
+  /**
+   * Determine if the array's key exists
+   * @param string $key the key to check
+   * @return bool true if the offset exists, false if not
+   * @link http://php.net/manual/en/arrayaccess.offsetexists.php
+   * @uses $value to check if $key exists
+   * @author Sean Coates <sean@php.net>
+   */
+  public function offsetExists($key) {
+    return isset($this->value[$key]);
+  }
+
+  /**
+   * Fetch a specific key from the CFArray
+   * @param string $key the key to check
+   * @return mixed the value associated with the key; null if the key is not found
+   * @link http://php.net/manual/en/arrayaccess.offsetget.php
+   * @uses get() to get the key's value
+   * @author Sean Coates <sean@php.net>
+   */
+  public function offsetGet($key) {
+    return $this->get($key);
+  }
+
+  /**
+   * Set a value in the array
+   * @param string $key the key to set
+   * @param string $value the value to set
+   * @return void
+   * @link http://php.net/manual/en/arrayaccess.offsetset.php
+   * @uses setValue() to set the key's new value
+   * @author Sean Coates <sean@php.net>
+   */
+  public function offsetSet($key, $value) {
+    return $this->setValue($value);
+  }
+
+  /**
+   * Unsets a value in the array
+   * <b>Note:</b> this dummy does nothing
+   * @param string $key the key to set
+   * @return void
+   * @link http://php.net/manual/en/arrayaccess.offsetunset.php
+   * @author Sean Coates <sean@php.net>
+   */
+  public function offsetUnset($key) {
+
+  }
+
+
+}
+
+/**
+ * Array Type of CFPropertyList
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @subpackage plist.types
+ */
+class CFDictionary extends CFType implements Iterator {
+  /**
+   * Position of iterator {@link http://php.net/manual/en/class.iterator.php}
+   * @var integer
+   */
+  protected $iteratorPosition = 0;
+
+  /**
+   * List of Keys for numerical iterator access {@link http://php.net/manual/en/class.iterator.php}
+   * @var array
+   */
+  protected $iteratorKeys = null;
+
+
+  /**
+   * Create new CFType.
+   * @param array $value Value of CFType
+   */
+  public function __construct($value=array()) {
+    $this->value = $value;
+  }
+
+  /**
+   * Set the CFType's value
+   * <b>Note:</b> this dummy does nothing
+   * @return void
+   */
+  public function setValue($value) {
+  }
+
+  /**
+   * Add CFType to collection.
+   * @param string $key Key to add to collection
+   * @param CFType $value CFType to add to collection, defaults to null which results in an empty {@link CFString}
+   * @return void
+   * @uses $value for adding $key $value pair
+   */
+  public function add($key, CFType $value=null) {
+    // anything but CFType is null, null is an empty string - sad but true
+    if( !$value )
+      $value = new CFString();
+
+    $this->value[$key] = $value;
+  }
+
+  /**
+   * Get CFType from collection.
+   * @param string $key Key of CFType to retrieve from collection
+   * @return CFType CFType found at $key, null else
+   * @uses $value for retrieving CFType of $key
+   */
+  public function get($key) {
+    if(isset($this->value[$key])) return $this->value[$key];
+    return null;
+  }
+
+  /**
+   * Generic getter (magic)
+   * @param integer $key Key of CFType to retrieve from collection
+   * @return CFType CFType found at $key, null else
+   * @link http://php.net/oop5.overloading
+   * @uses get() to retrieve the key's value
+   * @author Sean Coates <sean@php.net>
+   */
+  public function __get($key) {
+    return $this->get($key);
+  }
+
+  /**
+   * Remove CFType from collection.
+   * @param string $key Key of CFType to removes from collection
+   * @return CFType removed CFType, null else
+   * @uses $value for removing CFType of $key
+   */
+  public function del($key) {
+    if(isset($this->value[$key])) unset($this->value[$key]);
+  }
+
+
+  /************************************************************************************************
+   *    S E R I A L I Z I N G
+   ************************************************************************************************/
+
+  /**
+   * Get XML-Node.
+   * @param DOMDocument $doc DOMDocument to create DOMNode in
+   * @param string $nodeName For compatibility reasons; just ignore it
+   * @return DOMNode &lt;dict&gt;-Element
+   */
+  public function toXML(DOMDocument $doc,$nodeName="") {
+    $node = $doc->createElement('dict');
+
+    foreach($this->value as $key => $value) {
+      $node->appendChild($doc->createElement('key', $key));
+      $node->appendChild($value->toXML($doc));
+    }
+
+    return $node;
+  }
+
+  /**
+   * convert value to binary representation
+   * @param CFBinaryPropertyList The binary property list object
+   * @return The offset in the object table
+   */
+  public function toBinary(CFBinaryPropertyList &$bplist) {
+    return $bplist->dictToBinary($this);
+  }
+
+  /**
+   * Get CFType's value.
+   * @return array primitive value
+   * @uses $value for retrieving primitive of CFType
+   */
+  public function toArray() {
+    $a = array();
+
+    foreach($this->value as $key => $value) $a[$key] = $value->toArray();
+    return $a;
+  }
+
+
+  /************************************************************************************************
+   *    I T E R A T O R   I N T E R F A C E
+   ************************************************************************************************/
+
+  /**
+   * Rewind {@link $iteratorPosition} to first position (being 0)
+   * @link http://php.net/manual/en/iterator.rewind.php
+   * @return void
+   * @uses $iteratorPosition set to 0
+   * @uses $iteratorKeys store keys of {@link $value}
+   */
+  public function rewind() {
+    $this->iteratorPosition = 0;
+    $this->iteratorKeys = array_keys($this->value);
+  }
+
+  /**
+   * Get Iterator's current {@link CFType} identified by {@link $iteratorPosition}
+   * @link http://php.net/manual/en/iterator.current.php
+   * @return CFType current Item
+   * @uses $iteratorPosition identify current key
+   * @uses $iteratorKeys identify current value
+   */
+  public function current() {
+    return $this->value[$this->iteratorKeys[$this->iteratorPosition]];
+  }
+
+  /**
+   * Get Iterator's current key identified by {@link $iteratorPosition}
+   * @link http://php.net/manual/en/iterator.key.php
+   * @return string key of the current Item
+   * @uses $iteratorPosition identify current key
+   * @uses $iteratorKeys identify current value
+   */
+  public function key() {
+    return $this->iteratorKeys[$this->iteratorPosition];
+  }
+
+  /**
+   * Increment {@link $iteratorPosition} to address next {@see CFType}
+   * @link http://php.net/manual/en/iterator.next.php
+   * @return void
+   * @uses $iteratorPosition increment by 1
+   */
+  public function next() {
+    $this->iteratorPosition++;
+  }
+
+  /**
+   * Test if {@link $iteratorPosition} addresses a valid element of {@link $value}
+   * @link http://php.net/manual/en/iterator.valid.php
+   * @return boolean true if current position is valid, false else
+   * @uses $iteratorPosition test if within {@link $iteratorKeys}
+   * @uses $iteratorPosition test if within {@link $value}
+   */
+  public function valid() {
+    return isset($this->iteratorKeys[$this->iteratorPosition]) && isset($this->value[$this->iteratorKeys[$this->iteratorPosition]]);
+  }
+
+}
+
+# eof
diff --git a/lib/plist/classes/CFPropertyList/CFTypeDetector.php b/lib/plist/classes/CFPropertyList/CFTypeDetector.php
new file mode 100644 (file)
index 0000000..639f34e
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+
+ /**
+  * CFTypeDetector
+  * Interface for converting native PHP data structures to CFPropertyList objects.
+  * @author Rodney Rehm <rodney.rehm@medialize.de>
+  * @author Christian Kruse <cjk@wwwtech.de>
+  * @package plist
+  * @subpackage plist.types
+  * @example example-create-02.php Using {@link CFTypeDetector}
+  * @example example-create-03.php Using {@link CFTypeDetector} with {@link CFDate} and {@link CFData}
+  * @example example-create-04.php Using and extended {@link CFTypeDetector}
+  */
+
+namespace CFPropertyList;
+use \DateTime, \Iterator;
+
+class CFTypeDetector {
+
+  /**
+   * flag stating if all arrays should automatically be converted to {@link CFDictionary}
+   * @var boolean
+   */
+  protected $autoDictionary = false;
+
+  /**
+   * flag stating if exceptions should be suppressed or thrown
+   * @var boolean
+   */
+  protected $suppressExceptions = false;
+
+  /**
+   * name of a method that will be used for array to object conversations
+   * @var callable
+   */
+  protected $objectToArrayMethod = null;
+
+  /**
+   * flag stating if "123.23" should be converted to float (true) or preserved as string (false)
+   * @var boolean
+   */
+  protected $castNumericStrings = true;
+
+
+  /**
+   * Create new CFTypeDetector
+   * @param array $options Configuration for casting values [autoDictionary, suppressExceptions, objectToArrayMethod, castNumericStrings]
+   */
+  public function __construct(array $options=array()) {
+    //$autoDicitionary=false,$suppressExceptions=false,$objectToArrayMethod=null
+    foreach ($options as $key => $value) {
+      if (property_exists($this, $key)) {
+        $this->$key = $value;
+      }
+    }
+  }
+
+  /**
+   * Determine if an array is associative or numerical.
+   * Numerical Arrays have incrementing index-numbers that don't contain gaps.
+   * @param array $value Array to check indexes of
+   * @return boolean true if array is associative, false if array has numeric indexes
+   */
+  protected function isAssociativeArray($value) {
+    $numericKeys = true;
+    $i = 0;
+    foreach($value as $key => $v) {
+      if($i !== $key) {
+        $numericKeys = false;
+        break;
+      }
+      $i++;
+    }
+    return !$numericKeys;
+  }
+
+  /**
+   * Get the default value
+   * @return CFType the default value to return if no suitable type could be determined
+   */
+  protected function defaultValue() {
+    return new CFString();
+  }
+
+  /**
+   * Create CFType-structure by guessing the data-types.
+   * {@link CFArray}, {@link CFDictionary}, {@link CFBoolean}, {@link CFNumber} and {@link CFString} can be created, {@link CFDate} and {@link CFData} cannot.
+   * <br /><b>Note:</b>Distinguishing between {@link CFArray} and {@link CFDictionary} is done by examining the keys.
+   * Keys must be strictly incrementing integers to evaluate to a {@link CFArray}.
+   * Since PHP does not offer a function to test for associative arrays,
+   * this test causes the input array to be walked twice and thus work rather slow on large collections.
+   * If you work with large arrays and can live with all arrays evaluating to {@link CFDictionary},
+   * feel free to set the appropriate flag.
+   * <br /><b>Note:</b> If $value is an instance of CFType it is simply returned.
+   * <br /><b>Note:</b> If $value is neither a CFType, array, numeric, boolean nor string, it is omitted.
+   * @param mixed $value Value to convert to CFType
+   * @param boolean $autoDictionary if true {@link CFArray}-detection is bypassed and arrays will be returned as {@link CFDictionary}.
+   * @return CFType CFType based on guessed type
+   * @uses isAssociativeArray() to check if an array only has numeric indexes
+   */
+  public function toCFType($value) {
+    switch(true) {
+      case $value instanceof CFType:
+        return $value;
+      break;
+
+      case is_object($value):
+        // DateTime should be CFDate
+        if(class_exists( 'DateTime' ) && $value instanceof DateTime){
+          return new CFDate($value->getTimestamp());
+        }
+
+        // convert possible objects to arrays, arrays will be arrays
+        if($this->objectToArrayMethod && is_callable(array($value, $this->objectToArrayMethod))){
+          $value = call_user_func( array( $value, $this->objectToArrayMethod ) );
+        }
+
+        if(!is_array($value)){
+          if($this->suppressExceptions)
+            return $this->defaultValue();
+
+          throw new PListException('Could not determine CFType for object of type '. get_class($value));
+        }
+      /* break; omitted */
+
+      case $value instanceof Iterator:
+      case is_array($value):
+        // test if $value is simple or associative array
+        if(!$this->autoDictionary) {
+          if(!$this->isAssociativeArray($value)) {
+            $t = new CFArray();
+            foreach($value as $v) $t->add($this->toCFType($v));
+            return $t;
+          }
+        }
+
+        $t = new CFDictionary();
+        foreach($value as $k => $v) $t->add($k, $this->toCFType($v));
+
+        return $t;
+      break;
+
+      case is_bool($value):
+        return new CFBoolean($value);
+      break;
+
+      case is_null($value):
+        return new CFString();
+      break;
+
+      case is_resource($value):
+        if ($this->suppressExceptions) {
+          return $this->defaultValue();
+        }
+
+        throw new PListException('Could not determine CFType for resource of type '. get_resource_type($value));
+      break;
+
+      case is_numeric($value):
+        if (!$this->castNumericStrings && is_string($value)) {
+          return new CFString($value);
+        }
+
+        return new CFNumber($value);
+      break;
+
+      case is_string($value):
+        if(strpos($value, "\x00") !== false) {
+          return new CFData($value);
+        }
+        return new CFString($value);
+
+      break;
+
+      default:
+        if ($this->suppressExceptions) {
+          return $this->defaultValue();
+        }
+
+        throw new PListException('Could not determine CFType for '. gettype($value));
+      break;
+    }
+  }
+
+}
diff --git a/lib/plist/classes/CFPropertyList/IOException.php b/lib/plist/classes/CFPropertyList/IOException.php
new file mode 100644 (file)
index 0000000..aeae841
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+/**
+ * CFPropertyList
+ * {@link http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html Property Lists}
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @version $Id$
+ */
+namespace CFPropertyList;
+
+/**
+ * Basic Input / Output Exception
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ */
+class IOException extends \Exception {
+  /**
+   * Flag telling the File could not be found
+   */
+  const NOT_FOUND = 1;
+  
+  /**
+   * Flag telling the File is not readable
+   */
+  const NOT_READABLE = 2;
+  
+  /**
+   * Flag telling the File is not writable
+   */
+  const NOT_WRITABLE = 3;
+
+  /**
+   * Flag telling there was a read error
+   */
+  const READ_ERROR = 4;
+
+  /**
+   * Flag telling there was a read error
+   */
+  const WRITE_ERROR = 5;
+
+  /**
+   * Create new IOException
+   * @param string $path Source of the problem
+   * @param integer $type Type of the problem
+   */
+  public function __construct($path, $type=null) {
+    parent::__construct( $path, $type );
+  }
+  
+  /**
+   * Create new FileNotFound-Exception
+   * @param string $path Source of the problem
+   * @return IOException new FileNotFound-Exception
+   */
+  public static function notFound($path) {
+    return new IOException( $path, self::NOT_FOUND );
+  }
+
+  /**
+   * Create new FileNotReadable-Exception
+   * @param string $path Source of the problem
+   * @return IOException new FileNotReadable-Exception
+   */
+  public static function notReadable($path) {
+    return new IOException( $path, self::NOT_READABLE );
+  }
+
+  /**
+   * Create new FileNotWritable-Exception
+   * @param string $path Source of the problem
+   * @return IOException new FileNotWritable-Exception
+   */
+  public static function notWritable($path) {
+    return new IOException( $path, self::NOT_WRITABLE );
+  }
+
+  /**
+   * Create new ReadError-Exception
+   * @param string $path Source of the problem
+   * @return IOException new ReadError-Exception
+   */
+  public static function readError($path) {
+    return new IOException( $path, self::READ_ERROR );
+  }
+
+  /**
+   * Create new WriteError-Exception
+   * @param string $path Source of the problem
+   * @return IOException new WriteError-Exception
+   */
+  public static function writeError($path) {
+    return new IOException( $path, self::WRITE_ERROR );
+  }
+}
+
diff --git a/lib/plist/classes/CFPropertyList/PListException.php b/lib/plist/classes/CFPropertyList/PListException.php
new file mode 100644 (file)
index 0000000..958ed39
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/**
+ * CFPropertyList
+ * {@link http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html Property Lists}
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @version $Id$
+ */
+namespace CFPropertyList;
+
+/**
+ * Exception for errors with the PList format
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ */
+class PListException extends \Exception {
+
+}
+
+
diff --git a/lib/plist/readme_moodle.txt b/lib/plist/readme_moodle.txt
new file mode 100644 (file)
index 0000000..664f4b5
--- /dev/null
@@ -0,0 +1,20 @@
+CFPropertyList library
+---------------
+
+Downloaded last release from: https://github.com/TECLIB/CFPropertyList/releases/
+
+Import procedure:
+
+- Copy all the files from the CFPropertyList-XXX folder to into lib/plist and remove following files/dirs.
+
+Removed:
+ * .gitignore
+ * composer.json
+ * build.sh
+ * examples
+ * tests
+
+Added:
+ * readme_moodle.txt
+
+Downloaded version: 2.0.1
index f90da3d..2d294e0 100644 (file)
     <license>MIT</license>
     <version>1.1.8</version>
   </library>
+  <library>
+    <location>plist/</location>
+    <name>plist</name>
+    <license>MIT</license>
+    <version>2.0.1</version>
+  </library>
 </libraries>
index b9837e4..eb04ddc 100644 (file)
@@ -2,6 +2,9 @@ This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
 === 3.9 ===
+* Old Safe Exam Browser quiz access rule (quizaccess_safebrowser) replaced by new Safe Exam Browser access rule (quizaccess_seb).
+  Experimental setting enablesafebrowserintegration was deleted.
+* New CFPropertyList library has been added to Moodle core in /lib/plist.
 * behat_data_generators::the_following_exist() has been removed, please use
   behat_data_generators::the_following_entities_exist() instead. See MDL-67691 for more info.
 * admin/tool/task/cli/adhoc_task.php now observers the concurrency limits.
diff --git a/mod/quiz/accessrule/safebrowser/rule.php b/mod/quiz/accessrule/safebrowser/rule.php
deleted file mode 100644 (file)
index 763acdc..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Implementaton of the quizaccess_safebrowser plugin.
- *
- * @package    quizaccess
- * @subpackage safebrowser
- * @copyright  2011 The Open University
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php');
-
-
-/**
- * A rule representing the safe browser check.
- *
- * @copyright  2009 Oliver Rahs
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class quizaccess_safebrowser extends quiz_access_rule_base {
-
-    public static function make(quiz $quizobj, $timenow, $canignoretimelimits) {
-
-        if ($quizobj->get_quiz()->browsersecurity !== 'safebrowser') {
-            return null;
-        }
-
-        return new self($quizobj, $timenow);
-    }
-
-    public function prevent_access() {
-        if (!$this->check_safe_browser()) {
-            return get_string('safebrowsererror', 'quizaccess_safebrowser');
-        } else {
-            return false;
-        }
-    }
-
-    public function description() {
-        return get_string('safebrowsernotice', 'quizaccess_safebrowser');
-    }
-
-    public function setup_attempt_page($page) {
-        $page->set_title($this->quizobj->get_course()->shortname . ': ' . $page->title);
-        $page->set_cacheable(false);
-        $page->set_popup_notification_allowed(false); // Prevent message notifications.
-        $page->set_heading($page->title);
-        $page->set_pagelayout('secure');
-    }
-
-    /**
-     * Checks if browser is safe browser
-     *
-     * @return true, if browser is safe browser else false
-     */
-    public function check_safe_browser() {
-        return strpos($_SERVER['HTTP_USER_AGENT'], 'SEB') !== false;
-    }
-
-    /**
-     * @return array key => lang string any choices to add to the quiz Browser
-     *      security settings menu.
-     */
-    public static function get_browser_security_choices() {
-        global $CFG;
-
-        if (empty($CFG->enablesafebrowserintegration)) {
-            return array();
-        }
-
-        return array('safebrowser' =>
-                get_string('requiresafeexambrowser', 'quizaccess_safebrowser'));
-    }
-}
diff --git a/mod/quiz/accessrule/safebrowser/tests/rule_test.php b/mod/quiz/accessrule/safebrowser/tests/rule_test.php
deleted file mode 100644 (file)
index 09f1d57..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Unit tests for the quizaccess_safebrowser plugin.
- *
- * @package    quizaccess
- * @subpackage safebrowser
- * @category   phpunit
- * @copyright  2008 The Open University
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->dirroot . '/mod/quiz/accessrule/safebrowser/rule.php');
-
-
-/**
- * Unit tests for the quizaccess_safebrowser plugin.
- *
- * @copyright  2008 The Open University
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class quizaccess_safebrowser_testcase extends basic_testcase {
-    // Nothing very testable in this class, just test that it obeys the general access rule contact.
-    public function test_safebrowser_access_rule() {
-        $quiz = new stdClass();
-        $quiz->browsersecurity = 'safebrowser';
-        $cm = new stdClass();
-        $cm->id = 0;
-        $quizobj = new quiz($quiz, $cm, null);
-        $rule = new quizaccess_safebrowser($quizobj, 0);
-        $attempt = new stdClass();
-
-        // This next test assumes the unit tests are not being run using Safe Exam Browser!
-        $_SERVER['HTTP_USER_AGENT'] = 'unknonw browser';
-        $this->assertEquals(get_string('safebrowsererror', 'quizaccess_safebrowser'),
-            $rule->prevent_access());
-
-        $this->assertEquals(get_string('safebrowsernotice', 'quizaccess_safebrowser'),
-            $rule->description());
-        $this->assertFalse($rule->prevent_new_attempt(0, $attempt));
-        $this->assertFalse($rule->is_finished(0, $attempt));
-        $this->assertFalse($rule->end_time($attempt));
-        $this->assertFalse($rule->time_left_display($attempt, 0));
-    }
-}
diff --git a/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js b/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js
new file mode 100644 (file)
index 0000000..1a1bf50
Binary files /dev/null and b/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js differ
diff --git a/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map b/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map
new file mode 100644 (file)
index 0000000..0e721e5
Binary files /dev/null and b/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map differ
diff --git a/mod/quiz/accessrule/seb/amd/src/managetemplates.js b/mod/quiz/accessrule/seb/amd/src/managetemplates.js
new file mode 100644 (file)
index 0000000..1885a48
--- /dev/null
@@ -0,0 +1,68 @@
+/**
+ * Template management code.
+ *
+ * @module quizaccess_seb/managetemplates
+ * @class managetemplates
+ * @package quizaccess_seb
+ * @copyright  2020 Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ */
+define(
+    ['jquery', 'core/ajax', 'core/str', 'core/notification'],
+    function($, ajax, str, notification) {
+        var manager = {
+            /**
+             * Confirm removal of the specified template.
+             *
+             * @method removeTemplate
+             * @param {EventFacade} e The EventFacade
+             */
+            removeTemplate: function(e) {
+                e.preventDefault();
+                var targetUrl = $(e.currentTarget).attr('href');
+                str.get_strings([
+                    {
+                        key:        'confirmtemplateremovaltitle',
+                        component:  'quizaccess_seb'
+                    },
+                    {
+                        key:        'confirmtemplateremovalquestion',
+                        component:  'quizaccess_seb'
+                    },
+                    {
+                        key:        'yes',
+                        component:  'moodle'
+                    },
+                    {
+                        key:        'no',
+                        component:  'moodle'
+                    }
+                ])
+                .then(function(s) {
+                    notification.confirm(s[0], s[1], s[2], s[3], function() {
+                        window.location = targetUrl;
+                    });
+
+                    return;
+                })
+                .catch();
+            },
+
+            /**
+             * Setup the template management UI.
+             *
+             * @method setup
+             */
+            setup: function() {
+                $('body').delegate('[data-action="delete"]', 'click', manager.removeTemplate);
+            }
+        };
+
+        return /** @alias module:quizaccess_seb/managetemplates */ {
+            /**
+             * Setup the template management UI.
+             *
+             * @method setup
+             */
+            setup: manager.setup
+        };
+    });
diff --git a/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php b/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php
new file mode 100644 (file)
index 0000000..bc57ed8
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Backup instructions for the seb (Safe Exam Browser) quiz access subplugin.
+ *
+ * @package    quizaccess_seb
+ * @category   backup
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/backup_mod_quiz_access_subplugin.class.php');
+
+/**
+ * Backup instructions for the seb (Safe Exam Browser) quiz access subplugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_quizaccess_seb_subplugin extends backup_mod_quiz_access_subplugin {
+
+    /**
+     * Stores the data related to the Safe Exam Browser quiz settings and management for a particular quiz.
+     *
+     * @return backup_subplugin_element
+     */
+    protected function define_quiz_subplugin_structure() {
+        parent::define_quiz_subplugin_structure();
+        $quizid = backup::VAR_ACTIVITYID;
+
+        $subplugin = $this->get_subplugin_element();
+        $subpluginwrapper = new backup_nested_element($this->get_recommended_name());
+
+        $template = new \quizaccess_seb\template();
+        $blanktemplatearray = (array) $template->to_record();
+        unset($blanktemplatearray['usermodified']);
+        unset($blanktemplatearray['timemodified']);
+
+        $templatekeys = array_keys($blanktemplatearray);
+
+        $subplugintemplatesettings = new backup_nested_element('quizaccess_seb_template', null, $templatekeys);
+
+        // Get quiz settings keys to save.
+        $settings = new \quizaccess_seb\quiz_settings();
+        $blanksettingsarray = (array) $settings->to_record();
+        unset($blanksettingsarray['id']); // We don't need to save reference to settings record in current instance.
+        // We don't need to save the data about who last modified the settings as they will be overwritten on restore. Also
+        // means we don't have to think about user data for the backup.
+        unset($blanksettingsarray['usermodified']);
+        unset($blanksettingsarray['timemodified']);
+
+        $settingskeys = array_keys($blanksettingsarray);
+
+        // Save the settings.
+        $subpluginquizsettings = new backup_nested_element('quizaccess_seb_quizsettings', null, $settingskeys);
+
+        // Connect XML elements into the tree.
+        $subplugin->add_child($subpluginwrapper);
+        $subpluginwrapper->add_child($subpluginquizsettings);
+        $subpluginquizsettings->add_child($subplugintemplatesettings);
+
+        // Set source to populate the settings data by referencing the ID of quiz being backed up.
+        $subpluginquizsettings->set_source_table(quizaccess_seb\quiz_settings::TABLE, ['quizid' => $quizid]);
+
+        $subpluginquizsettings->annotate_files('quizaccess_seb', 'filemanager_sebconfigfile', null);
+
+        $params = ['id' => '../templateid'];
+        $subplugintemplatesettings->set_source_table(\quizaccess_seb\template::TABLE, $params);
+
+        return $subplugin;
+    }
+}
\ No newline at end of file
diff --git a/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php b/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php
new file mode 100644 (file)
index 0000000..928fb42
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Restore instructions for the seb (Safe Exam Browser) quiz access subplugin.
+ *
+ * @package    quizaccess_seb
+ * @category   backup
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\quiz_settings;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/restore_mod_quiz_access_subplugin.class.php');
+
+/**
+ * Restore instructions for the seb (Safe Exam Browser) quiz access subplugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_quizaccess_seb_subplugin extends restore_mod_quiz_access_subplugin {
+
+    /**
+     * Provides path structure required to restore data for seb quiz access plugin.
+     *
+     * @return array
+     */
+    protected function define_quiz_subplugin_structure() {
+        $paths = [];
+
+        // Quiz settings.
+        $path = $this->get_pathfor('/quizaccess_seb_quizsettings'); // Subplugin root path.
+        $paths[] = new restore_path_element('quizaccess_seb_quizsettings', $path);
+
+        // Template settings.
+        $path = $this->get_pathfor('/quizaccess_seb_quizsettings/quizaccess_seb_template');
+        $paths[] = new restore_path_element('quizaccess_seb_template', $path);
+
+        return $paths;
+    }
+
+    /**
+     * Process the restored data for the quizaccess_seb_quizsettings table.
+     *
+     * @param stdClass $data Data for quizaccess_seb_quizsettings retrieved from backup xml.
+     */
+    public function process_quizaccess_seb_quizsettings($data) {
+        global $DB, $USER;
+
+        // Process quizsettings.
+        $data = (object) $data;
+        $data->quizid = $this->get_new_parentid('quiz'); // Update quizid with new reference.
+        $data->cmid = $this->task->get_moduleid();
+
+        unset($data->id);
+        $data->timecreated = $data->timemodified = time();
+        $data->usermodified = $USER->id;
+        $DB->insert_record(quizaccess_seb\quiz_settings::TABLE, $data);
+
+        // Process attached files.
+        $this->add_related_files('quizaccess_seb', 'filemanager_sebconfigfile', null);
+    }
+
+    /**
+     * Process the restored data for the quizaccess_seb_template table.
+     *
+     * @param stdClass $data Data for quizaccess_seb_template retrieved from backup xml.
+     */
+    public function process_quizaccess_seb_template($data) {
+        global $DB;
+
+        $data = (object) $data;
+
+        $quizid = $this->get_new_parentid('quiz');
+
+        $template = null;
+        if ($this->task->is_samesite()) {
+            $template = \quizaccess_seb\template::get_record(['id' => $data->id]);
+        } else {
+            // In a different site, try to find existing template with the same name and content.
+            $candidates = \quizaccess_seb\template::get_records(['name' => $data->name]);
+            foreach ($candidates as $candidate) {
+                if ($candidate->get('content') == $data->content) {
+                    $template = $candidate;
+                    break;
+                }
+            }
+        }
+
+        if (empty($template)) {
+            unset($data->id);
+            $template = new \quizaccess_seb\template(0, $data);
+            $template->save();
+        }
+
+        // Update the restored quiz settings to use restored template.
+        $DB->set_field(\quizaccess_seb\quiz_settings::TABLE, 'templateid', $template->get('id'), ['quizid' => $quizid]);
+    }
+
+}
+
diff --git a/mod/quiz/accessrule/seb/classes/access_manager.php b/mod/quiz/accessrule/seb/classes/access_manager.php
new file mode 100644 (file)
index 0000000..6a6bfc9
--- /dev/null
@@ -0,0 +1,300 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Manage the access to the quiz.
+ *
+ * @package    quizaccess_seb
+ * @author     Tim Hunt
+ * @author     Luca Bösch <luca.boesch@bfh.ch>
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use context_module;
+use quiz;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Manage the access to the quiz.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class access_manager {
+
+    /** Header sent by Safe Exam Browser containing the Config Key hash. */
+    private const CONFIG_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH';
+
+    /** Header sent by Safe Exam Browser containing the Browser Exam Key hash. */
+    private const BROWSER_EXAM_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_REQUESTHASH';
+
+    /** @var quiz $quiz A quiz object containing all information pertaining to current quiz. */
+    private $quiz;
+
+    /** @var quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */
+    private $quizsettings;
+
+    /** @var context_module $context Context of this quiz activity. */
+    private $context;
+
+    /** @var string|null $validconfigkey Expected valid SEB config key.
+     */
+    private $validconfigkey = null;
+
+    /**
+     * The access_manager constructor.
+     *
+     * @param quiz $quiz The details of the quiz.
+     */
+    public function __construct(quiz $quiz) {
+        $this->quiz = $quiz;
+        $this->context = context_module::instance($quiz->get_cmid());
+        $this->quizsettings = quiz_settings::get_by_quiz_id($quiz->get_quizid());
+        $this->validconfigkey = quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid());
+    }
+
+    /**
+     * Check if the browser exam key hash in header matches one of the listed browser exam keys from quiz settings.
+     *
+     * @return bool True if header key matches one of the saved keys.
+     */
+    public function validate_browser_exam_keys() : bool {
+        // If browser exam keys are entered in settings, check they match the header.
+        $browserexamkeys = $this->quizsettings->get('allowedbrowserexamkeys');
+        if (empty($browserexamkeys)) {
+            return true; // If no browser exam keys, no check required.
+        }
+
+        // If the Browser Exam Key header isn't present, prevent access.
+        if (is_null($this->get_received_browser_exam_key())) {
+            return false;
+        }
+
+        return $this->check_browser_exam_keys($browserexamkeys, $this->get_received_browser_exam_key());
+    }
+
+    /**
+     * Check if the config key hash in header matches quiz settings.
+     *
+     * @return bool True if header key matches saved key.
+     */
+    public function validate_config_key() : bool {
+        // If using client config, or with no requirement, then no check required.
+        $requiredtype = $this->get_seb_use_type();
+        if ($requiredtype == settings_provider::USE_SEB_NO
+                || $requiredtype == settings_provider::USE_SEB_CLIENT_CONFIG) {
+            return true;
+        }
+
+        if (empty($this->validconfigkey)) {
+            return false; // No config key has been saved.
+        }
+
+        // If the Config Key header isn't present, prevent access.
+        if (is_null($this->get_received_config_key())) {
+            return false;
+        }
+
+        return $this->check_key($this->validconfigkey, $this->get_received_config_key());
+    }
+
+    /**
+     * Check if Safe Exam Browser is required to access quiz.
+     * If quizsettings do not exist, then there is no requirement for using SEB.
+     *
+     * @return bool If required.
+     */
+    public function seb_required() : bool {
+        if (!$this->quizsettings) {
+            return false;
+        } else {
+            return $this->get_seb_use_type() != settings_provider::USE_SEB_NO;
+        }
+    }
+
+    /**
+     * This is the basic check for the Safe Exam Browser previously used in the quizaccess_safebrowser plugin that
+     * managed basic Moodle interactions with SEB.
+     *
+     * @return bool
+     */
+    public function validate_basic_header() : bool {
+        if ($this->get_seb_use_type() == settings_provider::USE_SEB_CLIENT_CONFIG) {
+            return $this->is_using_seb();
+        }
+        return true;
+    }
+
+    /**
+     * Check if using Safe Exam Browser.
+     *
+     * @return bool
+     */
+    public function is_using_seb() : bool {
+        if (isset($_SERVER['HTTP_USER_AGENT'])) {
+            return strpos($_SERVER['HTTP_USER_AGENT'], 'SEB') !== false;
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if user has any capability to bypass the Safe Exam Browser requirement.
+     *
+     * @return bool True if user can bypass check.
+     */
+    public function can_bypass_seb() : bool {
+        return has_capability('quizaccess/seb:bypassseb', $this->context);
+    }
+
+    /**
+     * Return the full URL that was used to request the current page, which is
+     * what we need for verifying the X-SafeExamBrowser-RequestHash header.
+     */
+    private function get_this_page_url() : string {
+        global $CFG, $FULLME;
+        // If $FULLME not set fall back to wwwroot.
+        if ($FULLME == null) {
+            return $CFG->wwwroot;
+        }
+        return $FULLME;
+    }
+
+    /**
+     * Return expected SEB config key.
+     *
+     * @return string|null
+     */
+    public function get_valid_config_key() : ?string {
+        return $this->validconfigkey;
+    }
+
+    /**
+     * Getter for the quiz object.
+     *
+     * @return quiz
+     */
+    public function get_quiz() : quiz {
+        return $this->quiz;
+    }
+
+    /**
+     * Check the hash from the request header against the permitted browser exam keys.
+     *
+     * @param array $keys Allowed browser exam keys.
+     * @param string $header The value of the X-SafeExamBrowser-RequestHash to check.
+     * @return bool True if the hash matches.
+     */
+    private function check_browser_exam_keys(array $keys, string $header) : bool {
+        foreach ($keys as $key) {
+            if ($this->check_key($key, $header)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Check the hash from the request header against a single permitted key.
+     *
+     * @param string $key an allowed key.
+     * @param string $header the value of the X-SafeExamBrowser-RequestHash or X-SafeExamBrowser-ConfigKeyHash to check.
+     * @return bool true if the hash matches.
+     */
+    private function check_key($key, $header) : bool {
+        return hash('sha256', $this->get_this_page_url() . $key) === $header;
+    }
+
+    /**
+     * Returns Safe Exam Browser Config Key hash.
+     *
+     * @return string|null
+     */
+    public function get_received_config_key() {
+        if (isset($_SERVER[self::CONFIG_KEY_HEADER])) {
+            return trim($_SERVER[self::CONFIG_KEY_HEADER]);
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the Browser Exam Key hash.
+     *
+     * @return string|null
+     */
+    public function get_received_browser_exam_key() {
+        if (isset($_SERVER[self::BROWSER_EXAM_KEY_HEADER])) {
+            return trim($_SERVER[self::BROWSER_EXAM_KEY_HEADER]);
+        }
+
+        return null;
+    }
+
+    /**
+     * Get type of SEB usage for the quiz.
+     *
+     * @return int
+     */
+    public function get_seb_use_type() : int {
+        if (empty($this->quizsettings)) {
+            return settings_provider::USE_SEB_NO;
+        } else {
+            return $this->quizsettings->get('requiresafeexambrowser');
+        }
+    }
+
+    /**
+     * Should validate basic header?
+     *
+     * @return bool
+     */
+    public function should_validate_basic_header() : bool {
+        return in_array($this->get_seb_use_type(), [
+            settings_provider::USE_SEB_CLIENT_CONFIG,
+        ]);
+    }
+
+    /**
+     * Should validate SEB config key?
+     * @return bool
+     */
+    public function should_validate_config_key() : bool {
+        return in_array($this->get_seb_use_type(), [
+            settings_provider::USE_SEB_CONFIG_MANUALLY,
+            settings_provider::USE_SEB_TEMPLATE,
+            settings_provider::USE_SEB_UPLOAD_CONFIG,
+        ]);
+    }
+
+    /**
+     * Should validate browser exam key?
+     *
+     * @return bool
+     */
+    public function should_validate_browser_exam_key() : bool {
+        return in_array($this->get_seb_use_type(), [
+            settings_provider::USE_SEB_UPLOAD_CONFIG,
+            settings_provider::USE_SEB_CLIENT_CONFIG,
+        ]);
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/config_key.php b/mod/quiz/accessrule/seb/classes/config_key.php
new file mode 100644 (file)
index 0000000..53a5802
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for generating and representing a Safe Exam Browser config key.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for generating and representing a Safe Exam Browser config key.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class config_key {
+
+    /** @var string $hash The Config Key hash. */
+    private $hash;
+
+    /**
+     * The config_key constructor.
+     *
+     * @param string $hash The Config Key hash.
+     */
+    public function __construct(string $hash) {
+        $this->hash = $hash;
+    }
+
+    /**
+     * Generate the Config Key hash from an SEB Config XML string.
+     *
+     * See  https://safeexambrowser.org/developer/seb-config-key.html for more information about the process.
+     *
+     * @param string $xml A PList XML string, representing SEB config.
+     * @return config_key This config key instance.
+     */
+    public static function generate(string $xml) : config_key {
+        if (!empty($xml) && !helper::is_valid_seb_config($xml)) {
+            throw new \invalid_parameter_exception('Invalid a PList XML string, representing SEB config');
+        }
+
+        $plist = new property_list($xml);
+        // Remove the key "originatorVersion" first. This key is exempted from the SEB-JSON hash (it's a special key
+        // which doesn't have any functionality, it's just meta data indicating which SEB version saved the config file).
+        $plist->delete_element('originatorVersion');
+        // Convert the plist XML of a decrypted/unencrypted SEB config file to a ordered JSON-like "SEB-JSON" object.
+        $hash = $plist->to_json();
+        // Hash the JSON with SHA256. Defaults to required Base16 encoding.
+        $hash = hash('SHA256', $hash);
+
+        return new self($hash);
+    }
+
+    /**
+     * Get the Config Key hash.
+     *
+     * @return string The Config Key hash
+     */
+    public function get_hash() : string {
+        return $this->hash;
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/access_prevented.php b/mod/quiz/accessrule/seb/classes/event/access_prevented.php
new file mode 100644 (file)
index 0000000..c8c36bd
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when access to a quiz is prevented by this subplugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use core\event\base;
+use quizaccess_seb\access_manager;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when access to a quiz is prevented by this subplugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class access_prevented extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param access_manager $accessmanager Access manager.
+     * @param string $reason Reason that access was prevented.
+     * @return base
+     */
+    public static function create_strict(access_manager $accessmanager, string $reason) : base {
+        global $USER;
+
+        $other = [];
+        $other['reason'] = $reason;
+        $other['savedconfigkey'] = $accessmanager->get_valid_config_key();
+        $other['receivedconfigkey'] = $accessmanager->get_received_config_key();
+        $other['receivedbrowserexamkey'] = $accessmanager->get_received_browser_exam_key();
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $accessmanager->get_quiz()->get_quizid(),
+            'courseid' => $accessmanager->get_quiz()->get_courseid(),
+            'context' => $accessmanager->get_quiz()->get_context(),
+            'other' => $other,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quiz';
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:accessprevented', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        $description = "The user with id '$this->userid' has been prevented from accessing quiz with id '$this->objectid' by the "
+                . "Safe Exam Browser access plugin. The reason was '{$this->other['reason']}'. "
+            . "Expected config key: '{$this->other['savedconfigkey']}'. "
+            . "Received config key: '{$this->other['receivedconfigkey']}'. "
+            . "Received browser exam key: '{$this->other['receivedbrowserexamkey']}'.";
+
+        return $description;
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quiz', 'restore' => 'quiz');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [
+            'cmid' => ['db' => 'course_modules', 'restore' => 'course_modules']
+        ];
+    }
+}
\ No newline at end of file
diff --git a/mod/quiz/accessrule/seb/classes/event/template_created.php b/mod/quiz/accessrule/seb/classes/event/template_created.php
new file mode 100644 (file)
index 0000000..9f3bb35
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is created.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is created.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_created extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param template $template SEB template.
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(template $template, context_system $context) : base {
+        global $USER;
+        $tid = $template->get('id');
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $tid,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templatecreated', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $params = [
+            'id' => $this->objectid,
+            'action' => 'edit',
+        ];
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params);
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has created a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/template_deleted.php b/mod/quiz/accessrule/seb/classes/event/template_deleted.php
new file mode 100644 (file)
index 0000000..84379ed
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is deleted.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is deleted.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_deleted extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param string $id The id of the template
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(string $id, context_system $context) : base {
+        global $USER;
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $id,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templatedeleted', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has deleted a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/template_disabled.php b/mod/quiz/accessrule/seb/classes/event/template_disabled.php
new file mode 100644 (file)
index 0000000..45a6ca9
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is disabled.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is disabled.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_disabled extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param template $template SEB template.
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(template $template, context_system $context) : base {
+        global $USER;
+        $tid = $template->get('id');
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $tid,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templatedisabled', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $params = [
+            'id' => $this->objectid,
+            'action' => 'edit',
+        ];
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params);
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has disabled a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/template_enabled.php b/mod/quiz/accessrule/seb/classes/event/template_enabled.php
new file mode 100644 (file)
index 0000000..728c3b2
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is enabled.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is enabled.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_enabled extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param template $template SEB template.
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(template $template, context_system $context) : base {
+        global $USER;
+        $tid = $template->get('id');
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $tid,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templateenabled', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $params = [
+            'id' => $this->objectid,
+            'action' => 'edit',
+        ];
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params);
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has enabled a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/template_updated.php b/mod/quiz/accessrule/seb/classes/event/template_updated.php
new file mode 100644 (file)
index 0000000..28498d5
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is updated.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is updated.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_updated extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param template $template SEB template.
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(template $template, context_system $context) : base {
+        global $USER;
+        $tid = $template->get('id');
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $tid,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templateupdated', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $params = [
+            'id' => $this->objectid,
+            'action' => 'edit',
+        ];
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params);
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has updated a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/helper.php b/mod/quiz/accessrule/seb/classes/helper.php
new file mode 100644 (file)
index 0000000..2ef33ce
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Helper class.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+
+use CFPropertyList\CFPropertyList;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Helper class.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+    /**
+     * Get a filler icon for display in the actions column of a table.
+     *
+     * @param string $url The URL for the icon.
+     * @param string $icon The icon identifier.
+     * @param string $alt The alt text for the icon.
+     * @param string $iconcomponent The icon component.
+     * @param array $options Display options.
+     * @return string
+     */
+    public static function format_icon_link($url, $icon, $alt, $iconcomponent = 'moodle', $options = array()) {
+        global $OUTPUT;
+
+        return $OUTPUT->action_icon(
+            $url,
+            new \pix_icon($icon, $alt, $iconcomponent, [
+                'title' => $alt,
+            ]),
+            null,
+            $options
+        );
+    }
+
+    /**
+     * Validate seb config string.
+     *
+     * @param string $sebconfig
+     * @return bool
+     */
+    public static function is_valid_seb_config(string $sebconfig) : bool {
+        $result = true;
+
+        set_error_handler(function($errno, $errstr, $errfile, $errline ){
+            throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
+        });
+
+        $plist = new CFPropertyList();
+        try {
+            $plist->parse($sebconfig);
+        } catch (\ErrorException $e) {
+            $result = false;
+        } catch (\Exception $e) {
+            $result = false;
+        }
+
+        restore_error_handler();
+
+        return $result;
+    }
+
+    /**
+     * A helper function to get a list of seb config file headers.
+     *
+     * @param int|null $expiretime  Unix timestamp
+     * @return array
+     */
+    public static function get_seb_file_headers(int $expiretime = null) : array {
+        if (is_null($expiretime)) {
+            $expiretime = time();
+        }
+        $headers = [];
+        $headers[] = 'Cache-Control: private, max-age=1, no-transform';
+        $headers[] = 'Expires: '. gmdate('D, d M Y H:i:s', $expiretime) .' GMT';
+        $headers[] = 'Pragma: no-cache';
+        $headers[] = 'Content-Disposition: attachment; filename=config.seb';
+        $headers[] = 'Content-Type: application/seb';
+
+        return $headers;
+    }
+
+    /**
+     * Get seb config content for a particular quiz. This method checks caps.
+     *
+     * @param string $cmid The course module ID for a quiz with config.
+     * @return string SEB config string.
+     */
+    public static function get_seb_config_content(string $cmid) : string {
+        // Try and get the course module.
+        $cm = get_coursemodule_from_id('quiz', $cmid, 0, false, MUST_EXIST);
+
+        // Make sure the user is logged in and has access to the module.
+        require_login($cm->course, false, $cm);
+
+        // Retrieve the config for quiz.
+        $config = quiz_settings::get_config_by_quiz_id($cm->instance);
+        if (empty($config)) {
+            throw new \moodle_exception('noconfigfound', 'quizaccess_seb', '', $cm->id);
+        }
+        return $config;
+    }
+
+    /**
+     * Serve a file to browser for download.
+     *
+     * @param string $contents Contents of file.
+     */
+    public static function send_seb_config_file(string $contents) {
+        // We can now send the file back to the browser.
+        foreach (self::get_seb_file_headers() as $header) {
+            header($header);
+        }
+
+        echo($contents);
+    }
+
+}
+
diff --git a/mod/quiz/accessrule/seb/classes/hideif_rule.php b/mod/quiz/accessrule/seb/classes/hideif_rule.php
new file mode 100644 (file)
index 0000000..ed29150
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class to store data for "hide if" rules for the settings form.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to store data for "hide if" rules for the settings form.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class hideif_rule {
+
+    /**
+     * Name of the element to hide.
+     * @var string
+     */
+    protected $element;
+
+    /**
+     * Name of the element that $element is dependant on.
+     * @var string
+     */
+    protected $dependantname;
+
+    /**
+     * Condition. E.g. 'eq', 'noteq' and etc.
+     * @var string
+     */
+    protected $condition;
+
+    /**
+     * Value to check the $condition against.
+     * @var string
+     */
+    protected $dependantvalue;
+
+    /**
+     * Constructor.
+     *
+     * @param string $element Name of the element to hide.
+     * @param string $dependantname Name of the element that $element is dependant on.
+     * @param string $condition Condition. E.g. 'eq', 'noteq' and etc.
+     * @param string $dependantvalue Value to check the $condition against.
+     */
+    public function __construct(string $element, string $dependantname, string $condition, string $dependantvalue) {
+        $this->element = $element;
+        $this->dependantname = $dependantname;
+        $this->condition = $condition;
+        $this->dependantvalue = $dependantvalue;
+    }
+
+    /**
+     * Return name of the element to hide.
+     * @return string
+     */
+    public function get_element(): string {
+        return $this->element;
+    }
+
+    /**
+     * Returns name of the element that $element is dependant on.
+     * @return string
+     */
+    public function get_dependantname(): string {
+        return $this->dependantname;
+    }
+
+    /**
+     * Returns condition. E.g. 'eq', 'noteq' and etc
+     * @return string
+     */
+    public function get_condition(): string {
+        return $this->condition;
+    }
+
+    /**
+     * Returns value to check the $condition against.
+     * @return string
+     */
+    public function get_dependantvalue(): string {
+        return $this->dependantvalue;
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/classes/link_generator.php b/mod/quiz/accessrule/seb/classes/link_generator.php
new file mode 100644 (file)
index 0000000..05f204a
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Generate the links to open/download the Safe Exam Browser with correct settings.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use moodle_url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Generate the links to open/download the Safe Exam Browser with correct settings.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class link_generator {
+
+    /**
+     * Get a link to force the download of the file over https or sebs protocols.
+     *
+     * @param string $cmid Course module ID.
+     * @param bool $seb Whether to use a seb:// scheme or fall back to http:// scheme.
+     * @param bool $secure Whether to use HTTPS or HTTP protocol.
+     * @return string A URL.
+     */
+    public static function get_link(string $cmid, bool $seb = false, bool $secure = true) : string {
+        // Check if course module exists.
+        get_coursemodule_from_id('quiz', $cmid, 0, false, MUST_EXIST);
+
+        $url = new moodle_url('/mod/quiz/accessrule/seb/config.php?cmid=' . $cmid);
+        if ($seb) {
+            $secure ? $url->set_scheme('sebs') : $url->set_scheme('seb');
+        } else {
+            $secure ? $url->set_scheme('https') : $url->set_scheme('http');
+        }
+        return $url->out();
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/local/form/template.php b/mod/quiz/accessrule/seb/classes/local/form/template.php
new file mode 100644 (file)
index 0000000..f1a6c8e
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Form for manipulating with the template records.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\local\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Form for manipulating with the template records.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template extends \core\form\persistent {
+
+    /** @var string Persistent class name. */
+    protected static $persistentclass = 'quizaccess_seb\\template';
+
+    /**
+     * Form definition.
+     */
+    protected function definition() {
+        $mform = $this->_form;
+
+        $mform->addElement('text', 'name', get_string('name', 'quizaccess_seb'));
+        $mform->addRule('name', get_string('required'), 'required', null, 'client');
+        $mform->setType('name', PARAM_TEXT);
+
+        $mform->addElement('textarea', 'description', get_string('description', 'quizaccess_seb'));
+        $mform->setType('description', PARAM_TEXT);
+
+        if ($this->get_persistent()->get('id')) {
+            $mform->addElement('textarea', 'content', get_string('content', 'quizaccess_seb'), ['rows' => 20, 'cols' => 60]);
+            $mform->addRule('content', get_string('required'), 'required');
+        } else {
+            $mform->addElement('filepicker', 'content', get_string('content', 'quizaccess_seb'));
+            $mform->addRule('content', get_string('required'), 'required');
+        }
+
+        $mform->addElement('selectyesno', 'enabled', get_string('enabled', 'quizaccess_seb'));
+        $mform->setType('enabled', PARAM_INT);
+
+        $this->add_action_buttons();
+
+        if (!empty($this->get_persistent()) && !$this->get_persistent()->can_delete()) {
+            $mform->hardFreezeAllVisibleExcept([]);
+            $mform->addElement('cancel');
+        }
+    }
+
+    /**
+     * Filter out the foreign fields of the persistent.
+     *
+     * @param \stdClass $data The data to filter the fields out of.
+     * @return \stdClass.
+     */
+    protected function filter_data_for_persistent($data) {
+        // Uploading a new template file.
+        if (empty($this->get_persistent()->get('id'))) {
+            $files = $this->get_draft_files('content');
+            if ($files) {
+                $file = reset($files);
+                $data->content = $file->get_content();
+            } else {
+                // No file found. Remove content data and let persistent to return an error.
+                unset($data->content);
+            }
+        }
+
+        return parent::filter_data_for_persistent($data);
+    }
+
+    /**
+     * Extra validation.
+     *
+     * @param  \stdClass $data Data to validate.
+     * @param  array $files Array of files.
+     * @param  array $errors Currently reported errors.
+     * @return array of additional errors, or overridden errors.
+     */
+    protected function extra_validation($data, $files, array &$errors) {
+        $newerrors = [];
+
+        // Check name.
+        if (empty($data->name)) {
+            $newerrors['name'] = get_string('namerequired', 'quizaccess_seb');
+        }
+
+        return $newerrors;
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/local/table/template_list.php b/mod/quiz/accessrule/seb/classes/local/table/template_list.php
new file mode 100644 (file)
index 0000000..a405b69
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Templates table.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\local\table;
+
+use quizaccess_seb\helper;
+use quizaccess_seb\template;
+use quizaccess_seb\template_controller;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/tablelib.php');
+
+/**
+ * Templates table.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_list extends \flexible_table {
+
+    /**
+     * @var int Autogenerated id.
+     */
+    private static $autoid = 0;
+
+    /**
+     * Constructor
+     *
+     * @param string|null $id to be used by the table, autogenerated if null.
+     */
+    public function __construct($id = null) {
+        global $PAGE;
+
+        $id = (is_null($id) ? self::$autoid++ : $id);
+        parent::__construct('quizaccess_seb' . $id);
+
+        $this->define_baseurl($PAGE->url);
+        $this->set_attribute('class', 'generaltable admintable');
+
+        // Column definition.
+        $this->define_columns([
+            'name',
+            'description',
+            'enabled',
+            'used',
+            'actions',
+        ]);
+
+        $this->define_headers([
+            get_string('name', 'quizaccess_seb'),
+            get_string('description', 'quizaccess_seb'),
+            get_string('enabled', 'quizaccess_seb'),
+            get_string('used', 'quizaccess_seb'),
+            get_string('actions'),
+        ]);
+
+        $this->setup();
+    }
+
+    /**
+     * Display name column.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_name(template $data) : string {
+        return \html_writer::link(
+            new \moodle_url(template_controller::get_base_url(), [
+                'id' => $data->get('id'),
+                'action' => template_controller::ACTION_EDIT,
+            ]),
+            $data->get('name')
+        );
+    }
+
+    /**
+     * Display description column.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_description(template $data) : string {
+        return $data->get('description');
+    }
+
+    /**
+     * Display enabled column.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_enabled(template $data): string {
+        return empty($data->get('enabled')) ? get_string('no') : get_string('yes');
+    }
+
+    /**
+     * Display if a template is being used.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_used(template $data): string {
+        return $data->can_delete() ? get_string('no') : get_string('yes');
+    }
+
+    /**
+     * Display actions column.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_actions(template $data) : string {
+        $actions = [];
+
+        $actions[] = helper::format_icon_link(
+            new \moodle_url(template_controller::get_base_url(), [
+                'id'        => $data->get('id'),
+                'action'    => template_controller::ACTION_EDIT,
+            ]),
+            't/edit',
+            get_string('edit')
+        );
+
+        $actions[] = helper::format_icon_link(
+            new \moodle_url(template_controller::get_base_url(), [
+                'id'        => $data->get('id'),
+                'action'    => template_controller::ACTION_DELETE,
+                'sesskey'   => sesskey(),
+            ]),
+            't/delete',
+            get_string('delete'),
+            null,
+            [
+            'data-action' => 'delete',
+            'data-id' => $data->get('id'),
+            ]
+        );
+
+        return implode('&nbsp;', $actions);
+    }
+
+    /**
+     * Sets the data of the table.
+     *
+     * @param \quizaccess_seb\template[] $records An array with records.
+     */
+    public function display(array $records) {
+        foreach ($records as $record) {
+            $this->add_data_keyed($this->format_row($record));
+        }
+
+        $this->finish_output();
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/classes/privacy/provider.php b/mod/quiz/accessrule/seb/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..be9b65c
--- /dev/null
@@ -0,0 +1,290 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy Subsystem implementation for quizaccess_seb.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\privacy;
+
+use context;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\approved_userlist;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\userlist;
+use core_privacy\local\request\writer;
+use quizaccess_seb\quiz_settings;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem implementation for quizaccess_seb.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\core_userlist_provider,
+    \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Retrieve the user metadata stored by plugin.
+     *
+     * @param collection $collection Collection of metadata.
+     * @return collection Collection of metadata.
+     */
+    public static function get_metadata(collection $collection): collection {
+        $collection->add_database_table(
+            'quizaccess_seb_quizsettings',
+             [
+                 'quizid' => 'privacy:metadata:quizaccess_seb_quizsettings:quizid',
+                 'usermodified' => 'privacy:metadata:quizaccess_seb_quizsettings:usermodified',
+                 'timecreated' => 'privacy:metadata:quizaccess_seb_quizsettings:timecreated',
+                 'timemodified' => 'privacy:metadata:quizaccess_seb_quizsettings:timemodified',
+             ],
+            'privacy:metadata:quizaccess_seb_quizsettings'
+        );
+
+        $collection->add_database_table(
+            'quizaccess_seb_template',
+            [
+                'usermodified' => 'privacy:metadata:quizaccess_seb_template:usermodified',
+                'timecreated' => 'privacy:metadata:quizaccess_seb_template:timecreated',
+                'timemodified' => 'privacy:metadata:quizaccess_seb_template:timemodified',
+            ],
+            'privacy:metadata:quizaccess_seb_template'
+        );
+
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid The user to search.
+     * @return contextlist A list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid): contextlist {
+        $contextlist = new contextlist();
+
+        // The data is associated at the module context level, so retrieve the quiz context id.
+        $sql = "SELECT c.id
+                  FROM {quizaccess_seb_quizsettings} qs
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                  JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = :context
+                 WHERE qs.usermodified = :userid
+              GROUP BY c.id";
+
+        $params = [
+            'context' => CONTEXT_MODULE,
+            'userid' => $userid
+        ];
+
+        $contextlist->add_from_sql($sql, $params);
+
+        $sql = "SELECT c.id
+                  FROM {quizaccess_seb_template} tem
+                  JOIN {quizaccess_seb_quizsettings} qs ON qs.templateid = tem.id
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                  JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = :context
+                 WHERE qs.usermodified = :userid
+              GROUP BY c.id";
+
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        global $DB;
+
+        // Get all cmids that correspond to the contexts for a user.
+        $cmids = [];
+        foreach ($contextlist->get_contexts() as $context) {
+            if ($context->contextlevel === CONTEXT_MODULE) {
+                $cmids[] = $context->instanceid;
+            }
+        }
+
+        // Do nothing if no matching quiz settings are found for the user.
+        if (empty($cmids)) {
+            return;
+        }
+
+        list($insql, $params) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
+
+        // SEB quiz settings.
+        $sql = "SELECT qs.id as id,
+                       qs.quizid as quizid,
+                       qs.usermodified as usermodified,
+                       qs.timecreated as timecreated,
+                       qs.timemodified as timemodified
+                  FROM {quizaccess_seb_quizsettings} qs
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                 WHERE cm.id {$insql}";
+
+        $quizsettingslist = $DB->get_records_sql($sql, $params);
+        $index = 0;
+        foreach ($quizsettingslist as $quizsettings) {
+            // Data export is organised in: {Context}/{Plugin Name}/{Table name}/{index}/data.json.
+            $index++;
+            $subcontext = [
+                get_string('pluginname', 'quizaccess_seb'),
+                quiz_settings::TABLE,
+                $index
+            ];
+
+            $data = (object) [
+                'quizid' => $quizsettings->quizid,
+                'usermodified' => $quizsettings->usermodified,
+                'timecreated' => transform::datetime($quizsettings->timecreated),
+                'timemodified' => transform::datetime($quizsettings->timemodified)
+            ];
+
+            writer::with_context($context)->export_data($subcontext, $data);
+        }
+
+        // SEB template settings.
+        $sql = "SELECT tem.id as id,
+                       qs.quizid as quizid,
+                       tem.usermodified as usermodified,
+                       tem.timecreated as timecreated,
+                       tem.timemodified as timemodified
+                  FROM {quizaccess_seb_template} tem
+                  JOIN {quizaccess_seb_quizsettings} qs ON qs.templateid = tem.id
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                 WHERE cm.id {$insql}";
+
+        $templatesettingslist = $DB->get_records_sql($sql, $params);
+        $index = 0;
+        foreach ($templatesettingslist as $templatesetting) {
+            // Data export is organised in: {Context}/{Plugin Name}/{Table name}/{index}/data.json.
+            $index++;
+            $subcontext = [
+                get_string('pluginname', 'quizaccess_seb'),
+                template::TABLE,
+                $index
+            ];
+
+            $data = (object) [
+                'templateid' => $templatesetting->id,
+                'quizid' => $templatesetting->quizid,
+                'usermodified' => $templatesetting->usermodified,
+                'timecreated' => transform::datetime($templatesetting->timecreated),
+                'timemodified' => transform::datetime($templatesetting->timemodified)
+            ];
+
+            writer::with_context($context)->export_data($subcontext, $data);
+        }
+    }
+
+    /**
+     * Delete all data for all users in the specified context.
+     *
+     * @param context $context The specific context to delete data for.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        global $DB;
+
+        // Sanity check that context is at the module context level, then get the quizid.
+        if ($context->contextlevel !== CONTEXT_MODULE) {
+            return;
+        }
+
+        $cmid = $context->instanceid;
+        $quizid = $DB->get_field('course_modules', 'instance', ['id' => $cmid]);
+
+        $params['quizid'] = $quizid;
+        $select = "id IN (SELECT templateid FROM {quizaccess_seb_quizsettings} qs WHERE qs.quizid = :quizid)";
+        $DB->set_field_select('quizaccess_seb_quizsettings', 'usermodified', 0, "quizid = :quizid", $params);
+        $DB->set_field_select('quizaccess_seb_template', 'usermodified', 0, $select, $params);
+    }
+
+    /**
+     * Delete all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        global $DB;
+
+        // If the user has data, then only the User context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+
+        $params['usermodified'] = $contextlist->get_user()->id;
+        $DB->set_field_select('quizaccess_seb_quizsettings', 'usermodified', 0, "usermodified = :usermodified", $params);
+        $DB->set_field_select('quizaccess_seb_template', 'usermodified', 0, "usermodified = :usermodified", $params);
+    }
+
+    /**
+     * Get the list of users who have data within a context.
+     *
+     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(userlist $userlist) {
+        $context = $userlist->get_context();
+
+        if (!$context instanceof \context_module) {
+            return;
+        }
+
+        // The data is associated at the quiz module context level, so retrieve the user's context id.
+        $sql = "SELECT qs.usermodified AS userid
+                  FROM {quizaccess_seb_quizsettings} qs
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                 WHERE cm.id = ?";
+        $params = [$context->instanceid];
+        $userlist->add_from_sql('userid', $sql, $params);
+    }
+
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param approved_userlist $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+        global $DB;
+        $context = $userlist->get_context();
+
+        // Sanity check that context is at the Module context level.
+        if ($context->contextlevel !== CONTEXT_MODULE) {
+            return;
+        }
+
+        $userids = $userlist->get_userids();
+        list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
+
+        $DB->set_field_select('quizaccess_seb_quizsettings', 'usermodified', 0, "usermodified {$insql}", $inparams);
+        $DB->set_field_select('quizaccess_seb_template', 'usermodified', 0, "usermodified {$insql}", $inparams);
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/property_list.php b/mod/quiz/accessrule/seb/classes/property_list.php
new file mode 100644 (file)
index 0000000..57525e6
--- /dev/null
@@ -0,0 +1,389 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Wrapper for CFPropertyList to handle low level iteration.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use CFPropertyList\CFArray;
+use CFPropertyList\CFBoolean;
+use CFPropertyList\CFData;
+use CFPropertyList\CFDate;
+use CFPropertyList\CFDictionary;
+use CFPropertyList\CFNumber;
+use CFPropertyList\CFPropertyList;
+use CFPropertyList\CFString;
+use CFPropertyList\CFType;
+use \DateTime;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Wrapper for CFPropertyList to handle low level iteration.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class property_list {
+
+    /** A random 4 character unicode string to replace backslashes during json_encode. */
+    private const BACKSLASH_SUBSTITUTE = "ؼҷҍԴ";
+
+    /** @var CFPropertyList $cfpropertylist */
+    private $cfpropertylist;
+
+    /**
+     * property_list constructor.
+     *
+     * @param string $xml A Plist XML string.
+     */
+    public function __construct(string $xml = '') {
+        $this->cfpropertylist = new CFPropertyList();
+
+        if (empty($xml)) {
+            // If xml not provided, create a blank PList with root dictionary set up.
+            $this->cfpropertylist->add(new CFDictionary([]));
+        } else {
+            // Parse the XML into a PList object.
+            $this->cfpropertylist->parse($xml, CFPropertyList::FORMAT_XML);
+        }
+    }
+
+    /**
+     * Add a new element to the root dictionary element.
+     *
+     * @param string $key Key to assign to new element.
+     * @param CFType $element The new element. May be a collection such as an array.
+     */
+    public function add_element_to_root(string $key, CFType $element) {
+        // Get the PList's root dictionary and add new element.
+        $this->cfpropertylist->getValue()->add($key, $element);
+    }
+
+    /**
+     * Get value of element identified by key.
+     *
+     * @param string $key Key of element.
+     * @return mixed Value of element found, or null if none found.
+     */
+    public function get_element_value(string $key) {
+        $result = null;
+        $this->plist_map( function($elvalue, $elkey, $parent) use ($key, &$result) {
+            // Convert date to iso 8601 if date object.
+            if ($key === $elkey) {
+                $result = $elvalue->getValue();
+            }
+        }, $this->cfpropertylist->getValue());
+
+        if (is_array($result)) {
+            // Turn CFType elements in PHP elements.
+            $result = $this->array_serialize_cftypes($result);
+        }
+        return $result;
+    }
+
+    /**
+     * Update the value of any element with matching key.
+     *
+     * Only allow string, number and boolean elements to be updated.
+     *
+     * @param string $key Key of element to update.
+     * @param mixed $value Value to update element with.
+     */
+    public function update_element_value(string $key, $value) {
+        if (is_array($value)) {
+            throw new \invalid_parameter_exception('Use update_element_array to update a collection.');
+        }
+        $this->plist_map( function($elvalue, $elkey, $parent) use ($key, $value) {
+            // Set new value.
+            if ($key === $elkey) {
+                $element = $parent->get($elkey);
+                // Limit update to boolean and strings types, and check value matches expected type.
+                if (($element instanceof CFString && is_string($value))
+                        || ($element instanceof CFNumber && is_numeric($value))
+                        || ($element instanceof CFBoolean && is_bool($value))) {
+                    $element->setValue($value);
+                } else {
+                    throw new \invalid_parameter_exception(
+                            'Only string, number and boolean elements can be updated, or value type does not match element type: '
+                            . get_class($element));
+                }
+            }
+        }, $this->cfpropertylist->getValue());
+    }
+
+    /**
+     * Update the array of any dict or array element with matching key.
+     *
+     * Will replace array.
+     *
+     * @param string $key Key of element to update.
+     * @param array $value Array to update element with.
+     */
+    public function update_element_array(string $key, array $value) {
+        // Validate new array.
+        foreach ($value as $element) {
+            // If any element is not a CFType instance, then throw exception.
+            if (!($element instanceof CFType)) {
+                throw new \invalid_parameter_exception('New array must only contain CFType objects.');
+            }
+        }
+        $this->plist_map( function($elvalue, $elkey, $parent) use ($key, $value) {
+            if ($key === $elkey) {
+                $element = $parent->get($elkey);
+                // Replace existing element with new element and array but same key.
+                if ($element instanceof CFDictionary) {
+                    $parent->del($elkey);
+                    $parent->add($elkey, new CFDictionary($value));
+                } else if ($element instanceof CFArray) {
+                    $parent->del($elkey);
+                    $parent->add($elkey, new CFArray($value));
+                }
+            }
+        }, $this->cfpropertylist->getValue());
+    }
+
+    /**
+     * Delete any element with a matching key.
+     *
+     * @param string $key Key of element to delete.
+     */
+    public function delete_element(string $key) {
+        $this->plist_map( function($elvalue, $elkey, $parent) use ($key) {
+            // Convert date to iso 8601 if date object.
+            if ($key === $elkey) {
+                $parent->del($key);
+            }
+        }, $this->cfpropertylist->getValue());
+    }
+
+    /**
+     * Helper function to either set or update a CF type value to the plist.
+     *
+     * @param string $key
+     * @param CFType $input
+     */
+    public function set_or_update_value(string $key, CFType $input) {
+        $value = $this->get_element_value($key);
+        if (empty($value)) {
+            $this->add_element_to_root($key, $input);
+        } else {
+            $this->update_element_value($key, $input->getValue());
+        }
+    }
+
+    /**
+     * Convert the PList to XML.
+     *
+     * @return string XML ready for creating an XML file.
+     */
+    public function to_xml() : string {
+        return $this->cfpropertylist->toXML();
+    }
+
+    /**
+     * Return a JSON representation of the PList. The JSON is constructed to be used to generate a SEB Config Key.
+     *
+     * See the developer documention for SEB for more information on the requirements on generating a SEB Config Key.
+     * https://safeexambrowser.org/developer/seb-config-key.html
+     *
+     * 1. Don't add any whitespace or line formatting to the SEB-JSON string.
+     * 2. Don't add character escaping (also backshlashes "\" as found in URL filter rules should not be escaped).
+     * 3. All <dict> elements from the plist XML must be ordered (alphabetically sorted) by their key names. Use a
+     * recursive method to apply ordering also to nested dictionaries contained in the root-level dictionary and in
+     * arrays. Use non-localized (culture invariant), non-ASCII value based case insensitive ordering. For example the
+     * key <key>allowWlan</key> comes before <key>allowWLAN</key>. Cocoa/Obj-C and .NET/C# usually use this case
+     * insensitive ordering as default, but PHP for example doesn't.
+     * 4. Remove empty <dict> elements (key/value). Current versions of SEB clients should anyways not generate empty
+     * dictionaries, but this was possible with outdated versions. If config files have been generated that time, such
+     * elements might still be around.
+     * 5. All string elements must be UTF8 encoded.
+     * 6. Base16 strings should use lower-case a-f characters, even though this isn't relevant in the current
+     * implementation of the Config Key calculation.
+     * 7. <data> plist XML elements must be converted to Base64 strings.
+     * 8. <date> plist XML elements must be converted to ISO 8601 formatted strings.
+     *
+     * @return string A json encoded string.
+     */
+    public function to_json() : string {
+        // Create a clone of the PList, so main list isn't mutated.
+        $jsonplist = new CFPropertyList();
+        $jsonplist->parse($this->cfpropertylist->toXML(), CFPropertyList::FORMAT_XML);
+
+        // Pass root dict to recursively convert dates to ISO 8601 format, encode strings to UTF-8,
+        // lock data to Base 64 encoding and remove empty dictionaries.
+        $this->prepare_plist_for_json_encoding($jsonplist->getValue());
+
+        // Serialize PList to array.
+        $plistarray = $jsonplist->toArray();
+
+        // Sort array alphabetically by key using case insensitive, natural sorting. See point 3 for more information.
+        $plistarray = $this->array_sort($plistarray);
+
+        // Encode in JSON with following rules from SEB docs.
+        // 1. Don't add any whitespace or line formatting to the SEB-JSON string.
+        // 2. Don't add unicode or slash escaping.
+        $json = json_encode($plistarray, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
+
+        // There is no way to prevent json_encode from escaping backslashes. We replace each backslash with a unique string
+        // prior to encoding in prepare_plist_for_json_encoding(). We can then replace the substitute with a single backslash.
+        $json = str_replace(self::BACKSLASH_SUBSTITUTE, "\\", $json);
+        return $json;
+    }
+
+    /**
+     * Recursively convert PList date values from unix to iso 8601 format, and ensure strings are UTF 8 encoded.
+     *
+     * This will mutate the PList.
+     */
+
+    /**
+     * Recursively convert PList date values from unix to iso 8601 format, and ensure strings are UTF 8 encoded.
+     *
+     * This will mutate the PList.
+     * @param \Iterator $root The root element of the PList. Must be a dictionary or array.
+     */
+    private function prepare_plist_for_json_encoding($root) {
+        $this->plist_map( function($value, $key, $parent) {
+            // Convert date to ISO 8601 if date object.
+            if ($value instanceof CFDate) {
+                $date = DateTime::createFromFormat('U', $value->getValue());
+                $date->setTimezone(new \DateTimeZone('UTC')); // Zulu timezone a.k.a. UTC+00.
+                $isodate = $date->format('c');
+                $value->setValue($isodate);
+            }
+            // Make sure strings are UTF 8 encoded.
+            if ($value instanceof CFString) {
+                // As literal backslashes will be lost during encoding, we must replace them with a unique substitute to be
+                // reverted after JSON encoding.
+                $string = str_replace("\\", self::BACKSLASH_SUBSTITUTE, $value->getValue());
+                $value->setValue(mb_convert_encoding($string, 'UTF-8'));
+            }
+            // Data should remain base 64 encoded, so convert to base encoded string for export. Otherwise
+            // CFData will decode the data when serialized.
+            if ($value instanceof CFData) {
+                $data = trim($value->getCodedValue());
+                $parent->del($key);
+                $parent->add($key, new CFString($data));
+            }
+            // Empty dictionaries should be removed.
+            if ($value instanceof CFDictionary && empty($value->getValue())) {
+                $parent->del($key);
+            }
+        }, $root);
+
+    }
+
+    /**
+     * Iterate through the PList elements, and call the callback on each.
+     *
+     * @param callable $callback A callback function called for every element.
+     * @param \Iterator $root The root element of the PList. Must be a dictionary or array.
+     * @param bool $recursive Whether the function should traverse dicts and arrays recursively.
+     */
+    private function plist_map(callable $callback, \Iterator $root, bool $recursive = true) {
+        $root->rewind();
+        while ($root->valid()) {
+            $value = $root->current();
+            $key = $root->key();
+
+            // Recursively traverse all dicts and arrays if flag is true.
+            if ($recursive && $value instanceof \Iterator) {
+                $this->plist_map($callback, $value);
+            }
+
+            // Callback function called for every element.
+            $callback($value, $key, $root);
+
+            $root->next();
+        }
+    }
+
+    /**
+     * Recursively sort array alphabetically by key.
+     *
+     * @param array $array Top level array to process.
+     * @return array Processed array.
+     */
+    private function array_sort(array $array) {
+        foreach ($array as $key => $value) {
+            if (is_array($value)) {
+                $array[$key] = $this->array_sort($array[$key]);
+            }
+        }
+        // Sort assoc array. From SEB docs - "Use non-localized (culture invariant), non-ASCII value based case
+        // insensitive ordering."
+        if ($this->is_associative_array($array)) {
+            ksort($array, SORT_STRING | SORT_FLAG_CASE);
+        }
+
+        return $array;
+    }
+
+    /**
+     * Recursively remove empty arrays.
+     *
+     * @param array $array Top level array to process.
+     * @return array Processed array.
+     */
+    private function array_remove_empty_arrays(array $array) {
+        foreach ($array as $key => $value) {
+            if (is_array($value)) {
+                $array[$key] = $this->array_remove_empty_arrays($array[$key]);
+            }
+
+            // Remove empty arrays.
+            if (is_array($array[$key]) && empty($array[$key])) {
+                unset($array[$key]);
+            }
+        }
+
+        return $array;
+    }
+
+    /**
+     * If an array contains CFType objects, wrap array in a CFDictionary to allow recursive serialization of data
+     * into a standard PHP array.
+     *
+     * @param array $array Array containing CFType objects.
+     * @return array Standard PHP array.
+     */
+    private function array_serialize_cftypes(array $array) : array {
+        $array = new CFDictionary($array); // Convert back to CFDictionary so serialization is recursive.
+        return $array->toArray(); // Serialize.
+    }
+
+    /**
+     * Check if an array is associative or sequential.
+     *
+     * @param array $array Array to check.
+     * @return bool False if not associative.
+     */
+    private function is_associative_array(array $array) {
+        if (empty($array)) {
+            return false;
+        }
+        // Check that all keys are not sequential integers starting from 0 (Which is what PHP arrays have behind the scenes.)
+        return array_keys($array) !== range(0, count($array) - 1);
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/quiz_settings.php b/mod/quiz/accessrule/seb/classes/quiz_settings.php
new file mode 100644 (file)
index 0000000..9511efe
--- /dev/null
@@ -0,0 +1,648 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Entity model representing quiz settings for the seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use CFPropertyList\CFArray;
+use CFPropertyList\CFBoolean;
+use CFPropertyList\CFDictionary;
+use CFPropertyList\CFNumber;
+use CFPropertyList\CFString;
+use core\persistent;
+use lang_string;
+use moodle_exception;
+use moodle_url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Entity model representing quiz settings for the seb plugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_settings extends persistent {
+
+    /** Table name for the persistent. */
+    const TABLE = 'quizaccess_seb_quizsettings';
+
+    /** @var property_list $plist The SEB config represented as a Property List object. */
+    private $plist;
+
+    /** @var string $config The SEB config represented as a string. */
+    private $config;
+
+    /** @var string $configkey The SEB config key represented as a string. */
+    private $configkey;
+
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() : array {
+        return [
+            'quizid' => [
+                'type' => PARAM_INT,
+            ],
+            'cmid' => [
+                'type' => PARAM_INT,
+            ],
+            'templateid' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+            ],
+            'requiresafeexambrowser' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+            ],
+            'showsebtaskbar' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'showwificontrol' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'showreloadbutton' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'showtime' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'showkeyboardlayout' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'allowuserquitseb' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'quitpassword' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'linkquitseb' => [
+                'type' => PARAM_URL,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'userconfirmquit' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'enableaudiocontrol' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'muteonstartup' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'allowspellchecking' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'allowreloadinexam' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'activateurlfiltering' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'filterembeddedcontent' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'expressionsallowed' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'regexallowed' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'expressionsblocked' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'regexblocked' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'showsebdownloadlink' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'allowedbrowserexamkeys' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+        ];
+    }
+
+    /**
+     * Return an instance by quiz id.
+     *
+     * This method gets data from cache before doing any DB calls.
+     *
+     * @param int $quizid Quiz id.
+     * @return false|\quizaccess_seb\quiz_settings
+     */
+    public static function get_by_quiz_id(int $quizid) {
+        if ($data = self::get_quiz_settings_cache()->get($quizid)) {
+            return new static(0, $data);
+        }
+
+        return self::get_record(['quizid' => $quizid]);
+    }
+
+    /**
+     * Return cached SEB config represented as a string by quiz ID.
+     *
+     * @param int $quizid Quiz id.
+     * @return string|null
+     */
+    public static function get_config_by_quiz_id(int $quizid) : ?string {
+        $config = self::get_config_cache()->get($quizid);
+
+        if ($config !== false) {
+            return $config;
+        }
+
+        $config = null;
+        if ($settings = self::get_by_quiz_id($quizid)) {
+            $config = $settings->get_config();
+            self::get_config_cache()->set($quizid, $config);
+        }
+
+        return $config;
+    }
+
+    /**
+     * Return cached SEB config key by quiz ID.
+     *
+     * @param int $quizid Quiz id.
+     * @return string|null
+     */
+    public static function get_config_key_by_quiz_id(int $quizid) : ?string {
+        $configkey = self::get_config_key_cache()->get($quizid);
+
+        if ($configkey !== false) {
+            return $configkey;
+        }
+
+        $configkey = null;
+        if ($settings = self::get_by_quiz_id($quizid)) {
+            $configkey = $settings->get_config_key();
+            self::get_config_key_cache()->set($quizid, $configkey);
+        }
+
+        return $configkey;
+    }
+
+    /**
+     * Return SEB config key cache instance.
+     *
+     * @return \cache_application
+     */
+    private static function get_config_key_cache() : \cache_application {
+        return \cache::make('quizaccess_seb', 'configkey');
+    }
+
+    /**
+     * Return SEB config cache instance.
+     *
+     * @return \cache_application
+     */
+    private static function get_config_cache() : \cache_application {
+        return \cache::make('quizaccess_seb', 'config');
+    }
+
+    /**
+     * Return quiz settings cache object,
+     *
+     * @return \cache_application
+     */
+    private static function get_quiz_settings_cache() : \cache_application {
+        return \cache::make('quizaccess_seb', 'quizsettings');
+    }
+
+    /**
+     * Adds the new record to the cache.
+     */
+    protected function after_create() {
+        $this->after_save();
+    }
+
+    /**
+     * Updates the cache record.
+     *
+     * @param bool $result
+     */
+    protected function after_update($result) {
+        $this->after_save();
+    }
+
+    /**
+     * Helper method to execute common stuff after create and update.
+     */
+    private function after_save() {
+        self::get_quiz_settings_cache()->set($this->get('quizid'), $this->to_record());
+        self::get_config_cache()->set($this->get('quizid'), $this->config);
+        self::get_config_key_cache()->set($this->get('quizid'), $this->configkey);
+    }
+
+    /**
+     * Removes unnecessary stuff from db.
+     */
+    protected function before_delete() {
+        $key = $this->get('quizid');
+        self::get_quiz_settings_cache()->delete($key);
+        self::get_config_cache()->delete($key);
+        self::get_config_key_cache()->delete($key);
+    }
+
+    /**
+     * Validate the browser exam keys string.
+     *
+     * @param string $keys Newline separated browser exam keys.
+     * @return true|lang_string If there is an error, an error string is returned.
+     */
+    protected function validate_allowedbrowserexamkeys($keys) {
+        $keys = $this->split_keys($keys);
+        foreach ($keys as $i => $key) {
+            if (!preg_match('~^[a-f0-9]{64}$~', $key)) {
+                return new lang_string('allowedbrowserkeyssyntax', 'quizaccess_seb');
+            }
+        }
+        if (count($keys) != count(array_unique($keys))) {
+            return new lang_string('allowedbrowserkeysdistinct', 'quizaccess_seb');
+        }
+        return true;
+    }
+
+    /**
+     * Get the browser exam keys as a pre-split array instead of just as a string.
+     *
+     * @return array
+     */
+    protected function get_allowedbrowserexamkeys() : array {
+        $keysstring = $this->raw_get('allowedbrowserexamkeys');
+        $keysstring = empty($keysstring) ? '' : $keysstring;
+        return $this->split_keys($keysstring);
+    }
+
+    /**
+     * Hook to execute before an update.
+     *
+     * Please note that at this stage the data has already been validated and therefore
+     * any new data being set will not be validated before it is sent to the database.
+     */
+    protected function before_update() {
+        $this->before_save();
+    }
+
+    /**
+     * Hook to execute before a create.
+     *
+     * Please note that at this stage the data has already been validated and therefore
+     * any new data being set will not be validated before it is sent to the database.
+     */
+    protected function before_create() {
+        $this->before_save();
+    }
+
+    /**
+     * As there is no hook for before both create and update, this function is called by both hooks.
+     */
+    private function before_save() {
+        // Set template to 0 if using anything different to template.
+        if ($this->get('requiresafeexambrowser') != settings_provider::USE_SEB_TEMPLATE) {
+            $this->set('templateid', 0);
+        }
+
+        // Process configs to make sure that all data is set correctly.
+        $this->process_configs();
+    }
+
+    /**
+     * Before validate hook.
+     */
+    protected function before_validate() {
+        // Template can't be null.
+        if (is_null($this->get('templateid'))) {
+            $this->set('templateid', 0);
+        }
+    }
+
+    /**
+     * Create or update the config string based on the current quiz settings.
+     */
+    private function process_configs() {
+        switch ($this->get('requiresafeexambrowser')) {
+            case settings_provider::USE_SEB_NO:
+                $this->process_seb_config_no();
+                break;
+
+            case settings_provider::USE_SEB_CONFIG_MANUALLY:
+                $this->process_seb_config_manually();
+                break;
+
+            case settings_provider::USE_SEB_TEMPLATE:
+                $this->process_seb_template();
+                break;
+
+            case settings_provider::USE_SEB_UPLOAD_CONFIG:
+                $this->process_seb_upload_config();
+                break;
+
+            default: // Also settings_provider::USE_SEB_CLIENT_CONFIG.
+                $this->process_seb_client_config();
+        }
+
+        // Generate config key based on given SEB config.
+        if (!empty($this->config)) {
+            $this->configkey = config_key::generate($this->config)->get_hash();
+        } else {
+            $this->configkey = null;
+        }
+    }
+
+    /**
+     * Return SEB config key.
+     *
+     * @return string|null
+     */
+    public function get_config_key() : ?string {
+        $this->process_configs();
+
+        return $this->configkey;
+    }
+
+    /**
+     * Return string representation of the config.
+     *
+     * @return string|null
+     */
+    public function get_config() : ?string {
+        $this->process_configs();
+
+        return $this->config;
+    }
+
+    /**
+     * Case for USE_SEB_NO.
+     */
+    private function process_seb_config_no() {
+        $this->config = null;
+    }
+
+    /**
+     * Case for USE_SEB_CONFIG_MANUALLY. This creates a plist and applies all settings from the posted form, along with
+     * some defaults.
+     */
+    private function process_seb_config_manually() {
+        // If at any point a configuration file has been uploaded and parsed, clear the settings.
+        $this->plist = new property_list();
+
+        $this->process_bool_settings();
+        $this->process_quit_password_settings();
+        $this->process_quit_url_from_settings();
+        $this->process_url_filters();
+        $this->process_required_enforced_settings();
+
+        // One of the requirements for USE_SEB_CONFIG_MANUALLY is setting examSessionClearCookiesOnStart to false.
+        $this->plist->set_or_update_value('examSessionClearCookiesOnStart', new CFBoolean(false));
+        $this->config = $this->plist->to_xml();
+    }
+
+    /**
+     * Case for USE_SEB_TEMPLATE. This creates a plist from the template uploaded, then applies the quit password
+     * setting and some defaults.
+     */
+    private function process_seb_template() {
+        $template = template::get_record(['id' => $this->get('templateid')]);
+        $this->plist = new property_list($template->get('content'));
+
+        $this->process_quit_password_settings();
+        $this->process_quit_url_from_template_or_config();
+        $this->process_required_enforced_settings();
+
+        $this->config = $this->plist->to_xml();
+    }
+
+    /**
+     * Case for USE_SEB_UPLOAD_CONFIG. This creates a plist from an uploaded configuration file, then applies the quiz
+     * password settings and some defaults.
+     */
+    private function process_seb_upload_config() {
+        $file = settings_provider::get_module_context_sebconfig_file($this->get('cmid'));
+
+        // If there was no file, create an empty plist so the rest of this wont explode.
+        if (empty($file)) {
+            throw new moodle_exception('noconfigfilefound', 'quizaccess_seb', '', $this->get('cmid'));
+        } else {
+            $this->plist = new property_list($file->get_content());
+        }
+
+        $this->process_quit_password_settings();
+        $this->process_quit_url_from_template_or_config();
+        $this->process_required_enforced_settings();
+
+        $this->config = $this->plist->to_xml();
+    }
+
+    /**
+     * Case for USE_SEB_CLIENT_CONFIG. This creates an empty plist to remove the config stored.
+     */
+    private function process_seb_client_config() {
+        $this->config = null;
+    }
+
+    /**
+     * Sets or updates some sensible default settings, these are the items 'startURL' and 'sendBrowserExamKey'.
+     */
+    private function process_required_enforced_settings() {
+        global $CFG;
+
+        $quizurl = new moodle_url($CFG->wwwroot . "/mod/quiz/view.php", ['id' => $this->get('cmid')]);
+        $this->plist->set_or_update_value('startURL', new CFString($quizurl->out(true)));
+        $this->plist->set_or_update_value('sendBrowserExamKey', new CFBoolean(true));
+    }
+
+    /**
+     * Use the boolean map to add Moodle boolean setting to config PList.
+     */
+    private function process_bool_settings() {
+        $settings = $this->to_record();
+        $map = $this->get_bool_seb_setting_map();
+        foreach ($settings as $setting => $value) {
+            if (isset($map[$setting])) {
+                $enabled = $value == 1 ? true : false;
+                $this->plist->add_element_to_root($map[$setting], new CFBoolean($enabled));
+            }
+        }
+    }
+
+    /**
+     * Turn hashed quit password and quit link into PList strings and add to config PList.
+     */
+    private function process_quit_password_settings() {
+        $settings = $this->to_record();
+        if (!empty($settings->quitpassword) && is_string($settings->quitpassword)) {
+            // Hash quit password.
+            $hashedpassword = hash('SHA256', $settings->quitpassword);
+            $this->plist->add_element_to_root('hashedQuitPassword', new CFString($hashedpassword));
+        }
+    }
+
+    /**
+     * Sets the quitURL if found in the quiz_settings.
+     */
+    private function process_quit_url_from_settings() {
+        $settings = $this->to_record();
+        if (!empty($settings->linkquitseb) && is_string($settings->linkquitseb)) {
+            $this->plist->set_or_update_value('quitURL', new CFString($settings->linkquitseb));
+        }
+    }
+
+    /**
+     * Sets the quiz_setting's linkquitseb if a quitURL value was found in a template or uploaded config.
+     */
+    private function process_quit_url_from_template_or_config() {
+        // Does the plist (template or config file) have an existing quitURL?
+        $quiturl = $this->plist->get_element_value('quitURL');
+        if (!empty($quiturl)) {
+            $this->set('linkquitseb', $quiturl);
+        }
+    }
+
+    /**
+     * Turn return separated strings for URL filters into a PList array and add to config PList.
+     */
+    private function process_url_filters() {
+        $settings = $this->to_record();
+        // Create rules to each expression provided and add to config.
+        $urlfilterrules = [];
+        // Get all rules separated by newlines and remove empty rules.
+        $expallowed = array_filter(explode(PHP_EOL, $settings->expressionsallowed));
+        $expblocked = array_filter(explode(PHP_EOL, $settings->expressionsblocked));
+        $regallowed = array_filter(explode(PHP_EOL, $settings->regexallowed));
+        $regblocked = array_filter(explode(PHP_EOL, $settings->regexblocked));
+        foreach ($expallowed as $rulestring) {
+            $urlfilterrules[] = $this->create_filter_rule($rulestring, true, false);
+        }
+        foreach ($expblocked as $rulestring) {
+            $urlfilterrules[] = $this->create_filter_rule($rulestring, false, false);
+        }
+        foreach ($regallowed as $rulestring) {
+            $urlfilterrules[] = $this->create_filter_rule($rulestring, true, true);
+        }
+        foreach ($regblocked as $rulestring) {
+            $urlfilterrules[] = $this->create_filter_rule($rulestring, false, true);
+        }
+        $this->plist->add_element_to_root('URLFilterRules', new CFArray($urlfilterrules));
+    }
+
+    /**
+     * Create a CFDictionary represeting a URL filter rule.
+     *
+     * @param string $rulestring The expression to filter with.
+     * @param bool $allowed Allowed or blocked.
+     * @param bool $isregex Regex or simple.
+     * @return CFDictionary A PList dictionary.
+     */
+    private function create_filter_rule(string $rulestring, bool $allowed, bool $isregex) : CFDictionary {
+        $action = $allowed ? 1 : 0;
+        return new CFDictionary([
+                    'action' => new CFNumber($action),
+                    'active' => new CFBoolean(true),
+                    'expression' => new CFString(trim($rulestring)),
+                    'regex' => new CFBoolean($isregex),
+                    ]);
+    }
+
+    /**
+     * Map the settings that are booleans to the Safe Exam Browser config keys.
+     *
+     * @return array Moodle setting as key, SEB setting as value.
+     */
+    private function get_bool_seb_setting_map() : array {
+        return [
+            'activateurlfiltering' => 'URLFilterEnable',
+            'allowspellchecking' => 'allowSpellCheck',
+            'allowreloadinexam' => 'browserWindowAllowReload',
+            'allowuserquitseb' => 'allowQuit',
+            'enableaudiocontrol' => 'audioControlEnabled',
+            'filterembeddedcontent' => 'URLFilterEnableContentFilter',
+            'muteonstartup' => 'audioMute',
+            'showkeyboardlayout' => 'showInputLanguage',
+            'showreloadbutton' => 'showReloadButton',
+            'showsebtaskbar' => 'showTaskBar',
+            'showtime' => 'showTime',
+            'showwificontrol' => 'allowWlan',
+            'userconfirmquit' => 'quitURLConfirm',
+        ];
+    }
+
+    /**
+     * This helper method takes list of browser exam keys in a string and splits it into an array of separate keys.
+     *
+     * @param string|null $keys the allowed keys.
+     * @return array of string, the separate keys.
+     */
+    private function split_keys($keys) : array {
+        $keys = preg_split('~[ \t\n\r,;]+~', $keys, -1, PREG_SPLIT_NO_EMPTY);
+        foreach ($keys as $i => $key) {
+            $keys[$i] = strtolower($key);
+        }
+        return $keys;
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/settings_provider.php b/mod/quiz/accessrule/seb/classes/settings_provider.php
new file mode 100644 (file)
index 0000000..86ef593
--- /dev/null
@@ -0,0 +1,1119 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for providing quiz settings, to make setting up quiz form manageable.
+ *
+ * To make sure there are no inconsistencies between data sets, run tests in tests/phpunit/settings_provider_test.php.
+ *
+ * @package    quizaccess_seb
+ * @author     Luca Bösch <luca.boesch@bfh.ch>
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use context_module;
+use context_user;
+use lang_string;
+use stdClass;
+use stored_file;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Helper class for providing quiz settings, to make setting up quiz form manageable.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class settings_provider {
+
+    /**
+     * No SEB should be used.
+     */
+    const USE_SEB_NO = 0;
+
+    /**
+     * Use SEB and configure it manually.
+     */
+    const USE_SEB_CONFIG_MANUALLY = 1;
+
+    /**
+     * Use SEB config from pre configured template.
+     */
+    const USE_SEB_TEMPLATE = 2;
+
+    /**
+     * Use SEB config from uploaded config file.
+     */
+    const USE_SEB_UPLOAD_CONFIG = 3;
+
+    /**
+     * Use client config. Not SEB config is required.
+     */
+    const USE_SEB_CLIENT_CONFIG = 4;
+
+    /**
+     * Insert form element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param \HTML_QuickForm_element $element Element to insert.
+     * @param string $before Insert element before.
+     */
+    protected static function insert_element(\mod_quiz_mod_form $quizform,
+                                             \MoodleQuickForm $mform, \HTML_QuickForm_element $element, $before = 'security') {
+        $mform->insertElementBefore($element, $before);
+    }
+
+    /**
+     * Remove element from the form.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     */
+    protected static function remove_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string  $elementname) {
+        if ($mform->elementExists($elementname)) {
+            $mform->removeElement($elementname);
+            $mform->setDefault($elementname, null);
+        }
+    }
+
+    /**
+     * Add help button to the element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     */
+    protected static function add_help_button(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname) {
+        if ($mform->elementExists($elementname)) {
+            $mform->addHelpButton($elementname, $elementname, 'quizaccess_seb');
+        }
+    }
+
+    /**
+     * Set default value for the element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     * @param mixed $value Default value.
+     */
+    protected static function set_default(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string  $elementname, $value) {
+        $mform->setDefault($elementname, $value);
+    }
+
+    /**
+     * Set element type.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     * @param string $type Type of the form element.
+     */
+    protected static function set_type(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname, string $type) {
+        $mform->setType($elementname, $type);
+    }
+
+    /**
+     * Freeze form element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     */
+    protected static function freeze_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname) {
+        if ($mform->elementExists($elementname)) {
+            $mform->freeze($elementname);
+        }
+    }
+
+    /**
+     * Add SEB header element to  the form.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_header_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        global  $OUTPUT;
+
+        $element = $mform->createElement('header', 'seb', get_string('seb', 'quizaccess_seb'));
+        self::insert_element($quizform, $mform, $element);
+
+        // Display notification about locked settings.
+        if (self::is_seb_settings_locked($quizform->get_instance())) {
+            $notify = new \core\output\notification(
+                get_string('settingsfrozen', 'quizaccess_seb'),
+                \core\output\notification::NOTIFY_WARNING
+            );
+
+            $notifyelement = $mform->createElement('html', $OUTPUT->render($notify));
+            self::insert_element($quizform, $mform, $notifyelement);
+        }
+
+        if (self::is_conflicting_permissions($quizform->get_context())) {
+            $notify = new \core\output\notification(
+                get_string('conflictingsettings', 'quizaccess_seb'),
+                \core\output\notification::NOTIFY_WARNING
+            );
+
+            $notifyelement = $mform->createElement('html', $OUTPUT->render($notify));
+            self::insert_element($quizform, $mform, $notifyelement);
+        }
+    }
+
+    /**
+     * Add SEB usage element with all available options.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_usage_options(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        $element = $mform->createElement(
+            'select',
+            'seb_requiresafeexambrowser',
+            get_string('seb_requiresafeexambrowser', 'quizaccess_seb'),
+            self::get_requiresafeexambrowser_options($quizform->get_context())
+        );
+
+        self::insert_element($quizform, $mform, $element);
+        self::set_type($quizform, $mform, 'seb_requiresafeexambrowser', PARAM_INT);
+        self::set_default($quizform, $mform, 'seb_requiresafeexambrowser', self::USE_SEB_NO);
+        self::add_help_button($quizform, $mform, 'seb_requiresafeexambrowser');
+
+        if (self::is_conflicting_permissions($quizform->get_context())) {
+            self::freeze_element($quizform, $mform, 'seb_requiresafeexambrowser');
+        }
+    }
+
+    /**
+     * Add Templates element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_templates(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::can_use_seb_template($quizform->get_context()) || self::is_conflicting_permissions($quizform->get_context())) {
+            $element = $mform->createElement(
+                'select',
+                'seb_templateid',
+                get_string('seb_templateid', 'quizaccess_seb'),
+                self::get_template_options()
+            );
+        } else {
+            $element = $mform->createElement('hidden', 'seb_templateid');
+        }
+
+        self::insert_element($quizform, $mform, $element);
+        self::set_type($quizform, $mform, 'seb_templateid', PARAM_INT);
+        self::set_default($quizform, $mform, 'seb_templateid', 0);
+        self::add_help_button($quizform, $mform, 'seb_templateid');
+
+        // In case if the user can't use templates, but the quiz is configured to use them,
+        // we'd like to display template, but freeze it.
+        if (self::is_conflicting_permissions($quizform->get_context())) {
+            self::freeze_element($quizform, $mform, 'seb_templateid');
+        }
+    }
+
+    /**
+     * Add upload config file element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_config_file(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        $itemid = 0;
+
+        $draftitemid = 0;
+        file_prepare_draft_area(
+            $draftitemid,
+            $quizform->get_context()->id,
+            'quizaccess_seb',
+            'filemanager_sebconfigfile',
+            $itemid
+        );
+
+        if (self::can_upload_seb_file($quizform->get_context())) {
+            $element = $mform->createElement(
+                'filemanager',
+                'filemanager_sebconfigfile',
+                get_string('filemanager_sebconfigfile', 'quizaccess_seb'),
+                null,
+                self::get_filemanager_options()
+            );
+        } else {
+            $element = $mform->createElement('hidden', 'filemanager_sebconfigfile');
+        }
+
+        self::insert_element($quizform, $mform, $element);
+        self::set_type($quizform, $mform, 'filemanager_sebconfigfile', PARAM_RAW);
+        self::set_default($quizform, $mform, 'filemanager_sebconfigfile', $draftitemid);
+        self::add_help_button($quizform, $mform, 'filemanager_sebconfigfile');
+    }
+
+    /**
+     * Add Show Safe Exam Browser download button.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_show_download_link(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::can_change_seb_showsebdownloadlink($quizform->get_context())) {
+            $element = $mform->createElement('selectyesno',
+                'seb_showsebdownloadlink',
+                get_string('seb_showsebdownloadlink', 'quizaccess_seb')
+            );
+            self::insert_element($quizform, $mform, $element);
+            self::set_type($quizform, $mform, 'seb_showsebdownloadlink', PARAM_BOOL);
+            self::set_default($quizform, $mform, 'seb_showsebdownloadlink', 1);
+            self::add_help_button($quizform, $mform, 'seb_showsebdownloadlink');
+        }
+    }
+
+    /**
+     * Add Allowed Browser Exam Keys setting.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_allowedbrowserexamkeys(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::can_change_seb_allowedbrowserexamkeys($quizform->get_context())) {
+            $element = $mform->createElement('textarea',
+                'seb_allowedbrowserexamkeys',
+                get_string('seb_allowedbrowserexamkeys', 'quizaccess_seb')
+            );
+            self::insert_element($quizform, $mform, $element);
+            self::set_type($quizform, $mform, 'seb_allowedbrowserexamkeys', PARAM_RAW);
+            self::set_default($quizform, $mform, 'seb_allowedbrowserexamkeys', '');
+            self::add_help_button($quizform, $mform, 'seb_allowedbrowserexamkeys');
+        }
+    }
+
+    /**
+     * Add SEB config elements.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_config_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        $defaults = self::get_seb_config_element_defaults();
+        $types = self::get_seb_config_element_types();
+
+        foreach (self::get_seb_config_elements() as $name => $type) {
+            if (!self::can_manage_seb_config_setting($name, $quizform->get_context())) {
+                $type = 'hidden';
+            }
+
+            $element = $mform->createElement($type, $name, get_string($name, 'quizaccess_seb'));
+            self::insert_element($quizform, $mform, $element);
+            unset($element); // We need to make sure each &element only references the current element in loop.
+
+            self::add_help_button($quizform, $mform, $name);
+
+            if (isset($defaults[$name])) {
+                self::set_default($quizform, $mform, $name, $defaults[$name]);
+            }
+
+            if (isset($types[$name])) {
+                self::set_type($quizform, $mform, $name, $types[$name]);
+            }
+        }
+    }
+
+    /**
+     * Add setting fields.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    public static function add_seb_settings_fields(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::can_configure_seb($quizform->get_context())) {
+            self::add_seb_header_element($quizform, $mform);
+            self::add_seb_usage_options($quizform, $mform);
+            self::add_seb_templates($quizform, $mform);
+            self::add_seb_config_file($quizform, $mform);
+            self::add_seb_show_download_link($quizform, $mform);
+            self::add_seb_config_elements($quizform, $mform);
+            self::add_seb_allowedbrowserexamkeys($quizform, $mform);
+            self::hide_seb_elements($quizform, $mform);
+            self::lock_seb_elements($quizform, $mform);
+        }
+    }
+
+    /**
+     * Hide SEB elements if required.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function hide_seb_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        foreach (self::get_quiz_hideifs() as $elname => $rules) {
+            if ($mform->elementExists($elname)) {
+                foreach ($rules as $hideif) {
+                    $mform->hideIf(
+                        $hideif->get_element(),
+                        $hideif->get_dependantname(),
+                        $hideif->get_condition(),
+                        $hideif->get_dependantvalue()
+                    );
+                }
+            }
+        }
+    }
+
+    /**
+     * Lock SEB elements if required.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function lock_seb_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::is_seb_settings_locked($quizform->get_instance()) || self::is_conflicting_permissions($quizform->get_context())) {
+            // Freeze common quiz settings.
+            self::freeze_element($quizform, $mform, 'seb_requiresafeexambrowser');
+            self::freeze_element($quizform, $mform, 'seb_templateid');
+            self::freeze_element($quizform, $mform, 'seb_showsebdownloadlink');
+            self::freeze_element($quizform, $mform, 'seb_allowedbrowserexamkeys');
+
+            $quizsettings = quiz_settings::get_by_quiz_id((int) $quizform->get_instance());
+
+            // If the file has been uploaded, then replace it with the link to download the file.
+            if (!empty($quizsettings) && $quizsettings->get('requiresafeexambrowser') == self::USE_SEB_UPLOAD_CONFIG) {
+                self::remove_element($quizform, $mform, 'filemanager_sebconfigfile');
+                if ($link = self::get_uploaded_seb_file_download_link($quizform, $mform)) {
+                    $element = $mform->createElement(
+                        'static',
+                        'filemanager_sebconfigfile',
+                        get_string('filemanager_sebconfigfile', 'quizaccess_seb'),
+                        $link
+                    );
+                    self::insert_element($quizform, $mform, $element, 'seb_showsebdownloadlink');
+                }
+            }
+
+            // Remove template ID if not using template for this quiz.
+            if (empty($quizsettings) || $quizsettings->get('requiresafeexambrowser') != self::USE_SEB_TEMPLATE) {
+                $mform->removeElement('seb_templateid');
+            }
+
+            // Freeze all SEB specific settings.
+            foreach (self::get_seb_config_elements() as $element => $type) {
+                self::freeze_element($quizform, $mform, $element);
+            }
+        }
+    }
+
+    /**
+     * Return uploaded SEB config file link.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @return string
+     */
+    protected static function get_uploaded_seb_file_download_link(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) : string {
+        $link = '';
+        $file = self::get_module_context_sebconfig_file($quizform->get_coursemodule()->id);
+
+        if ($file) {
+            $url = \moodle_url::make_pluginfile_url(
+                $file->get_contextid(),
+                $file->get_component(),
+                $file->get_filearea(),
+                $file->get_itemid(),
+                $file->get_filepath(),
+                $file->get_filename(),
+                true
+            );
+            $link = \html_writer::link($url, get_string('downloadsebconfig', 'quizaccess_seb'));
+        }
+
+        return $link;
+    }
+
+    /**
+     * Get the type of element for each of the form elements in quiz settings.
+     *
+     * Contains all setting elements. Array key is name of 'form element'/'database column (excluding prefix)'.
+     *
+     * @return array All quiz form elements to be added and their types.
+     */
+    public static function get_seb_config_elements() : array {
+        return [
+            'seb_linkquitseb' => 'text',
+            'seb_userconfirmquit' => 'selectyesno',
+            'seb_allowuserquitseb' => 'selectyesno',
+            'seb_quitpassword' => 'passwordunmask',
+            'seb_allowreloadinexam' => 'selectyesno',
+            'seb_showsebtaskbar' => 'selectyesno',
+            'seb_showreloadbutton' => 'selectyesno',
+            'seb_showtime' => 'selectyesno',
+            'seb_showkeyboardlayout' => 'selectyesno',
+            'seb_showwificontrol' => 'selectyesno',
+            'seb_enableaudiocontrol' => 'selectyesno',
+            'seb_muteonstartup' => 'selectyesno',
+            'seb_allowspellchecking' => 'selectyesno',
+            'seb_activateurlfiltering' => 'selectyesno',
+            'seb_filterembeddedcontent' => 'selectyesno',
+            'seb_expressionsallowed' => 'textarea',
+            'seb_regexallowed' => 'textarea',
+            'seb_expressionsblocked' => 'textarea',
+            'seb_regexblocked' => 'textarea',
+        ];
+    }
+
+
+    /**
+     * Get the types of the quiz settings elements.
+     * @return array List of types for the setting elements.
+     */
+    public static function get_seb_config_element_types() : array {
+        return [
+            'seb_linkquitseb' => PARAM_RAW,
+            'seb_userconfirmquit' => PARAM_BOOL,
+            'seb_allowuserquitseb' => PARAM_BOOL,
+            'seb_quitpassword' => PARAM_RAW,
+            'seb_allowreloadinexam' => PARAM_BOOL,
+            'seb_showsebtaskbar' => PARAM_BOOL,
+            'seb_showreloadbutton' => PARAM_BOOL,
+            'seb_showtime' => PARAM_BOOL,
+            'seb_showkeyboardlayout' => PARAM_BOOL,
+            'seb_showwificontrol' => PARAM_BOOL,
+            'seb_enableaudiocontrol' => PARAM_BOOL,
+            'seb_muteonstartup' => PARAM_BOOL,
+            'seb_allowspellchecking' => PARAM_BOOL,
+            'seb_activateurlfiltering' => PARAM_BOOL,
+            'seb_filterembeddedcontent' => PARAM_BOOL,
+            'seb_expressionsallowed' => PARAM_RAW,
+            'seb_regexallowed' => PARAM_RAW,
+            'seb_expressionsblocked' => PARAM_RAW,
+            'seb_regexblocked' => PARAM_RAW,
+        ];
+    }
+
+    /**
+     * Check that we have conflicting permissions.
+     *
+     * In Some point we can have settings save by the person who use specific
+     * type of SEB usage (e.g. use templates). But then another person who can't
+     * use template (but still can update other settings) edit the same quiz. This is
+     * conflict of permissions and we'd like to build the settings form having this in
+     * mind.
+     *
+     * @param \context $context Context used with capability checking.
+     *
+     * @return bool
+     */
+    public static function is_conflicting_permissions(\context $context) {
+        if ($context instanceof \context_course) {
+            return false;
+        }
+
+        $settings = quiz_settings::get_record(['cmid' => (int) $context->instanceid]);
+
+        if (empty($settings)) {
+            return false;
+        }
+
+        if (!self::can_use_seb_template($context) &&
+            $settings->get('requiresafeexambrowser') == self::USE_SEB_TEMPLATE) {
+            return true;
+        }
+
+        if (!self::can_upload_seb_file($context) &&
+            $settings->get('requiresafeexambrowser') == self::USE_SEB_UPLOAD_CONFIG) {
+            return true;
+        }
+
+        if (!self::can_configure_manually($context) &&
+            $settings->get('requiresafeexambrowser') == self::USE_SEB_CONFIG_MANUALLY) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a list of all options of SEB usage.
+     *
+     * @param \context $context Context used with capability checking selection options.
+     * @return array
+     */
+    public static function get_requiresafeexambrowser_options(\context $context) : array {
+        $options[self::USE_SEB_NO] = get_string('no');
+
+        if (self::can_configure_manually($context) || self::is_conflicting_permissions($context)) {
+            $options[self::USE_SEB_CONFIG_MANUALLY] = get_string('seb_use_manually', 'quizaccess_seb');
+        }
+
+        if (self::can_use_seb_template($context) || self::is_conflicting_permissions($context)) {
+            if (!empty(self::get_template_options())) {
+                $options[self::USE_SEB_TEMPLATE] = get_string('seb_use_template', 'quizaccess_seb');
+            }
+        }
+
+        if (self::can_upload_seb_file($context) || self::is_conflicting_permissions($context)) {
+            $options[self::USE_SEB_UPLOAD_CONFIG] = get_string('seb_use_upload', 'quizaccess_seb');
+        }
+
+        $options[self::USE_SEB_CLIENT_CONFIG] = get_string('seb_use_client', 'quizaccess_seb');
+
+        return $options;
+    }
+
+    /**
+     * Returns a list of templates.
+     * @return array
+     */
+    protected static function get_template_options() : array {
+        $templates = [];
+        $records = template::get_records(['enabled' => 1], 'name');
+        if ($records) {
+            foreach ($records as $record) {
+                $templates[$record->get('id')] = $record->get('name');
+            }
+        }
+
+        return $templates;
+    }
+
+    /**
+     * Returns a list of options for the file manager element.
+     * @return array
+     */
+    public static function get_filemanager_options() : array {
+        return [
+            'subdirs' => 0,
+            'maxfiles' => 1,
+            'accepted_types' => ['.seb']
+        ];
+    }
+
+    /**
+     * Get the default values of the quiz settings.
+     *
+     * Array key is name of 'form element'/'database column (excluding prefix)'.
+     *
+     * @return array List of settings and their defaults.
+     */
+    public static function get_seb_config_element_defaults() : array {
+        return [
+            'seb_linkquitseb' => '',
+            'seb_userconfirmquit' => 1,
+            'seb_allowuserquitseb' => 1,
+            'seb_quitpassword' => '',
+            'seb_allowreloadinexam' => 1,
+            'seb_showsebtaskbar' => 1,
+            'seb_showreloadbutton' => 1,
+            'seb_showtime' => 1,
+            'seb_showkeyboardlayout' => 1,
+            'seb_showwificontrol' => 0,
+            'seb_enableaudiocontrol' => 0,
+            'seb_muteonstartup' => 0,
+            'seb_allowspellchecking' => 0,
+            'seb_activateurlfiltering' => 0,
+            'seb_filterembeddedcontent' => 0,
+            'seb_expressionsallowed' => '',
+            'seb_regexallowed' => '',
+            'seb_expressionsblocked' => '',
+            'seb_regexblocked' => '',
+        ];
+    }
+
+    /**
+     * Validate that if a file has been uploaded by current user, that it is a valid PLIST XML file.
+     * This function is only called if requiresafeexambrowser == settings_provider::USE_SEB_UPLOAD_CONFIG.
+     *
+     * @param string $itemid Item ID of file in user draft file area.
+     * @return void|lang_string
+     */
+    public static function validate_draftarea_configfile($itemid) {
+        // When saving the settings, this value will be null.
+        if (is_null($itemid)) {
+            return;
+        }
+        // If there is a config file uploaded, make sure it is a PList XML file.
+        $file = self::get_current_user_draft_file($itemid);
+
+        // If we require an SEB config uploaded, and the file exists, parse it.
+        if ($file) {
+            if (!helper::is_valid_seb_config($file->get_content())) {
+                return new lang_string('fileparsefailed', 'quizaccess_seb');
+            }
+        }
+
+        // If we require an SEB config uploaded, and the file does not exist, error.
+        if (!$file) {
+            return new lang_string('filenotpresent', 'quizaccess_seb');
+        }
+    }
+
+    /**
+     * Try and get a file in the user draft filearea by itemid.
+     *
+     * @param string $itemid Item ID of the file.
+     * @return stored_file|null Returns null if no file is found.
+     */
+    public static function get_current_user_draft_file(string $itemid) : ?stored_file {
+        global $USER;
+        $context = context_user::instance($USER->id);
+        $fs = get_file_storage();
+        if (!$files = $fs->get_area_files($context->id, 'user', 'draft', $itemid, 'id DESC', false)) {
+            return null;
+        }
+        return reset($files);
+    }
+
+    /**
+     * Get the file that is stored in the course module file area.
+     *
+     * @param string $cmid The course module id which is used as an itemid reference.
+     * @return stored_file|null Returns null if no file is found.
+     */
+    public static function get_module_context_sebconfig_file(string $cmid) : ?stored_file {
+        $fs = new \file_storage();
+        $context = context_module::instance($cmid);
+
+        if (!$files = $fs->get_area_files($context->id, 'quizaccess_seb', 'filemanager_sebconfigfile', 0,
+            'id DESC', false)) {
+            return null;
+        }
+
+        return reset($files);
+    }
+
+    /**
+     * Saves filemanager_sebconfigfile files to the moodle storage backend.
+     *
+     * @param string $draftitemid The id of the draft area to use.
+     * @param string $cmid The cmid of for the quiz.
+     * @return bool Always true
+     */
+    public static function save_filemanager_sebconfigfile_draftarea(string $draftitemid, string $cmid) : bool {
+        if ($draftitemid) {
+            $context = context_module::instance($cmid);
+            file_save_draft_area_files($draftitemid, $context->id, 'quizaccess_seb', 'filemanager_sebconfigfile',
+                0, []);
+        }
+
+        return true;
+    }
+
+    /**
+     * Cleanup function to delete the saved config when it has not been specified.
+     * This will be called when settings_provider::USE_SEB_UPLOAD_CONFIG is not true.
+     *
+     * @param string $cmid The cmid of for the quiz.
+     * @return bool Always true or exception if error occurred
+     */
+    public static function delete_uploaded_config_file(string $cmid) : bool {
+        $file = self::get_module_context_sebconfig_file($cmid);
+
+        if (!empty($file)) {
+            return $file->delete();
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if the current user can configure SEB.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_configure_seb(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', $context);
+    }
+
+    /**
+     * Check if the current user can use preconfigured templates.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_use_seb_template(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_seb_templateid', $context);
+    }
+
+    /**
+     * Check if the current user can upload own SEB config file.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_upload_seb_file(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_filemanager_sebconfigfile', $context);
+    }
+
+    /**
+     * Check if the current user can change Show Safe Exam Browser download button setting.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_change_seb_showsebdownloadlink(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_seb_showsebdownloadlink', $context);
+    }
+
+    /**
+     * Check if the current user can change Allowed Browser Exam Keys setting.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_change_seb_allowedbrowserexamkeys(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_seb_allowedbrowserexamkeys', $context);
+    }
+
+    /**
+     * Check if the current user can config SEB manually.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_configure_manually(\context $context) : bool {
+        foreach (self::get_seb_config_elements() as $name => $type) {
+            if (self::can_manage_seb_config_setting($name, $context)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if the current user can manage provided SEB setting.
+     *
+     * @param string $settingname Name of the setting.
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_manage_seb_config_setting(string $settingname, \context $context) : bool {
+        $capsttocheck = [];
+
+        foreach (self::get_seb_settings_map() as $type => $settings) {
+            $capsttocheck = self::build_config_capabilities_to_check($settingname, $settings);
+            if (!empty($capsttocheck)) {
+                break;
+            }
+        }
+
+        foreach ($capsttocheck as $capability) {
+            // Capability must exist.
+            if (!$capinfo = get_capability_info($capability)) {
+                throw new \coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
+            }
+        }
+
+        return has_all_capabilities($capsttocheck, $context);
+    }
+
+    /**
+     * Helper method to build a list of capabilities to check.
+     *
+     * @param string $settingname Given setting name to build caps for.
+     * @param array $settings A list of settings to go through.
+     * @return array
+     */
+    protected static function build_config_capabilities_to_check(string $settingname, array $settings) : array {
+        $capsttocheck = [];
+
+        foreach ($settings as $setting => $children) {
+            if ($setting == $settingname) {
+                $capsttocheck[$setting] = self::build_setting_capability_name($setting);
+                break; // Found what we need exit the loop.
+            }
+
+            // Recursively check all children.
+            $capsttocheck = self::build_config_capabilities_to_check($settingname, $children);
+            if (!empty($capsttocheck)) {
+                // Matching child found, add the parent capability to the list of caps to check.
+                $capsttocheck[$setting] = self::build_setting_capability_name($setting);
+                break; // Found what we need exit the loop.
+            }
+        }
+
+        return $capsttocheck;
+    }
+
+    /**
+     * Helper method to return a map of all settings.
+     *
+     * @return array
+     */
+    public static function get_seb_settings_map() : array {
+        return [
+            self::USE_SEB_NO => [
+
+            ],
+            self::USE_SEB_CONFIG_MANUALLY => [
+                'seb_showsebdownloadlink' => [],
+                'seb_linkquitseb' => [],
+                'seb_userconfirmquit' => [],
+                'seb_allowuserquitseb' => [
+                    'seb_quitpassword' => []
+                ],
+                'seb_allowreloadinexam' => [],
+                'seb_showsebtaskbar' => [
+                    'seb_showreloadbutton' => [],
+                    'seb_showtime' => [],
+                    'seb_showkeyboardlayout' => [],
+                    'seb_showwificontrol' => [],
+                ],
+                'seb_enableaudiocontrol' => [
+                    'seb_muteonstartup' => [],
+                ],
+                'seb_allowspellchecking' => [],
+                'seb_activateurlfiltering' => [
+                    'seb_filterembeddedcontent' => [],
+                    'seb_expressionsallowed' => [],
+                    'seb_regexallowed' => [],
+                    'seb_expressionsblocked' => [],
+                    'seb_regexblocked' => [],
+                ],
+            ],
+            self::USE_SEB_TEMPLATE => [
+                'seb_templateid' => [],
+                'seb_showsebdownloadlink' => [],
+                'seb_allowuserquitseb' => [
+                    'seb_quitpassword' => [],
+                ],
+            ],
+            self::USE_SEB_UPLOAD_CONFIG => [
+                'filemanager_sebconfigfile' => [],
+                'seb_showsebdownloadlink' => [],
+                'seb_allowuserquitseb' => [
+                    'seb_quitpassword' => [],
+                ],
+                'seb_allowedbrowserexamkeys' => [],
+            ],
+            self::USE_SEB_CLIENT_CONFIG => [
+                'seb_showsebdownloadlink' => [],
+                'seb_allowedbrowserexamkeys' => [],
+            ],
+        ];
+    }
+
+    /**
+     * Get allowed settings for provided SEB usage type.
+     *
+     * @param int $requiresafeexambrowser SEB usage type.
+     * @return array
+     */
+    private static function get_allowed_settings(int $requiresafeexambrowser) : array {
+        $result = [];
+        $map = self::get_seb_settings_map();
+
+        if (!key_exists($requiresafeexambrowser, $map)) {
+            return $result;
+        }
+
+        return self::build_allowed_settings($map[$requiresafeexambrowser]);
+    }
+
+    /**
+     * Recursive method to build a list of allowed settings.
+     *
+     * @param array $settings A list of settings from settings map.
+     * @return array
+     */
+    private static function build_allowed_settings(array $settings) : array {
+        $result = [];
+
+        foreach ($settings as $name => $children) {
+            $result[] = $name;
+            foreach ($children as $childname => $child) {
+                $result[] = $childname;
+                $result = array_merge($result, self::build_allowed_settings($child));
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Get the conditions that an element should be hid in the form. Expects matching using 'eq'.
+     *
+     * Array key is name of 'form element'/'database column (excluding prefix)'.
+     * Values are instances of hideif_rule class.
+     *
+     * @return array List of rules per element.
+     */
+    public static function get_quiz_hideifs() : array {
+        $hideifs = [];
+
+        // We are building rules based on the settings map, that means children will be dependant on parent.
+        // In most cases it's all pretty standard.
+        // However it could be some specific cases for some fields, which will be overridden later.
+        foreach (self::get_seb_settings_map() as $type => $settings) {
+            foreach ($settings as $setting => $children) {
+                $hideifs[$setting][] = new hideif_rule($setting, 'seb_requiresafeexambrowser', 'noteq', $type);
+
+                foreach ($children as $childname => $child) {
+                    $hideifs[$childname][] = new hideif_rule($childname, 'seb_requiresafeexambrowser', 'noteq', $type);
+                    $hideifs[$childname][] = new hideif_rule($childname, $setting, 'eq', 0);
+                }
+            }
+        }
+
+        // Specific case for "Enable quitting of SEB". It should available for Manual, Template and Uploaded config.
+        $hideifs['seb_allowuserquitseb'] = [
+            new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO),
+            new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CLIENT_CONFIG),
+        ];
+
+        // Specific case for "Quit password". It should be available for Manual, Template and Uploaded config. As it's parent.
+        $hideifs['seb_quitpassword'] = [
+            new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO),
+            new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CLIENT_CONFIG),
+            new hideif_rule('seb_quitpassword', 'seb_allowuserquitseb', 'eq', 0),
+        ];
+
+        // Specific case for "Show Safe Exam Browser download button". It should be available for all cases, except No Seb.
+        $hideifs['seb_showsebdownloadlink'] = [
+            new hideif_rule('seb_showsebdownloadlink', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO)
+        ];
+
+        // Specific case for "Allowed Browser Exam Keys". It should be available for Template and Browser config.
+        $hideifs['seb_allowedbrowserexamkeys'] = [
+            new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO),
+            new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CONFIG_MANUALLY),
+            new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_TEMPLATE),
+        ];
+
+        return $hideifs;
+    }
+
+    /**
+     * Build a capability name for the provided SEB setting.
+     *
+     * @param string $settingname Name of the setting.
+     * @return string
+     */
+    public static function build_setting_capability_name(string $settingname) : string {
+        if (!key_exists($settingname, self::get_seb_config_elements())) {
+            throw new \coding_exception('Incorrect SEB quiz setting ' . $settingname);
+        }
+
+        return 'quizaccess/seb:manage_' . $settingname;
+    }
+
+    /**
+     * Check if settings is locked.
+     *
+     * @param int $quizid Quiz ID.
+     * @return bool
+     */
+    public static function is_seb_settings_locked($quizid) : bool {
+        if (empty($quizid)) {
+            return false;
+        }
+
+        return quiz_has_attempts($quizid);
+    }
+
+    /**
+     * Filter a standard class by prefix.
+     *
+     * @param stdClass $settings Quiz settings object.
+     * @return stdClass Filtered object.
+     */
+    private static function filter_by_prefix(\stdClass $settings): stdClass {
+        $newsettings = new \stdClass();
+        foreach ($settings as $name => $setting) {
+            // Only add it, if not there.
+            if (strpos($name, "seb_") === 0) {
+                $newsettings->$name = $setting; // Add new key.
+            }
+        }
+        return $newsettings;
+    }
+
+    /**
+     * Filter settings based on the setting map. Set value of not allowed settings to null.
+     *
+     * @param stdClass $settings Quiz settings.
+     * @return \stdClass
+     */
+    private static function filter_by_settings_map(stdClass $settings) : stdClass {
+        if (!isset($settings->seb_requiresafeexambrowser)) {
+            return $settings;
+        }
+
+        $newsettings = new \stdClass();
+        $newsettings->seb_requiresafeexambrowser = $settings->seb_requiresafeexambrowser;
+        $allowedsettings = self::get_allowed_settings((int)$newsettings->seb_requiresafeexambrowser);
+        unset($settings->seb_requiresafeexambrowser);
+
+        foreach ($settings as $name => $value) {
+            if (!in_array($name, $allowedsettings)) {
+                $newsettings->$name = null;
+            } else {
+                $newsettings->$name = $value;
+            }
+        }
+
+        return $newsettings;
+    }
+
+    /**
+     * Filter quiz settings for this plugin only.
+     *
+     * @param stdClass $settings Quiz settings.
+     * @return stdClass Filtered settings.
+     */
+    public static function filter_plugin_settings(stdClass $settings) : stdClass {
+        $settings = self::filter_by_prefix($settings);
+        $settings = self::filter_by_settings_map($settings);
+
+        return self::strip_all_prefixes($settings);
+    }
+
+    /**
+     * Strip the seb_ prefix from each setting key.
+     *
+     * @param \stdClass $settings Object containing settings.
+     * @return \stdClass The modified settings object.
+     */
+    private static function strip_all_prefixes(\stdClass $settings): stdClass {
+        $newsettings = new \stdClass();
+        foreach ($settings as $name => $setting) {
+            $newname = preg_replace("/^seb_/", "", $name);
+            $newsettings->$newname = $setting; // Add new key.
+        }
+        return $newsettings;
+    }
+
+    /**
+     * Add prefix to string.
+     *
+     * @param string $name String to add prefix to.
+     * @return string String with prefix.
+     */
+    public static function add_prefix(string $name): string {
+        if (strpos($name, 'seb_') !== 0) {
+            $name = 'seb_' . $name;
+        }
+        return $name;
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/template.php b/mod/quiz/accessrule/seb/classes/template.php
new file mode 100644 (file)
index 0000000..708bf2a
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Entity model representing template settings for the seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use core\persistent;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Entity model representing template settings for the seb plugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template extends persistent {
+
+    /** Table name for the persistent. */
+    const TABLE = 'quizaccess_seb_template';
+
+    /** @var property_list $plist The SEB config represented as a Property List object. */
+    private $plist;
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'name' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+            ],
+            'description' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+            ],
+            'content' => [
+                'type' => PARAM_RAW,
+            ],
+            'enabled' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+            ],
+            'sortorder' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+            ],
+        ];
+    }
+
+    /**
+     * Hook to execute before an update.
+     *
+     * Please note that at this stage the data has already been validated and therefore
+     * any new data being set will not be validated before it is sent to the database.
+     */
+    protected function before_update() {
+        $this->before_save();
+    }
+
+    /**
+     * Hook to execute before a create.
+     *
+     * Please note that at this stage the data has already been validated and therefore
+     * any new data being set will not be validated before it is sent to the database.
+     */
+    protected function before_create() {
+        $this->before_save();
+    }
+
+    /**
+     * As there is no hook for before both create and update, this function is called by both hooks.
+     */
+    private function before_save() {
+        $this->plist = new property_list($this->get('content'));
+        $this->set('content', $this->plist->to_xml());
+    }
+
+    /**
+     * Validate template content.
+     *
+     * @param string $content Content string to validate.
+     *
+     * @return bool|\lang_string
+     */
+    protected function validate_content(string $content) {
+        if (helper::is_valid_seb_config($content)) {
+            return true;
+        } else {
+            return new \lang_string('invalidtemplate', 'quizaccess_seb');
+        }
+    }
+
+    /**
+     * Check if we can delete the template.
+     *
+     * @return bool
+     */
+    public function can_delete() : bool {
+        $result = true;
+
+        if ($this->get('id')) {
+            $settings = quiz_settings::get_records(['templateid' => $this->get('id')]);
+            $result = empty($settings);
+        }
+
+        return $result;
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/classes/template_controller.php b/mod/quiz/accessrule/seb/classes/template_controller.php
new file mode 100644 (file)
index 0000000..7613a5b
--- /dev/null
@@ -0,0 +1,384 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for manipulating with the template records.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use core\notification;
+use quizaccess_seb\local\table\template_list;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for manipulating with the template records.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_controller {
+    /**
+     * View action.
+     */
+    const ACTION_VIEW = 'view';
+
+    /**
+     * Add action.
+     */
+    const ACTION_ADD = 'add';
+
+    /**
+     * Edit action.
+     */
+    const ACTION_EDIT = 'edit';
+
+    /**
+     * Delete action.
+     */
+    const ACTION_DELETE = 'delete';
+
+    /**
+     * Hide action.
+     */
+    const ACTION_HIDE = 'hide';
+
+    /**
+     * Show action.
+     */
+    const ACTION_SHOW = 'show';
+
+
+    /**
+     * Locally cached $OUTPUT object.
+     * @var \bootstrap_renderer
+     */
+    protected $output;
+
+    /**
+     * region_manager constructor.
+     */
+    public function __construct() {
+        global $OUTPUT;
+
+        $this->output = $OUTPUT;
+    }
+
+    /**
+     * Execute required action.
+     *
+     * @param string $action Action to execute.
+     */
+    public function execute($action) {
+
+        $this->set_external_page();
+
+        switch($action) {
+            case self::ACTION_ADD:
+            case self::ACTION_EDIT:
+                $this->edit($action, optional_param('id', null, PARAM_INT));
+                break;
+
+            case self::ACTION_DELETE:
+                $this->delete(required_param('id', PARAM_INT));
+                break;
+
+            case self::ACTION_HIDE:
+                $this->hide(required_param('id', PARAM_INT));
+                break;
+
+            case self::ACTION_SHOW:
+                $this->show(required_param('id', PARAM_INT));
+                break;
+
+            case self::ACTION_VIEW:
+            default:
+                $this->view();
+                break;
+        }
+    }
+
+    /**
+     * Set external page for the manager.
+     */
+    protected function set_external_page() {
+        admin_externalpage_setup('quizaccess_seb/template');
+    }
+
+    /**
+     * Return record instance.
+     *
+     * @param int $id
+     * @param \stdClass|null $data
+     *
+     * @return \quizaccess_seb\template
+     */
+    protected function get_instance($id = 0, \stdClass $data = null) {
+        return new template($id, $data);
+    }
+
+    /**
+     * Print out all records in a table.
+     */
+    protected function display_all_records() {
+        $records = template::get_records([], 'id');
+
+        $table = new template_list();
+        $table->display($records);
+    }
+
+    /**
+     * Returns a text for create new record button.
+     * @return string
+     */
+    protected function get_create_button_text() : string {
+        return get_string('addtemplate', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns form for the record.
+     *
+     * @param \quizaccess_seb\template|null $instance
+     *
+     * @return \quizaccess_seb\local\form\template
+     */
+    protected function get_form($instance) : \quizaccess_seb\local\form\template {
+        global $PAGE;
+
+        return new \quizaccess_seb\local\form\template($PAGE->url->out(false), ['persistent' => $instance]);
+    }
+
+    /**
+     * View page heading string.
+     * @return string
+     */
+    protected function get_view_heading() : string {
+        return get_string('managetemplates', 'quizaccess_seb');
+    }
+
+    /**
+     * New record heading string.
+     * @return string
+     */
+    protected function get_new_heading() : string {
+        return get_string('newtemplate', 'quizaccess_seb');
+    }
+
+    /**
+     * Edit record heading string.
+     * @return string
+     */
+    protected function get_edit_heading() : string {
+        return get_string('edittemplate', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns base URL for the manager.
+     * @return string
+     */
+    public static function get_base_url() : string {
+        return '/mod/quiz/accessrule/seb/template.php';
+    }
+
+    /**
+     * Execute edit action.
+     *
+     * @param string $action Could be edit or create.
+     * @param null|int $id Id of the region or null if creating a new one.
+     */
+    protected function edit($action, $id = null) {
+        global $PAGE;
+
+        $PAGE->set_url(new \moodle_url(static::get_base_url(), ['action' => $action, 'id' => $id]));
+        $instance = null;
+
+        if ($id) {
+            $instance = $this->get_instance($id);
+        }
+
+        $form = $this->get_form($instance);
+
+        if ($form->is_cancelled()) {
+            redirect(new \moodle_url(static::get_base_url()));
+        } else if ($data = $form->get_data()) {
+            unset($data->submitbutton);
+            try {
+                if (empty($data->id)) {
+                    $data->content = $form->get_file_content('content');
+                    $persistent = $this->get_instance(0, $data);
+                    $persistent->create();
+
+                    \quizaccess_seb\event\template_created::create_strict(
+                        $persistent,
+                        \context_system::instance()
+                    )->trigger();
+                    $this->trigger_enabled_event($persistent);
+                } else {
+                    $instance->from_record($data);
+                    $instance->update();
+
+                    \quizaccess_seb\event\template_updated::create_strict(
+                        $instance,
+                        \context_system::instance()
+                    )->trigger();
+                    $this->trigger_enabled_event($instance);
+                }
+                notification::success(get_string('changessaved'));
+            } catch (\Exception $e) {
+                notification::error($e->getMessage());
+            }
+            redirect(new \moodle_url(static::get_base_url()));
+        } else {
+            if (empty($instance)) {
+                $this->header($this->get_new_heading());
+            } else {
+                if (!$instance->can_delete()) {
+                    notification::warning(get_string('cantedit', 'quizaccess_seb'));
+                }
+                $this->header($this->get_edit_heading());
+            }
+        }
+
+        $form->display();
+        $this->footer();
+    }
+
+    /**
+     * Execute delete action.
+     *
+     * @param int $id ID of the region.
+     */
+    protected function delete($id) {
+        require_sesskey();
+        $instance = $this->get_instance($id);
+
+        if ($instance->can_delete()) {
+            $instance->delete();
+            notification::success(get_string('deleted'));
+
+            \quizaccess_seb\event\template_deleted::create_strict(
+                $id,
+                \context_system::instance()
+            )->trigger();
+
+            redirect(new \moodle_url(static::get_base_url()));
+        } else {
+            notification::warning(get_string('cantdelete', 'quizaccess_seb'));
+            redirect(new \moodle_url(static::get_base_url()));
+        }
+    }
+
+    /**
+     * Execute view action.
+     */
+    protected function view() {
+        global $PAGE;
+
+        $this->header($this->get_view_heading());
+        $this->print_add_button();
+        $this->display_all_records();
+
+        // JS for Template management.
+        $PAGE->requires->js_call_amd('quizaccess_seb/managetemplates', 'setup');
+
+        $this->footer();
+    }
+
+    /**
+     * Show the template.
+     *
+     * @param int $id The ID of the template to show.
+     */
+    protected function show(int $id) {
+        $this->show_hide($id, 1);
+    }
+
+    /**
+     * Hide the template.
+     *
+     * @param int $id The ID of the template to hide.
+     */
+    protected function hide($id) {
+        $this->show_hide($id, 0);
+    }
+
+    /**
+     * Show or Hide the template.
+     *
+     * @param int $id The ID of the template to hide.
+     * @param int $visibility The intended visibility.
+     */
+    protected function show_hide(int $id, int $visibility) {
+        require_sesskey();
+        $template = $this->get_instance($id);
+        $template->set('enabled', $visibility);
+        $template->save();
+
+        $this->trigger_enabled_event($template);
+
+        redirect(new \moodle_url(self::get_base_url()));
+    }
+
+    /**
+     * Print out add button.
+     */
+    protected function print_add_button() {
+        echo $this->output->single_button(
+            new \moodle_url(static::get_base_url(), ['action' => self::ACTION_ADD]),
+            $this->get_create_button_text()
+        );
+    }
+
+    /**
+     * Print out page header.
+     * @param string $title Title to display.
+     */
+    protected function header($title) {
+        echo $this->output->header();
+        echo $this->output->heading($title);
+    }
+
+    /**
+     * Print out the page footer.
+     *
+     * @return void
+     */
+    protected function footer() {
+        echo $this->output->footer();
+    }
+
+    /**
+     * Helper function to fire off an event that informs of if a template is enabled or not.
+     *
+     * @param template $template The template persistent object.
+     */
+    private function trigger_enabled_event(template $template) {
+        $eventstring = ($template->get('enabled') == 0 ? 'disabled' : 'enabled');
+
+        $func = '\quizaccess_seb\event\template_' . $eventstring;
+        $func::create_strict(
+            $template,
+            \context_system::instance()
+        )->trigger();
+    }
+
+}
\ No newline at end of file
diff --git a/mod/quiz/accessrule/seb/config.php b/mod/quiz/accessrule/seb/config.php
new file mode 100644 (file)
index 0000000..522d57b
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Serves an encrypted/unencrypted string as a file for download.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../config.php');
+
+$cmid = required_param('cmid', PARAM_RAW);
+
+$config = \quizaccess_seb\helper::get_seb_config_content($cmid);
+\quizaccess_seb\helper::send_seb_config_file($config);
diff --git a/mod/quiz/accessrule/seb/db/access.php b/mod/quiz/accessrule/seb/db/access.php
new file mode 100644 (file)
index 0000000..c857fa9
--- /dev/null
@@ -0,0 +1,237 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Define capabilities for plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+    'quizaccess/seb:managetemplates' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+        )
+    ),
+    'quizaccess/seb:bypassseb' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_requiresafeexambrowser' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_templateid' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_filemanager_sebconfigfile' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showsebdownloadlink' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_allowedbrowserexamkeys' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_linkquitseb' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_userconfirmquit' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_allowuserquitseb' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_quitpassword' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_allowreloadinexam' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showsebtaskbar' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showreloadbutton' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showtime' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showkeyboardlayout' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+
+    'quizaccess/seb:manage_seb_showwificontrol' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_enableaudiocontrol' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_muteonstartup' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_allowspellchecking' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_activateurlfiltering' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_filterembeddedcontent' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_expressionsallowed' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_regexallowed' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_expressionsblocked' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_regexblocked' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+);
diff --git a/mod/quiz/accessrule/seb/db/caches.php b/mod/quiz/accessrule/seb/db/caches.php
new file mode 100644 (file)
index 0000000..4a93bc9
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Plugin cache definitions.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$definitions = [
+    'quizsettings' => [
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'staticacceleration' => true,
+    ],
+    'config' => [
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'staticacceleration' => true,
+    ],
+    'configkey' => [
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'staticacceleration' => true,
+    ],
+];
diff --git a/mod/quiz/accessrule/seb/db/install.php b/mod/quiz/accessrule/seb/db/install.php
new file mode 100644 (file)
index 0000000..466eb74
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Install script for plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot  . '/mod/quiz/accessrule/seb/lib.php');
+
+/**
+ * Custom code to be run on installing the plugin.
+ */
+function xmldb_quizaccess_seb_install() {
+    global $DB;
+
+    // Reconfigure all existing quizzes to use a new quizaccess_seb.
+    $params = ['browsersecurity' => 'safebrowser'];
+
+    $total = $DB->count_records('quiz', $params);
+    $rs = $DB->get_recordset('quiz', $params);
+
+    $i = 0;
+    $pbar = new progress_bar('updatequizrecords', 500, true);
+
+    foreach ($rs as $quiz) {
+        if (!$DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $quiz->id])) {
+            $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
+
+            $sebsettings = new stdClass();
+
+            $sebsettings->quizid = $quiz->id;
+            $sebsettings->cmid = $cm->id;
+            $sebsettings->templateid = 0;
+            $sebsettings->requiresafeexambrowser = \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG;
+            $sebsettings->showsebtaskbar = null;
+            $sebsettings->showwificontrol = null;
+            $sebsettings->showreloadbutton = null;
+            $sebsettings->showtime = null;
+            $sebsettings->showkeyboardlayout = null;
+            $sebsettings->allowuserquitseb = null;
+            $sebsettings->quitpassword = null;
+            $sebsettings->linkquitseb = null;
+            $sebsettings->userconfirmquit = null;
+            $sebsettings->enableaudiocontrol = null;
+            $sebsettings->muteonstartup = null;
+            $sebsettings->allowspellchecking = null;
+            $sebsettings->allowreloadinexam = null;
+            $sebsettings->activateurlfiltering = null;
+            $sebsettings->filterembeddedcontent = null;
+            $sebsettings->expressionsallowed = null;
+            $sebsettings->regexallowed = null;
+            $sebsettings->expressionsblocked = null;
+            $sebsettings->regexblocked = null;
+            $sebsettings->allowedbrowserexamkeys = null;
+            $sebsettings->showsebdownloadlink = 1;
+            $sebsettings->usermodified = get_admin()->id;
+            $sebsettings->timecreated = time();
+            $sebsettings->timemodified = time();
+
+            $DB->insert_record('quizaccess_seb_quizsettings', $sebsettings);
+
+            $quiz->browsersecurity = '-';
+            $DB->update_record('quiz', $quiz);
+        }
+
+        $i++;
+        $pbar->update($i, $total, "Reconfiguring existing quizzes to use a new SEB plugin - $i/$total.");
+    }
+
+    $rs->close();
+
+    return true;
+}
diff --git a/mod/quiz/accessrule/seb/db/install.xml b/mod/quiz/accessrule/seb/db/install.xml
new file mode 100644 (file)
index 0000000..1969ad3
--- /dev/null
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="mod/quiz/accessrule/seb/db" VERSION="2019120400" COMMENT="XMLDB file for Moodle mod/quiz/accessrule/seb"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd">
+  <TABLES>
+    <TABLE NAME="quizaccess_seb_quizsettings" COMMENT="Stores the quiz level Safe Exam Browser configuration.">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="quizid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to quiz id."/>
+        <FIELD NAME="cmid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to course module id."/>
+        <FIELD NAME="templateid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to quizaccess_seb_template.id."/>
+        <FIELD NAME="requiresafeexambrowser" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="Bool whether to require SEB."/>
+        <FIELD NAME="showsebtaskbar" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show SEB task bar"/>
+        <FIELD NAME="showwificontrol" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to allow user to control networking."/>
+        <FIELD NAME="showreloadbutton" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show reload button."/>
+        <FIELD NAME="showtime" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show the clock."/>
+        <FIELD NAME="showkeyboardlayout" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show keyboard layout."/>
+        <FIELD NAME="allowuserquitseb" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show quit button."/>
+        <FIELD NAME="quitpassword" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Quit password to exit SEB."/>
+        <FIELD NAME="linkquitseb" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Link to exit SEB."/>
+        <FIELD NAME="userconfirmquit" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether confirm quit popup should appear."/>
+        <FIELD NAME="enableaudiocontrol" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show volume and audio controls."/>
+        <FIELD NAME="muteonstartup" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether browser starts muted."/>
+        <FIELD NAME="allowspellchecking" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether spell checking will happen in SEB."/>
+        <FIELD NAME="allowreloadinexam" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether user can reload."/>
+        <FIELD NAME="activateurlfiltering" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether URLs will be filtered."/>
+        <FIELD NAME="filterembeddedcontent" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool wither embedded content will be filtered"/>
+        <FIELD NAME="expressionsallowed" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Comma or newline separated list of allowed expressions"/>
+        <FIELD NAME="regexallowed" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Regex of allowed URLs"/>
+        <FIELD NAME="expressionsblocked" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Comma or newline separated list of blocked expressions"/>
+        <FIELD NAME="regexblocked" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Regex of blocked URLs"/>
+        <FIELD NAME="allowedbrowserexamkeys" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="List of allowed browser exam keys."/>
+        <FIELD NAME="showsebdownloadlink" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether SEB download link should appear"/>
+        <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="quizid" TYPE="foreign-unique" FIELDS="quizid" REFTABLE="quiz" REFFIELDS="id"/>
+        <KEY NAME="cmid" TYPE="foreign-unique" FIELDS="cmid" REFTABLE="course_modules" REFFIELDS="id"/>
+        <KEY NAME="templateid" TYPE="foreign" FIELDS="templateid" REFTABLE="quizacces_seb_template" REFFIELDS="id"/>
+        <KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="quizaccess_seb_template" COMMENT="Templates for Safe Exam Browser configuration.">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Name of the template"/>
+        <FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="content" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Content of the template"/>
+        <FIELD NAME="enabled" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
+  </TABLES>
+</XMLDB>
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Strings for the quizaccess_plagiarismcheckbox plugin.
+ * Upgrade script for plugin.
  *
- * @package    quizaccess
- * @subpackage plagiarismcheckbox
- * @copyright  2011 The Open University
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->dirroot  . '/mod/quiz/accessrule/seb/lib.php');
+
+/**
+ * Function to upgrade quizaccess_seb plugin.
+ *
+ * @param int $oldversion The version we are upgrading from.
+ * @return bool Result.
+ */
+function xmldb_quizaccess_seb_upgrade($oldversion) {
+    global $DB;
+    $dbman = $DB->get_manager();
 
-$string['pluginname'] = 'Safe Exam Browser quiz access rule';
-$string['privacy:metadata'] = 'The Safe Exam Browser quiz access rule plugin does not store any personal data.';
-$string['requiresafeexambrowser'] = 'Require the use of Safe Exam Browser';
-$string['safebrowsererror'] = 'This quiz has been set up so that it may only be attempted using the Safe Exam Browser. You cannot attempt it from this web browser.';
-$string['safebrowsernotice'] = 'This quiz has been configured so that students may only attempt it using the Safe Exam Browser.';
+    return true;
+}
diff --git a/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php b/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php
new file mode 100644 (file)
index 0000000..1ec6f72
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for the quizaccess_seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Luca Bösch <luca.boesch@bfh.ch>
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['addtemplate'] = 'Add new template';
+$string['allowedbrowserkeysdistinct'] = 'The keys must all be different.';
+$string['allowedbrowserkeyssyntax'] = 'A key should be a 64-character hex string.';
+$string['cachedef_config'] = 'SEB config cache';
+$string['cachedef_configkey'] = 'SEB config key cache';
+$string['cachedef_quizsettings'] = 'SEB quiz settings cache';
+$string['cantdelete'] = 'Template can\'t be deleted as it has been used for one or more quizzes';
+$string['cantedit'] = 'Template can\'t be edited as it has been used for one or more quizzes';
+$string['clientrequiresseb'] = 'This quiz has been configured to use the Safe Exam Browser with client configuration.';
+$string['confirmtemplateremovalquestion'] = 'Are you sure you want to remove this template?';
+$string['confirmtemplateremovaltitle'] = 'Confirm template removal?';
+$string['conflictingsettings'] = 'You don\'t have permissions to update existing Safe Exam Browser settings';
+$string['content'] = 'Template';
+$string['description'] = 'Description';
+$string['disabledsettings'] = 'Disabled settings.';
+$string['disabledsettings_help'] = 'Safe Exam Browser quiz settings are unable to be updated if there is one or more existing quiz attempts. To re-enable settings, all quiz attempts must be deleted.';
+$string['downloadsebconfig'] = 'Download SEB config file';
+$string['duplicatetemplate'] = "Template with the same name already exists";
+$string['edittemplate'] = 'Edit template';
+$string['enabled'] = 'Enabled';
+$string['event:accessprevented'] = "Quiz access was prevented";
+$string['event:templatecreated'] = "SEB Template was created";
+$string['event:templatedeleted'] = "SEB Template was deleted";
+$string['event:templatedisabled'] = "SEB Template was disabled";
+$string['event:templateenabled'] = "SEB Template was enabled";
+$string['event:templateupdated'] = "SEB Template was updated";
+$string['exitsebbutton'] = 'Exit Safe Exam Browser';
+$string['filemanager_sebconfigfile'] = 'Upload Safe Exam Browser config file';
+$string['filemanager_sebconfigfile_help'] = 'Please upload your own Safe Exam Browser config file for this quiz.';
+$string['filenotpresent'] = 'Please upload a SEB config file.';
+$string['fileparsefailed'] = 'The uploaded file could not be saved as a SEB config file.';
+$string['httplinkbutton'] = 'Download Configuration';
+$string['invalid_browser_key'] = "Invalid SEB browser key";
+$string['invalid_config_key'] = "Invalid SEB config key";
+$string['invalidkeys'] = "The config key or browser exam keys could not be validated. Please ensure you are using the Safe Exam Browser with correct configuration file.";
+$string['invalidtemplate'] = "Invalid SEB config template";
+$string['manage_templates'] = 'Safe Exam Browser templates';
+$string['managetemplates'] = 'Manage templates';
+$string['missingrequiredsettings'] = 'Config settings not provided all required settings.';
+$string['name'] = 'Name';
+$string['newtemplate'] = 'New template';
+$string['noconfigfilefound'] = 'No uploaded SEB config file could be found for quiz with cmid: {$a}';
+$string['noconfigfound'] = 'No SEB config could be found for quiz with cmid: {$a}';
+$string['not_seb'] = "No SEB browser is being used";
+$string['notemplate'] = 'No template';
+$string['passwordnotset'] = 'Current settings require quizzes using the Safe Exam Browser to have a quiz password set.';
+$string['pluginname'] = 'Safe Exam Browser access rule';
+$string['privacy:metadata:quizaccess_seb_quizsettings'] = 'Safe Exam Browser settings for a quiz. This includes the ID of the last user to create or modify the settings.';
+$string['privacy:metadata:quizaccess_seb_quizsettings:quizid'] = 'ID of the quiz the settings exist for.';
+$string['privacy:metadata:quizaccess_seb_quizsettings:timecreated'] = 'Unix time that the settings were created.';
+$string['privacy:metadata:quizaccess_seb_quizsettings:timemodified'] = 'Unix time that the settings were last modified.';
+$string['privacy:metadata:quizaccess_seb_quizsettings:usermodified'] = 'ID of user who last created or modified the settings.';
+$string['privacy:metadata:quizaccess_seb_template'] = 'Safe Exam Browser template settings. This includes the ID of the last user to create or modify the template.';
+$string['privacy:metadata:quizaccess_seb_template:timecreated'] = 'Unix time that the template was created.';
+$string['privacy:metadata:quizaccess_seb_template:timemodified'] = 'Unix time that the template was last modified.';
+$string['privacy:metadata:quizaccess_seb_template:usermodified'] = 'ID of user who last created or modified the template.';
+$string['quizsettings'] = 'Quiz settings';
+$string['restoredfrom'] = '{$a->name} (restored via cmid {$a->cmid})';
+$string['seb'] = 'Safe Exam Browser';
+$string['seb:bypassseb'] = 'Bypass the requirement to view quiz in Safe Exam Browser.';
+$string['seb:manage_filemanager_sebconfigfile'] = 'Modify Safe Exam Browser quiz setting: Select SEB config file.';
+$string['seb:manage_seb_activateurlfiltering'] = 'Modify Safe Exam Browser quiz setting: Activate URL filtering.';
+$string['seb:manage_seb_allowedbrowserexamkeys'] = 'Modify Safe Exam Browser quiz setting: Modify Allowed Browser Exam Keys.';
+$string['seb:manage_seb_allowreloadinexam'] = 'Modify Safe Exam Browser quiz setting: Allow reload.';
+$string['seb:manage_seb_allowspellchecking'] = 'Modify Safe Exam Browser quiz setting: Enable spell checking.';
+$string['seb:manage_seb_allowuserquitseb'] = 'Modify Safe Exam Browser quiz setting: Allow quit.';
+$string['seb:manage_seb_enableaudiocontrol'] = 'Modify Safe Exam Browser quiz setting: Enable audio control.';
+$string['seb:manage_seb_expressionsallowed'] = 'Modify Safe Exam Browser quiz setting: Simple expressions allowed.';
+$string['seb:manage_seb_expressionsblocked'] = 'Modify Safe Exam Browser quiz setting: Simple expressions blocked.';
+$string['seb:manage_seb_filterembeddedcontent'] = 'Modify Safe Exam Browser quiz setting: Filter embedded content.';
+$string['seb:manage_seb_linkquitseb'] = 'Modify Safe Exam Browser quiz setting: Quit link.';
+$string['seb:manage_seb_muteonstartup'] = 'Modify Safe Exam Browser quiz setting: Mute on startup.';
+$string['seb:manage_seb_quitpassword'] = 'Modify Safe Exam Browser quiz setting: Quit password.';
+$string['seb:manage_seb_regexallowed'] = 'Modify Safe Exam Browser quiz setting: Regex expressions allowed.';
+$string['seb:manage_seb_regexblocked'] = 'Modify Safe Exam Browser quiz setting: Regex expressions blocked.';
+$string['seb:manage_seb_requiresafeexambrowser'] = 'Modify Safe Exam Browser quiz setting: Require Safe Exam Browser.';
+$string['seb:manage_seb_showkeyboardlayout'] = 'Modify Safe Exam Browser quiz setting: Show keyboard layout.';
+$string['seb:manage_seb_showreloadbutton'] = 'Modify Safe Exam Browser quiz setting: Show reload button.';
+$string['seb:manage_seb_showsebtaskbar'] = 'Modify Safe Exam Browser quiz setting: Show task bar.';
+$string['seb:manage_seb_showtime'] = 'Modify Safe Exam Browser quiz setting: Show time.';
+$string['seb:manage_seb_showwificontrol'] = 'Modify Safe Exam Browser quiz setting: Show Wi-Fi control.';
+$string['seb:manage_seb_showsebdownloadlink'] = 'Modify Safe Exam Browser quiz setting: Show download link.';
+$string['seb:manage_seb_templateid'] = 'Modify Safe Exam Browser quiz setting: Select SEB template.';
+$string['seb:manage_seb_userconfirmquit&#