Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * PHPUnit for property_list class.
  19   *
  20   * @package    quizaccess_seb
  21   * @author     Andrew Madden <andrewmadden@catalyst-au.net>
  22   * @copyright  2019 Catalyst IT
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  use quizaccess_seb\property_list;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /**
  31   * PHPUnit for property_list class.
  32   *
  33   * @copyright  2020 Catalyst IT
  34   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class quizaccess_seb_property_list_testcase extends advanced_testcase {
  37  
  38      /**
  39       * Test that an empty PList with a root dictionary is created.
  40       */
  41      public function test_create_empty_plist() {
  42          $emptyplist = new property_list();
  43          $xml = trim($emptyplist->to_xml());
  44          $this->assertEquals('<?xml version="1.0" encoding="UTF-8"?>
  45  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  46  <plist version="1.0"><dict/></plist>', $xml);
  47      }
  48  
  49      /**
  50       * Test that a Plist is constructed from an XML string.
  51       */
  52      public function test_construct_plist_from_xml() {
  53          $xml = $this->get_plist_xml_header()
  54              . "<key>testKey</key>"
  55              . "<string>testValue</string>"
  56              . $this->get_plist_xml_footer();
  57          $plist = new property_list($xml);
  58          $generatedxml = trim($plist->to_xml());
  59          $this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
  60  <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
  61  <plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml);
  62      }
  63  
  64      /**
  65       * Test that an element can be added to the root dictionary.
  66       */
  67      public function test_add_element_to_root() {
  68          $plist = new property_list();
  69          $newelement = new \CFPropertyList\CFString('testValue');
  70          $plist->add_element_to_root('testKey', $newelement);
  71          $generatedxml = trim($plist->to_xml());
  72          $this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
  73  <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
  74  <plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml);
  75      }
  76  
  77      /**
  78       * Test that an element's value can be retrieved.
  79       */
  80      public function test_get_element_value() {
  81          $xml = $this->get_plist_xml_header()
  82                  . "<key>testKey</key>"
  83                  . "<string>testValue</string>"
  84                  . $this->get_plist_xml_footer();
  85          $plist = new property_list($xml);
  86          $this->assertEquals('testValue', $plist->get_element_value('testKey'));
  87      }
  88  
  89      /**
  90       * Test that an element's value can be retrieved.
  91       */
  92      public function test_get_element_value_if_not_exists() {
  93          $plist = new property_list();
  94          $this->assertEmpty($plist->get_element_value('testKey'));
  95      }
  96  
  97      /**
  98       * Test an element's value can be retrieved if it is an array.
  99       */
 100      public function test_get_element_value_if_array() {
 101          $xml = $this->get_plist_xml_header()
 102              . "<key>testDict</key>"
 103              . "<dict>"
 104              . "<key>testKey</key>"
 105              . "<string>testValue</string>"
 106              . "</dict>"
 107              . $this->get_plist_xml_footer();
 108          $plist = new property_list($xml);
 109          $this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict'));
 110      }
 111  
 112      /**
 113       * Test that a element's value can be updated that is not an array or dictionary.
 114       *
 115       * @param string $xml XML to create PList.
 116       * @param string $key Key of element to try and update.
 117       * @param mixed $value Value to try to update with.
 118       *
 119       * @dataProvider good_update_data_provider
 120       */
 121      public function test_updating_element_value($xml, $key, $value) {
 122          $xml = $this->get_plist_xml_header()
 123              . $xml
 124              . $this->get_plist_xml_footer();
 125          $plist = new property_list($xml);
 126          $plist->update_element_value($key, $value);
 127          $this->assertEquals($value, $plist->get_element_value($key));
 128      }
 129  
 130      /**
 131       * Test that a element's value can be updated that is not an array or dictionary.
 132       *
 133       * @param string $xml XML to create PList.
 134       * @param string $key Key of element to try and update.
 135       * @param mixed $value Bad value to try to update with.
 136       * @param mixed $expected Expected value of element after update is called.
 137       * @param string $exceptionmessage Message of exception expected to be thrown.
 138  
 139       * @dataProvider bad_update_data_provider
 140       */
 141      public function test_updating_element_value_with_bad_data(string $xml, string $key, $value, $expected, $exceptionmessage) {
 142          $xml = $this->get_plist_xml_header()
 143              . $xml
 144              . $this->get_plist_xml_footer();
 145          $plist = new property_list($xml);
 146  
 147          $this->expectException(invalid_parameter_exception::class);
 148          $this->expectExceptionMessage($exceptionmessage);
 149  
 150          $plist->update_element_value($key, $value);
 151          $plistarray = json_decode($plist->to_json()); // Export elements.
 152          $this->assertEquals($expected, $plistarray->$key);
 153      }
 154  
 155      /**
 156       * Test that a dictionary can have it's value (array) updated.
 157       */
 158      public function test_updating_element_array_if_dictionary() {
 159          $xml = $this->get_plist_xml_header()
 160              . "<key>testDict</key>"
 161              . "<dict>"
 162              . "<key>testKey</key>"
 163              . "<string>testValue</string>"
 164              . "</dict>"
 165              . $this->get_plist_xml_footer();
 166          $plist = new property_list($xml);
 167          $plist->update_element_array('testDict', ['newKey' => new \CFPropertyList\CFString('newValue')]);
 168          $this->assertEquals(['newKey' => 'newValue'], $plist->get_element_value('testDict'));
 169      }
 170  
 171      /**
 172       * Test that a dictionary can have it's value (array) updated.
 173       */
 174      public function test_updating_element_array_if_dictionary_with_bad_data() {
 175          $xml = $this->get_plist_xml_header()
 176              . "<key>testDict</key>"
 177              . "<dict>"
 178              . "<key>testKey</key>"
 179              . "<string>testValue</string>"
 180              . "</dict>"
 181              . $this->get_plist_xml_footer();
 182          $plist = new property_list($xml);
 183  
 184          $this->expectException(invalid_parameter_exception::class);
 185          $this->expectExceptionMessage('New array must only contain CFType objects.');
 186  
 187          $plist->update_element_array('testDict', [false]);
 188          $this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict'));
 189          $this->assertDebuggingCalled('property_list: If updating an array in PList, it must only contain CFType objects.',
 190                  DEBUG_DEVELOPER);
 191      }
 192  
 193      /**
 194       * Test that an element can be deleted.
 195       */
 196      public function test_delete_element() {
 197          $xml = $this->get_plist_xml_header()
 198              . "<key>testKey</key>"
 199              . "<string>testValue</string>"
 200              . $this->get_plist_xml_footer();
 201          $plist = new property_list($xml);
 202          $plist->delete_element('testKey');
 203          $generatedxml = trim($plist->to_xml());
 204          $this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
 205  <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
 206  <plist version=\"1.0\"><dict/></plist>", $generatedxml);
 207      }
 208  
 209      /**
 210       * Test that an element can be deleted.
 211       */
 212      public function test_delete_element_if_not_exists() {
 213          $xml = $this->get_plist_xml_header()
 214              . "<key>testKey</key>"
 215              . "<string>testValue</string>"
 216              . $this->get_plist_xml_footer();
 217          $plist = new property_list($xml);
 218          $plist->delete_element('nonExistentKey');
 219          $generatedxml = trim($plist->to_xml());
 220          // The xml should be unaltered.
 221          $this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
 222  <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
 223  <plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml);
 224      }
 225  
 226      /**
 227       * Test that json is exported correctly according to SEB Config Key requirements.
 228       *
 229       * @param string $xml PList XML used to generate CFPropertyList.
 230       * @param string $expectedjson Expected JSON output.
 231       *
 232       * @dataProvider json_data_provider
 233       */
 234      public function test_export_to_json($xml, $expectedjson) {
 235          $xml = $this->get_plist_xml_header()
 236              . $xml
 237              . $this->get_plist_xml_footer();
 238          $plist = new property_list($xml);
 239          $generatedjson = $plist->to_json();
 240          $this->assertEquals($expectedjson, $generatedjson);
 241      }
 242  
 243      /**
 244       * Test that the xml is exported to JSON from a real SEB config file. Expected JSON extracted from SEB logs.
 245       */
 246      public function test_export_to_json_full_file() {
 247          $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted_mac_001.seb');
 248          $plist = new property_list($xml);
 249          $plist->delete_element('originatorVersion'); // JSON should not contain originatorVersion key.
 250          $generatedjson = $plist->to_json();
 251          $json = trim(file_get_contents(__DIR__ . '/fixtures/JSON_unencrypted_mac_001.txt'));
 252          $this->assertEquals($json, $generatedjson);
 253      }
 254  
 255      /**
 256       * Test the set_or_update_value function.
 257       */
 258      public function test_set_or_update_value() {
 259          $plist = new property_list();
 260  
 261          $this->assertEmpty($plist->get_element_value('string'));
 262          $this->assertEmpty($plist->get_element_value('bool'));
 263          $this->assertEmpty($plist->get_element_value('number'));
 264  
 265          // Setting values.
 266          $plist->set_or_update_value('string', new \CFPropertyList\CFString('initial string'));
 267          $plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(true));
 268          $plist->set_or_update_value('number', new \CFPropertyList\CFNumber('10'));
 269  
 270          $this->assertEquals('initial string', $plist->get_element_value('string'));
 271          $this->assertEquals(true, $plist->get_element_value('bool'));
 272          $this->assertEquals(10, $plist->get_element_value('number'));
 273  
 274          // Updating values.
 275          $plist->set_or_update_value('string', new \CFPropertyList\CFString('new string'));
 276          $plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(false));
 277          $plist->set_or_update_value('number', new \CFPropertyList\CFNumber('42'));
 278  
 279          $this->assertEquals('new string', $plist->get_element_value('string'));
 280          $this->assertEquals(false, $plist->get_element_value('bool'));
 281          $this->assertEquals(42, $plist->get_element_value('number'));
 282  
 283          // Type exception.
 284          $this->expectException(TypeError::class);
 285          $plist->set_or_update_value('someKey', 'We really need to pass in CFTypes here');
 286      }
 287  
 288      /**
 289       * Get a valid PList header. Must also use footer.
 290       *
 291       * @return string
 292       */
 293      private function get_plist_xml_header() : string {
 294          return "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
 295                  . "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" "
 296                  . "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
 297                  . "<plist version=\"1.0\">\n"
 298                  . "  <dict>";
 299      }
 300  
 301      /**
 302       * Get a valid PList footer. Must also use header.
 303       *
 304       * @return string
 305       */
 306      private function get_plist_xml_footer() : string {
 307          return "  </dict>\n"
 308                  . "</plist>";
 309      }
 310  
 311      /**
 312       * Data provider for good data on update.
 313       *
 314       * @return array Array with test data.
 315       */
 316      public function good_update_data_provider() : array {
 317          return [
 318              'Update string' => ['<key>testKey</key><string>testValue</string>', 'testKey', 'newValue'],
 319              'Update bool' => ['<key>testKey</key><true/>', 'testKey', false],
 320              'Update number' => ['<key>testKey</key><real>888</real>', 'testKey', 123.4],
 321          ];
 322      }
 323  
 324      /**
 325       * Data provider for bad data on update.
 326       *
 327       * @return array Array with test data.
 328       */
 329      public function bad_update_data_provider() : array {
 330  
 331          return [
 332              'Update string with bool' => ['<key>testKey</key><string>testValue</string>', 'testKey', true, 'testValue',
 333                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 334                      . 'or value type does not match element type: CFPropertyList\CFString'],
 335              'Update string with number' => ['<key>testKey</key><string>testValue</string>', 'testKey', 999, 'testValue',
 336                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 337                      . 'or value type does not match element type: CFPropertyList\CFString'],
 338              'Update string with null' => ['<key>testKey</key><string>testValue</string>', 'testKey', null, 'testValue',
 339                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 340                      . 'or value type does not match element type: CFPropertyList\CFString'],
 341              'Update string with array' => ['<key>testKey</key><string>testValue</string>', 'testKey', ['arrayValue'], 'testValue',
 342                      'Use update_element_array to update a collection.'],
 343              'Update bool with string' => ['<key>testKey</key><true/>', 'testKey', 'testValue', true,
 344                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 345                      . 'or value type does not match element type: CFPropertyList\CFBool'],
 346              'Update bool with number' => ['<key>testKey</key><true/>', 'testKey', 999, true,
 347                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 348                      . 'or value type does not match element type: CFPropertyList\CFBool'],
 349              'Update bool with null' => ['<key>testKey</key><true/>', 'testKey', null, true,
 350                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 351                      . 'or value type does not match element type: CFPropertyList\CFBool'],
 352              'Update bool with array' => ['<key>testKey</key><true/>', 'testKey', ['testValue'], true,
 353                      'Use update_element_array to update a collection.'],
 354              'Update number with string' => ['<key>testKey</key><real>888</real>', 'testKey', 'string', 888,
 355                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 356                      . 'or value type does not match element type: CFPropertyList\CFNumber'],
 357              'Update number with bool' => ['<key>testKey</key><real>888</real>', 'testKey', true, 888,
 358                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 359                      . 'or value type does not match element type: CFPropertyList\CFNumber'],
 360              'Update number with null' => ['<key>testKey</key><real>888</real>', 'testKey', null, 888,
 361                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 362                      . 'or value type does not match element type: CFPropertyList\CFNumber'],
 363              'Update number with array' => ['<key>testKey</key><real>888</real>', 'testKey', ['testValue'], 888,
 364                      'Use update_element_array to update a collection.'],
 365              'Update date with string' => ['<key>testKey</key><date>1940-10-09T22:13:56Z</date>', 'testKey', 'string',
 366                      '1940-10-10T06:13:56+08:00',
 367                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 368                      . 'or value type does not match element type: CFPropertyList\CFDate'],
 369              'Update data with number' => ['<key>testKey</key><data>testData</data>', 'testKey', 789, 'testData',
 370                      'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
 371                      . 'or value type does not match element type: CFPropertyList\CFData'],
 372          ];
 373      }
 374  
 375      /**
 376       * Data provider for expected JSON from PList.
 377       *
 378       * Examples extracted from requirements listed in SEB Config Key documents.
 379       * https://safeexambrowser.org/developer/seb-config-key.html
 380       *
 381       * 1. Date should be in ISO 8601 format.
 382       * 2. Data should be base 64 encoded.
 383       * 3. String should be UTF-8 encoded.
 384       * 4, 5, 6, 7. No requirements for bools, arrays or dicts.
 385       * 8. Empty dicts should not be included.
 386       * 9. JSON key ordering should be case insensitive, and use string ordering.
 387       * 10. URL forward slashes should not be escaped.
 388       *
 389       * @return array
 390       */
 391      public function json_data_provider() : array {
 392          $data = "blahblah";
 393          $base64data = base64_encode($data);
 394  
 395          return [
 396              'date' => ["<key>date</key><date>1940-10-09T22:13:56Z</date>", "{\"date\":\"1940-10-09T22:13:56+00:00\"}"],
 397              'data' => ["<key>data</key><data>$base64data</data>", "{\"data\":\"$base64data\"}"],
 398              'string' => ["<key>string</key><string>hello wörld</string>", "{\"string\":\"hello wörld\"}"],
 399              'string with 1 backslash' => ["<key>string</key><string>ws:\localhost</string>", "{\"string\":\"ws:\localhost\"}"],
 400              'string with 2 backslashes' => ["<key>string</key><string>ws:\\localhost</string>",
 401                      '{"string":"ws:\\localhost"}'],
 402              'string with 3 backslashes' => ["<key>string</key><string>ws:\\\localhost</string>",
 403                      '{"string":"ws:\\\localhost"}'],
 404              'string with 4 backslashes' => ["<key>string</key><string>ws:\\\\localhost</string>",
 405                      '{"string":"ws:\\\\localhost"}'],
 406              'string with 5 backslashes' => ["<key>string</key><string>ws:\\\\\localhost</string>",
 407                      '{"string":"ws:\\\\\localhost"}'],
 408              'bool' => ["<key>bool</key><true/>", "{\"bool\":true}"],
 409              'array' => ["<key>array</key><array><key>arraybool</key><false/><key>arraybool2</key><true/></array>"
 410                      , "{\"array\":[false,true]}"],
 411              'empty array' => ["<key>bool</key><true/><key>array</key><array/>"
 412                      , "{\"array\":[],\"bool\":true}"],
 413              'dict' => ["<key>dict</key><dict><key>dictbool</key><false/><key>dictbool2</key><true/></dict>"
 414                      , "{\"dict\":{\"dictbool\":false,\"dictbool2\":true}}"],
 415              'empty dict' => ["<key>bool</key><true/><key>emptydict</key><dict/>", "{\"bool\":true}"],
 416              'unordered elements' => ["<key>testKey</key>"
 417                      . "<string>testValue</string>"
 418                      . "<key>allowWLAN</key>"
 419                      . "<string>testValue2</string>"
 420                      . "<key>allowWlan</key>"
 421                      . "<string>testValue3</string>"
 422                      , "{\"allowWlan\":\"testValue3\",\"allowWLAN\":\"testValue2\",\"testKey\":\"testValue\"}"],
 423              'url' => ["<key>url</key><string>http://test.com</string>", "{\"url\":\"http://test.com\"}"],
 424              'assoc dict' => ["<key>dict</key><dict><key>banana</key><false/><key>apple</key><true/></dict>",
 425                      "{\"dict\":{\"apple\":true,\"banana\":false}}"],
 426              'seq array' => ["<key>array</key><array><key>1</key><false/><key>2</key><true/>
 427  <key>3</key><true/><key>4</key><true/><key>5</key><true/><key>6</key><true/>
 428  <key>7</key><true/><key>8</key><true/><key>9</key><true/><key>10</key><true/></array>",
 429                      "{\"array\":[false,true,true,true,true,true,true,true,true,true]}"],
 430          ];
 431      }
 432  }