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 311 and 401] [Versions 39 and 401] [Versions 400 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 core;
  18  
  19  use core\oauth2\access_token;
  20  use core\oauth2\api;
  21  use core\oauth2\endpoint;
  22  use core\oauth2\issuer;
  23  use core\oauth2\system_account;
  24  use \core\oauth2\user_field_mapping;
  25  
  26  /**
  27   * Tests for oauth2 apis (\core\oauth2\*).
  28   *
  29   * @package    core
  30   * @copyright  2017 Damyon Wiese
  31   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
  32   * @coversDefaultClass \core\oauth2\api
  33   */
  34  class oauth2_test extends \advanced_testcase {
  35  
  36      /**
  37       * Tests the crud operations on oauth2 issuers.
  38       */
  39      public function test_create_and_delete_standard_issuers() {
  40          $this->resetAfterTest();
  41          $this->setAdminUser();
  42          api::create_standard_issuer('google');
  43          api::create_standard_issuer('facebook');
  44          api::create_standard_issuer('microsoft');
  45          api::create_standard_issuer('nextcloud', 'https://dummy.local/nextcloud/');
  46  
  47          $issuers = api::get_all_issuers();
  48  
  49          $this->assertEquals($issuers[0]->get('name'), 'Google');
  50          $this->assertEquals($issuers[1]->get('name'), 'Facebook');
  51          $this->assertEquals($issuers[2]->get('name'), 'Microsoft');
  52          $this->assertEquals($issuers[3]->get('name'), 'Nextcloud');
  53  
  54          api::move_down_issuer($issuers[0]->get('id'));
  55  
  56          $issuers = api::get_all_issuers();
  57  
  58          $this->assertEquals($issuers[0]->get('name'), 'Facebook');
  59          $this->assertEquals($issuers[1]->get('name'), 'Google');
  60          $this->assertEquals($issuers[2]->get('name'), 'Microsoft');
  61          $this->assertEquals($issuers[3]->get('name'), 'Nextcloud');
  62  
  63          api::delete_issuer($issuers[1]->get('id'));
  64  
  65          $issuers = api::get_all_issuers();
  66  
  67          $this->assertEquals($issuers[0]->get('name'), 'Facebook');
  68          $this->assertEquals($issuers[1]->get('name'), 'Microsoft');
  69          $this->assertEquals($issuers[2]->get('name'), 'Nextcloud');
  70      }
  71  
  72      /**
  73       * Tests the crud operations on oauth2 issuers.
  74       */
  75      public function test_create_nextcloud_without_url() {
  76          $this->resetAfterTest();
  77          $this->setAdminUser();
  78  
  79          $this->expectException(\moodle_exception::class);
  80          api::create_standard_issuer('nextcloud');
  81      }
  82  
  83      /**
  84       * Tests we can list and delete each of the persistents related to an issuer.
  85       */
  86      public function test_getters() {
  87          $this->resetAfterTest();
  88          $this->setAdminUser();
  89          $issuer = api::create_standard_issuer('microsoft');
  90  
  91          $same = api::get_issuer($issuer->get('id'));
  92  
  93          foreach ($same->properties_definition() as $name => $def) {
  94              $this->assertTrue($issuer->get($name) == $same->get($name));
  95          }
  96  
  97          $endpoints = api::get_endpoints($issuer);
  98          $same = api::get_endpoint($endpoints[0]->get('id'));
  99          $this->assertEquals($endpoints[0]->get('id'), $same->get('id'));
 100          $this->assertEquals($endpoints[0]->get('name'), $same->get('name'));
 101  
 102          $todelete = $endpoints[0];
 103          api::delete_endpoint($todelete->get('id'));
 104          $endpoints = api::get_endpoints($issuer);
 105          $this->assertNotEquals($endpoints[0]->get('id'), $todelete->get('id'));
 106  
 107          $userfields = api::get_user_field_mappings($issuer);
 108          $same = api::get_user_field_mapping($userfields[0]->get('id'));
 109          $this->assertEquals($userfields[0]->get('id'), $same->get('id'));
 110  
 111          $todelete = $userfields[0];
 112          api::delete_user_field_mapping($todelete->get('id'));
 113          $userfields = api::get_user_field_mappings($issuer);
 114          $this->assertNotEquals($userfields[0]->get('id'), $todelete->get('id'));
 115      }
 116  
 117      /**
 118       * Data provider for \core_oauth2_testcase::test_get_system_oauth_client().
 119       *
 120       * @return array
 121       */
 122      public function system_oauth_client_provider() {
 123          return [
 124              [
 125                  (object) [
 126                      'access_token' => 'fdas...',
 127                      'token_type' => 'Bearer',
 128                      'expires_in' => '3600',
 129                      'id_token' => 'llfsd..',
 130                  ], HOURSECS - 10
 131              ],
 132              [
 133                  (object) [
 134                      'access_token' => 'fdas...',
 135                      'token_type' => 'Bearer',
 136                      'id_token' => 'llfsd..',
 137                  ], WEEKSECS
 138              ],
 139          ];
 140      }
 141  
 142      /**
 143       * Tests we can get a logged in oauth client for a system account.
 144       *
 145       * @dataProvider system_oauth_client_provider
 146       * @param \stdClass $responsedata The response data to be mocked.
 147       * @param int $expiresin The expected expiration time.
 148       */
 149      public function test_get_system_oauth_client($responsedata, $expiresin) {
 150          $this->resetAfterTest();
 151          $this->setAdminUser();
 152  
 153          $issuer = api::create_standard_issuer('microsoft');
 154  
 155          $requiredscopes = api::get_system_scopes_for_issuer($issuer);
 156          // Fake a system account.
 157          $data = (object) [
 158              'issuerid' => $issuer->get('id'),
 159              'refreshtoken' => 'abc',
 160              'grantedscopes' => $requiredscopes,
 161              'email' => 'sys@example.com',
 162              'username' => 'sys'
 163          ];
 164          $sys = new system_account(0, $data);
 165          $sys->create();
 166  
 167          // Fake a response with an access token.
 168          $response = json_encode($responsedata);
 169          \curl::mock_response($response);
 170          $client = api::get_system_oauth_client($issuer);
 171          $this->assertTrue($client->is_logged_in());
 172  
 173          // Check token expiry.
 174          $accesstoken = access_token::get_record(['issuerid' => $issuer->get('id')]);
 175  
 176          // Get the difference between the actual and expected expiry times.
 177          // They might differ by a couple of seconds depending on the timing when the token gets actually processed.
 178          $expiresdifference = time() + $expiresin - $accesstoken->get('expires');
 179  
 180          // Assert that the actual token expiration is more or less the same as the expected.
 181          $this->assertGreaterThanOrEqual(0, $expiresdifference);
 182          $this->assertLessThanOrEqual(3, $expiresdifference);
 183      }
 184  
 185      /**
 186       * Tests we can enable and disable an issuer.
 187       */
 188      public function test_enable_disable_issuer() {
 189          $this->resetAfterTest();
 190          $this->setAdminUser();
 191  
 192          $issuer = api::create_standard_issuer('microsoft');
 193  
 194          $issuerid = $issuer->get('id');
 195  
 196          api::enable_issuer($issuerid);
 197          $check = api::get_issuer($issuer->get('id'));
 198          $this->assertTrue((boolean)$check->get('enabled'));
 199  
 200          api::enable_issuer($issuerid);
 201          $check = api::get_issuer($issuer->get('id'));
 202          $this->assertTrue((boolean)$check->get('enabled'));
 203  
 204          api::disable_issuer($issuerid);
 205          $check = api::get_issuer($issuer->get('id'));
 206          $this->assertFalse((boolean)$check->get('enabled'));
 207  
 208          api::enable_issuer($issuerid);
 209          $check = api::get_issuer($issuer->get('id'));
 210          $this->assertTrue((boolean)$check->get('enabled'));
 211      }
 212  
 213      /**
 214       * Test the alloweddomains for an issuer.
 215       */
 216      public function test_issuer_alloweddomains() {
 217          $this->resetAfterTest();
 218          $this->setAdminUser();
 219  
 220          $issuer = api::create_standard_issuer('microsoft');
 221  
 222          $issuer->set('alloweddomains', '');
 223  
 224          // Anything is allowed when domain is empty.
 225          $this->assertTrue($issuer->is_valid_login_domain(''));
 226          $this->assertTrue($issuer->is_valid_login_domain('a@b'));
 227          $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com'));
 228  
 229          $issuer->set('alloweddomains', 'example.com');
 230  
 231          // One domain - must match exactly - no substrings etc.
 232          $this->assertFalse($issuer->is_valid_login_domain(''));
 233          $this->assertFalse($issuer->is_valid_login_domain('a@b'));
 234          $this->assertFalse($issuer->is_valid_login_domain('longer.example@example'));
 235          $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com'));
 236  
 237          $issuer->set('alloweddomains', 'example.com,example.net');
 238          // Multiple domains - must match any exactly - no substrings etc.
 239          $this->assertFalse($issuer->is_valid_login_domain(''));
 240          $this->assertFalse($issuer->is_valid_login_domain('a@b'));
 241          $this->assertFalse($issuer->is_valid_login_domain('longer.example@example'));
 242          $this->assertFalse($issuer->is_valid_login_domain('invalid@email@example.net'));
 243          $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.net'));
 244          $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com'));
 245  
 246          $issuer->set('alloweddomains', '*.example.com');
 247          // Wildcard.
 248          $this->assertFalse($issuer->is_valid_login_domain(''));
 249          $this->assertFalse($issuer->is_valid_login_domain('a@b'));
 250          $this->assertFalse($issuer->is_valid_login_domain('longer.example@example'));
 251          $this->assertFalse($issuer->is_valid_login_domain('longer.example@example.com'));
 252          $this->assertTrue($issuer->is_valid_login_domain('longer.example@sub.example.com'));
 253      }
 254  
 255      /**
 256       * Test endpoints creation for issuers.
 257       * @dataProvider create_endpoints_for_standard_issuer_provider
 258       *
 259       * @covers ::create_endpoints_for_standard_issuer
 260       *
 261       * @param string $type Issuer type to create.
 262       * @param string|null $discoveryurl Expected discovery URL or null if this endpoint doesn't exist.
 263       * @param bool $hasmappingfields True if it's expected the issuer to create has mapping fields.
 264       * @param string|null $baseurl The service URL (mandatory parameter for some issuers, such as NextCloud or IMS OBv2.1).
 265       * @param string|null $expectedexception Name of the expected expection or null if no exception will be thrown.
 266       */
 267      public function test_create_endpoints_for_standard_issuer(string $type, ?string $discoveryurl = null,
 268          bool $hasmappingfields = true, ?string $baseurl = null, ?string $expectedexception = null): void {
 269  
 270          $this->resetAfterTest();
 271  
 272          // Mark test as long because it connects with external services.
 273          if (!PHPUNIT_LONGTEST) {
 274              $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
 275          }
 276  
 277          $this->setAdminUser();
 278  
 279          // Method create_endpoints_for_standard_issuer is called internally from create_standard_issuer.
 280          if ($expectedexception) {
 281              $this->expectException($expectedexception);
 282          }
 283          $issuer = api::create_standard_issuer($type, $baseurl);
 284  
 285          // Check endpoints have been created.
 286          $endpoints = api::get_endpoints($issuer);
 287          $this->assertNotEmpty($endpoints);
 288          $this->assertNotEmpty($issuer->get('image'));
 289          // Check discovery URL.
 290          if ($discoveryurl) {
 291              $this->assertStringContainsString($discoveryurl, $issuer->get_endpoint_url('discovery'));
 292          } else {
 293              $this->assertFalse($issuer->get_endpoint_url('discovery'));
 294          }
 295          // Check userfield mappings.
 296          $userfieldmappings =api::get_user_field_mappings($issuer);
 297          if ($hasmappingfields) {
 298              $this->assertNotEmpty($userfieldmappings);
 299          } else {
 300              $this->assertEmpty($userfieldmappings);
 301          }
 302      }
 303  
 304      /**
 305       * Data provider for test_create_endpoints_for_standard_issuer.
 306       *
 307       * @return array
 308       */
 309      public function create_endpoints_for_standard_issuer_provider(): array {
 310          return [
 311              'Google' => [
 312                  'type' => 'google',
 313                  'discoveryurl' => '.well-known/openid-configuration',
 314              ],
 315              'Google will work too with a valid baseurl parameter' => [
 316                  'type' => 'google',
 317                  'discoveryurl' => '.well-known/openid-configuration',
 318                  'hasmappingfields' => true,
 319                  'baseurl' => 'https://accounts.google.com/',
 320              ],
 321              'IMS OBv2.1' => [
 322                  'type' => 'imsobv2p1',
 323                  'discoveryurl' => '.well-known/badgeconnect.json',
 324                  'hasmappingfields' => false,
 325                  'baseurl' => 'https://dc.imsglobal.org/',
 326              ],
 327              'IMS OBv2.1 without slash in baseurl should work too' => [
 328                  'type' => 'imsobv2p1',
 329                  'discoveryurl' => '.well-known/badgeconnect.json',
 330                  'hasmappingfields' => false,
 331                  'baseurl' => 'https://dc.imsglobal.org',
 332              ],
 333              'IMS OBv2.1 with empty baseurl should return an exception' => [
 334                  'type' => 'imsobv2p1',
 335                  'discoveryurl' => null,
 336                  'hasmappingfields' => false,
 337                  'baseurl' => null,
 338                  'expectedexception' => \moodle_exception::class,
 339              ],
 340              'Microsoft' => [
 341                  'type' => 'microsoft',
 342              ],
 343              'Facebook' => [
 344                  'type' => 'facebook',
 345              ],
 346              'NextCloud' => [
 347                  'type' => 'nextcloud',
 348                  'discoveryurl' => null,
 349                  'hasmappingfields' => true,
 350                  'baseurl' => 'https://dummy.local/nextcloud/',
 351              ],
 352              'NextCloud with empty baseurl should return an exception' => [
 353                  'type' => 'nextcloud',
 354                  'discoveryurl' => null,
 355                  'hasmappingfields' => true,
 356                  'baseurl' => null,
 357                  'expectedexception' => \moodle_exception::class,
 358              ],
 359              'Invalid type should return an exception' => [
 360                  'type' => 'fictitious',
 361                  'discoveryurl' => null,
 362                  'hasmappingfields' => true,
 363                  'baseurl' => null,
 364                  'expectedexception' => \moodle_exception::class,
 365              ],
 366          ];
 367      }
 368  
 369      /**
 370       * Test for get all issuers.
 371       */
 372      public function test_get_all_issuers() {
 373          $this->resetAfterTest();
 374          $this->setAdminUser();
 375          $googleissuer = api::create_standard_issuer('google');
 376          api::create_standard_issuer('facebook');
 377          api::create_standard_issuer('microsoft');
 378  
 379          // Set Google issuer to be shown only on login page.
 380          $record = $googleissuer->to_record();
 381          $record->showonloginpage = $googleissuer::LOGINONLY;
 382          api::update_issuer($record);
 383  
 384          $issuers = api::get_all_issuers();
 385          $this->assertCount(2, $issuers);
 386          $expected = ['Microsoft', 'Facebook'];
 387          $this->assertEqualsCanonicalizing($expected, [$issuers[0]->get_display_name(), $issuers[1]->get_display_name()]);
 388  
 389          $issuers = api::get_all_issuers(true);
 390          $this->assertCount(3, $issuers);
 391          $expected = ['Google', 'Microsoft', 'Facebook'];
 392          $this->assertEqualsCanonicalizing($expected,
 393              [$issuers[0]->get_display_name(), $issuers[1]->get_display_name(), $issuers[2]->get_display_name()]);
 394      }
 395  
 396      /**
 397       * Test for is available for login.
 398       */
 399      public function test_is_available_for_login() {
 400          $this->resetAfterTest();
 401          $this->setAdminUser();
 402          $googleissuer = api::create_standard_issuer('google');
 403  
 404          // Set Google issuer to be shown only on login page.
 405          $record = $googleissuer->to_record();
 406          $record->showonloginpage = $googleissuer::LOGINONLY;
 407          api::update_issuer($record);
 408  
 409          $this->assertFalse($googleissuer->is_available_for_login());
 410  
 411          // Set a clientid and clientsecret.
 412          $googleissuer->set('clientid', 'clientid');
 413          $googleissuer->set('clientsecret', 'secret');
 414          $googleissuer->update();
 415  
 416          $this->assertTrue($googleissuer->is_available_for_login());
 417  
 418          // Set showonloginpage to service only.
 419          $googleissuer->set('showonloginpage', issuer::SERVICEONLY);
 420          $googleissuer->update();
 421  
 422          $this->assertFalse($googleissuer->is_available_for_login());
 423  
 424          // Set showonloginpage to everywhere (service and login) and disable issuer.
 425          $googleissuer->set('showonloginpage', issuer::EVERYWHERE);
 426          $googleissuer->set('enabled', 0);
 427          $googleissuer->update();
 428  
 429          $this->assertFalse($googleissuer->is_available_for_login());
 430  
 431          // Enable issuer.
 432          $googleissuer->set('enabled', 1);
 433          $googleissuer->update();
 434  
 435          $this->assertTrue($googleissuer->is_available_for_login());
 436  
 437          // Remove userinfo endpoint from issuer.
 438          $endpoint = endpoint::get_record([
 439              'issuerid' => $googleissuer->get('id'),
 440              'name' => 'userinfo_endpoint'
 441          ]);
 442          api::delete_endpoint($endpoint->get('id'));
 443  
 444          $this->assertFalse($googleissuer->is_available_for_login());
 445      }
 446  
 447      /**
 448       * Data provider for test_get_internalfield_list and test_get_internalfields.
 449       *
 450       * @return array
 451       */
 452      public function create_custom_profile_fields(): array {
 453          return [
 454              'data' =>
 455              [
 456                  'given' => [
 457                      'Hobbies' => [
 458                          'shortname' => 'hobbies',
 459                          'name' => 'Hobbies',
 460                      ]
 461                  ],
 462                  'expected' => [
 463                      'Hobbies' => [
 464                          'shortname' => 'hobbies',
 465                          'name' => 'Hobbies',
 466                      ]
 467                  ]
 468              ],
 469              [
 470                  'given' => [
 471                      'Billing' => [
 472                          'shortname' => 'billingaddress',
 473                          'name' => 'Billing Address',
 474                      ],
 475                      'Payment' => [
 476                          'shortname' => 'creditcardnumber',
 477                          'name' => 'Credit Card Number',
 478                      ]
 479                  ],
 480                  'expected' => [
 481                      'Billing' => [
 482                          'shortname' => 'billingaddress',
 483                          'name' => 'Billing Address',
 484                      ],
 485                      'Payment' => [
 486                          'shortname' => 'creditcardnumber',
 487                          'name' => 'Credit Card Number',
 488                      ]
 489                  ]
 490              ]
 491          ];
 492      }
 493  
 494      /**
 495       * Test getting the list of internal fields.
 496       *
 497       * @dataProvider create_custom_profile_fields
 498       * @covers ::get_internalfield_list
 499       * @param array $given Categories and profile fields.
 500       * @param array $expected Expected value.
 501       */
 502      public function test_get_internalfield_list(array $given, array $expected): void {
 503          $this->resetAfterTest();
 504          self::generate_custom_profile_fields($given);
 505  
 506          $userfieldmapping = new user_field_mapping();
 507          $internalfieldlist = $userfieldmapping->get_internalfield_list();
 508  
 509          foreach ($expected as $category => $value) {
 510              // Custom profile fields must exist.
 511              $this->assertNotEmpty($internalfieldlist[$category]);
 512  
 513              // Category must have the custom profile fields with expected value.
 514              $this->assertEquals(
 515                  $internalfieldlist[$category][\core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname']],
 516                  $value['name']
 517              );
 518          }
 519      }
 520  
 521      /**
 522       * Test getting the list of internal fields with flat array.
 523       *
 524       * @dataProvider create_custom_profile_fields
 525       * @covers ::get_internalfields
 526       * @param array $given Categories and profile fields.
 527       * @param array $expected Expected value.
 528       */
 529      public function test_get_internalfields(array $given, array $expected): void {
 530          $this->resetAfterTest();
 531          self::generate_custom_profile_fields($given);
 532  
 533          $userfieldmapping = new user_field_mapping();
 534          $internalfields = $userfieldmapping->get_internalfields();
 535  
 536          // Custom profile fields must exist.
 537          foreach ($expected as $category => $value) {
 538              $this->assertContains( \core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname'], $internalfields);
 539          }
 540      }
 541  
 542      /**
 543       * Test getting the list of empty external/custom profile fields.
 544       *
 545       * @covers ::get_internalfields
 546       */
 547      public function test_get_empty_internalfield_list(): void {
 548  
 549          // Get internal (profile) fields.
 550          $userfieldmapping = new user_field_mapping();
 551          $internalfieldlist = $userfieldmapping->get_internalfields();
 552  
 553          // Get user fields.
 554          $userfields = array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username']);
 555  
 556          // Internal fields and user fields must exact same.
 557          $this->assertEquals($userfields, $internalfieldlist);
 558      }
 559  
 560      /**
 561       * Test getting Return the list of profile fields.
 562       *
 563       * @dataProvider create_custom_profile_fields
 564       * @covers ::get_profile_field_list
 565       * @param array $given Categories and profile fields.
 566       * @param array $expected Expected value.
 567       */
 568      public function test_get_profile_field_list(array $given, array $expected): void {
 569          $this->resetAfterTest();
 570          self::generate_custom_profile_fields($given);
 571  
 572          $profilefieldlist = get_profile_field_list();
 573  
 574          foreach ($expected as $category => $value) {
 575              $this->assertEquals(
 576                  $profilefieldlist[$category][\core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname']],
 577                  $value['name']
 578              );
 579          }
 580      }
 581  
 582      /**
 583       * Test getting the list of valid custom profile user fields.
 584       *
 585       * @dataProvider create_custom_profile_fields
 586       * @covers ::get_profile_field_names
 587       * @param array $given Categories and profile fields.
 588       * @param array $expected Expected value.
 589       */
 590      public function test_get_profile_field_names(array $given, array $expected): void {
 591          $this->resetAfterTest();
 592          self::generate_custom_profile_fields($given);
 593  
 594          $profilefieldnames = get_profile_field_names();
 595  
 596          // Custom profile fields must exist.
 597          foreach ($expected as $category => $value) {
 598              $this->assertContains( \core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname'], $profilefieldnames);
 599          }
 600      }
 601  
 602      /**
 603       * Generate data into DB for Testing getting user fields mapping.
 604       *
 605       * @param array $given Categories and profile fields.
 606       */
 607      private function generate_custom_profile_fields(array $given): void {
 608          // Create a profile category and the profile fields.
 609          foreach ($given as $category => $value) {
 610              $customprofilefieldcategory = ['name' => $category, 'sortorder' => 1];
 611              $category = $this->getDataGenerator()->create_custom_profile_field_category($customprofilefieldcategory);
 612              $this->getDataGenerator()->create_custom_profile_field(
 613                  ['shortname' => $value['shortname'],
 614                  'name' => $value['name'],
 615                  'categoryid' => $category->id,
 616                  'required' => 1, 'visible' => 1, 'locked' => 0, 'datatype' => 'text', 'defaultdata' => null]);
 617          }
 618      }
 619  
 620  }