Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

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