Merge branch 'MDL-68384-fix-spec-violations-310' of https://github.com/cengage/moodle...
authorAdrian Greeve <abgreeve@gmail.com>
Fri, 9 Oct 2020 04:05:34 +0000 (12:05 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Fri, 9 Oct 2020 04:05:34 +0000 (12:05 +0800)
1  2 
mod/lti/locallib.php
mod/lti/tests/locallib_test.php

diff --combined mod/lti/locallib.php
@@@ -126,7 -126,8 +126,8 @@@ function lti_get_jwt_claim_mapping() 
              'suffix' => 'dl',
              'group' => 'deep_linking_settings',
              'claim' => 'accept_copy_advice',
-             'isarray' => false
+             'isarray' => false,
+             'type' => 'boolean'
          ],
          'accept_media_types' => [
              'suffix' => 'dl',
              'suffix' => 'dl',
              'group' => 'deep_linking_settings',
              'claim' => 'accept_multiple',
-             'isarray' => false
+             'isarray' => false,
+             'type' => 'boolean'
          ],
          'accept_presentation_document_targets' => [
              'suffix' => 'dl',
              'suffix' => 'dl',
              'group' => 'deep_linking_settings',
              'claim' => 'accept_unsigned',
-             'isarray' => false
+             'isarray' => false,
+             'type' => 'boolean'
          ],
          'auto_create' => [
              'suffix' => 'dl',
              'group' => 'deep_linking_settings',
              'claim' => 'auto_create',
-             'isarray' => false
+             'isarray' => false,
+             'type' => 'boolean'
          ],
          'can_confirm' => [
              'suffix' => 'dl',
              'group' => 'deep_linking_settings',
              'claim' => 'can_confirm',
-             'isarray' => false
+             'isarray' => false,
+             'type' => 'boolean'
          ],
          'content_item_return_url' => [
              'suffix' => 'dl',
          'tool_consumer_info_product_family_code' => [
              'suffix' => '',
              'group' => 'tool_platform',
-             'claim' => 'family_code',
+             'claim' => 'product_family_code',
              'isarray' => false
          ],
          'tool_consumer_info_version' => [
              'isarray' => false
          ],
          'lis_outcome_service_url' => [
-             'suffix' => 'bos',
-             'group' => 'basicoutcomesservice',
+             'suffix' => 'bo',
+             'group' => 'basicoutcome',
              'claim' => 'lis_outcome_service_url',
              'isarray' => false
          ],
          'lis_result_sourcedid' => [
-             'suffix' => 'bos',
-             'group' => 'basicoutcomesservice',
+             'suffix' => 'bo',
+             'group' => 'basicoutcome',
              'claim' => 'lis_result_sourcedid',
              'isarray' => false
          ],
@@@ -1083,7 -1088,7 +1088,7 @@@ function lti_build_custom_parameters($t
   * @throws coding_exception For invalid media type and presentation target parameters.
   */
  function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
 -                                                  $presentationtargets = [], $autocreate = false, $multiple = false,
 +                                                  $presentationtargets = [], $autocreate = false, $multiple = true,
                                                    $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') {
      global $USER;
  
@@@ -1421,109 -1426,6 +1426,109 @@@ function lti_verify_jwt_signature($type
      return $tool;
  }
  
 +/**
 + * Converts LTI 1.1 Content Item for LTI Link to Form data.
 + *
 + * @param object $tool Tool for which the item is created for.
 + * @param object $typeconfig The tool configuration.
 + * @param object $item Item populated from JSON to be converted to Form form
 + *
 + * @return stdClass Form config for the item
 + */
 +function content_item_to_form(object $tool, object $typeconfig, object $item) : stdClass {
 +    $config = new stdClass();
 +    $config->name = '';
 +    if (isset($item->title)) {
 +        $config->name = $item->title;
 +    }
 +    if (empty($config->name)) {
 +        $config->name = $tool->name;
 +    }
 +    if (isset($item->text)) {
 +        $config->introeditor = [
 +            'text' => $item->text,
 +            'format' => FORMAT_PLAIN
 +        ];
 +    } else {
 +        $config->introeditor = [
 +            'text' => '',
 +            'format' => FORMAT_PLAIN
 +        ];
 +    }
 +    if (isset($item->icon->{'@id'})) {
 +        $iconurl = new moodle_url($item->icon->{'@id'});
 +        // Assign item's icon URL to secureicon or icon depending on its scheme.
 +        if (strtolower($iconurl->get_scheme()) === 'https') {
 +            $config->secureicon = $iconurl->out(false);
 +        } else {
 +            $config->icon = $iconurl->out(false);
 +        }
 +    }
 +    if (isset($item->url)) {
 +        $url = new moodle_url($item->url);
 +        $config->toolurl = $url->out(false);
 +        $config->typeid = 0;
 +    } else {
 +        $config->typeid = $tool->id;
 +    }
 +    $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
 +    $islti2 = $tool->ltiversion === LTI_VERSION_2;
 +    if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
 +        $acceptgrades = $typeconfig->lti_acceptgrades;
 +        if ($acceptgrades == LTI_SETTING_ALWAYS) {
 +            // We create a line item regardless if the definition contains one or not.
 +            $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
 +            $config->grade_modgrade_point = 100;
 +        }
 +        if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
 +            if (isset($item->lineItem)) {
 +                $lineitem = $item->lineItem;
 +                $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
 +                $maxscore = 100;
 +                if (isset($lineitem->scoreConstraints)) {
 +                    $sc = $lineitem->scoreConstraints;
 +                    if (isset($sc->totalMaximum)) {
 +                        $maxscore = $sc->totalMaximum;
 +                    } else if (isset($sc->normalMaximum)) {
 +                        $maxscore = $sc->normalMaximum;
 +                    }
 +                }
 +                $config->grade_modgrade_point = $maxscore;
 +                $config->lineitemresourceid = '';
 +                $config->lineitemtag = '';
 +                if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
 +                    $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:'';
 +                }
 +                if (isset($lineitem->tag)) {
 +                    $config->lineitemtag = $lineitem->tag?:'';
 +                }
 +            }
 +        }
 +    }
 +    $config->instructorchoicesendname = LTI_SETTING_NEVER;
 +    $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
 +    $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
 +    if (isset($item->placementAdvice->presentationDocumentTarget)) {
 +        if ($item->placementAdvice->presentationDocumentTarget === 'window') {
 +            $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW;
 +        } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') {
 +            $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
 +        } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') {
 +            $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED;
 +        }
 +    }
 +    if (isset($item->custom)) {
 +        $customparameters = [];
 +        foreach ($item->custom as $key => $value) {
 +            $customparameters[] = "{$key}={$value}";
 +        }
 +        $config->instructorcustomparameters = implode("\n", $customparameters);
 +    }
 +    // Including a JSON version of the form data to support adding many items in one submit.
 +    $config->contentitemjson = json_encode($item);
 +    return $config;
 +}
 +
  /**
   * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
   * selected content item. This configuration data can be then used when adding a tool into the course.
@@@ -1563,24 -1465,97 +1568,24 @@@ function lti_tool_configuration_from_co
      if (empty($items)) {
          throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
      }
 -    if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'}) || (count($items->{'@graph'}) > 1)) {
 +    if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) {
          throw new moodle_exception('errorinvalidresponseformat', 'mod_lti');
      }
  
      $config = null;
 -    if (!empty($items->{'@graph'})) {
 -        $item = $items->{'@graph'}[0];
 +    $items = $items->{'@graph'};
 +    if (!empty($items)) {
          $typeconfig = lti_get_type_type_config($tool->id);
 -
 -        $config = new stdClass();
 -        $config->name = '';
 -        if (isset($item->title)) {
 -            $config->name = $item->title;
 -        }
 -        if (empty($config->name)) {
 -            $config->name = $tool->name;
 -        }
 -        if (isset($item->text)) {
 -            $config->introeditor = [
 -                'text' => $item->text,
 -                'format' => FORMAT_PLAIN
 -            ];
 -        }
 -        if (isset($item->icon->{'@id'})) {
 -            $iconurl = new moodle_url($item->icon->{'@id'});
 -            // Assign item's icon URL to secureicon or icon depending on its scheme.
 -            if (strtolower($iconurl->get_scheme()) === 'https') {
 -                $config->secureicon = $iconurl->out(false);
 -            } else {
 -                $config->icon = $iconurl->out(false);
 -            }
 -        }
 -        if (isset($item->url)) {
 -            $url = new moodle_url($item->url);
 -            $config->toolurl = $url->out(false);
 -            $config->typeid = 0;
 +        if (count($items) == 1) {
 +            $config = content_item_to_form($tool, $typeconfig, $items[0]);
          } else {
 -            $config->typeid = $tool->id;
 -        }
 -        $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
 -        if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
 -            $acceptgrades = $typeconfig->lti_acceptgrades;
 -            if ($acceptgrades == LTI_SETTING_ALWAYS) {
 -                // We create a line item regardless if the definition contains one or not.
 -                $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
 -            }
 -            if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) {
 -                if (isset($item->lineItem)) {
 -                    $lineitem = $item->lineItem;
 -                    $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
 -                    $maxscore = 100;
 -                    if (isset($lineitem->scoreConstraints)) {
 -                        $sc = $lineitem->scoreConstraints;
 -                        if (isset($sc->totalMaximum)) {
 -                            $maxscore = $sc->totalMaximum;
 -                        } else if (isset($sc->normalMaximum)) {
 -                            $maxscore = $sc->normalMaximum;
 -                        }
 -                    }
 -                    $config->grade_modgrade_point = $maxscore;
 -                    $config->lineitemresourceid = '';
 -                    $config->lineitemtag = '';
 -                    if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) {
 -                        $config->lineitemresourceid = $lineitem->assignedActivity->activityId ? : '';
 -                    }
 -                    if (isset($lineitem->tag)) {
 -                        $config->lineitemtag = $lineitem->tag ? : '';
 -                    }
 -                }
 -            }
 -        }
 -        $config->instructorchoicesendname = LTI_SETTING_NEVER;
 -        $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER;
 -        $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT;
 -        if (isset($item->placementAdvice->presentationDocumentTarget)) {
 -            if ($item->placementAdvice->presentationDocumentTarget === 'window') {
 -                $config->launchcontainer = LTI_LAUNCH_CONTAINER_WINDOW;
 -            } else if ($item->placementAdvice->presentationDocumentTarget === 'frame') {
 -                $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
 -            } else if ($item->placementAdvice->presentationDocumentTarget === 'iframe') {
 -                $config->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED;
 +            $multiple = [];
 +            foreach ($items as $item) {
 +                $multiple[] = content_item_to_form($tool, $typeconfig, $item);
              }
 +            $config = new stdClass();
 +            $config->multiple = $multiple;
          }
 -        if (isset($item->custom)) {
 -            $customparameters = [];
 -            foreach ($item->custom as $key => $value) {
 -                $customparameters[] = "{$key}={$value}";
 -            }
 -            $config->instructorcustomparameters = implode("\n", $customparameters);
 -        }
 -        $config->contentitemjson = json_encode($item);
      }
      return $config;
  }
@@@ -1617,35 -1592,7 +1622,35 @@@ function lti_convert_content_items($par
                      $newitem->text = $item->html;
                      unset($newitem->html);
                  }
 -                if (isset($item->presentation)) {
 +                if (isset($item->iframe)) {
 +                    // DeepLinking allows multiple options to be declared as supported.
 +                    // We favor iframe over new window if both are specified.
 +                    $newitem->placementAdvice = new stdClass();
 +                    $newitem->placementAdvice->presentationDocumentTarget = 'iframe';
 +                    if (isset($item->iframe->width)) {
 +                        $newitem->placementAdvice->displayWidth = $item->iframe->width;
 +                    }
 +                    if (isset($item->iframe->height)) {
 +                        $newitem->placementAdvice->displayHeight = $item->iframe->height;
 +                    }
 +                    unset($newitem->iframe);
 +                    unset($newitem->window);
 +                } else if (isset($item->window)) {
 +                    $newitem->placementAdvice = new stdClass();
 +                    $newitem->placementAdvice->presentationDocumentTarget = 'window';
 +                    if (isset($item->window->targetName)) {
 +                        $newitem->placementAdvice->windowTarget = $item->window->targetName;
 +                    }
 +                    if (isset($item->window->width)) {
 +                        $newitem->placementAdvice->displayWidth = $item->window->width;
 +                    }
 +                    if (isset($item->window->height)) {
 +                        $newitem->placementAdvice->displayHeight = $item->window->height;
 +                    }
 +                    unset($newitem->window);
 +                } else if (isset($item->presentation)) {
 +                    // This may have been part of an early draft but is not in the final spec
 +                    // so keeping it around for now in case it's actually been used.
                      $newitem->placementAdvice = new stdClass();
                      if (isset($item->presentation->documentTarget)) {
                          $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;
@@@ -3250,9 -3197,12 +3255,12 @@@ function lti_sign_jwt($parms, $endpoint
          $claim = LTI_JWT_CLAIM_PREFIX;
          if (array_key_exists($key, $claimmapping)) {
              $mapping = $claimmapping[$key];
+             $type = $mapping["type"] ?? "string";
              if ($mapping['isarray']) {
                  $value = explode(',', $value);
                  sort($value);
+             } else if ($type == 'boolean') {
+                 $value = isset($value) && ($value == 'true');
              }
              if (!empty($mapping['suffix'])) {
                  $claim .= "-{$mapping['suffix']}";
@@@ -377,7 -377,7 +377,7 @@@ class mod_lti_locallib_testcase extend
          $this->assertEquals('frame,iframe,window', $params['accept_presentation_document_targets']);
          $this->assertEquals($returnurl->out(false), $params['content_item_return_url']);
          $this->assertEquals('false', $params['accept_unsigned']);
 -        $this->assertEquals('false', $params['accept_multiple']);
 +        $this->assertEquals('true', $params['accept_multiple']);
          $this->assertEquals('false', $params['accept_copy_advice']);
          $this->assertEquals('false', $params['auto_create']);
          $this->assertEquals($type->name, $params['title']);
                  'suffix' => 'dl',
                  'group' => 'deep_linking_settings',
                  'claim' => 'accept_copy_advice',
-                 'isarray' => false
+                 'isarray' => false,
+                 'type' => 'boolean'
              ],
              'accept_media_types' => [
                  'suffix' => 'dl',
                  'suffix' => 'dl',
                  'group' => 'deep_linking_settings',
                  'claim' => 'accept_multiple',
-                 'isarray' => false
+                 'isarray' => false,
+                 'type' => 'boolean'
              ],
              'accept_presentation_document_targets' => [
                  'suffix' => 'dl',
                  'suffix' => 'dl',
                  'group' => 'deep_linking_settings',
                  'claim' => 'accept_unsigned',
-                 'isarray' => false
+                 'isarray' => false,
+                 'type' => 'boolean'
              ],
              'auto_create' => [
                  'suffix' => 'dl',
                  'group' => 'deep_linking_settings',
                  'claim' => 'auto_create',
-                 'isarray' => false
+                 'isarray' => false,
+                 'type' => 'boolean'
              ],
              'can_confirm' => [
                  'suffix' => 'dl',
                  'group' => 'deep_linking_settings',
                  'claim' => 'can_confirm',
-                 'isarray' => false
+                 'isarray' => false,
+                 'type' => 'boolean'
              ],
              'content_item_return_url' => [
                  'suffix' => 'dl',
              'tool_consumer_info_product_family_code' => [
                  'suffix' => '',
                  'group' => 'tool_platform',
-                 'claim' => 'family_code',
+                 'claim' => 'product_family_code',
                  'isarray' => false
              ],
              'tool_consumer_info_version' => [
                  'isarray' => false
              ],
              'lis_outcome_service_url' => [
-                 'suffix' => 'bos',
-                 'group' => 'basicoutcomesservice',
+                 'suffix' => 'bo',
+                 'group' => 'basicoutcome',
                  'claim' => 'lis_outcome_service_url',
                  'isarray' => false
              ],
              'lis_result_sourcedid' => [
-                 'suffix' => 'bos',
-                 'group' => 'basicoutcomesservice',
+                 'suffix' => 'bo',
+                 'group' => 'basicoutcome',
                  'claim' => 'lis_result_sourcedid',
                  'isarray' => false
              ],
@@@ -1185,6 -1190,7 +1190,6 @@@ MwIDAQA
       */
      public function test_lti_verify_jwt_signature_no_public_key() {
          $this->resetAfterTest();
 -
          $this->setAdminUser();
  
          // Create a tool type, associated with that proxy.
              'url' => 'http://example.com/messages/launch',
              'title' => 'Test title',
              'text' => 'Test text',
 -            'frame' => []
 +            'iframe' => []
 +        ];
 +        $contentitems[] = [
 +            'type' => 'ltiResourceLink',
 +            'url' => 'http://example.com/messages/launch2',
 +            'title' => 'Test title2',
 +            'text' => 'Test text2',
 +            'iframe' => [
 +                'height' => 200,
 +                'width' => 300
 +            ],
 +            'window' => []
 +        ];
 +        $contentitems[] = [
 +            'type' => 'ltiResourceLink',
 +            'url' => 'http://example.com/messages/launch3',
 +            'title' => 'Test title3',
 +            'text' => 'Test text3',
 +            'window' => [
 +                'targetName' => 'test-win',
 +                'height' => 400
 +            ]
          ];
  
          $contentitems = json_encode($contentitems);
          $objgraph->url = 'http://example.com/messages/launch';
          $objgraph->title = 'Test title';
          $objgraph->text = 'Test text';
 -        $objgraph->frame = [];
 +        $objgraph->placementAdvice = new stdClass();
 +        $objgraph->placementAdvice->presentationDocumentTarget = 'iframe';
          $objgraph->{$strtype} = 'LtiLinkItem';
          $objgraph->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
  
 +        $objgraph2 = new stdClass();
 +        $objgraph2->url = 'http://example.com/messages/launch2';
 +        $objgraph2->title = 'Test title2';
 +        $objgraph2->text = 'Test text2';
 +        $objgraph2->placementAdvice = new stdClass();
 +        $objgraph2->placementAdvice->presentationDocumentTarget = 'iframe';
 +        $objgraph2->placementAdvice->displayHeight = 200;
 +        $objgraph2->placementAdvice->displayWidth = 300;
 +        $objgraph2->{$strtype} = 'LtiLinkItem';
 +        $objgraph2->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
 +
 +        $objgraph3 = new stdClass();
 +        $objgraph3->url = 'http://example.com/messages/launch3';
 +        $objgraph3->title = 'Test title3';
 +        $objgraph3->text = 'Test text3';
 +        $objgraph3->placementAdvice = new stdClass();
 +        $objgraph3->placementAdvice->presentationDocumentTarget = 'window';
 +        $objgraph3->placementAdvice->displayHeight = 400;
 +        $objgraph3->placementAdvice->windowTarget = 'test-win';
 +        $objgraph3->{$strtype} = 'LtiLinkItem';
 +        $objgraph3->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
 +
          $expected = new stdClass();
          $expected->{$strcontext} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
          $expected->{$strgraph} = [];
          $expected->{$strgraph}[] = $objgraph;
 +        $expected->{$strgraph}[] = $objgraph2;
 +        $expected->{$strgraph}[] = $objgraph3;
  
          $this->assertEquals($expected, $jsondecode);
      }
  
 +    /**
 +     * Test adding a single gradable item through content item.
 +     */
 +    public function test_lti_tool_configuration_from_content_item_single_gradable() {
 +        $this->resetAfterTest();
 +        $this->setAdminUser();
 +
 +        $type = new stdClass();
 +        $type->name = "Test tool";
 +        $type->baseurl = "http://example.com";
 +        $config = new stdClass();
 +        $config->lti_acceptgrades = LTI_SETTING_DELEGATE;
 +        $typeid = lti_add_type($type, $config);
 +
 +        $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
 +        $contentitems = [];
 +        $contentitems[] = [
 +            'type' => 'ltiResourceLink',
 +            'url' => 'http://example.com/messages/launch',
 +            'title' => 'Test title',
 +            'lineItem' => [
 +                'resourceId' => 'r12345',
 +                'tag' => 'final',
 +                'scoreMaximum' => 10.0
 +            ],
 +            'frame' => []
 +        ];
 +        $contentitemsjson13 = json_encode($contentitems);
 +        $json11 = lti_convert_content_items($contentitemsjson13);
 +
 +        $config = lti_tool_configuration_from_content_item($typeid,
 +                                                           'ContentItemSelection',
 +                                                           $type->ltiversion,
 +                                                           'ConsumerKey',
 +                                                           $json11);
 +
 +        $this->assertEquals($contentitems[0]['url'], $config->toolurl);
 +        $this->assertEquals(LTI_SETTING_ALWAYS, $config->instructorchoiceacceptgrades);
 +        $this->assertEquals($contentitems[0]['lineItem']['tag'], $config->lineitemtag);
 +        $this->assertEquals($contentitems[0]['lineItem']['resourceId'], $config->lineitemresourceid);
 +        $this->assertEquals($contentitems[0]['lineItem']['scoreMaximum'], $config->grade_modgrade_point);
 +    }
 +
 +    /**
 +     * Test adding multiple gradable items through content item.
 +     */
 +    public function test_lti_tool_configuration_from_content_item_multiple() {
 +        $this->resetAfterTest();
 +        $this->setAdminUser();
 +
 +        $type = new stdClass();
 +        $type->name = "Test tool";
 +        $type->baseurl = "http://example.com";
 +        $config = new stdClass();
 +        $config->lti_acceptgrades = LTI_SETTING_DELEGATE;
 +        $typeid = lti_add_type($type, $config);
 +
 +        $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
 +        $contentitems = [];
 +        $contentitems[] = [
 +            'type' => 'ltiResourceLink',
 +            'url' => 'http://example.com/messages/launch',
 +            'title' => 'Test title',
 +            'text' => 'Test text',
 +            'icon' => [
 +                'url' => 'http://lti.example.com/image.jpg',
 +                'width' => 100
 +            ],
 +            'frame' => []
 +        ];
 +        $contentitems[] = [
 +            'type' => 'ltiResourceLink',
 +            'url' => 'http://example.com/messages/launchgraded',
 +            'title' => 'Test Graded',
 +            'lineItem' => [
 +                'resourceId' => 'r12345',
 +                'tag' => 'final',
 +                'scoreMaximum' => 10.0
 +            ],
 +            'frame' => []
 +        ];
 +        $contentitemsjson13 = json_encode($contentitems);
 +        $json11 = lti_convert_content_items($contentitemsjson13);
 +
 +        $config = lti_tool_configuration_from_content_item($typeid,
 +                                                           'ContentItemSelection',
 +                                                           $type->ltiversion,
 +                                                           'ConsumerKey',
 +                                                           $json11);
 +        $this->assertNotNull($config->multiple);
 +        $this->assertEquals(2, count( $config->multiple ));
 +        $this->assertEquals($contentitems[0]['title'], $config->multiple[0]->name);
 +        $this->assertEquals($contentitems[0]['url'], $config->multiple[0]->toolurl);
 +        $this->assertEquals(LTI_SETTING_NEVER, $config->multiple[0]->instructorchoiceacceptgrades);
 +        $this->assertEquals($contentitems[1]['url'], $config->multiple[1]->toolurl);
 +        $this->assertEquals(LTI_SETTING_ALWAYS, $config->multiple[1]->instructorchoiceacceptgrades);
 +        $this->assertEquals($contentitems[1]['lineItem']['tag'], $config->multiple[1]->lineitemtag);
 +        $this->assertEquals($contentitems[1]['lineItem']['resourceId'], $config->multiple[1]->lineitemresourceid);
 +        $this->assertEquals($contentitems[1]['lineItem']['scoreMaximum'], $config->multiple[1]->grade_modgrade_point);
 +    }
 +
 +    /**
 +     * Test adding a single non gradable item through content item.
 +     */
 +    public function test_lti_tool_configuration_from_content_item_single() {
 +        $this->resetAfterTest();
 +        $this->setAdminUser();
 +
 +        $type = new stdClass();
 +        $type->name = "Test tool";
 +        $type->baseurl = "http://example.com";
 +        $config = new stdClass();
 +        $typeid = lti_add_type($type, $config);
 +
 +        $generator = $this->getDataGenerator()->get_plugin_generator('mod_lti');
 +        $contentitems = [];
 +        $contentitems[] = [
 +            'type' => 'ltiResourceLink',
 +            'url' => 'http://example.com/messages/launch',
 +            'title' => 'Test title',
 +            'text' => 'Test text',
 +            'icon' => [
 +                'url' => 'http://lti.example.com/image.jpg',
 +                'width' => 100
 +            ],
 +            'frame' => []
 +        ];
 +        $contentitemsjson13 = json_encode($contentitems);
 +        $json11 = lti_convert_content_items($contentitemsjson13);
 +
 +        $config = lti_tool_configuration_from_content_item($typeid,
 +                                                           'ContentItemSelection',
 +                                                           $type->ltiversion,
 +                                                           'ConsumerKey',
 +                                                           $json11);
 +        $this->assertEquals($contentitems[0]['title'], $config->name);
 +        $this->assertEquals($contentitems[0]['text'], $config->introeditor['text']);
 +        $this->assertEquals($contentitems[0]['url'], $config->toolurl);
 +        $this->assertEquals($contentitems[0]['icon']['url'], $config->icon);
 +        $this->assertEquals(LTI_SETTING_NEVER, $config->instructorchoiceacceptgrades);
 +
 +    }
 +
      /**
       * Test lti_sign_jwt().
       */