Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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   * Unit tests for all Privacy Providers.
  19   *
  20   * @package     core_privacy
  21   * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
  22   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace core_privacy\privacy;
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  use core_privacy\manager;
  29  use core_privacy\local\metadata\collection;
  30  use core_privacy\local\metadata\types\type;
  31  use core_privacy\local\metadata\types\database_table;
  32  use core_privacy\local\metadata\types\external_location;
  33  use core_privacy\local\metadata\types\plugin_type_link;
  34  use core_privacy\local\metadata\types\subsystem_link;
  35  use core_privacy\local\metadata\types\user_preference;
  36  
  37  /**
  38   * Unit tests for all Privacy Providers.
  39   *
  40   * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
  41   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   */
  43  class provider_test extends \advanced_testcase {
  44      /**
  45       * Returns a list of frankenstyle names of core components (plugins and subsystems).
  46       *
  47       * @return array the array of frankenstyle component names with the relevant class name.
  48       */
  49      public function get_component_list() {
  50          $components = ['core' => [
  51              'component' => 'core',
  52              'classname' => manager::get_provider_classname_for_component('core')
  53          ]];
  54          // Get all plugins.
  55          $plugintypes = \core_component::get_plugin_types();
  56          foreach ($plugintypes as $plugintype => $typedir) {
  57              $plugins = \core_component::get_plugin_list($plugintype);
  58              foreach ($plugins as $pluginname => $plugindir) {
  59                  $frankenstyle = $plugintype . '_' . $pluginname;
  60                  $components[$frankenstyle] = [
  61                      'component' => $frankenstyle,
  62                      'classname' => manager::get_provider_classname_for_component($frankenstyle),
  63                  ];
  64  
  65              }
  66          }
  67          // Get all subsystems.
  68          foreach (\core_component::get_core_subsystems() as $name => $path) {
  69              if (isset($path)) {
  70                  $frankenstyle = 'core_' . $name;
  71                  $components[$frankenstyle] = [
  72                      'component' => $frankenstyle,
  73                      'classname' => manager::get_provider_classname_for_component($frankenstyle),
  74                  ];
  75              }
  76          }
  77          return $components;
  78      }
  79  
  80      /**
  81       * Test that the specified null_provider works as expected.
  82       *
  83       * @dataProvider null_provider_provider
  84       * @param   string  $component The name of the component.
  85       * @param   string  $classname The name of the class for privacy
  86       */
  87      public function test_null_provider($component, $classname) {
  88          $reason = $classname::get_reason();
  89          $this->assertIsString($reason);
  90  
  91          $this->assertIsString(get_string($reason, $component));
  92          $this->assertDebuggingNotCalled();
  93      }
  94  
  95      /**
  96       * Data provider for the null_provider tests.
  97       *
  98       * @return array
  99       */
 100      public function null_provider_provider() {
 101          return array_filter($this->get_component_list(), function($component) {
 102                  return static::component_implements(
 103                      $component['classname'],
 104                      \core_privacy\local\metadata\null_provider::class
 105                  );
 106          });
 107      }
 108  
 109      /**
 110       * Test that the specified metadata_provider works as expected.
 111       *
 112       * @dataProvider metadata_provider_provider
 113       * @param   string  $component The name of the component.
 114       * @param   string  $classname The name of the class for privacy
 115       */
 116      public function test_metadata_provider($component, $classname) {
 117          global $DB;
 118  
 119          $collection = new collection($component);
 120          $metadata = $classname::get_metadata($collection);
 121          $this->assertInstanceOf(collection::class, $metadata);
 122          $this->assertSame($collection, $metadata);
 123          $this->assertContainsOnlyInstancesOf(type::class, $metadata->get_collection());
 124  
 125          foreach ($metadata->get_collection() as $item) {
 126              // All items must have a valid string name.
 127              // Note: This is not a string identifier.
 128              $this->assertIsString($item->get_name());
 129  
 130              if ($item instanceof database_table) {
 131                  // Check that the table is valid.
 132                  $this->assertTrue($DB->get_manager()->table_exists($item->get_name()));
 133              }
 134  
 135              if ($item instanceof \core_privacy\local\metadata\types\plugintype_link) {
 136                  // Check that plugin type is valid.
 137                  $this->assertTrue(array_key_exists($item->get_name(), \core_component::get_plugin_types()));
 138              }
 139  
 140              if ($item instanceof subsystem_link) {
 141                  // Check that core subsystem exists.
 142                  list($plugintype, $pluginname) = \core_component::normalize_component($item->get_name());
 143                  $this->assertEquals('core', $plugintype);
 144                  $this->assertTrue(\core_component::is_core_subsystem($pluginname));
 145              }
 146  
 147              if ($summary = $item->get_summary()) {
 148                  // Summary is optional, but when provided must be a valid string identifier.
 149                  $this->assertIsString($summary);
 150  
 151                  // Check that the string is also correctly defined.
 152                  $this->assertIsString(get_string($summary, $component));
 153                  $this->assertDebuggingNotCalled();
 154              }
 155  
 156              if ($fields = $item->get_privacy_fields()) {
 157                  // Privacy fields are optional, but when provided must be a valid string identifier.
 158                  foreach ($fields as $field => $identifier) {
 159                      $this->assertIsString($field);
 160                      $this->assertIsString($identifier);
 161  
 162                      // Check that the string is also correctly defined.
 163                      $this->assertIsString(get_string($identifier, $component));
 164                      $this->assertDebuggingNotCalled();
 165                  }
 166              }
 167          }
 168      }
 169  
 170      /**
 171       * Test that all providers implement some form of compliant provider.
 172       *
 173       * @dataProvider get_component_list
 174       * @param string $component frankenstyle component name, e.g. 'mod_assign'
 175       * @param string $classname the fully qualified provider classname
 176       */
 177      public function test_all_providers_compliant($component, $classname) {
 178          $manager = new manager();
 179          $this->assertTrue($manager->component_is_compliant($component));
 180      }
 181  
 182      /**
 183       * Ensure that providers do not throw an error when processing a deleted user.
 184       *
 185       * @dataProvider    is_user_data_provider
 186       * @param   string  $component
 187       */
 188      public function test_component_understands_deleted_users($component) {
 189          $this->resetAfterTest();
 190  
 191          // Create a user.
 192          $user = $this->getDataGenerator()->create_user();
 193  
 194          // Delete the user and their context.
 195          delete_user($user);
 196          $usercontext = \context_user::instance($user->id);
 197          $usercontext->delete();
 198  
 199          $contextlist = manager::component_class_callback($component, \core_privacy\local\request\core_user_data_provider::class,
 200                  'get_contexts_for_userid', [$user->id]);
 201  
 202          $this->assertInstanceOf(\core_privacy\local\request\contextlist::class, $contextlist);
 203      }
 204  
 205      /**
 206       * Ensure that providers do not throw an error when processing a deleted user.
 207       *
 208       * @dataProvider    is_user_data_provider
 209       * @param   string  $component
 210       */
 211      public function test_userdata_provider_implements_userlist($component) {
 212          $classname = manager::get_provider_classname_for_component($component);
 213          $this->assertTrue(is_subclass_of($classname, \core_privacy\local\request\core_userlist_provider::class));
 214      }
 215  
 216      /**
 217       * Data provider for the metadata\provider tests.
 218       *
 219       * @return array
 220       */
 221      public function metadata_provider_provider() {
 222          return array_filter($this->get_component_list(), function($component) {
 223                  return static::component_implements(
 224                      $component['classname'],
 225                      \core_privacy\local\metadata\provider::class
 226                  );
 227          });
 228      }
 229  
 230      /**
 231       * List of providers which implement the core_user_data_provider.
 232       *
 233       * @return array
 234       */
 235      public function is_user_data_provider() {
 236          return array_filter($this->get_component_list(), function($component) {
 237                  return static::component_implements(
 238                      $component['classname'],
 239                      \core_privacy\local\request\core_user_data_provider::class
 240                  );
 241          });
 242      }
 243  
 244      /**
 245       * Checks whether the component's provider class implements the specified interface, either directly or as a grandchild.
 246       *
 247       * @param   string  $providerclass The name of the class to test.
 248       * @param   string  $interface the name of the interface we want to check.
 249       * @return  bool    Whether the class implements the interface.
 250       */
 251      protected static function component_implements($providerclass, $interface) {
 252          if (class_exists($providerclass) && interface_exists($interface)) {
 253              return is_subclass_of($providerclass, $interface);
 254          }
 255  
 256          return false;
 257      }
 258  
 259      /**
 260       * Finds user fields in a table
 261       *
 262       * Returns fields that have foreign key to user table and fields that are named 'userid'.
 263       *
 264       * @param \xmldb_table $table
 265       * @return array
 266       */
 267      protected function get_userid_fields(\xmldb_table $table) {
 268          $userfields = [];
 269  
 270          // Find all fields that have a foreign key to 'id' field in 'user' table.
 271          $keys = $table->getKeys();
 272          foreach ($keys as $key) {
 273              $reffields = $key->getRefFields();
 274              $fields = $key->getFields();
 275              if ($key->getRefTable() === 'user' && count($reffields) == 1 && $reffields[0] == 'id' && count($fields) == 1) {
 276                  $userfields[$fields[0]] = $fields[0];
 277              }
 278          }
 279          // Find fields with the name 'userid' even if they don't have a foreign key.
 280          $fields = $table->getFields();
 281          foreach ($fields as $field) {
 282              if ($field->getName() == 'userid') {
 283                  $userfields['userid'] = 'userid';
 284              }
 285          }
 286  
 287          return $userfields;
 288      }
 289  
 290      /**
 291       * Test that all tables with user fields are covered by metadata providers
 292       */
 293      public function test_table_coverage() {
 294          global $DB;
 295          $dbman = $DB->get_manager();
 296          $tables = [];
 297  
 298          foreach ($dbman->get_install_xml_files() as $filename) {
 299              $xmldbfile = new \xmldb_file($filename);
 300              if (!$xmldbfile->loadXMLStructure()) {
 301                  continue;
 302              }
 303              $structure = $xmldbfile->getStructure();
 304              $tablelist = $structure->getTables();
 305  
 306              foreach ($tablelist as $table) {
 307                  if ($fields = $this->get_userid_fields($table)) {
 308                      $tables[$table->getName()] = '  - ' . $table->getName() . ' (' . join(', ', $fields) . ')';
 309                  }
 310              }
 311          }
 312  
 313          $componentlist = $this->metadata_provider_provider();
 314          foreach ($componentlist as $componentarray) {
 315              $component = $componentarray['component'];
 316              $classname = $componentarray['classname'];
 317              $collection = new collection($component);
 318              $metadata = $classname::get_metadata($collection);
 319              foreach ($metadata->get_collection() as $item) {
 320                  if ($item instanceof database_table) {
 321                      unset($tables[$item->get_name()]);
 322                  }
 323              }
 324          }
 325  
 326          if ($tables) {
 327              $this->fail("The following tables with user fields must be covered with metadata providers: \n".
 328                  join("\n", $tables));
 329          }
 330  
 331      }
 332  }