diff --git a/README.md b/README.md index d722e60..c469c46 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ -This set of PHP classes encapsulates the code required by an LTI compliant tool provider to communicate with an LTI tool consumer. -It includes support for LTI 1.1 and the unofficial extensions to Basic LTI, as well as the registration process and services of LTI 1.2/2.0. -Support has also been added for the Names and Role Provisioning service and the Result and Score services where these are supported using the -OAuth 1 security model (support for the new security model in LTI 1.3 will be in a forthcoming update). -These classes are designed as an update to the LTI Tool Provider class library (http://www.spvsoftwareproducts.com/php/lti_tool_provider/) and -a replacement for the library at https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP which is no longer supported. +This set of PHP classes encapsulates the code required by an LTI compliant tool provider to communicate with an LTI tool consumer. It includes support for LTI 1.1 and the unofficial extensions to Basic LTI, as well as the registration process and services of LTI 1.2/2.0, and the new security model introduced by LTI 1.3. The Names and Role Provisioning service and the Assignment and Grade services (Line Item, Result and Score) are also supported using either of the LTI security models. + +These classes are designed as an update to the LTI Tool Provider class library (http://www.spvsoftwareproducts.com/php/lti_tool_provider/) and a replacement for the library at https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP which is no longer supported. Whilst supporting LTI is relatively simple, the benefits to using a class library like this one are: + * the abstraction layer provided by the classes keeps the LTI communications separate from the application code; * the code can be re-used between multiple tool providers; * LTI data is transformed into useful objects and missing data automatically replaced with sensible defaults; diff --git a/composer.json b/composer.json index b0e3851..3146dcc 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ } ], "require": { - "php": ">=5.6.0" + "php": ">=5.6.0", + "firebase/php-jwt": "^5.0.0" }, "autoload": { "psr-4": { diff --git a/sql/lti4-tables-mssql.sql b/sql/lti4-tables-mssql.sql new file mode 100644 index 0000000..dd7731e --- /dev/null +++ b/sql/lti4-tables-mssql.sql @@ -0,0 +1,144 @@ +CREATE TABLE lti2_consumer ( + consumer_pk int IDENTITY NOT NULL, + name varchar(50) NOT NULL, + consumer_key varchar(255) DEFAULT NULL, + secret varchar(1024) DEFAULT NULL, + client_id varchar(255) DEFAULT NULL, + platform_id varchar(255) DEFAULT NULL, + deployment_id varchar(255) DEFAULT NULL, + public_key text DEFAULT NULL, + lti_version varchar(10) DEFAULT NULL, + signature_method varchar(15) NOT NULL DEFAULT 'HMAC-SHA1', + consumer_name varchar(255) DEFAULT NULL, + consumer_version varchar(255) DEFAULT NULL, + consumer_guid varchar(1024) DEFAULT NULL, + profile text DEFAULT NULL, + tool_proxy text DEFAULT NULL, + settings text DEFAULT NULL, + protected bit NOT NULL, + enabled bit NOT NULL, + enable_from datetime2 DEFAULT NULL, + enable_until datetime2 DEFAULT NULL, + last_access date DEFAULT NULL, + created datetime2 NOT NULL, + updated datetime2 NOT NULL, + PRIMARY KEY (consumer_pk), + CONSTRAINT UC_lti2_consumer_consumer_key UNIQUE (consumer_key), + CONSTRAINT UC_lti2_consumer_platform UNIQUE (platform_id, client_id, deployment_id) +); + +CREATE TABLE lti2_nonce ( + consumer_pk int NOT NULL, + value varchar(50) NOT NULL, + expires datetime2 NOT NULL, + PRIMARY KEY (consumer_pk, value) +); + +ALTER TABLE lti2_nonce + ADD CONSTRAINT lti2_nonce_lti2_consumer_FK1 + FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +CREATE TABLE lti2_access_token ( + consumer_pk int NOT NULL, + scopes text NOT NULL, + token varchar(2000) NOT NULL, + expires datetime2 NOT NULL, + created datetime2 NOT NULL, + updated datetime2 NOT NULL, + PRIMARY KEY (consumer_pk) +); + +ALTER TABLE lti2_access_token + ADD CONSTRAINT lti2_access_token_lti2_consumer_FK1 + FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +CREATE TABLE lti2_context ( + context_pk int NOT NULL IDENTITY, + consumer_pk int NOT NULL, + title varchar(255) DEFAULT NULL, + lti_context_id varchar(255) NOT NULL, + type varchar(50) DEFAULT NULL, + settings text DEFAULT NULL, + created datetime2 NOT NULL, + updated datetime2 NOT NULL, + PRIMARY KEY (context_pk) +); + +ALTER TABLE lti2_context + ADD CONSTRAINT lti2_context_lti2_consumer_FK1 + FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +CREATE INDEX lti2_context_consumer_id_IDX + ON lti2_context (consumer_pk); + +CREATE TABLE lti2_resource_link ( + resource_link_pk int IDENTITY, + context_pk int DEFAULT NULL, + consumer_pk int DEFAULT NULL, + title varchar(255) DEFAULT NULL, + lti_resource_link_id varchar(255) NOT NULL, + settings text, + primary_resource_link_pk int DEFAULT NULL, + share_approved bit DEFAULT NULL, + created datetime2 NOT NULL, + updated datetime2 NOT NULL, + PRIMARY KEY (resource_link_pk) +); + +ALTER TABLE lti2_resource_link + ADD CONSTRAINT lti2_resource_link_lti2_consumer_FK1 + FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +ALTER TABLE lti2_resource_link + ADD CONSTRAINT lti2_resource_link_lti2_context_FK1 + FOREIGN KEY (context_pk) + REFERENCES lti2_context (context_pk); + +ALTER TABLE lti2_resource_link + ADD CONSTRAINT lti2_resource_link_lti2_resource_link_FK1 + FOREIGN KEY (primary_resource_link_pk) + REFERENCES lti2_resource_link (resource_link_pk); + +CREATE INDEX lti2_resource_link_consumer_pk_IDX + ON lti2_resource_link (consumer_pk); + +CREATE INDEX lti2_resource_link_context_pk_IDX + ON lti2_resource_link (context_pk); + +CREATE TABLE lti2_user_result ( + user_result_pk int IDENTITY, + resource_link_pk int NOT NULL, + lti_user_id varchar(255) NOT NULL, + lti_result_sourcedid varchar(1024) NOT NULL, + created datetime2 NOT NULL, + updated datetime2 NOT NULL, + PRIMARY KEY (user_result_pk) +); + +ALTER TABLE lti2_user_result + ADD CONSTRAINT lti2_user_result_lti2_resource_link_FK1 + FOREIGN KEY (resource_link_pk) + REFERENCES lti2_resource_link (resource_link_pk); + +CREATE INDEX lti2_user_result_resource_link_pk_IDX + ON lti2_user_result (resource_link_pk); + +CREATE TABLE lti2_share_key ( + share_key_id varchar(32) NOT NULL, + resource_link_pk int NOT NULL, + auto_approve bit NOT NULL, + expires datetime2 NOT NULL, + PRIMARY KEY (share_key_id) +); + +ALTER TABLE lti2_share_key + ADD CONSTRAINT lti2_share_key_lti2_resource_link_FK1 + FOREIGN KEY (resource_link_pk) + REFERENCES lti2_resource_link (resource_link_pk); + +CREATE INDEX lti2_share_key_resource_link_pk_IDX + ON lti2_share_key (resource_link_pk); diff --git a/sql/lti4-tables-mysql.sql b/sql/lti4-tables-mysql.sql new file mode 100644 index 0000000..84ac654 --- /dev/null +++ b/sql/lti4-tables-mysql.sql @@ -0,0 +1,163 @@ +CREATE TABLE lti2_consumer ( + consumer_pk int(11) NOT NULL AUTO_INCREMENT, + name varchar(50) NOT NULL, + consumer_key varchar(255) DEFAULT NULL, + secret varchar(1024) DEFAULT NULL, + platform_id VARCHAR(255) DEFAULT NULL, + client_id VARCHAR(255) DEFAULT NULL, + deployment_id VARCHAR(255) DEFAULT NULL, + public_key text DEFAULT NULL, + lti_version varchar(10) DEFAULT NULL, + signature_method varchar(15) NOT NULL DEFAULT 'HMAC-SHA1', + consumer_name varchar(255) DEFAULT NULL, + consumer_version varchar(255) DEFAULT NULL, + consumer_guid varchar(1024) DEFAULT NULL, + profile text DEFAULT NULL, + tool_proxy text DEFAULT NULL, + settings text DEFAULT NULL, + protected tinyint(1) NOT NULL, + enabled tinyint(1) NOT NULL, + enable_from datetime DEFAULT NULL, + enable_until datetime DEFAULT NULL, + last_access date DEFAULT NULL, + created datetime NOT NULL, + updated datetime NOT NULL, + PRIMARY KEY (consumer_pk) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE lti2_consumer + ADD UNIQUE INDEX lti2_consumer_consumer_key_UNIQUE (consumer_key ASC); + +ALTER TABLE lti2_consumer + ADD UNIQUE INDEX lti2_consumer_platform_UNIQUE (platform_id ASC, client_id ASC, deployment_id ASC); + +CREATE TABLE lti2_nonce ( + consumer_pk int(11) NOT NULL, + value varchar(50) NOT NULL, + expires datetime NOT NULL, + PRIMARY KEY (consumer_pk, value) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE lti2_nonce + ADD CONSTRAINT lti2_nonce_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +CREATE TABLE lti2_access_token ( + consumer_pk int(11) NOT NULL, + scopes text NOT NULL, + token varchar(2000) NOT NULL, + expires datetime NOT NULL, + created datetime NOT NULL, + updated datetime NOT NULL, + PRIMARY KEY (consumer_pk) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE lti2_access_token + ADD CONSTRAINT lti2_access_token_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +CREATE TABLE lti2_context ( + context_pk int(11) NOT NULL AUTO_INCREMENT, + consumer_pk int(11) NOT NULL, + title varchar(255) DEFAULT NULL, + lti_context_id varchar(255) NOT NULL, + type varchar(50) DEFAULT NULL, + settings text DEFAULT NULL, + created datetime NOT NULL, + updated datetime NOT NULL, + PRIMARY KEY (context_pk) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE lti2_context + ADD CONSTRAINT lti2_context_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +ALTER TABLE lti2_context + ADD INDEX lti2_context_consumer_id_IDX (consumer_pk ASC); + +CREATE TABLE lti2_resource_link ( + resource_link_pk int(11) AUTO_INCREMENT, + context_pk int(11) DEFAULT NULL, + consumer_pk int(11) DEFAULT NULL, + title varchar(255) DEFAULT NULL, + lti_resource_link_id varchar(255) NOT NULL, + settings text, + primary_resource_link_pk int(11) DEFAULT NULL, + share_approved tinyint(1) DEFAULT NULL, + created datetime NOT NULL, + updated datetime NOT NULL, + PRIMARY KEY (resource_link_pk) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE lti2_resource_link + ADD CONSTRAINT lti2_resource_link_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +ALTER TABLE lti2_resource_link + ADD CONSTRAINT lti2_resource_link_lti2_context_FK1 FOREIGN KEY (context_pk) + REFERENCES lti2_context (context_pk); + +ALTER TABLE lti2_resource_link + ADD CONSTRAINT lti2_resource_link_lti2_resource_link_FK1 FOREIGN KEY (primary_resource_link_pk) + REFERENCES lti2_resource_link (resource_link_pk); + +ALTER TABLE lti2_resource_link + ADD INDEX lti2_resource_link_consumer_pk_IDX (consumer_pk ASC); + +ALTER TABLE lti2_resource_link + ADD INDEX lti2_resource_link_context_pk_IDX (context_pk ASC); + +CREATE TABLE lti2_user ( + user_pk int(11) AUTO_INCREMENT, + consumer_pk int(11) NOT NULL, + lti_user_id varchar(255) NOT NULL, + created datetime NOT NULL, + updated datetime NOT NULL, + PRIMARY KEY (user_pk) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE lti2_user + ADD CONSTRAINT lti2_user_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +ALTER TABLE lti2_user + ADD UNIQUE INDEX lti2_user_lti_user_id_IDX (consumer_pk, lti_user_id ASC); + +CREATE TABLE lti2_user_result ( + user_result_pk int(11) AUTO_INCREMENT, + user_pk int(11) NOT NULL, + resource_link_pk int(11) NOT NULL, + lti_result_sourcedid varchar(1024) NOT NULL, + created datetime NOT NULL, + updated datetime NOT NULL, + PRIMARY KEY (user_result_pk) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE lti2_user_result + ADD CONSTRAINT lti2_user_result_lti2_user_FK1 FOREIGN KEY (user_pk) + REFERENCES lti2_user (user_pk); + +ALTER TABLE lti2_user_result + ADD CONSTRAINT lti2_user_result_lti2_resource_link_FK1 FOREIGN KEY (resource_link_pk) + REFERENCES lti2_resource_link (resource_link_pk); + +ALTER TABLE lti2_user_result + ADD INDEX lti2_user_result_user_pk_IDX (user_pk ASC); + +ALTER TABLE lti2_user_result + ADD INDEX lti2_user_result_resource_link_pk_IDX (resource_link_pk ASC); + +CREATE TABLE lti2_share_key ( + share_key_id varchar(32) NOT NULL, + resource_link_pk int(11) NOT NULL, + auto_approve tinyint(1) NOT NULL, + expires datetime NOT NULL, + PRIMARY KEY (share_key_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE lti2_share_key + ADD CONSTRAINT lti2_share_key_lti2_resource_link_FK1 FOREIGN KEY (resource_link_pk) + REFERENCES lti2_resource_link (resource_link_pk); + +ALTER TABLE lti2_share_key + ADD INDEX lti2_share_key_resource_link_pk_IDX (resource_link_pk ASC); diff --git a/sql/lti4-tables-oracle.sql b/sql/lti4-tables-oracle.sql new file mode 100644 index 0000000..f94a13f --- /dev/null +++ b/sql/lti4-tables-oracle.sql @@ -0,0 +1,140 @@ +CREATE TABLE lti2_consumer ( + consumer_pk number GENERATED ALWAYS AS IDENTITY, + name varchar(50) NOT NULL, + consumer_key varchar(255) DEFAULT NULL, + secret varchar(1024) DEFAULT NULL, + platform_id varchar(255) DEFAULT NULL, + client_id varchar(255) DEFAULT NULL, + deployment_id varchar(255) DEFAULT NULL, + public_key clob DEFAULT NULL, + lti_version varchar(12) DEFAULT NULL, + signature_method varchar(15) DEFAULT 'HMAC-SHA1' NOT NULL, + consumer_name varchar(255) DEFAULT NULL, + consumer_version varchar(255) DEFAULT NULL, + consumer_guid varchar(1024) DEFAULT NULL, + profile clob DEFAULT NULL, + tool_proxy clob DEFAULT NULL, + settings clob DEFAULT NULL, + protected number(1) NOT NULL, + enabled number(1) NOT NULL, + enable_from timestamp DEFAULT NULL, + enable_until timestamp DEFAULT NULL, + last_access date DEFAULT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + CONSTRAINT lti2_consumer_PK PRIMARY KEY (consumer_pk) +); + +CREATE UNIQUE INDEX lti2_consumer_consumer_key_UNIQUE + ON lti2_consumer (consumer_key); + +CREATE UNIQUE INDEX lti2_consumer_platform_UNIQUE + ON lti2_consumer (platform_id, client_id, deployment_id); + +CREATE TABLE lti2_nonce ( + consumer_pk number NOT NULL, + value varchar(50) NOT NULL, + expires timestamp NOT NULL, + CONSTRAINT lti2_nonce_PK PRIMARY KEY (consumer_pk, value) +); + +ALTER TABLE lti2_nonce + ADD CONSTRAINT lti2_nonce_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +CREATE TABLE lti2_access_token ( + consumer_pk number NOT NULL, + scopes clob NOT NULL, + token varchar(2000) NOT NULL, + expires timestamp NOT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + CONSTRAINT lti2_access_token_PK PRIMARY KEY (consumer_pk) +); + +ALTER TABLE lti2_access_token + ADD CONSTRAINT lti2_access_token_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +CREATE TABLE lti2_context ( + context_pk number GENERATED ALWAYS AS IDENTITY, + consumer_pk number NOT NULL, + title varchar(255) DEFAULT NULL, + lti_context_id varchar(255) DEFAULT NULL, + type varchar(50) DEFAULT NULL, + settings clob DEFAULT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + CONSTRAINT lti2_context_PK PRIMARY KEY (context_pk) +); + +ALTER TABLE lti2_context + ADD CONSTRAINT lti2_context_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +CREATE INDEX lti2_context_consumer_id_IDX + ON lti2_context (consumer_pk ASC); + +CREATE TABLE lti2_resource_link ( + resource_link_pk number GENERATED ALWAYS AS IDENTITY, + context_pk number DEFAULT NULL, + consumer_pk number DEFAULT NULL, + title varchar(255) DEFAULT NULL, + lti_resource_link_id varchar(255) NOT NULL, + settings clob DEFAULT NULL, + primary_resource_link_pk number DEFAULT NULL, + share_approved number(1) DEFAULT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + CONSTRAINT lti2_resource_link_PK PRIMARY KEY (resource_link_pk) +); + +ALTER TABLE lti2_resource_link + ADD CONSTRAINT lti2_resource_link_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); + +ALTER TABLE lti2_resource_link + ADD CONSTRAINT lti2_resource_link_lti2_context_FK1 FOREIGN KEY (context_pk) + REFERENCES lti2_context (context_pk); + +ALTER TABLE lti2_resource_link + ADD CONSTRAINT lti2_resource_link_lti2_resource_link_FK1 FOREIGN KEY (primary_resource_link_pk) + REFERENCES lti2_resource_link (resource_link_pk); + +CREATE INDEX lti2_resource_link_consumer_pk_IDX + ON lti2_resource_link (consumer_pk ASC); + +CREATE INDEX lti2_resource_link_context_pk_IDX + ON lti2_resource_link (context_pk ASC); + +CREATE TABLE lti2_user_result ( + user_result_pk number GENERATED ALWAYS AS IDENTITY, + resource_link_pk number NOT NULL, + lti_user_id varchar(255) NOT NULL, + lti_result_sourcedid varchar(1024) NOT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + CONSTRAINT lti2_user_result_PK PRIMARY KEY (user_result_pk) +); + +ALTER TABLE lti2_user_result + ADD CONSTRAINT lti2_user_result_lti2_resource_link_FK1 FOREIGN KEY (resource_link_pk) + REFERENCES lti2_resource_link (resource_link_pk); + +CREATE INDEX lti2_user_result_resource_link_pk_IDX + ON lti2_user_result (resource_link_pk ASC); + +CREATE TABLE lti2_share_key ( + share_key_id varchar(32) NOT NULL, + resource_link_pk number NOT NULL, + auto_approve number(1) NOT NULL, + expires timestamp NOT NULL, + CONSTRAINT lti2_share_key_PK PRIMARY KEY (share_key_id) +); + +ALTER TABLE lti2_share_key + ADD CONSTRAINT lti2_share_key_lti2_resource_link_FK1 FOREIGN KEY (resource_link_pk) + REFERENCES lti2_resource_link (resource_link_pk); + +CREATE INDEX lti2_share_key_resource_link_pk_IDX + ON lti2_share_key (resource_link_pk ASC); diff --git a/sql/lti4-tables-pgsql.sql b/sql/lti4-tables-pgsql.sql new file mode 100644 index 0000000..b28e532 --- /dev/null +++ b/sql/lti4-tables-pgsql.sql @@ -0,0 +1,89 @@ +CREATE TABLE lti2_consumer ( + consumer_pk SERIAL, + name varchar(50) NOT NULL, + consumer_key varchar(255) DEFAULT NULL, + secret varchar(1024) DEFAULT NULL, + platform_id varchar(255) DEFAULT NULL, + client_id varchar(255) DEFAULT NULL, + deployment_id varchar(255) DEFAULT NULL, + public_key text DEFAULT NULL, + lti_version varchar(10) DEFAULT NULL, + signature_method varchar(15) NOT NULL DEFAULT 'HMAC-SHA1', + consumer_name varchar(255) DEFAULT NULL, + consumer_version varchar(255) DEFAULT NULL, + consumer_guid varchar(1024) DEFAULT NULL, + profile text DEFAULT NULL, + tool_proxy text DEFAULT NULL, + settings text DEFAULT NULL, + protected boolean NOT NULL, + enabled boolean NOT NULL, + enable_from timestamp DEFAULT NULL, + enable_until timestamp DEFAULT NULL, + last_access date DEFAULT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + PRIMARY KEY (consumer_pk), + UNIQUE (consumer_key), + UNIQUE (platform_id, client_id, deployment_id) +); + +CREATE TABLE lti2_nonce ( + consumer_pk integer NOT NULL REFERENCES lti2_consumer, + value varchar(50) NOT NULL, + expires timestamp NOT NULL, + PRIMARY KEY (consumer_pk, value) +); + +CREATE TABLE lti2_access_token ( + consumer_pk integer NOT NULL REFERENCES lti2_consumer, + scopes text NOT NULL, + token varchar(2000) NOT NULL, + expires timestamp NOT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + PRIMARY KEY (consumer_pk) +); + +CREATE TABLE lti2_context ( + context_pk SERIAL, + consumer_pk integer NOT NULL REFERENCES lti2_consumer, + title varchar(255) DEFAULT NULL, + lti_context_id varchar(255) NOT NULL, + type varchar(50) DEFAULT NULL, + settings text DEFAULT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + PRIMARY KEY (context_pk) +); + +CREATE TABLE lti2_resource_link ( + resource_link_pk SERIAL, + context_pk integer DEFAULT NULL REFERENCES lti2_context, + consumer_pk integer DEFAULT NULL REFERENCES lti2_consumer, + title varchar(255) DEFAULT NULL, + lti_resource_link_id varchar(255) NOT NULL, + settings text, + primary_resource_link_pk integer DEFAULT NULL REFERENCES lti2_resource_link, + share_approved boolean DEFAULT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + PRIMARY KEY (resource_link_pk) +); + +CREATE TABLE lti2_user_result ( + user_result_pk SERIAL, + resource_link_pk integer NOT NULL REFERENCES lti2_resource_link, + lti_user_id varchar(255) NOT NULL, + lti_result_sourcedid varchar(1024) NOT NULL, + created timestamp NOT NULL, + updated timestamp NOT NULL, + PRIMARY KEY (user_result_pk) +); + +CREATE TABLE lti2_share_key ( + share_key_id varchar(32) NOT NULL, + resource_link_pk integer NOT NULL REFERENCES lti2_resource_link, + auto_approve boolean NOT NULL, + expires timestamp NOT NULL, + PRIMARY KEY (share_key_id) +); diff --git a/sql/lti4-update-mysql.sql b/sql/lti4-update-mysql.sql new file mode 100644 index 0000000..6a20863 --- /dev/null +++ b/sql/lti4-update-mysql.sql @@ -0,0 +1,25 @@ +ALTER TABLE lti2_consumer + DROP COLUMN consumer_key, + CHANGE COLUMN consumer_key256 consumer_key VARCHAR(255) DEFAULT NULL, + CHANGE COLUMN secret secret VARCHAR(1024) DEFAULT NULL, + ADD COLUMN platform_id VARCHAR(255) DEFAULT NULL AFTER secret, + ADD COLUMN client_id VARCHAR(255) DEFAULT NULL AFTER platform_id, + ADD COLUMN deployment_id VARCHAR(255) DEFAULT NULL AFTER client_id, + ADD COLUMN public_key text DEFAULT NULL AFTER deployment_id; + +ALTER TABLE lti2_consumer + ADD UNIQUE INDEX lti2_consumer_platform_UNIQUE (platform_id ASC, client_id ASC, deployment_id ASC); + +CREATE TABLE lti2_access_token ( + consumer_pk int(11) NOT NULL, + scopes text NOT NULL, + token varchar(2000) NOT NULL, + expires datetime NOT NULL, + created datetime NOT NULL, + updated datetime NOT NULL, + PRIMARY KEY (consumer_pk) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE lti2_access_token + ADD CONSTRAINT lti2_access_token_lti2_consumer_FK1 FOREIGN KEY (consumer_pk) + REFERENCES lti2_consumer (consumer_pk); diff --git a/src/AccessToken.php b/src/AccessToken.php new file mode 100644 index 0000000..44a5acb --- /dev/null +++ b/src/AccessToken.php @@ -0,0 +1,186 @@ + + * @copyright SPV Software Products + * @version 3.0.0 + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class AccessToken +{ + + /** + * Access token string. + * + * @var string|null $token + */ + public $token = null; + + /** + * Timestamp at which the token string expires. + * + * @var datetime|null $expires + */ + public $expires = null; + + /** + * Scope(s) for which the access token is valid. + * + * @var array $scopes + */ + public $scopes = array(); + + /** + * Platform for this context. + * + * @var Platform|null $platform + */ + private $platform = null; + + /** + * Timestamp for when the object was created. + * + * @var int|null $created + */ + public $created = null; + + /** + * Timestamp for when the object was last updated. + * + * @var int|null $updated + */ + public $updated = null; + + /** + * Class constructor. + * + * @param Platform $platform Platform + * @param array|null $scopes Scopes for which the access token is valid + * @param string $token Access token string + * @param datetime $expires Time in seconds after which the token string will expire + */ + public function __construct($platform, $scopes = null, $token = null, $expires = null) + { + $this->platform = $platform; + $this->scopes = $scopes; + if (!empty($token)) { + $this->token = $token; + } + if (!empty($expires)) { + $this->expires = time() + $expires; + } + $this->created = null; + $this->updated = null; + if (empty($scopes)) { + $this->load(); + } + } + + /** + * Get platform. + * + * @return Platform Platform object for this resource link. + */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Load a nonce value from the database. + * + * @return bool True if the nonce value was successfully loaded + */ + public function load() + { + return $this->platform->getDataConnector()->loadAccessToken($this); + } + + /** + * Save a nonce value in the database. + * + * @return bool True if the nonce value was successfully saved + */ + public function save() + { + sort($this->scopes); + return $this->platform->getDataConnector()->saveAccessToken($this); + } + + /** + * Check if a valid access token exists for a specific scope. + * + * @param string $scope Access scope + * + * @return bool True if there is an unexpired access token for specified scope + */ + public function hasScope($scope) + { + if (substr($scope, -9) === '.readonly') { + $scope2 = substr($scope, 0, -9); + } else { + $scope2 = $scope; + } + return !empty($this->token) && (empty($this->expires) || ($this->expires > time())) && + (empty($this->scopes) || (in_array($scope, $this->scopes) || in_array($scope2, $this->scopes))); + } + + /** + * Obtain a valid access token for a scope. + * + * @param string $scope Access scope + * + * @return AccessToken New access token + */ + public function get($scope) + { + $url = $this->platform->accessTokenUrl; + if (!empty($url) && !$this->hasScope($scope) && !empty(Tool::$defaultTool) && !empty(Tool::$defaultTool->rsaKey)) { + if (!empty(Tool::$defaultTool)) { + $scopesRequested = Tool::$defaultTool->requiredScopes; + if (substr($scope, -9) === '.readonly') { + $scope2 = substr($scope, 0, -9); + } else { + $scope2 = $scope; + } + if (!in_array($scope, $scopesRequested) && !in_array($scope2, $scopesRequested)) { + $scopesRequested[] = $scope; + } + } else { + $scopesRequested = array($scope); + } + $method = 'POST'; + $type = 'application/x-www-form-urlencoded'; + $body = array( + 'grant_type' => 'client_credentials', + 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + 'scope' => implode(' ', $scopesRequested) + ); + $body = $this->platform->signServiceRequest($url, $method, $type, $body); + $http = new HttpMessage($url, $method, $body); + if ($http->send() && !empty($http->response)) { + $http->responseJson = json_decode($http->response); + if (!is_null($http->responseJson) && !empty($http->responseJson->access_token) && !empty($http->responseJson->expires_in)) { + if (isset($http->responseJson->scope)) { + $scopesAccepted = explode(' ', $http->responseJson->scope); + } else { + $scopesAccepted = $scopesRequested; + } + $this->scopes = $scopesAccepted; + $this->token = $http->responseJson->access_token; + $this->expires = time() + $http->responseJson->expires_in; + $this->save(); + } + } + } + + return $this; + } + +} diff --git a/src/ApiHook/ApiContext.php b/src/ApiHook/ApiContext.php index c5da44f..ae38f64 100644 --- a/src/ApiHook/ApiContext.php +++ b/src/ApiHook/ApiContext.php @@ -3,7 +3,7 @@ namespace ceLTIc\LTI\ApiHook; /** - * Class to implement context services for a tool consumer via its proprietary API + * Class to implement context services for a platform via its proprietary API * * @author Stephen P Vickers * @copyright SPV Software Products diff --git a/src/ApiHook/ApiHook.php b/src/ApiHook/ApiHook.php index 7ec358a..95c7290 100644 --- a/src/ApiHook/ApiHook.php +++ b/src/ApiHook/ApiHook.php @@ -37,6 +37,11 @@ trait ApiHook */ public static $TOOL_SETTINGS_SERVICE_HOOK = "ToolSettings"; + /** + * Access Token service hook name. + */ + public static $ACCESS_TOKEN_SERVICE_HOOK = "AccessToken"; + /** * API hook class names. */ @@ -84,8 +89,8 @@ private static function hasApiHook($hookName, $familyCode) /** * Check if an API hook is registered and configured. * - * @param string $hookName Name of hook - * @param ToolConsumer|Context|ResourceLink $sourceObject Source object for which hook is to be used + * @param string $hookName Name of hook + * @param Platform|Context|ResourceLink $sourceObject Source object for which hook is to be used * * @return bool True if the API hook is registered and configured */ @@ -102,27 +107,6 @@ private static function hasConfiguredApiHook($hookName, $familyCode, $sourceObje return $ok; } - /** - * Check if a service is available. - * - * @param string $serviceName Name of service - * @param string|array $endpointSettingNames Name of setting or array of setting names - * - * @return bool True if the service is available - */ - private function hasService($serviceName, $endpointSettingNames) - { - $found = false; - if (!is_array($endpointSettingNames)) { - $found = !empty($this->getSetting($endpointSettingNames)); - } else { - foreach ($endpointSettingNames as $endpointSettingName) { - $found = $found || !empty($this->getSetting($endpointSettingName)); - } - } - return $found || self::hasApiHook($serviceName, $this->getConsumer()->getFamilyCode()); - } - } ?> diff --git a/src/ApiHook/ApiPlatform.php b/src/ApiHook/ApiPlatform.php new file mode 100644 index 0000000..8b3e622 --- /dev/null +++ b/src/ApiHook/ApiPlatform.php @@ -0,0 +1,64 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class ApiPlatform +{ + + /** + * Platform object. + * + * @var \ceLTIc\LTI\Platform|null $platform + */ + protected $platform = null; + + /** + * Class constructor. + * + * @param \ceLTIc\LTI\Platform $platform + */ + public function __construct($platform) + { + $this->platform = $platform; + } + + /** + * Check if the API hook has been configured. + */ + public function isConfigured() + { + return true; + } + + /** + * Get Tool Settings. + * + * @param bool $simple True if all the simple media type is to be used (optional, default is true) + * + * @return mixed The array of settings if successful, otherwise false + */ + public function getToolSettings($simple = true) + { + return false; + } + + /** + * Perform a Tool Settings service request. + * + * @param array $settings An associative array of settings (optional, default is none) + * + * @return bool True if action was successful, otherwise false + */ + public function setToolSettings($settings = array()) + { + return false; + } + +} diff --git a/src/ApiHook/ApiResourceLink.php b/src/ApiHook/ApiResourceLink.php index 6feb264..f617cba 100644 --- a/src/ApiHook/ApiResourceLink.php +++ b/src/ApiHook/ApiResourceLink.php @@ -3,7 +3,7 @@ namespace ceLTIc\LTI\ApiHook; /** - * Class to implement resource link services for a tool consumer via its proprietary API + * Class to implement resource link services for a platform via its proprietary API * * @author Stephen P Vickers * @copyright SPV Software Products diff --git a/src/ApiHook/ApiTool.php b/src/ApiHook/ApiTool.php new file mode 100644 index 0000000..2cf07fc --- /dev/null +++ b/src/ApiHook/ApiTool.php @@ -0,0 +1,60 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class ApiTool +{ + + /** + * Tool object. + * + * @var \ceLtic\LTI\Tool|null $tool + */ + protected $tool = null; + + /** + * Class constructor. + * + * @param \ceLtic\LTI\Tool|null $tool + */ + public function __construct($tool) + { + $this->tool = $tool; + } + + /** + * Check if the API hook has been configured. + */ + public function isConfigured() + { + return true; + } + + /** + * Get the User ID. + * + * @return string User ID value, or empty string if not available. + */ + public function getUserId() + { + return ''; + } + + /** + * Get the Context ID. + * + * @return string Context ID value, or empty string if not available. + */ + public function getContextId() + { + return ''; + } + +} diff --git a/src/ApiHook/ApiToolConsumer.php b/src/ApiHook/ApiToolConsumer.php index 9c387d0..30d86ad 100644 --- a/src/ApiHook/ApiToolConsumer.php +++ b/src/ApiHook/ApiToolConsumer.php @@ -2,63 +2,31 @@ namespace ceLTIc\LTI\ApiHook; +use ceLTIc\LTI\Util; + /** - * Class to implement Tool Consumer services for a tool consumer via its proprietary API + * Class to implement services for a tool consumer via its proprietary API + * + * @deprecated Use ApiPlatform instead + * @see ApiPlatform * * @author Stephen P Vickers * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 */ -class ApiToolConsumer +class ApiToolConsumer extends ApiPlatform { - /** - * Tool Consumer object. - * - * @var \ceLTIc\LTI\ToolConsumer|null $consumer - */ - protected $consumer = null; - /** * Class constructor. * - * @param \ceLTIc\LTI\ToolConsmumer $consumer + * @param \ceLTIc\LTI\ToolConsumer $consumer */ public function __construct($consumer) { - $this->consumer = $consumer; - } - - /** - * Check if the API hook has been configured. - */ - public function isConfigured() - { - return true; - } - - /** - * Get Tool Settings. - * - * @param bool $simple True if all the simple media type is to be used (optional, default is true) - * - * @return mixed The array of settings if successful, otherwise false - */ - public function getToolSettings($simple = true) - { - return false; - } - - /** - * Perform a Tool Settings service request. - * - * @param array $settings An associative array of settings (optional, default is none) - * - * @return bool True if action was successful, otherwise false - */ - public function setToolSettings($settings = array()) - { - return false; + parent::__construct($consumer); + Util::logDebug('Class ceLTIc\LTI\ApiHook\ApiToolConsumer has been deprecated; please use ceLTIc\LTI\ApiHook\ApiPlatform instead.', + true); } } diff --git a/src/ApiHook/ApiToolProvider.php b/src/ApiHook/ApiToolProvider.php index 6075e48..cf05885 100644 --- a/src/ApiHook/ApiToolProvider.php +++ b/src/ApiHook/ApiToolProvider.php @@ -2,23 +2,21 @@ namespace ceLTIc\LTI\ApiHook; +use ceLTIc\LTI\Util; + /** - * Class to implement tool consumer specific functions for LTI messages + * Class to implement tool provider specific functions for LTI messages + * + * @deprecated Use ApiTool instead + * @see ApiTool * * @author Stephen P Vickers * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 */ -class ApiToolProvider +class ApiToolProvider extends ApiTool { - /** - * Tool Provider object. - * - * @var \ceLtic\LTI\ToolProvider|null $toolProvider - */ - protected $toolProvider = null; - /** * Class constructor. * @@ -26,35 +24,9 @@ class ApiToolProvider */ public function __construct($toolProvider) { - $this->toolProvider = $toolProvider; - } - - /** - * Check if the API hook has been configured. - */ - public function isConfigured() - { - return true; - } - - /** - * Get the User ID. - * - * @return string User ID value, or empty string if not available. - */ - public function getUserId() - { - return ''; - } - - /** - * Get the Context ID. - * - * @return string Context ID value, or empty string if not available. - */ - public function getContextId() - { - return ''; + parent::__construct($toolProvider); + Util::logDebug('Class ceLTIc\LTI\ApiHook\ApiToolProvider has been deprecated; please use ceLTIc\LTI\ApiHook\ApiTool instead.', + true); } } diff --git a/src/ApiHook/canvas/CanvasApi.php b/src/ApiHook/canvas/CanvasApi.php index fa8a9ec..6e0c39e 100644 --- a/src/ApiHook/canvas/CanvasApi.php +++ b/src/ApiHook/canvas/CanvasApi.php @@ -46,9 +46,9 @@ trait CanvasApi */ public function isConfigured() { - $consumer = $this->sourceObject->getConsumer(); + $platform = $this->sourceObject->getPlatform(); - return !empty($consumer->getSetting('canvas.domain')) && !empty($consumer->getSetting('canvas.token')); + return !empty($platform->getSetting('canvas.domain')) && !empty($platform->getSetting('canvas.token')); } /** @@ -60,15 +60,15 @@ public function isConfigured() */ private function get($withGroups) { - $consumer = $this->sourceObject->getConsumer(); - $this->domain = $consumer->getSetting('canvas.domain'); - $this->token = $consumer->getSetting('canvas.token'); + $platform = $this->sourceObject->getPlatform(); + $this->domain = $platform->getSetting('canvas.domain'); + $this->token = $platform->getSetting('canvas.token'); $this->courseId = $this->sourceObject->getSetting('custom_canvas_course_id'); - $perPage = $consumer->getSetting('canvas.per_page', strval(self::$DEFAULT_PER_PAGE)); + $perPage = $platform->getSetting('canvas.per_page', strval(self::$DEFAULT_PER_PAGE)); if (!is_numeric($perPage)) { $perPage = self::$DEFAULT_PER_PAGE; } - $prefix = $consumer->getSetting('canvas.group_set_prefix'); + $prefix = $platform->getSetting('canvas.group_set_prefix'); if ($this->domain && $this->token && $this->courseId) { if ($withGroups) { $this->setGroupSets($perPage, $prefix); @@ -199,7 +199,7 @@ private function getUsers($perPage, $withGroups) } } $user->setNames('', '', $enrolment->name); - $user->setEmail($enrolment->email, $this->sourceObject->getConsumer()->defaultEmail); + $user->setEmail($enrolment->email, $this->sourceObject->getPlatform()->defaultEmail); $user->username = $enrolment->login_id; $user->sourcedId = $enrolment->sis_user_id; if (!empty($enrolment->group_ids)) { diff --git a/src/ApiHook/canvas/CanvasApiContext.php b/src/ApiHook/canvas/CanvasApiContext.php index 9058c3e..c935dbb 100644 --- a/src/ApiHook/canvas/CanvasApiContext.php +++ b/src/ApiHook/canvas/CanvasApiContext.php @@ -5,7 +5,7 @@ use ceLTIc\LTI\ApiHook\ApiContext; /** - * Class to implement Resource Link services for a Canvas tool consumer via its proprietary API. + * Class to implement Resource Link services for a Canvas platform via its proprietary API. * * @author Simon Booth * @author Stephen P Vickers diff --git a/src/ApiHook/canvas/CanvasApiResourceLink.php b/src/ApiHook/canvas/CanvasApiResourceLink.php index f2ee516..f3dc3f3 100644 --- a/src/ApiHook/canvas/CanvasApiResourceLink.php +++ b/src/ApiHook/canvas/CanvasApiResourceLink.php @@ -6,7 +6,7 @@ use ceLTIc\LTI\UserResult; /** - * Class to implement Resource Link services for a Canvas tool consumer via its proprietary API. + * Class to implement Resource Link services for a Canvas platform via its proprietary API. * * @author Simon Booth * @author Stephen P Vickers diff --git a/src/ApiHook/canvas/CanvasApiToolProvider.php b/src/ApiHook/canvas/CanvasApiToolProvider.php index 0663c99..5e10aba 100644 --- a/src/ApiHook/canvas/CanvasApiToolProvider.php +++ b/src/ApiHook/canvas/CanvasApiToolProvider.php @@ -16,7 +16,7 @@ class CanvasApiToolProvider extends \ceLTIc\LTI\ApiHook\ApiToolProvider public function getUserId() { $userId = ''; - $messageParameters = $this->toolProvider->getMessageParameters(); + $messageParameters = $this->tool->getMessageParameters(); if (isset($messageParameters['custom_canvas_user_id'])) { $userId = trim($messageParameters['custom_canvas_user_id']); } diff --git a/src/ApiHook/moodle/MoodleApi.php b/src/ApiHook/moodle/MoodleApi.php index 8039753..44a6f55 100644 --- a/src/ApiHook/moodle/MoodleApi.php +++ b/src/ApiHook/moodle/MoodleApi.php @@ -46,9 +46,9 @@ trait MoodleApi */ public function isConfigured() { - $consumer = $this->sourceObject->getConsumer(); + $platform = $this->sourceObject->getPlatform(); - return !empty($consumer->getSetting('moodle.url')) && !empty($consumer->getSetting('moodle.token')); + return !empty($platform->getSetting('moodle.url')) && !empty($platform->getSetting('moodle.token')); } /** @@ -60,16 +60,16 @@ public function isConfigured() */ private function get($withGroups) { - $consumer = $this->sourceObject->getConsumer(); - $this->url = $consumer->getSetting('moodle.url'); - $this->token = $consumer->getSetting('moodle.token'); - $perPage = $consumer->getSetting('moodle.per_page', ''); + $platform = $this->sourceObject->getPlatform(); + $this->url = $platform->getSetting('moodle.url'); + $this->token = $platform->getSetting('moodle.token'); + $perPage = $platform->getSetting('moodle.per_page', ''); if (!is_numeric($perPage)) { $perPage = self::$DEFAULT_PER_PAGE; } else { $perPage = intval($perPage); } - $prefix = $consumer->getSetting('moodle.grouping_prefix'); + $prefix = $platform->getSetting('moodle.grouping_prefix'); if ($this->url && $this->token && $this->courseId) { if ($withGroups) { $this->setGroupings($prefix); @@ -206,7 +206,7 @@ private function getUsers($perPage, $withGroups) $user = new UserResult(); $user->ltiUserId = $userId; } - $user->setEmail($enrolment->email, $this->sourceObject->getConsumer()->defaultEmail); + $user->setEmail($enrolment->email, $this->sourceObject->getPlatform()->defaultEmail); $user->setNames($enrolment->firstname, $enrolment->lastname, $enrolment->fullname); $user->username = $enrolment->username; $user->sourcedId = $enrolment->idnumber; diff --git a/src/ApiHook/moodle/MoodleApiContext.php b/src/ApiHook/moodle/MoodleApiContext.php index 302c686..3933d13 100644 --- a/src/ApiHook/moodle/MoodleApiContext.php +++ b/src/ApiHook/moodle/MoodleApiContext.php @@ -5,7 +5,7 @@ use ceLTIc\LTI\ApiHook\ApiContext; /** - * Class to implement Context services for a Moodle tool consumer via its web services. + * Class to implement Context services for a Moodle platform via its web services. * * @author Tony Butler * @author Stephen P Vickers diff --git a/src/ApiHook/moodle/MoodleApiResourceLink.php b/src/ApiHook/moodle/MoodleApiResourceLink.php index ac94f63..7dcbb23 100644 --- a/src/ApiHook/moodle/MoodleApiResourceLink.php +++ b/src/ApiHook/moodle/MoodleApiResourceLink.php @@ -5,7 +5,7 @@ use ceLTIc\LTI\ApiHook\ApiResourceLink; /** - * Class to implement Resource Link services for a Moodle tool consumer via its web services. + * Class to implement Resource Link services for a Moodle platform via its web services. * * @author Tony Butler * @author Stephen P Vickers diff --git a/src/ConsumerNonce.php b/src/ConsumerNonce.php index 49f435f..2b8a3b1 100644 --- a/src/ConsumerNonce.php +++ b/src/ConsumerNonce.php @@ -2,102 +2,41 @@ namespace ceLTIc\LTI; +use ceLTIc\LTI\Util; + /** * Class to represent a tool consumer nonce * + * @deprecated Use PlatformNonce instead + * @see PlatformNonce + * * @author Stephen P Vickers * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 */ -class ConsumerNonce +class ConsumerNonce extends PlatformNonce { - /** - * Maximum age nonce values will be retained for (in minutes). - */ - const MAX_NONCE_AGE = 30; // in minutes - - /** - * Maximum length which can be stored. - * - * Characters are removed from the beginning of the value when too long. - * - * @var int $maximumLength - */ - public static $maximumLength = 50; - - /** - * Timestamp for when the nonce value expires. - * - * @var int|null $expires - */ - public $expires = null; - - /** - * Tool Consumer to which this nonce applies. - * - * @var ToolConsumer|null $consumer - */ - private $consumer = null; - - /** - * Nonce value. - * - * @var string|null $value - */ - private $value = null; - /** * Class constructor. * - * @param ToolConsumer $consumer Consumer object - * @param string $value Nonce value (optional, default is null) + * @param ToolConsumer $consumer Tool consumer object + * @param string $value Nonce value (optional, default is null) */ public function __construct($consumer, $value = null) { - $this->consumer = $consumer; - $this->value = substr($value, -self::$maximumLength); - $this->expires = time() + (self::MAX_NONCE_AGE * 60); - } - - /** - * Load a nonce value from the database. - * - * @return bool True if the nonce value was successfully loaded - */ - public function load() - { - return $this->consumer->getDataConnector()->loadConsumerNonce($this); - } - - /** - * Save a nonce value in the database. - * - * @return bool True if the nonce value was successfully saved - */ - public function save() - { - return $this->consumer->getDataConnector()->saveConsumerNonce($this); + parent::__construct($consumer, $value); + Util::logDebug('Class ceLTIc\LTI\ConsumerNonce has been deprecated; please use ceLTIc\LTI\PlatformNonce instead.', true); } /** * Get tool consumer. * - * @return ToolConsumer Consumer for this nonce + * @return ToolConsumer Tool consumer for this nonce */ public function getConsumer() { - return $this->consumer; - } - - /** - * Get outcome value. - * - * @return string Outcome value - */ - public function getValue() - { - return $this->value; + return $this->getPlatform(); } } diff --git a/src/Content/FileItem.php b/src/Content/FileItem.php new file mode 100644 index 0000000..c83d351 --- /dev/null +++ b/src/Content/FileItem.php @@ -0,0 +1,96 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class FileItem extends Item +{ + + /** + * Copy advice for content-item. + * + * @var bool|null $copyAdvice + */ + private $copyAdvice = null; + + /** + * Expiry date/time for content-item. + * + * @var int|null $expiresAt + */ + private $expiresAt = null; + + /** + * Class constructor. + * + * @param Placement $placementAdvice Placement object for item (optional) + * @param string $id URL of content-item (optional) + */ + function __construct($placementAdvice = null, $id = null) + { + parent::__construct(Item::TYPE_FILE, $placementAdvice, $id); + } + + /** + * Set copy advice for the content-item. + * + * @param bool|null $copyAdvice Copy advice value + */ + public function setCopyAdvice($copyAdvice) + { + $this->copyAdvice = $copyAdvice; + } + + /** + * Set expiry date/time for the content-item. + * + * @param int|null $expiresAt Expiry date/time + */ + public function setExpiresAt($expiresAt) + { + $this->expiresAt = $expiresAt; + } + + public function toJsonldObject() + { + $item = parent::toJsonldObject(); + if (!is_null($this->copyAdvice)) { + $item->copyAdvice = $this->copyAdvice; + } + if (!empty($this->expiresAt)) { + $item->expiresAt = gmdate('Y-m-d\TH:i:s\Z', $this->expiresAt); + } + + return $item; + } + + public function toJsonObject() + { + $item = parent::toJsonObject(); + if (!empty($this->expiresAt)) { + $item->expiresAt = gmdate('Y-m-d\TH:i:s\Z', $this->expiresAt); + } + + return $item; + } + + protected function fromJsonObject($item) + { + parent::fromJsonObject($item); + foreach (get_object_vars($item) as $name => $value) { + switch ($name) { + case 'copyAdvice': + case 'expiresAt': + $this->{$name} = $item->{$name}; + break; + } + } + } + +} diff --git a/src/Content/Image.php b/src/Content/Image.php new file mode 100644 index 0000000..0af78ad --- /dev/null +++ b/src/Content/Image.php @@ -0,0 +1,119 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class Image +{ + + /** + * URL of image. + * + * @var string $url + */ + private $url = null; + + /** + * Width of image. + * + * @var int|null $width + */ + private $width = null; + + /** + * Height of image. + * + * @var int|null $height + */ + private $height = null; + + /** + * Class constructor. + * + * @param string $url URL of image + * @param int $width Width of image in pixels (optional) + * @param int $height Height of image in pixels (optional) + */ + function __construct($url, $width = null, $height = null) + { + $this->url = $url; + $this->height = $height; + $this->width = $width; + } + + /** + * Generate the JSON-LD object representation of the image. + * + * @return object + */ + public function toJsonldObject() + { + $image = new \stdClass(); + $image->{'@id'} = $this->url; + if (!is_null($this->width)) { + $image->width = $this->width; + } + if (!is_null($this->height)) { + $image->height = $this->height; + } + + return $image; + } + + /** + * Generate the JSON object representation of the image. + * + * @return string + */ + public function toJsonObject() + { + $image = new \stdClass(); + $image->url = $this->url; + if (!is_null($this->width)) { + $image->width = $this->width; + } + if (!is_null($this->height)) { + $image->height = $this->height; + } + + return $image; + } + + public static function fromJsonObject($item) + { + $obj = null; + $width = null; + $height = null; + if (is_object($item)) { + $url = null; + foreach (get_object_vars($item) as $name => $value) { + switch ($name) { + case '@id': + case 'url': + $url = $item->{$name}; + break; + case 'width': + $width = $item->width; + break; + case 'height': + $height = $item->height; + break; + } + } + } else { + $url = $item; + } + if ($url) { + $obj = new Image($url, $height, $width); + } + + return $obj; + } + +} diff --git a/src/Content/Item.php b/src/Content/Item.php new file mode 100644 index 0000000..9c7dfbf --- /dev/null +++ b/src/Content/Item.php @@ -0,0 +1,420 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class Item +{ + + /** + * Type for link content-item. + */ + const TYPE_LINK = 'link'; + + /** + * Type for LTI link content-item. + */ + const TYPE_LTI_LINK = 'ltiResourceLink'; + + /** + * Type for file content-item. + */ + const TYPE_FILE = 'file'; + + /** + * Type for HTML content-item. + */ + const TYPE_HTML = 'html'; + + /** + * Type for image content-item. + */ + const TYPE_IMAGE = 'image'; + + /** + * Media type for LTI launch links. + */ + const LTI_LINK_MEDIA_TYPE = 'application/vnd.ims.lti.v1.ltilink'; + + /** + * Type of content-item. + * + * @var string|null $type + */ + private $type = null; + + /** + * ID of content-item. + * + * @var string|null $id + */ + private $id = null; + + /** + * Array of placement objects for content-item. + * + * @var array $placements + */ + private $placements = array(); + + /** + * URL of content-item. + * + * @var string|null $url + */ + private $url = null; + + /** + * Media type of content-item. + * + * @var string|null $mediaType + */ + private $mediaType = null; + + /** + * Title of content-item. + * + * @var string|null $title + */ + private $title = null; + + /** + * Description of content-item. + * + * @var string|null $text + */ + private $text = null; + + /** + * Icon image object for content-item. + * + * @var Image|null $icon + */ + private $icon = null; + + /** + * Thumbnail image object for content-item. + * + * @var Image|null $thumbnail + */ + private $thumbnail = null; + + /** + * Class constructor. + * + * @param string $type Class type of content-item + * @param Placement $placementAdvice Placement object for item (optional) + * @param string $id URL of content-item (optional) + */ + function __construct($type, $placementAdvice = null, $id = null) + { + $this->type = $type; + if (!empty($placementAdvice)) { + $this->placements[$placementAdvice->documentTarget] = $placementAdvice; + } + $this->id = $id; + } + + /** + * Set a URL value for the content-item. + * + * @param string $url URL value + */ + public function setUrl($url) + { + $this->url = $url; + } + + /** + * Set a media type value for the content-item. + * + * @param string $mediaType Media type value + */ + public function setMediaType($mediaType) + { + $this->mediaType = $mediaType; + } + + /** + * Set a title value for the content-item. + * + * @param string $title Title value + */ + public function setTitle($title) + { + $this->title = $title; + } + + /** + * Set a link text value for the content-item. + * + * @param string $text Link text value + */ + public function setText($text) + { + $this->text = $text; + } + + /** + * Add a placement for the content-item. + * + * @param Placement $placementAdvice Placement advice object + */ + public function addPlacementAdvice($placementAdvice) + { + if (!empty($placementAdvice)) { + $this->placements[$placementAdvice->documentTarget] = $placementAdvice; + } + } + + /** + * Set an icon image for the content-item. + * + * @param Image $icon Icon image object + */ + public function setIcon($icon) + { + $this->icon = $icon; + } + + /** + * Set a thumbnail image for the content-item. + * + * @param Image $thumbnail Thumbnail image object + */ + public function setThumbnail($thumbnail) + { + $this->thumbnail = $thumbnail; + } + + /** + * Wrap the content items to form a complete application/vnd.ims.lti.v1.contentitems+json media type instance. + * + * @param mixed $items An array of content items or a single item + * @param string $ltiVersion LTI version in use + * + * @return string + */ + public static function toJson($items, $ltiVersion = Util::LTI_VERSION1) + { + if (!is_array($items)) { + $items = array($items); + } + if ($ltiVersion !== Util::LTI_VERSION1P3) { + $obj = new \stdClass(); + $obj->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem'; + $obj->{'@graph'} = array(); + foreach ($items as $item) { + $obj->{'@graph'}[] = $item->toJsonldObject(); + } + } else { + $obj = array(); + foreach ($items as $item) { + $obj[] = $item->toJsonObject(); + } + } + + return json_encode($obj); + } + + /** + * Wrap the content items to form a complete application/vnd.ims.lti.v1.contentitems+json media type instance. + * + * @param object $items A JSON object representing Content-Items + * + * @return array Array of Item objects + */ + public static function fromJson($items) + { + $isJsonLd = isset($items->{'@graph'}); + if ($isJsonLd) { + $items = $items->{'@graph'}; + } + if (!is_array($items)) { + $items = array($items); + } + $objs = array(); + foreach ($items as $item) { + $obj = null; + if (isset($item->{'@type'})) { + switch ($item->{'@type'}) { + case 'ContentItem': + $obj = new Item('ContentItem'); + break; + case 'LtiLinkItem': + $obj = new LtiLinkItem(); + break; + case 'FileItem': + $obj = new FileItem(); + break; + } + } elseif (isset($item->type)) { + switch ($item->type) { + case self::TYPE_LINK: + case self::TYPE_HTML: + case self::TYPE_IMAGE: + $obj = new Item($item->type); + break; + case self::TYPE_LTI_LINK: + $obj = new LtiLinkItem(); + break; + case self::TYPE_FILE: + $obj = new FileItem(); + break; + } + } + if ($obj) { + $obj->fromJsonObject($item); + $objs[] = $obj; + } + } + + return $objs; + } + + protected function toJsonldObject() + { + $item = new \stdClass(); + if (!empty($this->id)) { + $item->{'@id'} = $this->id; + } + if (!empty($this->type)) { + if ($this->type === self::TYPE_LTI_LINK) { + $item->{'@type'} = 'LtiLinkItem'; + } elseif ($this->type === self::TYPE_FILE) { + $item->{'@type'} = 'FileItem'; + } else { + $item->{'@type'} = 'ContentItem'; + } + } else { + $item->{'@type'} = 'ContentItem'; + } + if (!empty($this->title)) { + $item->title = $this->title; + } + if (!empty($this->text)) { + $item->text = $this->text; + } + if (!empty($this->url)) { + $item->url = $this->url; + } + if (!empty($this->mediaType)) { + $item->mediaType = $this->mediaType; + } + if (!empty($this->placements)) { + $placement = reset($this->placements); + $obj = $placement->toJsonldObject(); + if (!empty($obj)) { + $item->placementAdvice = $obj; + } + } + if (!empty($this->icon)) { + $item->icon = $this->icon->toJsonldObject(); + } + if (!empty($this->thumbnail)) { + $item->thumbnail = $this->thumbnail->toJsonldObject(); + } + + return $item; + } + + protected function toJsonObject() + { + $item = new \stdClass(); + switch ($this->type) { + case 'LtiLinkItem': + $item->type = self::TYPE_LTI_LINK; + break; + case 'FileItem': + $item->type = self::TYPE_FILE; + break; + case 'ContentItem': + if (empty($this->url)) { + $item->type = self::TYPE_HTML; + } elseif (!empty($this->mediaType) && (strpos($this->mediaType, 'image') === 0)) { + $item->type = self::TYPE_IMAGE; + } else { + $item->type = self::TYPE_LINK; + } + break; + default: + $item->type = $this->type; + break; + } + if (!empty($this->title)) { + $item->title = $this->title; + } + if (!empty($this->text)) { + $item->text = $this->text; + } + if (!empty($this->url)) { + $item->url = $this->url; + } + foreach ($this->placements as $type => $placement) { + switch ($type) { + case Placement::TYPE_EMBED: + case Placement::TYPE_IFRAME: + case Placement::TYPE_WINDOW: + $obj = $placement->toJsonObject(); + break; + case Placement::TYPE_FRAME: + $obj = $placement->toJsonObject(); + break; + default: + $obj = null; + break; + } + if (!empty($obj)) { + $item->{$type} = $obj; + } + } + if (!empty($this->icon)) { + $item->icon = $this->icon->toJsonObject(); + } + if (!empty($this->thumbnail)) { + $item->thumbnail = $this->thumbnail->toJsonObject(); + } + + return $item; + } + + protected function fromJsonObject($item) + { + if (isset($item->{'@id'})) { + $this->id = $item->{'@id'}; + } + $placements = array(); + foreach (get_object_vars($item) as $name => $value) { + switch ($name) { + case 'title': + case 'text': + case 'url': + case 'mediaType': + $this->{$name} = $item->{$name}; + break; + case 'placementAdvice': + $this->addPlacementAdvice(Placement::fromJsonObject($item->{$name})); + break; + case 'embed': + case 'window': + case 'iframe': + $item->{$name}->documentTarget = $name; + $this->addPlacementAdvice(Placement::fromJsonObject($item->{$name})); + break; + case 'icon': + case 'thumbnail': + $this->{$name} = Image::fromJsonObject($item->{$name}); + break; + } + } + } + +} diff --git a/src/Content/LineItem.php b/src/Content/LineItem.php new file mode 100644 index 0000000..e026a59 --- /dev/null +++ b/src/Content/LineItem.php @@ -0,0 +1,156 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class LineItem +{ + + /** + * Label of line-item. + * + * @var string|null $label + */ + private $label = null; + + /** + * Maximum score of line-item. + * + * @var int|null $scoreMaximum + */ + private $scoreMaximum = null; + + /** + * Resource ID associated with line-item. + * + * @var string|null $resourceId + */ + private $resourceId = null; + + /** + * Tag of line-item. + * + * @var string|null $tag + */ + private $tag = null; + + /** + * Class constructor. + * + * @param string $label Label + * @param int $scoreMaximum Maximum score + * @param string $resourceId Resource ID (optional) + * @param string $tag Tag (optional) + */ + function __construct($label, $scoreMaximum, $resourceId = null, $tag = null) + { + $this->label = $label; + $this->scoreMaximum = $scoreMaximum; + $this->resourceId = $resourceId; + $this->tag = $tag; + } + + /** + * Generate the JSON-LD object representation of the line-item. + * + * @return object + */ + public function toJsonldObject() + { + $lineItem = new \stdClass(); + + $lineItem->{'@type'} = 'LineItem'; + $lineItem->label = $this->label; + $lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#normalScore'; + if (!empty($this->resourceId)) { + $lineItem->assignedActivity = new \stdClass(); + $lineItem->assignedActivity->activityId = $this->resourceId; + } + $lineItem->scoreConstraints = new \stdClass(); + $lineItem->scoreConstraints->{'@type'} = 'NumericLimits'; + $lineItem->scoreConstraints->normalMaximum = $this->scoreMaximum; + + return $lineItem; + } + + /** + * Generate the JSON object representation of the line-item. + * + * @return object + */ + public function toJsonObject() + { + $lineItem = new \stdClass(); + + $lineItem->label = $this->label; + $lineItem->scoreMaximum = $this->scoreMaximum; + if (!empty($this->resourceId)) { + $lineItem->resourceId = $this->resourceId; + } + if (!empty($this->tag)) { + $lineItem->tag = $this->tag; + } + + return $lineItem; + } + + public static function fromJsonObject($item) + { + $obj = null; + $label = null; + $reportingMethod = null; + $scoreMaximum = null; + $activityId = null; + $tag = null; + $available = null; + $submission = null; + foreach (get_object_vars($item) as $name => $value) { + switch ($name) { + case 'label': + $label = $item->label; + break; + case 'reportingMethod': + $reportingMethod = $item->reportingMethod; + break; + case 'scoreConstraints': + $scoreConstraints = $item->scoreConstraints; + break; + case 'scoreMaximum': + $scoreMaximum = $item->scoreMaximum; + break; + case 'assignedActivity': + if (isset($item->assignedActivity->activityId)) { + $activityId = $item->assignedActivity->activityId; + } + break; + case 'resourceId': + $activityId = $item->resourceId; + break; + case 'tag': + $tag = $item->tag; + break; + } + } + if (is_null($scoreMaximum) && $label && $reportingMethod && $scoreConstraints) { + foreach (get_object_vars($scoreConstraints) as $name => $value) { + $method = str_replace('Maximum', 'Score', $name); + if (substr($reportingMethod, -strlen($method)) === $method) { + $scoreMaximum = $value; + break; + } + } + if (!is_null($scoreMaximum)) { + $obj = new LineItem($label, $scoreMaximum, $activityId, $tag); + } + } + + return $obj; + } + +} diff --git a/src/Content/LtiLinkItem.php b/src/Content/LtiLinkItem.php new file mode 100644 index 0000000..8254e3e --- /dev/null +++ b/src/Content/LtiLinkItem.php @@ -0,0 +1,146 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class LtiLinkItem extends Item +{ + + /** + * Custom parameters for content-item. + * + * @var array $custom + */ + private $custom = array(); + + /** + * Line-item object for content-item. + * + * @var LineItem|null $lineItem + */ + private $lineItem = null; + + /** + * Time period for availability. + * + * @var string|null $available + */ + private $available = null; + + /** + * Class constructor. + * + * @param Placement $placementAdvice Placement object for item (optional) + * @param string $id URL of content-item (optional) + */ + function __construct($placementAdvice = null, $id = null) + { + parent::__construct(Item::TYPE_LTI_LINK, $placementAdvice, $id); + } + + /** + * Add a custom parameter for the content-item. + * + * @param string $name Name of parameter + * @param string|null $value Value of parameter + */ + public function addCustom($name, $value = null) + { + if (!empty($name)) { + if (!empty($value)) { + $this->custom[$name] = $value; + } else { + reset($this->custom[$name]); + } + } + } + + /** + * Set a line-item for the content-item. + * + * @param LineItem $lineItem Line-item + */ + public function setLineItem($lineItem) + { + $this->lineItem = $lineItem; + } + + /** + * Set an availability time period for the content-item. + * + * @param TimePeriod $available Time period + */ + public function setAvailable($available) + { + $this->available = $available; + } + + /** + * Set a submission time period for the content-item. + * + * @param TimePeriod $submission Time period + */ + public function setSubmission($submission) + { + $this->submission = $submission; + } + + public function toJsonldObject() + { + $item = parent::toJsonldObject(); + if (!empty($this->lineItem)) { + $item->lineItem = $this->lineItem->toJsonldObject(); + } + if (!empty($this->custom)) { + $item->custom = $this->custom; + } + + return $item; + } + + public function toJsonObject() + { + $item = parent::toJsonObject(); + if (!empty($this->lineItem)) { + $item->lineItem = $this->lineItem->toJsonObject(); + } + if (!empty($this->custom)) { + $item->custom = $this->custom; + } + + return $item; + } + + protected function fromJsonObject($item) + { + parent::fromJsonObject($item); + $url = null; + $width = null; + $height = null; + foreach (get_object_vars($item) as $name => $value) { + switch ($name) { + case 'custom': + foreach ($item->custom as $paramName => $paramValue) { + $this->addCustom($paramName, $paramValue); + } + break; + case 'lineItem': + $this->setLineItem(LineItem::fromJsonObject($item->lineItem)); + break; + case 'available': + $this->setAvailable(TimePeriod::fromJsonObject($item->available)); + break; + case 'submission': + $this->setSubmission(TimePeriod::fromJsonObject($item->submission)); + break; + } + } + } + +} diff --git a/src/Content/Placement.php b/src/Content/Placement.php new file mode 100644 index 0000000..53016bd --- /dev/null +++ b/src/Content/Placement.php @@ -0,0 +1,218 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class Placement +{ + + /** + * Embed placement type. + */ + const TYPE_EMBED = 'embed'; + + /** + * iFrame placement type. + */ + const TYPE_IFRAME = 'iframe'; + + /** + * Frame placement type. + */ + const TYPE_FRAME = 'frame'; + + /** + * Window placement type. + */ + const TYPE_WINDOW = 'window'; + + /** + * Popup placement type. + */ + const TYPE_POPUP = 'popup'; + + /** + * Overlay placement type. + */ + const TYPE_OVERLAY = 'overlay'; + + /** + * Location to open content in. + * + * @var string|null $documentTarget + */ + public $documentTarget = null; + + /** + * Name of window target. + * + * @var string|null $windowTarget + */ + private $windowTarget = null; + + /** + * Comma-separated list of window features. + * + * @var string|null $windowFeatures + */ + private $windowFeatures = null; + + /** + * URL of iframe src. + * + * @var string|null $url + */ + private $url = null; + + /** + * Width of item location. + * + * @var int|null $displayWidth + */ + private $displayWidth = null; + + /** + * Height of item location. + * + * @var int|null $displayHeight + */ + private $displayHeight = null; + + /** + * Class constructor. + * + * @param string $documentTarget Location to open content in + * @param int $displayWidth Width of item location (optional) + * @param int $displayHeight Height of item location (optional) + * @param string $windowTarget Name of window target (optional) + * @param string $windowFeatures List of window features (optional) + * @param string $url URL for iframe src (optional) + */ + function __construct($documentTarget, $displayWidth = null, $displayHeight = null, $windowTarget = null, $windowFeatures = null, + $url = null) + { + $this->documentTarget = $documentTarget; + $this->displayWidth = $displayWidth; + $this->displayHeight = $displayHeight; + $this->windowTarget = $windowTarget; + $this->windowFeatures = $windowFeatures; + $this->url = $url; + } + + /** + * Generate the JSON-LD object representation of the placement. + * + * @return object + */ + public function toJsonldObject() + { + if (!empty($this->documentTarget)) { + $placement = new \stdClass(); + $placement->presentationDocumentTarget = $this->documentTarget; + if (!is_null($this->displayHeight)) { + $placement->displayHeight = $this->displayHeight; + } + if (!is_null($this->displayWidth)) { + $placement->displayWidth = $this->displayWidth; + } + if (!empty($this->windowTarget)) { + $placement->windowTarget = $this->windowTarget; + } + } else { + $placement = null; + } + + return $placement; + } + + /** + * Generate the JSON object representation of the placement. + * + * @return object + */ + public function toJsonObject() + { + if (!empty($this->documentTarget)) { + $placement = new \stdClass(); + switch ($this->documentTarget) { + case self::TYPE_IFRAME: + $placement->src = $this->url; + if (!is_null($this->displayWidth)) { + $placement->width = $this->displayWidth; + } + if (!is_null($this->displayHeight)) { + $placement->height = $this->displayHeight; + } + break; + case self::TYPE_WINDOW: + if (!is_null($this->displayWidth)) { + $placement->width = $this->displayWidth; + } + if (!is_null($this->displayHeight)) { + $placement->height = $this->displayHeight; + } + if (!is_null($this->windowTarget)) { + $placement->targetName = $this->windowTarget; + } + if (!is_null($this->windowFeatures)) { + $placement->windowFeatures = $this->windowFeatures; + } + break; + } + } else { + $placement = null; + } + + return $placement; + } + + public static function fromJsonObject($item) + { + $obj = null; + $documentTarget = null; + $displayWidth = null; + $displayHeight = null; + $windowTarget = null; + $windowFeatures = null; + $url = null; + foreach (get_object_vars($item) as $name => $value) { + switch ($name) { + case 'presentationDocumentTarget': + case 'documentTarget': + $documentTarget = $value; + break; + case 'displayWidth': + case 'width': + $displayWidth = $value; + break; + case 'displayHeight': + case 'height': + $displayHeight = $value; + break; + case 'windowTarget': + case 'targetName': + $windowTarget = $value; + break; + case 'windowFeatures': + $windowFeatures = $value; + break; + case 'url': + case 'src': + $url = $value; + break; + } + } + if ($documentTarget) { + $obj = new Placement($documentTarget, $displayWidth, $displayHeight, $windowTarget, $windowFeatures, $url); + } + + return $obj; + } + +} diff --git a/src/Content/TimePeriod.php b/src/Content/TimePeriod.php new file mode 100644 index 0000000..2e13d2f --- /dev/null +++ b/src/Content/TimePeriod.php @@ -0,0 +1,96 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class TimePeriod +{ + + /** + * Start date/time. + * + * @var int|null $startDateTime + */ + private $startDateTime = null; + + /** + * End date/time. + * + * @var int|null $endDateTime + */ + private $endDateTime = null; + + /** + * Class constructor. + * + * @param int $startDateTime Start date/time + * @param int $endDateTime End date/time + */ + function __construct($startDateTime, $endDateTime) + { + $this->startDateTime = $startDateTime; + $this->endDateTime = $endDateTime; + } + + /** + * Generate the JSON-LD object representation of the time period. + * + * @return object + */ + public function toJsonldObject() + { + return new \stdClass(); + } + + /** + * Generate the JSON object representation of the image. + * + * @return string + */ + public function toJsonObject() + { + $timePeriod = new \stdClass(); + if (!is_null($this->startDateTime)) { + $timePeriod->startDateTime = $this->startDateTime; + } + if (!is_null($this->endDateTime)) { + $timePeriod->endDateTime = $this->endDateTime; + } + + return $timePeriod; + } + + public static function fromJsonObject($item) + { + $obj = null; + $startDateTime = null; + $endDateTime = null; + if (is_object($item)) { + $url = null; + foreach (get_object_vars($item) as $name => $value) { + switch ($name) { + case 'startDateTime': + $startDateTime = $item->startDateTime; + break; + case 'endDateTime': + $endDateTime = $item->endDateTime; + break; + } + } + } else { + $url = $item; + } + if ($startDateTime || $endDateTime) { + $obj = new TimePeriod($startDateTime, $endDateTime); + } + + return $obj; + } + +} diff --git a/src/ContentItem.php b/src/ContentItem.php index 21cb71e..7243b08 100644 --- a/src/ContentItem.php +++ b/src/ContentItem.php @@ -2,21 +2,22 @@ namespace ceLTIc\LTI; +use ceLTIc\LTI\Content\Item; +use ceLTIc\LTI\Content\ContentItemPlacement; + /** * Class to represent a content-item object * + * @deprecated Use Content::Item instead + * @see Content::Item + * * @author Stephen P Vickers * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 */ -class ContentItem +class ContentItem extends Item { - /** - * Media type for LTI launch links. - */ - const LTI_LINK_MEDIA_TYPE = 'application/vnd.ims.lti.v1.ltilink'; - /** * Class constructor. * @@ -26,90 +27,8 @@ class ContentItem */ function __construct($type, $placementAdvice = null, $id = null) { - $this->{'@type'} = $type; - if (is_object($placementAdvice) && (count(get_object_vars($placementAdvice)) > 0)) { - $this->placementAdvice = $placementAdvice; - } - if (!empty($id)) { - $this->{'@id'} = $id; - } - } - - /** - * Set a URL value for the content-item. - * - * @param string $url URL value - */ - public function setUrl($url) - { - if (!empty($url)) { - $this->url = $url; - } else { - unset($this->url); - } - } - - /** - * Set a media type value for the content-item. - * - * @param string $mediaType Media type value - */ - public function setMediaType($mediaType) - { - if (!empty($mediaType)) { - $this->mediaType = $mediaType; - } else { - unset($this->mediaType); - } - } - - /** - * Set a title value for the content-item. - * - * @param string $title Title value - */ - public function setTitle($title) - { - if (!empty($title)) { - $this->title = $title; - } elseif (isset($this->title)) { - unset($this->title); - } - } - - /** - * Set a link text value for the content-item. - * - * @param string $text Link text value - */ - public function setText($text) - { - if (!empty($text)) { - $this->text = $text; - } elseif (isset($this->text)) { - unset($this->text); - } - } - - /** - * Wrap the content items to form a complete application/vnd.ims.lti.v1.contentitems+json media type instance. - * - * @param mixed $items An array of content items or a single item - * - * @return string - */ - public static function toJson($items) - { - $obj = new \stdClass(); - $obj->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem'; - if (!is_array($items)) { - $obj->{'@graph'} = array(); - $obj->{'@graph'}[] = $items; - } else { - $obj->{'@graph'} = $items; - } - - return json_encode($obj); + parent::__construct($type, $placementAdvice, $id); + Util::logDebug('Class ceLTIc\LTI\ContentItem has been deprecated; please use ceLTIc\LTI\Content\Item instead.', true); } } diff --git a/src/ContentItemImage.php b/src/ContentItemImage.php index 864bce6..32a1310 100644 --- a/src/ContentItemImage.php +++ b/src/ContentItemImage.php @@ -2,14 +2,19 @@ namespace ceLTIc\LTI; +use ceLTIc\LTI\Content\Image; + /** * Class to represent a content-item image object * + * @deprecated Use Content::Image instead + * @see Content::Image + * * @author Stephen P Vickers * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 */ -class ContentItemImage +class ContentItemImage extends Image { /** @@ -21,13 +26,9 @@ class ContentItemImage */ function __construct($id, $height = null, $width = null) { - $this->{'@id'} = $id; - if (!is_null($height)) { - $this->height = $height; - } - if (!is_null($width)) { - $this->width = $width; - } + parent::__construct($id, $width, $height); + Util::logDebug('Class ceLTIc\LTI\ContentItemImage has been deprecated; please use ceLTIc\LTI\Content\Image instead ' . + '(note change of parameter order in constructor).', true); } } diff --git a/src/ContentItemPlacement.php b/src/ContentItemPlacement.php index 24764c7..2c67216 100644 --- a/src/ContentItemPlacement.php +++ b/src/ContentItemPlacement.php @@ -2,14 +2,19 @@ namespace ceLTIc\LTI; +use ceLTIc\LTI\Content\Placement; + /** * Class to represent a content-item placement object * + * @deprecated Use Content::Placement instead + * @see Content::Placement + * * @author Stephen P Vickers * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 */ -class ContentItemPlacement +class ContentItemPlacement extends Placement { /** @@ -22,18 +27,9 @@ class ContentItemPlacement */ function __construct($displayWidth, $displayHeight, $documentTarget, $windowTarget) { - if (!empty($displayWidth)) { - $this->displayWidth = $displayWidth; - } - if (!empty($displayHeight)) { - $this->displayHeight = $displayHeight; - } - if (!empty($documentTarget)) { - $this->documentTarget = $documentTarget; - } - if (!empty($windowTarget)) { - $this->windowTarget = $windowTarget; - } + parent::__construct($documentTarget, $displayWidth, $displayHeight, $windowTarget); + Util::logDebug('Class ceLTIc\LTI\ContentItemPlacement has been deprecated; please use ceLTIc\LTI\Content\Placement instead ' . + '(note change in parameter order for constructor).', true); } } diff --git a/src/Context.php b/src/Context.php index e2a26aa..65713a5 100644 --- a/src/Context.php +++ b/src/Context.php @@ -5,9 +5,10 @@ use ceLTIc\LTI\Service; use ceLTIc\LTI\Http\HttpMessage; use ceLTIc\LTI\ApiHook\ApiHook; +use ceLTIc\LTI\Util; /** - * Class to represent a tool consumer context + * Class to represent a platform context * * @author Stephen P Vickers * @copyright SPV Software Products @@ -39,14 +40,14 @@ class Context public $type = null; /** - * UserResult group sets (null if the consumer does not support the groups enhancement) + * UserResult group sets (null if the platform does not support the groups enhancement) * * @var array|null $groupSets */ public $groupSets = null; /** - * UserResult groups (null if the consumer does not support the groups enhancement) + * UserResult groups (null if the platform does not support the groups enhancement) * * @var array|null $groups */ @@ -74,18 +75,18 @@ class Context public $updated = null; /** - * Tool Consumer for this context. + * Platform for this context. * - * @var ToolConsumer|null $consumer + * @var Platform|null $platform */ - private $consumer = null; + private $platform = null; /** - * Tool Consumer ID for this context. + * Platform ID for this context. * - * @var int|null $consumerId + * @var int|null $platformId */ - private $consumerId = null; + private $platformId = null; /** * ID for this context. @@ -174,36 +175,66 @@ public function delete() /** * Get tool consumer. * + * @deprecated Use getPlatform() instead + * @see Context::getPlatform() + * * @return ToolConsumer Tool consumer object for this context. */ public function getConsumer() { - if (is_null($this->consumer)) { - $this->consumer = ToolConsumer::fromRecordId($this->consumerId, $this->getDataConnector()); - } - - return $this->consumer; + Util::logDebug('Method ceLTIc\LTI\Context::getConsumer() has been deprecated; please use ceLTIc\LTI\Context::getPlatform() instead.', + true); + return $this->getPlatform(); } /** * Set tool consumer ID. * + * @deprecated Use setPlatformId() instead + * @see Context::setPlatformId() + * * @param int $consumerId Tool Consumer ID for this resource link. */ public function setConsumerId($consumerId) { - $this->consumer = null; - $this->consumerId = $consumerId; + Util::logDebug('Method ceLTIc\LTI\Context::setConsumerId() has been deprecated; please use ceLTIc\LTI\Context::setPlatformId() instead.', + true); + $this->setPlatformId($consumerId); + } + + /** + * Get platform. + * + * @return Platform Platform object for this context. + */ + public function getPlatform() + { + if (is_null($this->platform)) { + $this->platform = Platform::fromRecordId($this->platformId, $this->getDataConnector()); + } + + return $this->platform; + } + + /** + * Set platform ID. + * + * @param int $platformId Platform ID for this resource link. + */ + public function setPlatformId($platformId) + { + $this->platform = null; + $this->platformId = $platformId; } /** - * Get tool consumer key. + * Get consumer key. * - * @return string Consumer key value for this context. + * @return string Consumer key value for this context. */ public function getKey() { - return $this->getConsumer()->getKey(); + return $this->getPlatform()->getKey(); } /** @@ -329,7 +360,7 @@ public function hasToolSettingsService() { $has = !empty($this->getSetting('custom_context_setting_url')); if (!$has) { - $has = self::hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this); + $has = self::hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this); } return $has; } @@ -353,8 +384,8 @@ public function getToolSettings($mode = Service\ToolSettings::MODE_CURRENT_LEVEL $this->lastServiceRequest = $service->getHttpMessage(); $ok = $settings !== false; } - if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this)) { - $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode()); + if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this)) { + $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode()); $hook = new $className($this); $settings = $hook->getToolSettings($mode, $simple); } @@ -363,7 +394,7 @@ public function getToolSettings($mode = Service\ToolSettings::MODE_CURRENT_LEVEL } /** - * Perform a Tool Settings service request. + * Set Tool Settings. * * @param array $settings An associative array of settings (optional, default is none) * @@ -378,8 +409,8 @@ public function setToolSettings($settings = array()) $ok = $service->set($settings); $this->lastServiceRequest = $service->getHttpMessage(); } - if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this)) { - $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode()); + if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this)) { + $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode()); $hook = new $className($this); $ok = $hook->setToolSettings($settings); } @@ -397,6 +428,8 @@ public function setToolSettings($settings = array()) */ public function hasMembershipService() { + Util::logDebug('Method ceLTIc\LTI\Context::hasMembershipService() has been deprecated; please use ceLTIc\LTI\Context::hasMembershipsService() instead.', + true); return $this->hasMembershipsService(); } @@ -409,7 +442,7 @@ public function hasMembershipsService() { $has = !empty($this->getSetting('custom_context_memberships_url')) || !empty($this->getSetting('custom_context_memberships_v2_url')); if (!$has) { - $has = self::hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this); + $has = self::hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this); } return $has; } @@ -424,6 +457,8 @@ public function hasMembershipsService() */ public function getMembership() { + Util::logDebug('Method ceLTIc\LTI\Context::getMembership() has been deprecated; please use ceLTIc\LTI\Context::getMemberships() instead.', + true); return $this->getMemberships(); } @@ -439,14 +474,14 @@ public function getMemberships($withGroups = false) $ok = false; $userResults = array(); $hasLtiService = !empty($this->getSetting('custom_context_memberships_url')) || !empty($this->getSetting('custom_context_memberships_v2_url')); - $hasApiHook = $this->hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this); + $hasApiHook = $this->hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this); if ($hasLtiService && (!$withGroups || !$hasApiHook)) { if (!empty($this->getSetting('custom_context_memberships_v2_url'))) { $url = $this->getSetting('custom_context_memberships_v2_url'); - $format = Service\Membership::MEMBERSHIPS_MEDIA_TYPE_NRPS; + $format = Service\Membership::MEDIA_TYPE_MEMBERSHIPS_NRPS; } else { $url = $this->getSetting('custom_context_memberships_url'); - $format = Service\Membership::MEMBERSHIPS_MEDIA_TYPE_V1; + $format = Service\Membership::MEDIA_TYPE_MEMBERSHIPS_V1; } $service = new Service\Membership($this, $url, $format); $userResults = $service->get(); @@ -454,7 +489,7 @@ public function getMemberships($withGroups = false) $ok = $userResults !== false; } if (!$ok && $hasApiHook) { - $className = $this->getApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode()); + $className = $this->getApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode()); $hook = new $className($this); $userResults = $hook->getMemberships($withGroups); } @@ -491,9 +526,8 @@ public function getLineItems($resourceId = null, $tag = null, $limit = null) $this->extResponse = ''; $this->extResponseHeaders = ''; $this->lastServiceRequest = null; - $url = $this->getSetting('custom_lineitems_url'); - if (!empty($url)) { - $lineItemService = new Service\LineItem($this->getConsumer(), $url, $limit); + $lineItemService = $this->getLineItemService(); + if (!empty($lineItemService)) { $lineItems = $lineItemService->getAll(null, $resourceId, $tag); $http = $lineItemService->getHttpMessage(); $this->extResponse = $http->response; @@ -544,16 +578,32 @@ public static function fromRecordId($id, $dataConnector) /** * Class constructor from consumer. * + * @deprecated Use fromPlatform() instead + * @see Context::fromPlatform() + * * @param ToolConsumer $consumer Consumer instance * @param string $ltiContextId LTI Context ID value * * @return Context */ public static function fromConsumer($consumer, $ltiContextId) + { + return self::fromPlatform($consumer, $ltiContextId); + } + + /** + * Class constructor from platform. + * + * @param Platform $platform Platform instance + * @param string $ltiContextId LTI Context ID value + * + * @return Context + */ + public static function fromPlatform($platform, $ltiContextId) { $context = new Context(); - $context->consumer = $consumer; - $context->dataConnector = $consumer->getDataConnector(); + $context->platform = $platform; + $context->dataConnector = $platform->getDataConnector(); $context->ltiContextId = $ltiContextId; if (!empty($ltiContextId)) { $context->load(); @@ -589,7 +639,7 @@ private function getLineItemService() { $url = $this->getSetting('custom_lineitems_url'); if (!empty($url)) { - $lineItemService = new Service\LineItem($this->getConsumer(), $url); + $lineItemService = new Service\LineItem($this->getPlatform(), $url); } else { $lineItemService = false; } diff --git a/src/DataConnector/DataConnector.php b/src/DataConnector/DataConnector.php index 0c2f1e7..0242c64 100644 --- a/src/DataConnector/DataConnector.php +++ b/src/DataConnector/DataConnector.php @@ -2,14 +2,14 @@ namespace ceLTIc\LTI\DataConnector; -use ceLTIc\LTI; -use ceLTIc\LTI\ConsumerNonce; +use ceLTIc\LTI\PlatformNonce; use ceLTIc\LTI\Context; use ceLTIc\LTI\ResourceLink; use ceLTIc\LTI\ResourceLinkShare; use ceLTIc\LTI\ResourceLinkShareKey; -use ceLTIc\LTI\ToolConsumer; +use ceLTIc\LTI\Platform; use ceLTIc\LTI\UserResult; +use ceLTIc\LTI\Util; /** * Class to provide a connection to a persistent store for LTI objects @@ -24,9 +24,17 @@ class DataConnector { /** - * Default name for database table used to store tool consumers. + * Default name for database table used to store platforms. */ - const CONSUMER_TABLE_NAME = 'lti2_consumer'; + const PLATFORM_TABLE_NAME = 'lti2_consumer'; + + /** + * Default name for database table used to store platforms. + * + * @deprecated Use DataConnector::PLATFORM_TABLE_NAME instead + * @see DataConnector::PLATFORM_TABLE_NAME + */ + const CONSUMER_TABLE_NAME = self::PLATFORM_TABLE_NAME; /** * Default name for database table used to store contexts. @@ -53,6 +61,11 @@ class DataConnector */ const NONCE_TABLE_NAME = 'lti2_nonce'; + /** + * Default name for database table used to store access token values. + */ + const ACCESS_TOKEN_TABLE_NAME = 'lti2_access_token'; + /** * Database connection. * @@ -94,61 +107,127 @@ public function __construct($db, $dbTableNamePrefix = '') } ### -### ToolConsumer methods +### Platform methods ### /** * Load tool consumer object. * - * @param ToolConsumer $consumer ToolConsumer object + * @deprecated Use loadPlatform() instead + * @see DataConnector::loadPlatform() + * + * @param ToolConsumer $consumer Tool consumer object * * @return bool True if the tool consumer object was successfully loaded */ public function loadToolConsumer($consumer) { - $consumer->secret = 'secret'; - $consumer->enabled = true; - $now = time(); - $consumer->created = $now; - $consumer->updated = $now; - - return true; + Util::logDebug('Method ceLTIc\LTI\DataConnector\DataConnector::loadToolConsumer() has been deprecated; please use ceLTIc\LTI\DataConnector\DataConnector::loadPlatform() instead.', + true); + return $this->loadPlatform($consumer); } /** * Save tool consumer object. * - * @param ToolConsumer $consumer Consumer object + * @deprecated Use savePlatform() instead + * @see DataConnector::savePlatform() + * + * @param ToolConsumer $consumer Tool consumer object * * @return bool True if the tool consumer object was successfully saved */ public function saveToolConsumer($consumer) { - $consumer->updated = time(); - - return true; + Util::logDebug('Method ceLTIc\LTI\DataConnector\DataConnector::saveToolConsumer() has been deprecated; please use ceLTIc\LTI\DataConnector\DataConnector::savePlatform() instead.', + true); + return $this->savePlatform($consumer); } /** * Delete tool consumer object. * - * @param ToolConsumer $consumer Consumer object + * @deprecated Use deletePlatform() instead + * @see DataConnector::deletePlatform() + * + * @param ToolConsumer $consumer Tool consumer object * * @return bool True if the tool consumer object was successfully deleted */ public function deleteToolConsumer($consumer) { - $consumer->initialize(); - - return true; + Util::logDebug('Method ceLTIc\LTI\DataConnector\DataConnector::deleteToolConsumer() has been deprecated; please use ceLTIc\LTI\DataConnector\DataConnector::deletePlatform() instead.', + true); + return $this->deletePlatform($consumer); } /** * Load tool consumer objects. * - * @return ToolConsumer[] Array of all defined ToolConsumer objects + * @deprecated Use getPlatforms() instead + * @see DataConnector::getPlatforms() + * + * @return ToolConsumer[] Array of all defined tool consumer objects */ public function getToolConsumers() + { + Util::logDebug('Method ceLTIc\LTI\DataConnector\DataConnector::getToolConsumers() has been deprecated; please use ceLTIc\LTI\DataConnector\DataConnector::getPlatforms() instead.', + true); + return $this->getPlatforms(); + } + + /** + * Load platform object. + * + * @param Platform $platform Platform object + * + * @return bool True if the platform object was successfully loaded + */ + public function loadPlatform($platform) + { + $platform->secret = 'secret'; + $platform->enabled = true; + $now = time(); + $platform->created = $now; + $platform->updated = $now; + + return true; + } + + /** + * Save platform object. + * + * @param Platform $platform Platform object + * + * @return bool True if the platform object was successfully saved + */ + public function savePlatform($platform) + { + $platform->updated = time(); + + return true; + } + + /** + * Delete platform object. + * + * @param Platform $platform Platform object + * + * @return bool True if the platform object was successfully deleted + */ + public function deletePlatform($platform) + { + $platform->initialize(); + + return true; + } + + /** + * Load platform objects. + * + * @return Platform[] Array of all defined Platform objects + */ + public function getPlatforms() { return array(); } @@ -279,29 +358,120 @@ public function getSharesResourceLink($resourceLink) } ### -### ConsumerNonce methods +### PlatformNonce methods ### /** * Load nonce object. * + * @deprecated Use loadPlatformNonce() instead + * @see DataConnector::loadPlatformNonce() + * * @param ConsumerNonce $nonce Nonce object * * @return bool True if the nonce object was successfully loaded */ public function loadConsumerNonce($nonce) { - return false; // assume the nonce does not already exist + Util::logDebug('Method ceLTIc\LTI\DataConnector\DataConnector::loadConsumerNonce() has been deprecated; please use ceLTIc\LTI\DataConnector\DataConnector::loadPlatformNonce() instead.', + true); + return $this->loadPlatformNonce($nonce); } /** * Save nonce object. * + * @deprecated Use savePlatformNonce() instead + * @see DataConnector::savePlatformNonce() + * * @param ConsumerNonce $nonce Nonce object * * @return bool True if the nonce object was successfully saved */ public function saveConsumerNonce($nonce) + { + Util::logDebug('Method ceLTIc\LTI\DataConnector\DataConnector::saveConsumerNonce() has been deprecated; please use ceLTIc\LTI\DataConnector\DataConnector::savePlatformNonce() instead.', + true); + return $this->savePlatformNonce($nonce); + } + + /** + * Delete nonce object. + * + * @deprecated Use deletePlatformNonce() instead + * @see DataConnector::deletePlatformNonce() + * + * @param ConsumerNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully deleted + */ + public function deleteConsumerNonce($nonce) + { + Util::logDebug('Method ceLTIc\LTI\DataConnector\DataConnector::deleteConsumerNonce() has been deprecated; please use ceLTIc\LTI\DataConnector\DataConnector::deletePlatformNonce() instead.', + true); + return $this->deletePlatformNonce($nonce); + } + + /** + * Load nonce object. + * + * @param PlatformNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully loaded + */ + public function loadPlatformNonce($nonce) + { + return false; // assume the nonce does not already exist + } + + /** + * Save nonce object. + * + * @param PlatformNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully saved + */ + public function savePlatformNonce($nonce) + { + return true; + } + + /** + * Delete nonce object. + * + * @param PlatformNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully deleted + */ + public function deletePlatformNonce($nonce) + { + return true; + } + +### +### AccessToken methods +### + + /** + * Load access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the nonce object was successfully loaded + */ + public function loadAccessToken($accessToken) + { + return false; // assume the access token does not already exist + } + + /** + * Save access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the access token object was successfully saved + */ + public function saveAccessToken($accessToken) { return true; } @@ -450,22 +620,18 @@ public static function getDataConnector($db = null, $dbTableNamePrefix = '', $ty * * The generated string will only comprise letters (upper- and lower-case) and digits. * + * @deprecated Use Util::getRandomString() instead + * @see Util::getRandomString() + * * @param int $length Length of string to be generated (optional, default is 8 characters) * * @return string Random string */ public static function getRandomString($length = 8) { - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - $value = ''; - $charsLength = strlen($chars) - 1; - - for ($i = 1; $i <= $length; $i++) { - $value .= $chars[rand(0, $charsLength)]; - } - - return $value; + Util::logDebug('Method ceLTIc\LTI\DataConnector::getRandomString() has been deprecated; please use ceLTIc\LTI\Util::getRandomString() instead.', + true); + return Util::getRandomString($length); } /** @@ -510,19 +676,36 @@ public static function quoted($value, $addQuotes = true) } /** - * Return a hash of a consumer key for values longer than 255 characters. + * Adjust the settings for any platform properties being stored as a setting value. * - * @param string $key - * @return string + * @param Platform $platform Platform object + * @param bool $isSave True if the settings are being saved */ - protected static function getConsumerKey($key) + protected function fixPlatformSettings($platform, $isSave) { - $len = strlen($key); - if ($len > 255) { - $key = 'sha512:' . hash('sha512', $key); + if (!$isSave) { + $platform->authorizationServerId = $platform->getSetting('_authorization_server_id', $platform->authorizationServerId); + $platform->setSetting('_authorization_server_id'); + $platform->authenticationUrl = $platform->getSetting('_authentication_request_url', $platform->authenticationUrl); + $platform->setSetting('_authentication_request_url'); + $platform->accessTokenUrl = $platform->getSetting('_oauth2_access_token_url', $platform->accessTokenUrl); + $platform->setSetting('_oauth2_access_token_url'); + $platform->jku = $platform->getSetting('_jku', $platform->jku); + $platform->setSetting('_jku'); + $platform->encryptionMethod = $platform->getSetting('_encryption_method', $platform->encryptionMethod); + $platform->setSetting('_encryption_method'); + $platform->debugMode = $platform->getSetting('_debug', $platform->debugMode ? 'true' : 'false') === 'true'; + $platform->setSetting('_debug'); + } else { + $platform->setSetting('_authorization_server_id', + !empty($platform->authorizationServerId) ? $platform->authorizationServerId : null); + $platform->setSetting('_authentication_request_url', + !empty($platform->authenticationUrl) ? $platform->authenticationUrl : null); + $platform->setSetting('_oauth2_access_token_url', !empty($platform->accessTokenUrl) ? $platform->accessTokenUrl : null); + $platform->setSetting('_jku', !empty($platform->jku) ? $platform->jku : null); + $platform->setSetting('_encryption_method', !empty($platform->encryptionMethod) ? $platform->encryptionMethod : null); + $platform->setSetting('_debug', $platform->debugMode ? 'true' : null); } - - return $key; } } diff --git a/src/DataConnector/DataConnector_mysql.php b/src/DataConnector/DataConnector_mysql.php index 4383af9..a84d628 100644 --- a/src/DataConnector/DataConnector_mysql.php +++ b/src/DataConnector/DataConnector_mysql.php @@ -3,18 +3,21 @@ namespace ceLTIc\LTI\DataConnector; use ceLTIc\LTI; -use ceLTIc\LTI\ConsumerNonce; +use ceLTIc\LTI\PlatformNonce; use ceLTIc\LTI\Context; use ceLTIc\LTI\ResourceLink; use ceLTIc\LTI\ResourceLinkShare; use ceLTIc\LTI\ResourceLinkShareKey; -use ceLTIc\LTI\ToolConsumer; +use ceLTIc\LTI\Platform; use ceLTIc\LTI\UserResult; use ceLTIc\LTI\Util; /** * Class to represent an LTI Data Connector for MySQL * + * @deprecated Use DataConnector_mysqli instead + * @see DataConnector_mysqli + * * @author Stephen P Vickers * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 @@ -26,174 +29,227 @@ class DataConnector_mysql extends DataConnector { + + /** + * Class constructor + * + * @param object|resource $db Database connection object + * @param string $dbTableNamePrefix Prefix for database table names (optional, default is none) + */ + public function __construct($db, $dbTableNamePrefix = '') + { + parent::__construct($db, $dbTableNamePrefix); + Util::logDebug('Class ceLTIc\LTI\DataConnector\DataConnector_mysql has been deprecated; please use ceLTIc\LTI\DataConnector\DataConnector_mysqli instead.', + true); + } + ### -### ToolConsumer methods +### Platform methods ### /** - * Load tool consumer object. + * Load platform object. * - * @param ToolConsumer $consumer ToolConsumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully loaded + * @return bool True if the platform object was successfully loaded */ - public function loadToolConsumer($consumer) + public function loadPlatform($platform) { $ok = false; - if (!is_null($consumer->getRecordId())) { - $sql = sprintf('SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + if (!is_null($platform->getRecordId())) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - "WHERE consumer_pk = %d", $consumer->getRecordId()); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE consumer_pk = %d', $platform->getRecordId()); + } elseif (!empty($platform->platformId)) { + if (empty($platform->clientId)) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) ' . + 'GROUP BY platform_id, client_id', $this->escape($platform->platformId)); + } elseif (empty($platform->deploymentId)) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) AND (client_id = %s)', $this->escape($platform->platformId), + $this->escape($platform->clientId)); + } else { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) AND (client_id = %s) AND (deployment_id = %s)', $this->escape($platform->platformId), + $this->escape($platform->clientId), $this->escape($platform->deploymentId)); + } } else { - $key256 = static::getConsumerKey($consumer->getKey()); - $sql = sprintf('SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - "WHERE consumer_key256 = %s", $this->escape($key256)); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE consumer_key = %s', $this->escape($platform->getKey())); } $rsConsumer = $this->executeQuery($sql); if ($rsConsumer) { - while ($row = mysql_fetch_object($rsConsumer)) { - if (empty($key256) || empty($row->consumer_key) || ($consumer->getKey() === $row->consumer_key)) { - $consumer->setRecordId(intval($row->consumer_pk)); - $consumer->name = $row->name; - $consumer->setkey(empty($row->consumer_key) ? $row->consumer_key256 : $row->consumer_key); - $consumer->secret = $row->secret; - $consumer->ltiVersion = $row->lti_version; - $consumer->signatureMethod = $row->signature_method; - $consumer->consumerName = $row->consumer_name; - $consumer->consumerVersion = $row->consumer_version; - $consumer->consumerGuid = $row->consumer_guid; - $consumer->profile = json_decode($row->profile); - $consumer->toolProxy = $row->tool_proxy; - $settings = json_decode($row->settings, true); - if (!is_array($settings)) { - $settings = @unserialize($row->settings); // check for old serialized setting - } - if (!is_array($settings)) { - $settings = array(); - } - $consumer->setSettings($settings); - $consumer->protected = (intval($row->protected) === 1); - $consumer->enabled = (intval($row->enabled) === 1); - $consumer->enableFrom = null; - if (!is_null($row->enable_from)) { - $consumer->enableFrom = strtotime($row->enable_from); - } - $consumer->enableUntil = null; - if (!is_null($row->enable_until)) { - $consumer->enableUntil = strtotime($row->enable_until); - } - $consumer->lastAccess = null; - if (!is_null($row->last_access)) { - $consumer->lastAccess = strtotime($row->last_access); - } - $consumer->created = strtotime($row->created); - $consumer->updated = strtotime($row->updated); - $ok = true; - break; + $row = mysql_fetch_object($rsConsumer); + if ($row) { + $platform->setRecordId(intval($row->consumer_pk)); + $platform->name = $row->name; + $platform->setkey($row->consumer_key); + $platform->secret = $row->secret; + $platform->platformId = $row->platform_id; + $platform->clientId = $row->client_id; + $platform->deploymentId = $row->deployment_id; + $platform->rsaKey = $row->public_key; + $platform->ltiVersion = $row->lti_version; + $platform->signatureMethod = $row->signature_method; + $platform->consumerName = $row->consumer_name; + $platform->consumerVersion = $row->consumer_version; + $platform->consumerGuid = $row->consumer_guid; + $platform->profile = json_decode($row->profile); + $platform->toolProxy = $row->tool_proxy; + $settings = json_decode($row->settings, true); + if (!is_array($settings)) { + $settings = @unserialize($row->settings); // check for old serialized setting } + if (!is_array($settings)) { + $settings = array(); + } + $platform->setSettings($settings); + $platform->protected = (intval($row->protected) === 1); + $platform->enabled = (intval($row->enabled) === 1); + $platform->enableFrom = null; + if (!is_null($row->enable_from)) { + $platform->enableFrom = strtotime($row->enable_from); + } + $platform->enableUntil = null; + if (!is_null($row->enable_until)) { + $platform->enableUntil = strtotime($row->enable_until); + } + $platform->lastAccess = null; + if (!is_null($row->last_access)) { + $platform->lastAccess = strtotime($row->last_access); + } + $platform->created = strtotime($row->created); + $platform->updated = strtotime($row->updated); + $this->fixPlatformSettings($platform, false); + $ok = true; } - mysql_free_result($rsConsumer); } return $ok; } /** - * Save tool consumer object. + * Save platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully saved + * @return bool True if the platform object was successfully saved */ - public function saveToolConsumer($consumer) + public function savePlatform($platform) { - $id = $consumer->getRecordId(); - $key = $consumer->getKey(); - $key256 = static::getConsumerKey($key); - if ($key === $key256) { - $key = null; - } - $protected = ($consumer->protected) ? 1 : 0; - $enabled = ($consumer->enabled) ? 1 : 0; - $profile = (!empty($consumer->profile)) ? json_encode($consumer->profile) : null; - $settingsValue = json_encode($consumer->getSettings()); + $id = $platform->getRecordId(); + $protected = ($platform->protected) ? 1 : 0; + $enabled = ($platform->enabled) ? 1 : 0; + $profile = (!empty($platform->profile)) ? json_encode($platform->profile) : null; + $this->fixPlatformSettings($platform, true); + $settingsValue = json_encode($platform->getSettings()); + $this->fixPlatformSettings($platform, false); $time = time(); $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $from = null; - if (!is_null($consumer->enableFrom)) { - $from = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableFrom); + if (!is_null($platform->enableFrom)) { + $from = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableFrom); } $until = null; - if (!is_null($consumer->enableUntil)) { - $until = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableUntil); + if (!is_null($platform->enableUntil)) { + $until = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableUntil); } $last = null; - if (!is_null($consumer->lastAccess)) { - $last = date($this->dateFormat, $consumer->lastAccess); + if (!is_null($platform->lastAccess)) { + $last = date($this->dateFormat, $platform->lastAccess); } if (empty($id)) { - $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' (consumer_key256, consumer_key, name, ' . - 'secret, lti_version, signature_method, consumer_name, consumer_version, consumer_guid, profile, ' . - 'tool_proxy, settings, protected, enabled, ' . + $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' (consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated) ' . - 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %d, %d, %s, %s, %s, %s, %s)', $this->escape($key256), - $this->escape($key), $this->escape($consumer->name), $this->escape($consumer->secret), - $this->escape($consumer->ltiVersion), $this->escape($consumer->signatureMethod), - $this->escape($consumer->consumerName), $this->escape($consumer->consumerVersion), - $this->escape($consumer->consumerGuid), $this->escape($profile), $this->escape($consumer->toolProxy), + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %d, %d, %s, %s, %s, %s, %s)', + $this->escape($platform->getKey()), $this->escape($platform->name), $this->escape($platform->secret), + $this->escape($platform->platformId), $this->escape($platform->clientId), $this->escape($platform->deploymentId), + $this->escape($platform->rsaKey), $this->escape($platform->ltiVersion), $this->escape($platform->signatureMethod), + $this->escape($platform->consumerName), $this->escape($platform->consumerVersion), + $this->escape($platform->consumerGuid), $this->escape($profile), $this->escape($platform->toolProxy), $this->escape($settingsValue), $protected, $enabled, $this->escape($from), $this->escape($until), $this->escape($last), $this->escape($now), $this->escape($now)); } else { - $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' SET ' . - 'consumer_key256 = %s, consumer_key = %s, ' . - 'name = %s, secret= %s, lti_version = %s, signature_method = %s, consumer_name = %s, consumer_version = %s, consumer_guid = %s, ' . + $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' SET ' . + 'consumer_key = %s, name = %s, secret= %s, ' . + 'platform_id = %s, client_id = %s, deployment_id = %s, public_key = %s, ' . + 'lti_version = %s, signature_method = %s, ' . + 'consumer_name = %s, consumer_version = %s, consumer_guid = %s, ' . 'profile = %s, tool_proxy = %s, settings = %s, ' . 'protected = %d, enabled = %d, enable_from = %s, enable_until = %s, last_access = %s, updated = %s ' . - 'WHERE consumer_pk = %d', $this->escape($key256), $this->escape($key), $this->escape($consumer->name), - $this->escape($consumer->secret), $this->escape($consumer->ltiVersion), $this->escape($consumer->signatureMethod), - $this->escape($consumer->consumerName), $this->escape($consumer->consumerVersion), - $this->escape($consumer->consumerGuid), $this->escape($profile), $this->escape($consumer->toolProxy), - $this->escape($settingsValue), $protected, $enabled, $this->escape($from), $this->escape($until), - $this->escape($last), $this->escape($now), $consumer->getRecordId()); + 'WHERE consumer_pk = %d', $this->escape($platform->getKey()), $this->escape($platform->name), + $this->escape($platform->secret), $this->escape($platform->platformId), $this->escape($platform->clientId), + $this->escape($platform->deploymentId), $this->escape($platform->rsaKey), $this->escape($platform->ltiVersion), + $this->escape($platform->signatureMethod), $this->escape($platform->consumerName), + $this->escape($platform->consumerVersion), $this->escape($platform->consumerGuid), $this->escape($profile), + $this->escape($platform->toolProxy), $this->escape($settingsValue), $protected, $enabled, $this->escape($from), + $this->escape($until), $this->escape($last), $this->escape($now), $platform->getRecordId()); } $ok = $this->executeQuery($sql); if ($ok) { if (empty($id)) { - $consumer->setRecordId(mysql_insert_id()); - $consumer->created = $time; + $platform->setRecordId(mysql_insert_id()); + $platform->created = $time; } - $consumer->updated = $time; + $platform->updated = $time; } return $ok; } /** - * Delete tool consumer object. + * Delete platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully deleted + * @return bool True if the platform object was successfully deleted */ - public function deleteToolConsumer($consumer) + public function deletePlatform($platform) { +// Delete any access token value for this consumer + $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' WHERE consumer_pk = %d', + $platform->getRecordId()); + $this->executeQuery($sql); + // Delete any nonce values for this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE consumer_pk = %d', - $consumer->getRecordId()); + $platform->getRecordId()); $this->executeQuery($sql); // Delete any outstanding share keys for resource links for this consumer $sql = sprintf('DELETE sk ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' sk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON sk.resource_link_pk = rl.resource_link_pk ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any outstanding share keys for resource links for contexts in this consumer @@ -201,14 +257,14 @@ public function deleteToolConsumer($consumer) "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' sk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON sk.resource_link_pk = rl.resource_link_pk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any users in resource links for this consumer $sql = sprintf('DELETE u ' . "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' u ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON u.resource_link_pk = rl.resource_link_pk ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any users in resource links for contexts in this consumer @@ -216,14 +272,14 @@ public function deleteToolConsumer($consumer) "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' u ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON u.resource_link_pk = rl.resource_link_pk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Update any resource links for which this consumer is acting as a primary resource link $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' prl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON prl.primary_resource_link_pk = rl.resource_link_pk ' . 'SET prl.primary_resource_link_pk = NULL, prl.share_approved = NULL ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); // Update any resource links for contexts in which this consumer is acting as a primary resource link @@ -231,71 +287,76 @@ public function deleteToolConsumer($consumer) "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON prl.primary_resource_link_pk = rl.resource_link_pk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . 'SET prl.primary_resource_link_pk = NULL, prl.share_approved = NULL ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); // Delete any resource links for this consumer $sql = sprintf('DELETE rl ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any resource links for contexts in this consumer $sql = sprintf('DELETE rl ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any contexts for this consumer $sql = sprintf('DELETE c ' . "FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete consumer $sql = sprintf('DELETE c ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' c ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' c ' . + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); if ($ok) { - $consumer->initialize(); + $platform->initialize(); } return $ok; } /** - * Load all tool consumers from the database. + * Load all platforms from the database. * - * @return ToolConsumer[] An array of the ToolConsumer objects + * @return Platform[] An array of the Platform objects */ - public function getToolConsumers() + public function getPlatforms() { $consumers = array(); - $sql = 'SELECT consumer_pk, consumer_key256, consumer_key, name, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = 'SELECT consumer_pk, consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, ' . 'protected, enabled, enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'ORDER BY name'; $rsConsumers = $this->executeQuery($sql); if ($rsConsumers) { while ($row = mysql_fetch_object($rsConsumers)) { - $key = empty($row->consumer_key) ? $row->consumer_key256 : $row->consumer_key; - $consumer = new ToolConsumer($key, $this); - $consumer->setRecordId(intval($row->consumer_pk)); - $consumer->name = $row->name; - $consumer->secret = $row->secret; - $consumer->ltiVersion = $row->lti_version; - $consumer->signatureMethod = $row->signature_method; - $consumer->consumerName = $row->consumer_name; - $consumer->consumerVersion = $row->consumer_version; - $consumer->consumerGuid = $row->consumer_guid; - $consumer->profile = json_decode($row->profile); - $consumer->toolProxy = $row->tool_proxy; + $platform = new Platform($this); + $platform->setRecordId(intval($row->consumer_pk)); + $platform->name = $row->name; + $platform->setKey($row->consumer_key); + $platform->secret = $row->secret; + $platform->platformId = $row->platform_id; + $platform->clientId = $row->client_id; + $platform->deploymentId = $row->deployment_id; + $platform->rsaKey = $row->public_key; + $platform->ltiVersion = $row->lti_version; + $platform->signatureMethod = $row->signature_method; + $platform->consumerName = $row->consumer_name; + $platform->consumerVersion = $row->consumer_version; + $platform->consumerGuid = $row->consumer_guid; + $platform->profile = json_decode($row->profile); + $platform->toolProxy = $row->tool_proxy; $settings = json_decode($row->settings, true); if (!is_array($settings)) { $settings = @unserialize($row->settings); // check for old serialized setting @@ -303,24 +364,25 @@ public function getToolConsumers() if (!is_array($settings)) { $settings = array(); } - $consumer->setSettings($settings); - $consumer->protected = (intval($row->protected) === 1); - $consumer->enabled = (intval($row->enabled) === 1); - $consumer->enableFrom = null; + $platform->setSettings($settings); + $platform->protected = (intval($row->protected) === 1); + $platform->enabled = (intval($row->enabled) === 1); + $platform->enableFrom = null; if (!is_null($row->enable_from)) { - $consumer->enableFrom = strtotime($row->enable_from); + $platform->enableFrom = strtotime($row->enable_from); } - $consumer->enableUntil = null; + $platform->enableUntil = null; if (!is_null($row->enable_until)) { - $consumer->enableUntil = strtotime($row->enable_until); + $platform->enableUntil = strtotime($row->enable_until); } - $consumer->lastAccess = null; + $platform->lastAccess = null; if (!is_null($row->last_access)) { - $consumer->lastAccess = strtotime($row->last_access); + $platform->lastAccess = strtotime($row->last_access); } - $consumer->created = strtotime($row->created); - $consumer->updated = strtotime($row->updated); - $consumers[] = $consumer; + $platform->created = strtotime($row->created); + $platform->updated = strtotime($row->updated); + $this->fixPlatformSettings($platform, false); + $consumers[] = $platform; } mysql_free_result($rsConsumers); } @@ -349,15 +411,15 @@ public function loadContext($context) } else { $sql = sprintf('SELECT context_pk, consumer_pk, title, lti_context_id, type, settings, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' ' . - 'WHERE (consumer_pk = %d) AND (lti_context_id = %s)', $context->getConsumer()->getRecordId(), + 'WHERE (consumer_pk = %d) AND (lti_context_id = %s)', $context->getPlatform()->getRecordId(), $this->escape($context->ltiContextId)); } - $rs_context = $this->executeQuery($sql); - if ($rs_context) { - $row = mysql_fetch_object($rs_context); + $rsContext = $this->executeQuery($sql); + if ($rsContext) { + $row = mysql_fetch_object($rsContext); if ($row) { $context->setRecordId(intval($row->context_pk)); - $context->setConsumerId(intval($row->consumer_pk)); + $context->setPlatformId(intval($row->consumer_pk)); $context->title = $row->title; $context->ltiContextId = $row->lti_context_id; $context->type = $row->type; @@ -391,7 +453,7 @@ public function saveContext($context) $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $settingsValue = json_encode($context->getSettings()); $id = $context->getRecordId(); - $consumer_pk = $context->getConsumer()->getRecordId(); + $consumer_pk = $context->getPlatform()->getRecordId(); if (empty($id)) { $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' (consumer_pk, title, ' . 'lti_context_id, type, settings, created, updated) ' . @@ -497,7 +559,7 @@ public function loadResourceLink($resourceLink) "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' r LEFT OUTER JOIN ' . $this->dbTableNamePrefix . static::CONTEXT_TABLE_NAME . ' c ON r.context_pk = c.context_pk ' . ' WHERE ((r.consumer_pk = %d) OR (c.consumer_pk = %d)) AND (lti_resource_link_id = %s)', - $resourceLink->getConsumer()->getRecordId(), $resourceLink->getConsumer()->getRecordId(), + $resourceLink->getPlatform()->getRecordId(), $resourceLink->getPlatform()->getRecordId(), $this->escape($resourceLink->getId())); } $rsResourceLink = $this->executeQuery($sql); @@ -511,9 +573,9 @@ public function loadResourceLink($resourceLink) $resourceLink->setContextId(null); } if (!is_null($row->consumer_pk)) { - $resourceLink->setConsumerId(intval($row->consumer_pk)); + $resourceLink->setPlatformId(intval($row->consumer_pk)); } else { - $resourceLink->setConsumerId(null); + $resourceLink->setPlatformId(null); } $resourceLink->title = $row->title; $resourceLink->ltiResourceLinkId = $row->lti_resource_link_id; @@ -571,7 +633,7 @@ public function saveResourceLink($resourceLink) $consumerId = 'NULL'; $contextId = strval($resourceLink->getContextId()); } else { - $consumerId = strval($resourceLink->getConsumer()->getRecordId()); + $consumerId = strval($resourceLink->getPlatform()->getRecordId()); $contextId = 'NULL'; } $id = $resourceLink->getRecordId(); @@ -710,20 +772,20 @@ public function getSharesResourceLink($resourceLink) $sql = sprintf('SELECT c.consumer_name, r.resource_link_pk, r.title, r.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' AS r ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' AS c ON r.consumer_pk = c.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' AS c ON r.consumer_pk = c.consumer_pk ' . 'WHERE (r.primary_resource_link_pk = %d) ' . 'UNION ' . 'SELECT c2.consumer_name, r2.resource_link_pk, r2.title, r2.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' AS r2 ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' AS x ON r2.context_pk = x.context_pk ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' AS c2 ON x.consumer_pk = c2.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' AS c2 ON x.consumer_pk = c2.consumer_pk ' . 'WHERE (r2.primary_resource_link_pk = %d) ' . 'ORDER BY consumer_name, title', $resourceLink->getRecordId(), $resourceLink->getRecordId()); $rsShare = $this->executeQuery($sql); if ($rsShare) { while ($row = mysql_fetch_object($rsShare)) { $share = new LTI\ResourceLinkShare(); - $share->consumer_name = $row->consumer_name; + $share->consumerName = $row->consumer_name; $share->resourceLinkId = intval($row->resource_link_pk); $share->title = $row->title; $share->approved = (intval($row->share_approved) === 1); @@ -735,17 +797,17 @@ public function getSharesResourceLink($resourceLink) } ### -### ConsumerNonce methods +### PlatformNonce methods ### /** * Load nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully loaded */ - public function loadConsumerNonce($nonce) + public function loadPlatformNonce($nonce) { $ok = false; @@ -756,10 +818,10 @@ public function loadConsumerNonce($nonce) // Load the nonce $sql = sprintf("SELECT value AS T FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE (consumer_pk = %d) AND (value = %s)', - $nonce->getConsumer()->getRecordId(), $this->escape($nonce->getValue())); - $rs_nonce = $this->executeQuery($sql, false); - if ($rs_nonce) { - $row = mysql_fetch_object($rs_nonce); + $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue())); + $rsNonce = $this->executeQuery($sql, false); + if ($rsNonce) { + $row = mysql_fetch_object($rsNonce); if ($row !== false) { $ok = true; } @@ -771,15 +833,100 @@ public function loadConsumerNonce($nonce) /** * Save nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully saved */ - public function saveConsumerNonce($nonce) + public function savePlatformNonce($nonce) { $expires = date("{$this->dateFormat} {$this->timeFormat}", $nonce->expires); $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . " (consumer_pk, value, expires) VALUES (%d, %s, %s)", - $nonce->getConsumer()->getRecordId(), $this->escape($nonce->getValue()), $this->escape($expires)); + $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue()), $this->escape($expires)); + $ok = $this->executeQuery($sql); + + return $ok; + } + + /** + * Delete nonce object. + * + * @param PlatformNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully deleted + */ + public function deletePlatformNonce($nonce) + { + $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = %d) AND (value = %s)', $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue())); + $ok = $this->executeQuery($sql); + + return $ok; + } + +### +### AccessToken methods +### + + /** + * Load access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the nonce object was successfully loaded + */ + public function loadAccessToken($accessToken) + { + $ok = false; + + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $sql = sprintf('SELECT scopes, token, expires, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = %d)', $consumer_pk); + $rsAccessToken = $this->executeQuery($sql, false); + if ($rsAccessToken) { + $row = mysql_fetch_object($rsAccessToken); + if ($row) { + $scopes = json_decode($row->scopes, true); + if (!is_array($scopes)) { + $scopes = array(); + } + $accessToken->scopes = $scopes; + $accessToken->token = $row->token; + $accessToken->expires = strtotime($row->expires); + $accessToken->created = strtotime($row->created); + $accessToken->updated = strtotime($row->updated); + $ok = true; + } + } + + return $ok; + } + + /** + * Save access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the access token object was successfully saved + */ + public function saveAccessToken($accessToken) + { + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $scopes = json_encode($accessToken->scopes, JSON_UNESCAPED_SLASHES); + $token = $accessToken->token; + $expires = date("{$this->dateFormat} {$this->timeFormat}", $accessToken->expires); + $time = time(); + $now = date("{$this->dateFormat} {$this->timeFormat}", $time); + if (empty($accessToken->created)) { + $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + '(consumer_pk, scopes, token, expires, created, updated) ' . + 'VALUES (%d, %s, %s, %s, %s, %s)', $consumer_pk, $this->escape($scopes), $this->escape($token), + $this->escape($expires), $this->escape($now), $this->escape($now)); + } else { + $sql = sprintf('UPDATE ' . $this->dbTableNamePrefix . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'SET scopes = %s, token = %s, expires = %s, updated = %s WHERE consumer_pk = %d', $this->escape($scopes), + $this->escape($token), $this->escape($expires), $this->escape($now), $consumer_pk); + } $ok = $this->executeQuery($sql); return $ok; @@ -889,11 +1036,11 @@ public function loadUserResult($userresult) $sql = sprintf('SELECT user_result_pk, resource_link_pk, lti_user_id, lti_result_sourcedid, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'WHERE (resource_link_pk = %d) AND (lti_user_id = %s)', $userresult->getResourceLink()->getRecordId(), - $this->escape($userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY))); + $this->escape($userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY))); } - $rsUser = $this->executeQuery($sql); - if ($rsUser) { - $row = mysql_fetch_object($rsUser); + $rsUserResult = $this->executeQuery($sql); + if ($rsUserResult) { + $row = mysql_fetch_object($rsUserResult); if ($row) { $userresult->setRecordId(intval($row->user_result_pk)); $userresult->setResourceLinkId(intval($row->resource_link_pk)); @@ -923,12 +1070,12 @@ public function saveUserResult($userresult) $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' (resource_link_pk, ' . 'lti_user_id, lti_result_sourcedid, created, updated) ' . 'VALUES (%d, %s, %s, %s, %s)', $userresult->getResourceLink()->getRecordId(), - $this->escape($userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY)), - $this->escape($userresult->ltiResultSourcedId), $this->escape($now), $this->escape($now)); + $this->escape($userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY)), $this->escape($userresult->ltiResultSourcedId), + $this->escape($now), $this->escape($now)); } else { $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'SET lti_user_id = %s, lti_result_sourcedid = %s, updated = %s ' . - 'WHERE (user_result_pk = %d)', $this->escape($userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY)), + 'WHERE (user_result_pk = %d)', $this->escape($userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY)), $this->escape($userresult->ltiResultSourcedId), $this->escape($now), $userresult->getRecordId()); } $ok = $this->executeQuery($sql); diff --git a/src/DataConnector/DataConnector_mysqli.php b/src/DataConnector/DataConnector_mysqli.php index 1c5fb8d..ffd10e6 100644 --- a/src/DataConnector/DataConnector_mysqli.php +++ b/src/DataConnector/DataConnector_mysqli.php @@ -3,12 +3,12 @@ namespace ceLTIc\LTI\DataConnector; use ceLTIc\LTI; -use ceLTIc\LTI\ConsumerNonce; +use ceLTIc\LTI\PlatformNonce; use ceLTIc\LTI\Context; use ceLTIc\LTI\ResourceLink; use ceLTIc\LTI\ResourceLinkShare; use ceLTIc\LTI\ResourceLinkShareKey; -use ceLTIc\LTI\ToolConsumer; +use ceLTIc\LTI\Platform; use ceLTIc\LTI\UserResult; use ceLTIc\LTI\Util; @@ -27,173 +27,212 @@ class DataConnector_mysqli extends DataConnector { ### -### ToolConsumer methods +### Platform methods ### /** - * Load tool consumer object. + * Load platform object. * - * @param ToolConsumer $consumer ToolConsumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully loaded + * @return bool True if the platform object was successfully loaded */ - public function loadToolConsumer($consumer) + public function loadPlatform($platform) { $ok = false; - if (!is_null($consumer->getRecordId())) { - $sql = sprintf('SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + if (!is_null($platform->getRecordId())) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - "WHERE consumer_pk = %d", $consumer->getRecordId()); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE consumer_pk = %d', $platform->getRecordId()); + } elseif (!empty($platform->platformId)) { + if (empty($platform->clientId)) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) ' . + 'GROUP BY platform_id, client_id', $this->escape($platform->platformId)); + } elseif (empty($platform->deploymentId)) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) AND (client_id = %s)', $this->escape($platform->platformId), + $this->escape($platform->clientId)); + } else { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id,client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) AND (client_id = %s) AND (deployment_id = %s)', $this->escape($platform->platformId), + $this->escape($platform->clientId), $this->escape($platform->deploymentId)); + } } else { - $key256 = static::getConsumerKey($consumer->getKey()); - $sql = sprintf('SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - "WHERE consumer_key256 = %s", $this->escape($key256)); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE consumer_key = %s', $this->escape($platform->getKey())); } $rsConsumer = $this->executeQuery($sql); if ($rsConsumer) { - while ($row = mysqli_fetch_object($rsConsumer)) { - if (empty($key256) || empty($row->consumer_key) || ($consumer->getKey() === $row->consumer_key)) { - $consumer->setRecordId(intval($row->consumer_pk)); - $consumer->name = $row->name; - $consumer->setkey(empty($row->consumer_key) ? $row->consumer_key256 : $row->consumer_key); - $consumer->secret = $row->secret; - $consumer->ltiVersion = $row->lti_version; - $consumer->signatureMethod = $row->signature_method; - $consumer->consumerName = $row->consumer_name; - $consumer->consumerVersion = $row->consumer_version; - $consumer->consumerGuid = $row->consumer_guid; - $consumer->profile = json_decode($row->profile); - $consumer->toolProxy = $row->tool_proxy; - $settings = json_decode($row->settings, true); - if (!is_array($settings)) { - $settings = @unserialize($row->settings); // check for old serialized setting - } - if (!is_array($settings)) { - $settings = array(); - } - $consumer->setSettings($settings); - $consumer->protected = (intval($row->protected) === 1); - $consumer->enabled = (intval($row->enabled) === 1); - $consumer->enableFrom = null; - if (!is_null($row->enable_from)) { - $consumer->enableFrom = strtotime($row->enable_from); - } - $consumer->enableUntil = null; - if (!is_null($row->enable_until)) { - $consumer->enableUntil = strtotime($row->enable_until); - } - $consumer->lastAccess = null; - if (!is_null($row->last_access)) { - $consumer->lastAccess = strtotime($row->last_access); - } - $consumer->created = strtotime($row->created); - $consumer->updated = strtotime($row->updated); - $ok = true; - break; + $row = mysqli_fetch_object($rsConsumer); + if ($row) { + $platform->setRecordId(intval($row->consumer_pk)); + $platform->name = $row->name; + $platform->setkey($row->consumer_key); + $platform->secret = $row->secret; + $platform->platformId = $row->platform_id; + $platform->clientId = $row->client_id; + $platform->deploymentId = $row->deployment_id; + $platform->rsaKey = $row->public_key; + $platform->ltiVersion = $row->lti_version; + $platform->signatureMethod = $row->signature_method; + $platform->consumerName = $row->consumer_name; + $platform->consumerVersion = $row->consumer_version; + $platform->consumerGuid = $row->consumer_guid; + $platform->profile = json_decode($row->profile); + $platform->toolProxy = $row->tool_proxy; + $settings = json_decode($row->settings, true); + if (!is_array($settings)) { + $settings = @unserialize($row->settings); // check for old serialized setting } + if (!is_array($settings)) { + $settings = array(); + } + $platform->setSettings($settings); + $platform->protected = (intval($row->protected) === 1); + $platform->enabled = (intval($row->enabled) === 1); + $platform->enableFrom = null; + if (!is_null($row->enable_from)) { + $platform->enableFrom = strtotime($row->enable_from); + } + $platform->enableUntil = null; + if (!is_null($row->enable_until)) { + $platform->enableUntil = strtotime($row->enable_until); + } + $platform->lastAccess = null; + if (!is_null($row->last_access)) { + $platform->lastAccess = strtotime($row->last_access); + } + $platform->created = strtotime($row->created); + $platform->updated = strtotime($row->updated); + $this->fixPlatformSettings($platform, false); + $ok = true; } - mysqli_free_result($rsConsumer); } return $ok; } /** - * Save tool consumer object. + * Save platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully saved + * @return bool True if the platform object was successfully saved */ - public function saveToolConsumer($consumer) + public function savePlatform($platform) { - $id = $consumer->getRecordId(); - $key = $consumer->getKey(); - $key256 = static::getConsumerKey($key); - if ($key === $key256) { - $key = null; - } - $protected = ($consumer->protected) ? 1 : 0; - $enabled = ($consumer->enabled) ? 1 : 0; - $profile = (!empty($consumer->profile)) ? json_encode($consumer->profile) : null; - $settingsValue = json_encode($consumer->getSettings()); + $id = $platform->getRecordId(); + $protected = ($platform->protected) ? 1 : 0; + $enabled = ($platform->enabled) ? 1 : 0; + $profile = (!empty($platform->profile)) ? json_encode($platform->profile) : null; + $this->fixPlatformSettings($platform, true); + $settingsValue = json_encode($platform->getSettings()); + $this->fixPlatformSettings($platform, false); $time = time(); $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $from = null; - if (!is_null($consumer->enableFrom)) { - $from = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableFrom); + if (!is_null($platform->enableFrom)) { + $from = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableFrom); } $until = null; - if (!is_null($consumer->enableUntil)) { - $until = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableUntil); + if (!is_null($platform->enableUntil)) { + $until = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableUntil); } $last = null; - if (!is_null($consumer->lastAccess)) { - $last = date($this->dateFormat, $consumer->lastAccess); + if (!is_null($platform->lastAccess)) { + $last = date($this->dateFormat, $platform->lastAccess); } if (empty($id)) { - $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' (consumer_key256, consumer_key, name, ' . - 'secret, lti_version, signature_method, consumer_name, consumer_version, consumer_guid, profile, ' . - 'tool_proxy, settings, protected, enabled, ' . + $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' (consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated) ' . - 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %d, %d, %s, %s, %s, %s, %s)', $this->escape($key256), - $this->escape($key), $this->escape($consumer->name), $this->escape($consumer->secret), - $this->escape($consumer->ltiVersion), $this->escape($consumer->signatureMethod), - $this->escape($consumer->consumerName), $this->escape($consumer->consumerVersion), - $this->escape($consumer->consumerGuid), $this->escape($profile), $this->escape($consumer->toolProxy), + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %d, %d, %s, %s, %s, %s, %s)', + $this->escape($platform->getKey()), $this->escape($platform->name), $this->escape($platform->secret), + $this->escape($platform->platformId), $this->escape($platform->clientId), $this->escape($platform->deploymentId), + $this->escape($platform->rsaKey), $this->escape($platform->ltiVersion), $this->escape($platform->signatureMethod), + $this->escape($platform->consumerName), $this->escape($platform->consumerVersion), + $this->escape($platform->consumerGuid), $this->escape($profile), $this->escape($platform->toolProxy), $this->escape($settingsValue), $protected, $enabled, $this->escape($from), $this->escape($until), $this->escape($last), $this->escape($now), $this->escape($now)); } else { - $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' SET ' . - 'consumer_key256 = %s, consumer_key = %s, ' . - 'name = %s, secret= %s, lti_version = %s, signature_method = %s, consumer_name = %s, consumer_version = %s, consumer_guid = %s, ' . + $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' SET ' . + 'consumer_key = %s, name = %s, secret= %s, ' . + 'platform_id = %s, client_id = %s, deployment_id = %s, public_key = %s, ' . + 'lti_version = %s, signature_method = %s, ' . + 'consumer_name = %s, consumer_version = %s, consumer_guid = %s, ' . 'profile = %s, tool_proxy = %s, settings = %s, ' . 'protected = %d, enabled = %d, enable_from = %s, enable_until = %s, last_access = %s, updated = %s ' . - 'WHERE consumer_pk = %d', $this->escape($key256), $this->escape($key), $this->escape($consumer->name), - $this->escape($consumer->secret), $this->escape($consumer->ltiVersion), $this->escape($consumer->signatureMethod), - $this->escape($consumer->consumerName), $this->escape($consumer->consumerVersion), - $this->escape($consumer->consumerGuid), $this->escape($profile), $this->escape($consumer->toolProxy), - $this->escape($settingsValue), $protected, $enabled, $this->escape($from), $this->escape($until), - $this->escape($last), $this->escape($now), $consumer->getRecordId()); + 'WHERE consumer_pk = %d', $this->escape($platform->getKey()), $this->escape($platform->name), + $this->escape($platform->secret), $this->escape($platform->platformId), $this->escape($platform->clientId), + $this->escape($platform->deploymentId), $this->escape($platform->rsaKey), $this->escape($platform->ltiVersion), + $this->escape($platform->signatureMethod), $this->escape($platform->consumerName), + $this->escape($platform->consumerVersion), $this->escape($platform->consumerGuid), $this->escape($profile), + $this->escape($platform->toolProxy), $this->escape($settingsValue), $protected, $enabled, $this->escape($from), + $this->escape($until), $this->escape($last), $this->escape($now), $platform->getRecordId()); } $ok = $this->executeQuery($sql); if ($ok) { if (empty($id)) { - $consumer->setRecordId(mysqli_insert_id($this->db)); - $consumer->created = $time; + $platform->setRecordId(mysqli_insert_id($this->db)); + $platform->created = $time; } - $consumer->updated = $time; + $platform->updated = $time; } return $ok; } /** - * Delete tool consumer object. + * Delete platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully deleted + * @return bool True if the platform object was successfully deleted */ - public function deleteToolConsumer($consumer) + public function deletePlatform($platform) { +// Delete any access token value for this consumer + $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' WHERE consumer_pk = %d', + $platform->getRecordId()); + $this->executeQuery($sql); + // Delete any nonce values for this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE consumer_pk = %d', - $consumer->getRecordId()); + $platform->getRecordId()); $this->executeQuery($sql); // Delete any outstanding share keys for resource links for this consumer $sql = sprintf('DELETE sk ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' sk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON sk.resource_link_pk = rl.resource_link_pk ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any outstanding share keys for resource links for contexts in this consumer @@ -201,14 +240,14 @@ public function deleteToolConsumer($consumer) "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' sk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON sk.resource_link_pk = rl.resource_link_pk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any users in resource links for this consumer $sql = sprintf('DELETE u ' . "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' u ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON u.resource_link_pk = rl.resource_link_pk ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any users in resource links for contexts in this consumer @@ -216,14 +255,14 @@ public function deleteToolConsumer($consumer) "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' u ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON u.resource_link_pk = rl.resource_link_pk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Update any resource links for which this consumer is acting as a primary resource link $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' prl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON prl.primary_resource_link_pk = rl.resource_link_pk ' . 'SET prl.primary_resource_link_pk = NULL, prl.share_approved = NULL ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); // Update any resource links for contexts in which this consumer is acting as a primary resource link @@ -231,71 +270,76 @@ public function deleteToolConsumer($consumer) "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON prl.primary_resource_link_pk = rl.resource_link_pk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . 'SET prl.primary_resource_link_pk = NULL, prl.share_approved = NULL ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); // Delete any resource links for this consumer $sql = sprintf('DELETE rl ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any resource links for contexts in this consumer $sql = sprintf('DELETE rl ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any contexts for this consumer $sql = sprintf('DELETE c ' . "FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete consumer $sql = sprintf('DELETE c ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' c ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' c ' . + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); if ($ok) { - $consumer->initialize(); + $platform->initialize(); } return $ok; } /** - * Load all tool consumers from the database. + * Load all platforms from the database. * - * @return ToolConsumer[] An array of the ToolConsumer objects + * @return Platform[] An array of the Platform objects */ - public function getToolConsumers() + public function getPlatforms() { $consumers = array(); - $sql = 'SELECT consumer_pk, consumer_key256, consumer_key, name, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = 'SELECT consumer_pk, consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, ' . 'protected, enabled, enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'ORDER BY name'; $rsConsumers = $this->executeQuery($sql); if ($rsConsumers) { while ($row = mysqli_fetch_object($rsConsumers)) { - $key = empty($row->consumer_key) ? $row->consumer_key256 : $row->consumer_key; - $consumer = new ToolConsumer($key, $this); - $consumer->setRecordId(intval($row->consumer_pk)); - $consumer->name = $row->name; - $consumer->secret = $row->secret; - $consumer->ltiVersion = $row->lti_version; - $consumer->signatureMethod = $row->signature_method; - $consumer->consumerName = $row->consumer_name; - $consumer->consumerVersion = $row->consumer_version; - $consumer->consumerGuid = $row->consumer_guid; - $consumer->profile = json_decode($row->profile); - $consumer->toolProxy = $row->tool_proxy; + $platform = new Platform($this); + $platform->setRecordId(intval($row->consumer_pk)); + $platform->name = $row->name; + $platform->setKey($row->consumer_key); + $platform->secret = $row->secret; + $platform->platformId = $row->platform_id; + $platform->clientId = $row->client_id; + $platform->deploymentId = $row->deployment_id; + $platform->rsaKey = $row->public_key; + $platform->ltiVersion = $row->lti_version; + $platform->signatureMethod = $row->signature_method; + $platform->consumerName = $row->consumer_name; + $platform->consumerVersion = $row->consumer_version; + $platform->consumerGuid = $row->consumer_guid; + $platform->profile = json_decode($row->profile); + $platform->toolProxy = $row->tool_proxy; $settings = json_decode($row->settings, true); if (!is_array($settings)) { $settings = @unserialize($row->settings); // check for old serialized setting @@ -303,24 +347,25 @@ public function getToolConsumers() if (!is_array($settings)) { $settings = array(); } - $consumer->setSettings($settings); - $consumer->protected = (intval($row->protected) === 1); - $consumer->enabled = (intval($row->enabled) === 1); - $consumer->enableFrom = null; + $platform->setSettings($settings); + $platform->protected = (intval($row->protected) === 1); + $platform->enabled = (intval($row->enabled) === 1); + $platform->enableFrom = null; if (!is_null($row->enable_from)) { - $consumer->enableFrom = strtotime($row->enable_from); + $platform->enableFrom = strtotime($row->enable_from); } - $consumer->enableUntil = null; + $platform->enableUntil = null; if (!is_null($row->enable_until)) { - $consumer->enableUntil = strtotime($row->enable_until); + $platform->enableUntil = strtotime($row->enable_until); } - $consumer->lastAccess = null; + $platform->lastAccess = null; if (!is_null($row->last_access)) { - $consumer->lastAccess = strtotime($row->last_access); + $platform->lastAccess = strtotime($row->last_access); } - $consumer->created = strtotime($row->created); - $consumer->updated = strtotime($row->updated); - $consumers[] = $consumer; + $platform->created = strtotime($row->created); + $platform->updated = strtotime($row->updated); + $this->fixPlatformSettings($platform, false); + $consumers[] = $platform; } mysqli_free_result($rsConsumers); } @@ -349,15 +394,15 @@ public function loadContext($context) } else { $sql = sprintf('SELECT context_pk, consumer_pk, title, lti_context_id, type, settings, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' ' . - 'WHERE (consumer_pk = %d) AND (lti_context_id = %s)', $context->getConsumer()->getRecordId(), + 'WHERE (consumer_pk = %d) AND (lti_context_id = %s)', $context->getPlatform()->getRecordId(), $this->escape($context->ltiContextId)); } - $rs_context = $this->executeQuery($sql); - if ($rs_context) { - $row = mysqli_fetch_object($rs_context); + $rsContext = $this->executeQuery($sql); + if ($rsContext) { + $row = mysqli_fetch_object($rsContext); if ($row) { $context->setRecordId(intval($row->context_pk)); - $context->setConsumerId(intval($row->consumer_pk)); + $context->setPlatformId(intval($row->consumer_pk)); $context->title = $row->title; $context->ltiContextId = $row->lti_context_id; $context->type = $row->type; @@ -391,7 +436,7 @@ public function saveContext($context) $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $settingsValue = json_encode($context->getSettings()); $id = $context->getRecordId(); - $consumer_pk = $context->getConsumer()->getRecordId(); + $consumer_pk = $context->getPlatform()->getRecordId(); if (empty($id)) { $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' (consumer_pk, title, ' . 'lti_context_id, type, settings, created, updated) ' . @@ -497,7 +542,7 @@ public function loadResourceLink($resourceLink) "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' r LEFT OUTER JOIN ' . $this->dbTableNamePrefix . static::CONTEXT_TABLE_NAME . ' c ON r.context_pk = c.context_pk ' . ' WHERE ((r.consumer_pk = %d) OR (c.consumer_pk = %d)) AND (lti_resource_link_id = %s)', - $resourceLink->getConsumer()->getRecordId(), $resourceLink->getConsumer()->getRecordId(), + $resourceLink->getPlatform()->getRecordId(), $resourceLink->getPlatform()->getRecordId(), $this->escape($resourceLink->getId())); } $rsResourceLink = $this->executeQuery($sql); @@ -511,9 +556,9 @@ public function loadResourceLink($resourceLink) $resourceLink->setContextId(null); } if (!is_null($row->consumer_pk)) { - $resourceLink->setConsumerId(intval($row->consumer_pk)); + $resourceLink->setPlatformId(intval($row->consumer_pk)); } else { - $resourceLink->setConsumerId(null); + $resourceLink->setPlatformId(null); } $resourceLink->title = $row->title; $resourceLink->ltiResourceLinkId = $row->lti_resource_link_id; @@ -571,7 +616,7 @@ public function saveResourceLink($resourceLink) $consumerId = 'NULL'; $contextId = strval($resourceLink->getContextId()); } else { - $consumerId = strval($resourceLink->getConsumer()->getRecordId()); + $consumerId = strval($resourceLink->getPlatform()->getRecordId()); $contextId = 'NULL'; } $id = $resourceLink->getRecordId(); @@ -710,13 +755,13 @@ public function getSharesResourceLink($resourceLink) $sql = sprintf('SELECT c.consumer_name, r.resource_link_pk, r.title, r.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' AS r ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' AS c ON r.consumer_pk = c.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' AS c ON r.consumer_pk = c.consumer_pk ' . 'WHERE (r.primary_resource_link_pk = %d) ' . 'UNION ' . 'SELECT c2.consumer_name, r2.resource_link_pk, r2.title, r2.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' AS r2 ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' AS x ON r2.context_pk = x.context_pk ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' AS c2 ON x.consumer_pk = c2.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' AS c2 ON x.consumer_pk = c2.consumer_pk ' . 'WHERE (r2.primary_resource_link_pk = %d) ' . 'ORDER BY consumer_name, title', $resourceLink->getRecordId(), $resourceLink->getRecordId()); $rsShare = $this->executeQuery($sql); @@ -735,17 +780,17 @@ public function getSharesResourceLink($resourceLink) } ### -### ConsumerNonce methods +### PlatformNonce methods ### /** * Load nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully loaded */ - public function loadConsumerNonce($nonce) + public function loadPlatformNonce($nonce) { $ok = false; @@ -756,10 +801,10 @@ public function loadConsumerNonce($nonce) // Load the nonce $sql = sprintf("SELECT value AS T FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE (consumer_pk = %d) AND (value = %s)', - $nonce->getConsumer()->getRecordId(), $this->escape($nonce->getValue())); - $rs_nonce = $this->executeQuery($sql); - if ($rs_nonce) { - if (mysqli_fetch_object($rs_nonce)) { + $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue())); + $rsNonce = $this->executeQuery($sql, false); + if ($rsNonce) { + if (mysqli_fetch_object($rsNonce)) { $ok = true; } } @@ -770,15 +815,100 @@ public function loadConsumerNonce($nonce) /** * Save nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully saved */ - public function saveConsumerNonce($nonce) + public function savePlatformNonce($nonce) { $expires = date("{$this->dateFormat} {$this->timeFormat}", $nonce->expires); $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . " (consumer_pk, value, expires) VALUES (%d, %s, %s)", - $nonce->getConsumer()->getRecordId(), $this->escape($nonce->getValue()), $this->escape($expires)); + $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue()), $this->escape($expires)); + $ok = $this->executeQuery($sql); + + return $ok; + } + + /** + * Delete nonce object. + * + * @param PlatformNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully deleted + */ + public function deletePlatformNonce($nonce) + { + $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE (consumer_pk = %d) AND (value = %s)', + $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue())); + $ok = $this->executeQuery($sql); + + return $ok; + } + +### +### AccessToken methods +### + + /** + * Load access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the nonce object was successfully loaded + */ + public function loadAccessToken($accessToken) + { + $ok = false; + + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $sql = sprintf('SELECT scopes, token, expires, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = %d)', $consumer_pk); + $rsAccessToken = $this->executeQuery($sql, false); + if ($rsAccessToken) { + $row = mysqli_fetch_object($rsAccessToken); + if ($row) { + $scopes = json_decode($row->scopes, true); + if (!is_array($scopes)) { + $scopes = array(); + } + $accessToken->scopes = $scopes; + $accessToken->token = $row->token; + $accessToken->expires = strtotime($row->expires); + $accessToken->created = strtotime($row->created); + $accessToken->updated = strtotime($row->updated); + $ok = true; + } + } + + return $ok; + } + + /** + * Save access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the access token object was successfully saved + */ + public function saveAccessToken($accessToken) + { + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $scopes = json_encode($accessToken->scopes, JSON_UNESCAPED_SLASHES); + $token = $accessToken->token; + $expires = date("{$this->dateFormat} {$this->timeFormat}", $accessToken->expires); + $time = time(); + $now = date("{$this->dateFormat} {$this->timeFormat}", $time); + if (empty($accessToken->created)) { + $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + '(consumer_pk, scopes, token, expires, created, updated) ' . + 'VALUES (%d, %s, %s, %s, %s, %s)', $consumer_pk, $this->escape($scopes), $this->escape($token), + $this->escape($expires), $this->escape($now), $this->escape($now)); + } else { + $sql = sprintf('UPDATE ' . $this->dbTableNamePrefix . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'SET scopes = %s, token = %s, expires = %s, updated = %s WHERE consumer_pk = %d', $this->escape($scopes), + $this->escape($token), $this->escape($expires), $this->escape($now), $consumer_pk); + } $ok = $this->executeQuery($sql); return $ok; @@ -888,11 +1018,11 @@ public function loadUserResult($userresult) $sql = sprintf('SELECT user_result_pk, resource_link_pk, lti_user_id, lti_result_sourcedid, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'WHERE (resource_link_pk = %d) AND (lti_user_id = %s)', $userresult->getResourceLink()->getRecordId(), - $this->escape($userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY))); + $this->escape($userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY))); } - $rsUser = $this->executeQuery($sql); - if ($rsUser) { - $row = mysqli_fetch_object($rsUser); + $rsUserResult = $this->executeQuery($sql); + if ($rsUserResult) { + $row = mysqli_fetch_object($rsUserResult); if ($row) { $userresult->setRecordId(intval($row->user_result_pk)); $userresult->setResourceLinkId(intval($row->resource_link_pk)); @@ -922,8 +1052,8 @@ public function saveUserResult($userresult) $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' (resource_link_pk, ' . 'lti_user_id, lti_result_sourcedid, created, updated) ' . 'VALUES (%d, %s, %s, %s, %s)', $userresult->getResourceLink()->getRecordId(), - $this->escape($userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY)), - $this->escape($userresult->ltiResultSourcedId), $this->escape($now), $this->escape($now)); + $this->escape($userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY)), $this->escape($userresult->ltiResultSourcedId), + $this->escape($now), $this->escape($now)); } else { $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'SET lti_result_sourcedid = %s, updated = %s ' . diff --git a/src/DataConnector/DataConnector_oci.php b/src/DataConnector/DataConnector_oci.php index efa14c8..a4db8b8 100644 --- a/src/DataConnector/DataConnector_oci.php +++ b/src/DataConnector/DataConnector_oci.php @@ -3,12 +3,12 @@ namespace ceLTIc\LTI\DataConnector; use ceLTIc\LTI; -use ceLTIc\LTI\ConsumerNonce; +use ceLTIc\LTI\PlatformNonce; use ceLTIc\LTI\Context; use ceLTIc\LTI\ResourceLink; use ceLTIc\LTI\ResourceLinkShare; use ceLTIc\LTI\ResourceLinkShareKey; -use ceLTIc\LTI\ToolConsumer; +use ceLTIc\LTI\Platform; use ceLTIc\LTI\UserResult; use ceLTIc\LTI\Util; @@ -39,145 +39,192 @@ public function __construct($db, $dbTableNamePrefix = '') } ### -### ToolConsumer methods +### Platform methods ### /** - * Load tool consumer object. + * Load platform object. * - * @param ToolConsumer $consumer ToolConsumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully loaded + * @return bool True if the platform object was successfully loaded */ - public function loadToolConsumer($consumer) + public function loadPlatform($platform) { $ok = false; - if (!is_null($consumer->getRecordId())) { - $sql = 'SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + if (!is_null($platform->getRecordId())) { + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'WHERE consumer_pk = :id'; $query = oci_parse($this->db, $sql); - $id = $consumer->getRecordId(); + $id = $platform->getRecordId(); oci_bind_by_name($query, 'id', $id); + } elseif (!empty($platform->platformId)) { + if (empty($platform->clientId)) { + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = :platform_id) ' . + 'GROUP BY platform_id, client_id'; + $query = oci_parse($this->db, $sql); + oci_bind_by_name($query, 'platform_id', $platform->platformId); + } elseif (empty($platform->deploymentId)) { + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = :platform_id) AND (client_id = :client_id)'; + $query = oci_parse($this->db, $sql); + oci_bind_by_name($query, 'platform_id', $platform->platformId); + oci_bind_by_name($query, 'client_id', $platform->clientId); + } else { + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = :platform_id) AND (client_id = :client_id) AND (deployment_id = :deployment_id)'; + $query = oci_parse($this->db, $sql); + oci_bind_by_name($query, 'platform_id', $platform->platformId); + oci_bind_by_name($query, 'client_id', $platform->clientId); + oci_bind_by_name($query, 'deployment_id', $platform->deploymentId); + } } else { - $sql = 'SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - 'WHERE consumer_key256 = :key256'; + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE consumer_key = :key'; $query = oci_parse($this->db, $sql); - $key256 = static::getConsumerKey($consumer->getKey()); - oci_bind_by_name($query, 'key256', $key256); + $consumer_key = $platform->getKey(); + oci_bind_by_name($query, 'key', $consumer_key); } - - if ($this->executeQuery($sql, $query)) { - while ($row = oci_fetch_assoc($query)) { - $row = array_change_key_case($row); - if (empty($key256) || empty($row['consumer_key']) || ($consumer->getKey() === $row['consumer_key'])) { - $consumer->setRecordId(intval($row['consumer_pk'])); - $consumer->name = $row['name']; - $consumer->setkey(empty($row['consumer_key']) ? $row['consumer_key256'] : $row['consumer_key']); - $consumer->secret = $row['secret']; - $consumer->ltiVersion = $row['lti_version']; - $consumer->signatureMethod = $row['signature_method']; - $consumer->consumerName = $row['consumer_name']; - $consumer->consumerVersion = $row['consumer_version']; - $consumer->consumerGuid = $row['consumer_guid']; - $consumer->profile = json_decode($row['profile']); - $consumer->toolProxy = $row['tool_proxy']; - $settingsValue = $row['settings']->load(); - if (is_string($settingsValue)) { - $settings = json_decode($settingsValue, true); - if (!is_array($settings)) { - $settings = @unserialize($settingsValue); // check for old serialized setting - } - if (!is_array($settings)) { - $settings = array(); - } - } else { - $settings = array(); - } - $consumer->setSettings($settings); - $consumer->protected = (intval($row['protected']) === 1); - $consumer->enabled = (intval($row['enabled']) === 1); - $consumer->enableFrom = null; - if (!is_null($row['enable_from'])) { - $consumer->enableFrom = strtotime($row['enable_from']); - } - $consumer->enableUntil = null; - if (!is_null($row['enable_until'])) { - $consumer->enableUntil = strtotime($row['enable_until']); - } - $consumer->lastAccess = null; - if (!is_null($row['last_access'])) { - $consumer->lastAccess = strtotime($row['last_access']); - } - $consumer->created = strtotime($row['created']); - $consumer->updated = strtotime($row['updated']); - $ok = true; - break; + $ok = $this->executeQuery($sql, $query); + if ($ok) { + $row = oci_fetch_assoc($query); + $ok = ($row !== false); + } + if ($ok) { + $row = array_change_key_case($row); + $platform->setRecordId(intval($row['consumer_pk'])); + $platform->name = $row['name']; + $platform->setkey($row['consumer_key']); + $platform->secret = $row['secret']; + $platform->platformId = $row['platform_id']; + $platform->clientId = $row['client_id']; + $platform->deploymentId = $row['deployment_id']; + $platform->rsaKey = $row['public_key']; + $platform->ltiVersion = $row['lti_version']; + $platform->signatureMethod = $row['signature_method']; + $platform->consumerName = $row['consumer_name']; + $platform->consumerVersion = $row['consumer_version']; + $platform->consumerGuid = $row['consumer_guid']; + $platform->profile = json_decode($row['profile']); + $platform->toolProxy = $row['tool_proxy']; + $settingsValue = $row['settings']->load(); + if (is_string($settingsValue)) { + $settings = json_decode($settingsValue, true); + if (!is_array($settings)) { + $settings = @unserialize($settingsValue); // check for old serialized setting } + if (!is_array($settings)) { + $settings = array(); + } + } else { + $settings = array(); + } + $platform->setSettings($settings); + $platform->protected = (intval($row['protected']) === 1); + $platform->enabled = (intval($row['enabled']) === 1); + $platform->enableFrom = null; + if (!is_null($row['enable_from'])) { + $platform->enableFrom = strtotime($row['enable_from']); + } + $platform->enableUntil = null; + if (!is_null($row['enable_until'])) { + $platform->enableUntil = strtotime($row['enable_until']); } + $platform->lastAccess = null; + if (!is_null($row['last_access'])) { + $platform->lastAccess = strtotime($row['last_access']); + } + $platform->created = strtotime($row['created']); + $platform->updated = strtotime($row['updated']); + $this->fixPlatformSettings($platform, false); } return $ok; } /** - * Save tool consumer object. + * Save platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully saved + * @return bool True if the platform object was successfully saved */ - public function saveToolConsumer($consumer) + public function savePlatform($platform) { - $id = $consumer->getRecordId(); - $key = $consumer->getKey(); - $key256 = $this->getConsumerKey($key); - if ($key === $key256) { - $key = null; - } - $protected = ($consumer->protected) ? 1 : 0; - $enabled = ($consumer->enabled) ? 1 : 0; - $profile = (!empty($consumer->profile)) ? json_encode($consumer->profile) : null; - $settingsValue = json_encode($consumer->getSettings()); + $id = $platform->getRecordId(); + $consumer_key = $platform->getKey(); + $protected = ($platform->protected) ? 1 : 0; + $enabled = ($platform->enabled) ? 1 : 0; + $profile = (!empty($platform->profile)) ? json_encode($platform->profile) : null; + $this->fixPlatformSettings($platform, true); + $settingsValue = json_encode($platform->getSettings()); + $this->fixPlatformSettings($platform, false); $time = time(); $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $from = null; - if (!is_null($consumer->enableFrom)) { - $from = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableFrom); + if (!is_null($platform->enableFrom)) { + $from = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableFrom); } $until = null; - if (!is_null($consumer->enableUntil)) { - $until = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableUntil); + if (!is_null($platform->enableUntil)) { + $until = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableUntil); } $last = null; - if (!is_null($consumer->lastAccess)) { - $last = date($this->dateFormat, $consumer->lastAccess); + if (!is_null($platform->lastAccess)) { + $last = date($this->dateFormat, $platform->lastAccess); } if (empty($id)) { - $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' (consumer_key256, consumer_key, name, ' . - 'secret, lti_version, signature_method, consumer_name, consumer_version, consumer_guid, profile, tool_proxy, settings, protected, enabled, ' . + $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' (consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated) ' . - 'VALUES (:key256, :key, :name, :secret, :lti_version, :signature_method, :consumer_name, :consumer_version, :consumer_guid, :profile, :tool_proxy, :settings, ' . + 'VALUES (:key, :name, :secret, ' . + ':platform_id, :client_id, :deployment_id, :public_key, ' . + ':lti_version, :signature_method, ' . + ':consumer_name, :consumer_version, :consumer_guid, :profile, :tool_proxy, :settings, ' . ':protected, :enabled, :enable_from, :enable_until, :last_access, :created, :updated) returning consumer_pk into :pk'; $query = oci_parse($this->db, $sql); - oci_bind_by_name($query, 'key256', $key256); - oci_bind_by_name($query, 'key', $key); - oci_bind_by_name($query, 'name', $consumer->name); - oci_bind_by_name($query, 'secret', $consumer->secret); - oci_bind_by_name($query, 'lti_version', $consumer->ltiVersion); - oci_bind_by_name($query, 'signature_method', $consumer->signatureMethod); - oci_bind_by_name($query, 'consumer_name', $consumer->consumerName); - oci_bind_by_name($query, 'consumer_version', $consumer->consumerVersion); - oci_bind_by_name($query, 'consumer_guid', $consumer->consumerGuid); + oci_bind_by_name($query, 'key', $consumer_key); + oci_bind_by_name($query, 'name', $platform->name); + oci_bind_by_name($query, 'secret', $platform->secret); + oci_bind_by_name($query, 'platform_id', $platform->platformId); + oci_bind_by_name($query, 'client_id', $platform->clientId); + oci_bind_by_name($query, 'deployment_id', $platform->deploymentId); + oci_bind_by_name($query, 'public_key', $platform->rsaKey); + oci_bind_by_name($query, 'lti_version', $platform->ltiVersion); + oci_bind_by_name($query, 'signature_method', $platform->signatureMethod); + oci_bind_by_name($query, 'consumer_name', $platform->consumerName); + oci_bind_by_name($query, 'consumer_version', $platform->consumerVersion); + oci_bind_by_name($query, 'consumer_guid', $platform->consumerGuid); oci_bind_by_name($query, 'profile', $profile); - oci_bind_by_name($query, 'tool_proxy', $consumer->toolProxy); + oci_bind_by_name($query, 'tool_proxy', $platform->toolProxy); oci_bind_by_name($query, 'settings', $settingsValue); oci_bind_by_name($query, 'protected', $protected); oci_bind_by_name($query, 'enabled', $enabled); @@ -188,25 +235,29 @@ public function saveToolConsumer($consumer) oci_bind_by_name($query, 'updated', $now); oci_bind_by_name($query, 'pk', $pk); } else { - $sql = 'UPDATE ' . $this->dbTableNamePrefix . static::CONSUMER_TABLE_NAME . ' ' . - 'SET consumer_key256 = :key256, consumer_key = :key, name = :name, secret = :secret, lti_version = :lti_version, ' . - 'signature_method = :signature_method, consumer_name = :consumer_name, ' . - 'consumer_version = :consumer_version, consumer_guid = :consumer_guid, ' . + $sql = 'UPDATE ' . $this->dbTableNamePrefix . static::PLATFORM_TABLE_NAME . ' ' . + 'SET consumer_key = :key, name = :name, secret = :secret, ' . + 'platform_id = :platform_id, client_id = :client_id, deployment_id = :deployment_id, ' . + 'public_key = :public_key, lti_version = :lti_version, signature_method = :signature_method, ' . + 'consumer_name = :consumer_name, consumer_version = :consumer_version, consumer_guid = :consumer_guid, ' . 'profile = :profile, tool_proxy = :tool_proxy, settings = :settings, ' . 'protected = :protected, enabled = :enabled, enable_from = :enable_from, enable_until = :enable_until, last_access = :last_access, updated = :updated ' . 'WHERE consumer_pk = :id'; $query = oci_parse($this->db, $sql); - oci_bind_by_name($query, 'key256', $key256); - oci_bind_by_name($query, 'key', $key); - oci_bind_by_name($query, 'name', $consumer->name); - oci_bind_by_name($query, 'secret', $consumer->secret); - oci_bind_by_name($query, 'lti_version', $consumer->ltiVersion); - oci_bind_by_name($query, 'signature_method', $consumer->signatureMethod); - oci_bind_by_name($query, 'consumer_name', $consumer->consumerName); - oci_bind_by_name($query, 'consumer_version', $consumer->consumerVersion); - oci_bind_by_name($query, 'consumer_guid', $consumer->consumerGuid); + oci_bind_by_name($query, 'key', $consumer_key); + oci_bind_by_name($query, 'name', $platform->name); + oci_bind_by_name($query, 'secret', $platform->secret); + oci_bind_by_name($query, 'platform_id', $platform->platformId); + oci_bind_by_name($query, 'client_id', $platform->clientId); + oci_bind_by_name($query, 'deployment_id', $platform->deploymentId); + oci_bind_by_name($query, 'public_key', $platform->rsaKey); + oci_bind_by_name($query, 'lti_version', $platform->ltiVersion); + oci_bind_by_name($query, 'signature_method', $platform->signatureMethod); + oci_bind_by_name($query, 'consumer_name', $platform->consumerName); + oci_bind_by_name($query, 'consumer_version', $platform->consumerVersion); + oci_bind_by_name($query, 'consumer_guid', $platform->consumerGuid); oci_bind_by_name($query, 'profile', $profile); - oci_bind_by_name($query, 'tool_proxy', $consumer->toolProxy); + oci_bind_by_name($query, 'tool_proxy', $platform->toolProxy); oci_bind_by_name($query, 'settings', $settingsValue); oci_bind_by_name($query, 'protected', $protected); oci_bind_by_name($query, 'enabled', $enabled); @@ -219,25 +270,31 @@ public function saveToolConsumer($consumer) $ok = $this->executeQuery($sql, $query); if ($ok) { if (empty($id)) { - $consumer->setRecordId(intval($pk)); - $consumer->created = $time; + $platform->setRecordId(intval($pk)); + $platform->created = $time; } - $consumer->updated = $time; + $platform->updated = $time; } return $ok; } /** - * Delete tool consumer object. + * Delete platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully deleted + * @return bool True if the platform object was successfully deleted */ - public function deleteToolConsumer($consumer) + public function deletePlatform($platform) { - $id = $consumer->getRecordId(); + $id = $platform->getRecordId(); + +// Delete any access token for this consumer + $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' WHERE consumer_pk = :id'; + $query = oci_parse($this->db, $sql); + oci_bind_by_name($query, 'id', $id); + $this->executeQuery($sql, $query); // Delete any nonce values for this consumer $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE consumer_pk = :id'; @@ -321,33 +378,34 @@ public function deleteToolConsumer($consumer) $this->executeQuery($sql, $query); // Delete consumer - $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'WHERE consumer_pk = :id'; $query = oci_parse($this->db, $sql); oci_bind_by_name($query, 'id', $id); $ok = $this->executeQuery($sql, $query); if ($ok) { - $consumer->initialize(); + $platform->initialize(); } return $ok; } /** - * Load tool consumer objects. + * Load platform objects. * - * @return ToolConsumer[] Array of all defined ToolConsumer objects + * @return Platform[] Array of all defined Platform objects */ - public function getToolConsumers() + public function getPlatforms() { $consumers = array(); - $sql = 'SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'ORDER BY name'; $query = oci_parse($this->db, $sql); $ok = ($query !== false); @@ -359,18 +417,22 @@ public function getToolConsumers() if ($ok) { while ($row = oci_fetch_assoc($query)) { $row = array_change_key_case($row); - $key = empty($row['consumer_key']) ? $row['consumer_key256'] : $row['consumer_key']; - $consumer = new LTI\ToolConsumer($key, $this); - $consumer->setRecordId(intval($row['consumer_pk'])); - $consumer->name = $row['name']; - $consumer->secret = $row['secret']; - $consumer->ltiVersion = $row['lti_version']; - $consumer->signatureMethod = $row['signature_method']; - $consumer->consumerName = $row['consumer_name']; - $consumer->consumerVersion = $row['consumer_version']; - $consumer->consumerGuid = $row['consumer_guid']; - $consumer->profile = json_decode($row['profile']); - $consumer->toolProxy = $row['tool_proxy']; + $platform = new Platform($this); + $platform->setRecordId(intval($row['consumer_pk'])); + $platform->name = $row['name']; + $platform->setKey($row['consumer_key']); + $platform->secret = $row['secret']; + $platform->platformId = $row['platform_id']; + $platform->clientId = $row['client_id']; + $platform->deploymentId = $row['deployment_id']; + $platform->rsaKey = $row['public_key']; + $platform->ltiVersion = $row['lti_version']; + $platform->signatureMethod = $row['signature_method']; + $platform->consumerName = $row['consumer_name']; + $platform->consumerVersion = $row['consumer_version']; + $platform->consumerGuid = $row['consumer_guid']; + $platform->profile = json_decode($row['profile']); + $platform->toolProxy = $row['tool_proxy']; $settingsValue = $row['settings']->load(); if (is_string($settingsValue)) { $settings = json_decode($settingsValue, true); @@ -383,24 +445,25 @@ public function getToolConsumers() } else { $settings = array(); } - $consumer->setSettings($settings); - $consumer->protected = (intval($row['protected']) === 1); - $consumer->enabled = (intval($row['enabled']) === 1); - $consumer->enableFrom = null; + $platform->setSettings($settings); + $platform->protected = (intval($row['protected']) === 1); + $platform->enabled = (intval($row['enabled']) === 1); + $platform->enableFrom = null; if (!is_null($row['enable_from'])) { - $consumer->enableFrom = strtotime($row['enable_from']); + $platform->enableFrom = strtotime($row['enable_from']); } - $consumer->enableUntil = null; + $platform->enableUntil = null; if (!is_null($row['enable_until'])) { - $consumer->enableUntil = strtotime($row['enable_until']); + $platform->enableUntil = strtotime($row['enable_until']); } - $consumer->lastAccess = null; + $platform->lastAccess = null; if (!is_null($row['last_access'])) { - $consumer->lastAccess = strtotime($row['last_access']); + $platform->lastAccess = strtotime($row['last_access']); } - $consumer->created = strtotime($row['created']); - $consumer->updated = strtotime($row['updated']); - $consumers[] = $consumer; + $platform->created = strtotime($row['created']); + $platform->updated = strtotime($row['updated']); + $this->fixPlatformSettings($platform, true); + $consumers[] = $platform; } } @@ -433,7 +496,7 @@ public function loadContext($context) "FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' ' . 'WHERE (consumer_pk = :cid) AND (lti_context_id = :ctx)'; $query = oci_parse($this->db, $sql); - $id = $context->getConsumer()->getRecordId(); + $id = $context->getPlatform()->getRecordId(); oci_bind_by_name($query, 'cid', $id); oci_bind_by_name($query, 'ctx', $context->ltiContextId); } @@ -445,7 +508,7 @@ public function loadContext($context) if ($ok) { $row = array_change_key_case($row); $context->setRecordId(intval($row['context_pk'])); - $context->setConsumerId(intval($row['consumer_pk'])); + $context->setPlatformId(intval($row['consumer_pk'])); $context->ltiContextId = $row['title']; $context->ltiContextId = $row['lti_context_id']; $context->type = $row['type']; @@ -482,7 +545,7 @@ public function saveContext($context) $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $settingsValue = json_encode($context->getSettings()); $id = $context->getRecordId(); - $consumer_pk = $context->getConsumer()->getRecordId(); + $consumer_pk = $context->getPlatform()->getRecordId(); if (empty($id)) { $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' (consumer_pk, title, ' . 'lti_context_id, type, settings, created, updated) ' . @@ -619,9 +682,9 @@ public function loadResourceLink($resourceLink) $this->dbTableNamePrefix . static::CONTEXT_TABLE_NAME . ' c ON r.context_pk = c.context_pk ' . ' WHERE ((r.consumer_pk = :id1) OR (c.consumer_pk = :id2)) AND (lti_resource_link_id = :rlid)'; $query = oci_parse($this->db, $sql); - $id1 = $resourceLink->getConsumer()->getRecordId(); + $id1 = $resourceLink->getPlatform()->getRecordId(); oci_bind_by_name($query, 'id1', $id1); - $id2 = $resourceLink->getConsumer()->getRecordId(); + $id2 = $resourceLink->getPlatform()->getRecordId(); oci_bind_by_name($query, 'id2', $id2); $id = $resourceLink->getId(); oci_bind_by_name($query, 'rlid', $id); @@ -641,9 +704,9 @@ public function loadResourceLink($resourceLink) $resourceLink->setContextId(null); } if (!is_null($row['consumer_pk'])) { - $resourceLink->setConsumerId(intval($row['consumer_pk'])); + $resourceLink->setPlatformId(intval($row['consumer_pk'])); } else { - $resourceLink->setConsumerId(null); + $resourceLink->setPlatformId(null); } $resourceLink->ltiResourceLinkId = $row['lti_resource_link_id']; $settings = $row['settings']->load(); @@ -699,7 +762,7 @@ public function saveResourceLink($resourceLink) $consumerId = null; $contextId = $resourceLink->getContextId(); } else { - $consumerId = $resourceLink->getConsumer()->getRecordId(); + $consumerId = $resourceLink->getPlatform()->getRecordId(); $contextId = null; } if (empty($resourceLink->primaryResourceLinkId)) { @@ -887,13 +950,13 @@ public function getSharesResourceLink($resourceLink) $sql = 'SELECT c.consumer_name, r.resource_link_pk, r.title, r.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' r ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' c ON r.consumer_pk = c.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' c ON r.consumer_pk = c.consumer_pk ' . 'WHERE (r.primary_resource_link_pk = :id1) ' . 'UNION ' . 'SELECT c2.consumer_name, r2.resource_link_pk, r2.title, r2.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' r2 ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' x ON r2.context_pk = x.context_pk ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' c2 ON x.consumer_pk = c2.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' c2 ON x.consumer_pk = c2.consumer_pk ' . 'WHERE (r2.primary_resource_link_pk = :id2) ' . 'ORDER BY consumer_name, title'; $query = oci_parse($this->db, $sql); @@ -903,7 +966,7 @@ public function getSharesResourceLink($resourceLink) while ($row = oci_fetch_assoc($query)) { $row = array_change_key_case($row); $share = new LTI\ResourceLinkShare(); - $share->consumer_name = $row['consumer_name']; + $share->consumerName = $row['consumer_name']; $share->resourceLinkId = intval($row['resource_link_pk']); $share->title = $row['title']; $share->approved = (intval($row['share_approved']) === 1); @@ -915,17 +978,17 @@ public function getSharesResourceLink($resourceLink) } ### -### ConsumerNonce methods +### PlatformNonce methods ### /** * Load nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully loaded */ - public function loadConsumerNonce($nonce) + public function loadPlatformNonce($nonce) { // Delete any expired nonce values $now = date("{$this->dateFormat} {$this->timeFormat}", time()); @@ -935,7 +998,7 @@ public function loadConsumerNonce($nonce) $this->executeQuery($sql, $query); // Load the nonce - $id = $nonce->getConsumer()->getRecordId(); + $id = $nonce->getPlatform()->getRecordId(); $value = $nonce->getValue(); $sql = "SELECT value T FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE (consumer_pk = :id) AND (value = :value)'; $query = oci_parse($this->db, $sql); @@ -955,13 +1018,13 @@ public function loadConsumerNonce($nonce) /** * Save nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully saved */ - public function saveConsumerNonce($nonce) + public function savePlatformNonce($nonce) { - $id = $nonce->getConsumer()->getRecordId(); + $id = $nonce->getPlatform()->getRecordId(); $value = $nonce->getValue(); $expires = date("{$this->dateFormat} {$this->timeFormat}", $nonce->expires); $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' (consumer_pk, value, expires) VALUES (:id, :value, :expires)'; @@ -974,6 +1037,109 @@ public function saveConsumerNonce($nonce) return $ok; } + /** + * Delete nonce object. + * + * @param PlatformNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully deleted + */ + public function deletePlatformNonce($nonce) + { + $id = $nonce->getPlatform()->getRecordId(); + $value = $nonce->getValue(); + $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE (consumer_pk = :id) AND (value = :value)'; + $query = oci_parse($this->db, $sql); + oci_bind_by_name($query, 'id', $id); + oci_bind_by_name($query, 'value', $value); + $ok = $this->executeQuery($sql, $query); + + return $ok; + } + +### +### AccessToken methods +### + + /** + * Load access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the nonce object was successfully loaded + */ + public function loadAccessToken($accessToken) + { + $ok = false; + + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $sql = "SELECT scopes, token, expires, created, updated FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = :consumer_pk)'; + $query = oci_parse($this->db, $sql); + oci_bind_by_name($query, 'consumer_pk', $consumer_pk); + $this->executeQuery($sql, $query, false); + if ($this->executeQuery($sql, $query)) { + $row = oci_fetch_assoc($query); + if ($row !== false) { + $row = array_change_key_case($row); + $scopes = json_decode($row['scopes']->load(), true); + if (!is_array($scopes)) { + $scopes = array(); + } + $accessToken->scopes = $scopes; + $accessToken->token = $row['token']; + $accessToken->expires = strtotime($row['expires']); + $accessToken->created = strtotime($row['created']); + $accessToken->updated = strtotime($row['updated']); + $ok = true; + } + } + + return $ok; + } + + /** + * Save access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the access token object was successfully saved + */ + public function saveAccessToken($accessToken) + { + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $scopes = json_encode($accessToken->scopes, JSON_UNESCAPED_SLASHES); + $token = $accessToken->token; + $expires = date("{$this->dateFormat} {$this->timeFormat}", $accessToken->expires); + $time = time(); + $now = date("{$this->dateFormat} {$this->timeFormat}", $time); + if (empty($accessToken->created)) { + $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + '(consumer_pk, scopes, token, expires, created, updated) ' . + 'VALUES (:consumer_pk, :scopes, :token, :expires, :created, :updated)'; + $query = oci_parse($this->db, $sql); + oci_bind_by_name($query, 'consumer_pk', $consumer_pk); + oci_bind_by_name($query, 'scopes', $scopes); + oci_bind_by_name($query, 'token', $token); + oci_bind_by_name($query, 'expires', $expires); + oci_bind_by_name($query, 'created', $now); + oci_bind_by_name($query, 'updated', $now); + } else { + $sql = 'UPDATE ' . $this->dbTableNamePrefix . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'SET scopes = :scopes, token = :token, expires = :expires, updated = :updated ' . + 'WHERE consumer_pk = :consumer_pk'; + $query = oci_parse($this->db, $sql); + oci_bind_by_name($query, 'scopes', $scopes); + oci_bind_by_name($query, 'token', $token); + oci_bind_by_name($query, 'expires', $expires); + oci_bind_by_name($query, 'updated', $now); + oci_bind_by_name($query, 'consumer_pk', $consumer_pk); + } + $ok = $this->executeQuery($sql, $query); + + return $ok; + } + ### ### ResourceLinkShareKey methods ### @@ -1008,7 +1174,7 @@ public function loadResourceLinkShareKey($shareKey) if ($row !== false) { $row = array_change_key_case($row); $shareKey->resourceLinkId = intval($row['resource_link_pk']); - $shareKey->autoApprove = (intval($row['auto_approve']) === 1); + $shareKey->autoApprove = ($row['auto_approve'] === 1); $shareKey->expires = strtotime($row['expires']); $ok = true; } @@ -1091,7 +1257,7 @@ public function loadUserResult($userresult) oci_bind_by_name($query, 'id', $id); } else { $id = $userresult->getResourceLink()->getRecordId(); - $uid = $userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY); + $uid = $userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY); $sql = 'SELECT user_result_pk, resource_link_pk, lti_user_id, lti_result_sourcedid, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'WHERE (resource_link_pk = :id) AND (lti_user_id = :u_id)'; @@ -1134,7 +1300,7 @@ public function saveUserResult($userresult) $query = oci_parse($this->db, $sql); $rlid = $userresult->getResourceLink()->getRecordId(); oci_bind_by_name($query, 'rlid', $rlid); - $uid = $userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY); + $uid = $userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY); oci_bind_by_name($query, 'u_id', $uid); $sourcedid = $userresult->ltiResultSourcedId; oci_bind_by_name($query, 'sourcedid', $sourcedid); diff --git a/src/DataConnector/DataConnector_pdo.php b/src/DataConnector/DataConnector_pdo.php index d3b1653..eceeb88 100644 --- a/src/DataConnector/DataConnector_pdo.php +++ b/src/DataConnector/DataConnector_pdo.php @@ -3,12 +3,12 @@ namespace ceLTIc\LTI\DataConnector; use ceLTIc\LTI; -use ceLTIc\LTI\ConsumerNonce; +use ceLTIc\LTI\PlatformNonce; use ceLTIc\LTI\Context; use ceLTIc\LTI\ResourceLink; use ceLTIc\LTI\ResourceLinkShare; use ceLTIc\LTI\ResourceLinkShareKey; -use ceLTIc\LTI\ToolConsumer; +use ceLTIc\LTI\Platform; use ceLTIc\LTI\UserResult; use ceLTIc\LTI\Util; @@ -21,153 +21,188 @@ */ class DataConnector_pdo extends DataConnector { - - /** - * Class constructor - * - * @param object $db Database connection object - * @param string $dbTableNamePrefix Prefix for database table names (optional, default is none) - */ - public function __construct($db, $dbTableNamePrefix = '') - { - parent::__construct($db, $dbTableNamePrefix); - } - ### -### ToolConsumer methods +### Platform methods ### /** - * Load tool consumer object. + * Load platform object. * - * @param ToolConsumer $consumer ToolConsumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully loaded + * @return bool True if the platform object was successfully loaded */ - public function loadToolConsumer($consumer) + public function loadPlatform($platform) { $ok = false; - if (!is_null($consumer->getRecordId())) { - $sql = 'SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $allowMultiple = false; + if (!is_null($platform->getRecordId())) { + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'WHERE consumer_pk = :id'; $query = $this->db->prepare($sql); - $id = $consumer->getRecordId(); + $id = $platform->getRecordId(); $query->bindValue('id', $id, \PDO::PARAM_INT); + } elseif (!empty($platform->platformId)) { + if (empty($platform->clientId)) { + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = :platform_id) ' . + 'GROUP BY platform_id, client_id'; + $query = $this->db->prepare($sql); + $query->bindValue('platform_id', $platform->platformId, \PDO::PARAM_STR); + } elseif (empty($platform->deploymentId)) { + $allowMultiple = true; + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = :platform_id) AND (client_id = :client_id)'; + $query = $this->db->prepare($sql); + $query->bindValue('platform_id', $platform->platformId, \PDO::PARAM_STR); + $query->bindValue('client_id', $platform->clientId, \PDO::PARAM_STR); + } else { + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = :platform_id) AND (client_id = :client_id) AND (deployment_id = :deployment_id)'; + $query = $this->db->prepare($sql); + $query->bindValue('platform_id', $platform->platformId, \PDO::PARAM_STR); + $query->bindValue('client_id', $platform->clientId, \PDO::PARAM_STR); + $query->bindValue('deployment_id', $platform->deploymentId, \PDO::PARAM_STR); + } } else { - $sql = 'SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - 'WHERE consumer_key256 = :key256'; + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (consumer_key = :key)'; $query = $this->db->prepare($sql); - $key256 = static::getConsumerKey($consumer->getKey()); - $query->bindValue('key256', $key256, \PDO::PARAM_STR); + $query->bindValue('key', $platform->getKey(), \PDO::PARAM_STR); } - - if ($this->executeQuery($sql, $query)) { - while ($row = $query->fetch(\PDO::FETCH_ASSOC)) { - $row = array_change_key_case($row); - if (empty($key256) || empty($row['consumer_key']) || ($consumer->getKey() === $row['consumer_key'])) { - $consumer->setRecordId(intval($row['consumer_pk'])); - $consumer->name = $row['name']; - $consumer->setkey(empty($row['consumer_key']) ? $row['consumer_key256'] : $row['consumer_key']); - $consumer->secret = $row['secret']; - $consumer->ltiVersion = $row['lti_version']; - $consumer->signatureMethod = $row['signature_method']; - $consumer->consumerName = $row['consumer_name']; - $consumer->consumerVersion = $row['consumer_version']; - $consumer->consumerGuid = $row['consumer_guid']; - $consumer->profile = json_decode($row['profile']); - $consumer->toolProxy = $row['tool_proxy']; - $settings = json_decode($row['settings'], true); - if (!is_array($settings)) { - $settings = @unserialize($row['settings']); // check for old serialized setting - } - if (!is_array($settings)) { - $settings = array(); - } - $consumer->setSettings($settings); - $consumer->protected = (intval($row['protected']) === 1); - $consumer->enabled = (intval($row['enabled']) === 1); - $consumer->enableFrom = null; - if (!is_null($row['enable_from'])) { - $consumer->enableFrom = strtotime($row['enable_from']); - } - $consumer->enableUntil = null; - if (!is_null($row['enable_until'])) { - $consumer->enableUntil = strtotime($row['enable_until']); - } - $consumer->lastAccess = null; - if (!is_null($row['last_access'])) { - $consumer->lastAccess = strtotime($row['last_access']); - } - $consumer->created = strtotime($row['created']); - $consumer->updated = strtotime($row['updated']); - $ok = true; - break; - } + $ok = $this->executeQuery($sql, $query); + if ($ok) { + $rows = $query->fetchAll(\PDO::FETCH_ASSOC); + $ok = ($rows !== false) && (count($rows) > 0) && ($allowMultiple || (count($rows) === 1)); + } + if ($ok) { + $row = array_change_key_case($rows[0]); + $platform->setRecordId(intval($row['consumer_pk'])); + $platform->name = $row['name']; + $platform->setkey($row['consumer_key']); + $platform->secret = $row['secret']; + $platform->platformId = $row['platform_id']; + $platform->clientId = $row['client_id']; + $platform->deploymentId = $row['deployment_id']; + $platform->rsaKey = $row['public_key']; + $platform->ltiVersion = $row['lti_version']; + $platform->signatureMethod = $row['signature_method']; + $platform->consumerName = $row['consumer_name']; + $platform->consumerVersion = $row['consumer_version']; + $platform->consumerGuid = $row['consumer_guid']; + $platform->profile = json_decode($row['profile']); + $platform->toolProxy = $row['tool_proxy']; + $settings = json_decode($row['settings'], true); + if (!is_array($settings)) { + $settings = @unserialize($row['settings']); // check for old serialized setting + } + if (!is_array($settings)) { + $settings = array(); + } + $platform->setSettings($settings); + $platform->protected = (intval($row['protected']) === 1); + $platform->enabled = (intval($row['enabled']) === 1); + $platform->enableFrom = null; + if (!is_null($row['enable_from'])) { + $platform->enableFrom = strtotime($row['enable_from']); + } + $platform->enableUntil = null; + if (!is_null($row['enable_until'])) { + $platform->enableUntil = strtotime($row['enable_until']); } + $platform->lastAccess = null; + if (!is_null($row['last_access'])) { + $platform->lastAccess = strtotime($row['last_access']); + } + $platform->created = strtotime($row['created']); + $platform->updated = strtotime($row['updated']); + $this->fixPlatformSettings($platform, false); } return $ok; } /** - * Save tool consumer object. + * Save platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully saved + * @return bool True if the platform object was successfully saved */ - public function saveToolConsumer($consumer) + public function savePlatform($platform) { - $id = $consumer->getRecordId(); - $key = $consumer->getKey(); - $key256 = $this->getConsumerKey($key); - if ($key === $key256) { - $key = null; - } - $protected = ($consumer->protected) ? 1 : 0; - $enabled = ($consumer->enabled) ? 1 : 0; - $profile = (!empty($consumer->profile)) ? json_encode($consumer->profile) : null; - $settingsValue = json_encode($consumer->getSettings()); + $id = $platform->getRecordId(); + $protected = ($platform->protected) ? 1 : 0; + $enabled = ($platform->enabled) ? 1 : 0; + $profile = (!empty($platform->profile)) ? json_encode($platform->profile) : null; + $this->fixPlatformSettings($platform, true); + $settingsValue = json_encode($platform->getSettings()); + $this->fixPlatformSettings($platform, false); $time = time(); $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $from = null; - if (!is_null($consumer->enableFrom)) { - $from = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableFrom); + if (!is_null($platform->enableFrom)) { + $from = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableFrom); } $until = null; - if (!is_null($consumer->enableUntil)) { - $until = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableUntil); + if (!is_null($platform->enableUntil)) { + $until = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableUntil); } $last = null; - if (!is_null($consumer->lastAccess)) { - $last = date($this->dateFormat, $consumer->lastAccess); + if (!is_null($platform->lastAccess)) { + $last = date($this->dateFormat, $platform->lastAccess); } if (empty($id)) { - $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' (consumer_key256, consumer_key, name, ' . - 'secret, lti_version, signature_method, consumer_name, consumer_version, consumer_guid, profile, tool_proxy, settings, protected, enabled, ' . + $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' (consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated) ' . - 'VALUES (:key256, :key, :name, :secret, :lti_version, :signature_method, :consumer_name, :consumer_version, :consumer_guid, :profile, :tool_proxy, :settings, ' . + 'VALUES (:key, :name, :secret, ' . + ':platform_id, :client_id, :deployment_id, :public_key, ' . + ':lti_version, :signature_method, ' . + ':consumer_name, :consumer_version, :consumer_guid, :profile, :tool_proxy, :settings, ' . ':protected, :enabled, :enable_from, :enable_until, :last_access, :created, :updated)'; $query = $this->db->prepare($sql); - $query->bindValue('key256', $key256, \PDO::PARAM_STR); - $query->bindValue('key', $key, \PDO::PARAM_STR); - $query->bindValue('name', $consumer->name, \PDO::PARAM_STR); - $query->bindValue('secret', $consumer->secret, \PDO::PARAM_STR); - $query->bindValue('lti_version', $consumer->ltiVersion, \PDO::PARAM_STR); - $query->bindValue('signature_method', $consumer->signatureMethod, \PDO::PARAM_STR); - $query->bindValue('consumer_name', $consumer->consumerName, \PDO::PARAM_STR); - $query->bindValue('consumer_version', $consumer->consumerVersion, \PDO::PARAM_STR); - $query->bindValue('consumer_guid', $consumer->consumerGuid, \PDO::PARAM_STR); + $query->bindValue('key', $platform->getKey(), \PDO::PARAM_STR); + $query->bindValue('name', $platform->name, \PDO::PARAM_STR); + $query->bindValue('secret', $platform->secret, \PDO::PARAM_STR); + $query->bindValue('platform_id', $platform->platformId, \PDO::PARAM_STR); + $query->bindValue('client_id', $platform->clientId, \PDO::PARAM_STR); + $query->bindValue('deployment_id', $platform->deploymentId, \PDO::PARAM_STR); + $query->bindValue('public_key', $platform->rsaKey, \PDO::PARAM_STR); + $query->bindValue('lti_version', $platform->ltiVersion, \PDO::PARAM_STR); + $query->bindValue('signature_method', $platform->signatureMethod, \PDO::PARAM_STR); + $query->bindValue('consumer_name', $platform->consumerName, \PDO::PARAM_STR); + $query->bindValue('consumer_version', $platform->consumerVersion, \PDO::PARAM_STR); + $query->bindValue('consumer_guid', $platform->consumerGuid, \PDO::PARAM_STR); $query->bindValue('profile', $profile, \PDO::PARAM_STR); - $query->bindValue('tool_proxy', $consumer->toolProxy, \PDO::PARAM_STR); + $query->bindValue('tool_proxy', $platform->toolProxy, \PDO::PARAM_STR); $query->bindValue('settings', $settingsValue, \PDO::PARAM_STR); $query->bindValue('protected', $protected, \PDO::PARAM_INT); $query->bindValue('enabled', $enabled, \PDO::PARAM_INT); @@ -177,25 +212,30 @@ public function saveToolConsumer($consumer) $query->bindValue('created', $now, \PDO::PARAM_STR); $query->bindValue('updated', $now, \PDO::PARAM_STR); } else { - $sql = 'UPDATE ' . $this->dbTableNamePrefix . static::CONSUMER_TABLE_NAME . ' ' . - 'SET consumer_key256 = :key256, consumer_key = :key, name = :name, secret = :secret, lti_version = :lti_version, ' . - 'signature_method = :signature_method, consumer_name = :consumer_name, ' . - 'consumer_version = :consumer_version, consumer_guid = :consumer_guid, ' . + $sql = 'UPDATE ' . $this->dbTableNamePrefix . static::PLATFORM_TABLE_NAME . ' ' . + 'SET consumer_key = :key, name = :name, secret = :secret, ' . + 'platform_id = :platform_id, client_id = :client_id, deployment_id = :deployment_id, ' . + 'public_key = :public_key, lti_version = :lti_version, signature_method = :signature_method, ' . + 'consumer_name = :consumer_name, consumer_version = :consumer_version, consumer_guid = :consumer_guid, ' . 'profile = :profile, tool_proxy = :tool_proxy, settings = :settings, ' . - 'protected = :protected, enabled = :enabled, enable_from = :enable_from, enable_until = :enable_until, last_access = :last_access, updated = :updated ' . + 'protected = :protected, enabled = :enabled, enable_from = :enable_from, enable_until = :enable_until, ' . + 'last_access = :last_access, updated = :updated ' . 'WHERE consumer_pk = :id'; $query = $this->db->prepare($sql); - $query->bindValue('key256', $key256, \PDO::PARAM_STR); - $query->bindValue('key', $key, \PDO::PARAM_STR); - $query->bindValue('name', $consumer->name, \PDO::PARAM_STR); - $query->bindValue('secret', $consumer->secret, \PDO::PARAM_STR); - $query->bindValue('lti_version', $consumer->ltiVersion, \PDO::PARAM_STR); - $query->bindValue('signature_method', $consumer->signatureMethod, \PDO::PARAM_STR); - $query->bindValue('consumer_name', $consumer->consumerName, \PDO::PARAM_STR); - $query->bindValue('consumer_version', $consumer->consumerVersion, \PDO::PARAM_STR); - $query->bindValue('consumer_guid', $consumer->consumerGuid, \PDO::PARAM_STR); + $query->bindValue('key', $platform->getKey(), \PDO::PARAM_STR); + $query->bindValue('name', $platform->name, \PDO::PARAM_STR); + $query->bindValue('secret', $platform->secret, \PDO::PARAM_STR); + $query->bindValue('platform_id', $platform->platformId, \PDO::PARAM_STR); + $query->bindValue('client_id', $platform->clientId, \PDO::PARAM_STR); + $query->bindValue('deployment_id', $platform->deploymentId, \PDO::PARAM_STR); + $query->bindValue('public_key', $platform->rsaKey, \PDO::PARAM_STR); + $query->bindValue('lti_version', $platform->ltiVersion, \PDO::PARAM_STR); + $query->bindValue('signature_method', $platform->signatureMethod, \PDO::PARAM_STR); + $query->bindValue('consumer_name', $platform->consumerName, \PDO::PARAM_STR); + $query->bindValue('consumer_version', $platform->consumerVersion, \PDO::PARAM_STR); + $query->bindValue('consumer_guid', $platform->consumerGuid, \PDO::PARAM_STR); $query->bindValue('profile', $profile, \PDO::PARAM_STR); - $query->bindValue('tool_proxy', $consumer->toolProxy, \PDO::PARAM_STR); + $query->bindValue('tool_proxy', $platform->toolProxy, \PDO::PARAM_STR); $query->bindValue('settings', $settingsValue, \PDO::PARAM_STR); $query->bindValue('protected', $protected, \PDO::PARAM_INT); $query->bindValue('enabled', $enabled, \PDO::PARAM_INT); @@ -208,25 +248,31 @@ public function saveToolConsumer($consumer) $ok = $this->executeQuery($sql, $query); if ($ok) { if (empty($id)) { - $consumer->setRecordId($this->getLastInsertId(static::CONSUMER_TABLE_NAME)); - $consumer->created = $time; + $platform->setRecordId($this->getLastInsertId(static::PLATFORM_TABLE_NAME)); + $platform->created = $time; } - $consumer->updated = $time; + $platform->updated = $time; } return $ok; } /** - * Delete tool consumer object. + * Delete platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully deleted + * @return bool True if the platform object was successfully deleted */ - public function deleteToolConsumer($consumer) + public function deletePlatform($platform) { - $id = $consumer->getRecordId(); + $id = $platform->getRecordId(); + +// Delete any access token for this consumer + $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' WHERE consumer_pk = :id'; + $query = $this->db->prepare($sql); + $query->bindValue('id', $id, \PDO::PARAM_INT); + $this->executeQuery($sql, $query); // Delete any nonce values for this consumer $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE consumer_pk = :id'; @@ -310,33 +356,34 @@ public function deleteToolConsumer($consumer) $this->executeQuery($sql, $query); // Delete consumer - $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'WHERE consumer_pk = :id'; $query = $this->db->prepare($sql); $query->bindValue('id', $id, \PDO::PARAM_INT); $ok = $this->executeQuery($sql, $query); if ($ok) { - $consumer->initialize(); + $platform->initialize(); } return $ok; } /** - * Load tool consumer objects. + * Load platform objects. * - * @return ToolConsumer[] Array of all defined ToolConsumer objects + * @return Platform[] Array of all defined Platform objects */ - public function getToolConsumers() + public function getPlatforms() { $consumers = array(); - $sql = 'SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = 'SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'ORDER BY name'; $query = $this->db->prepare($sql); $ok = ($query !== false); @@ -348,18 +395,21 @@ public function getToolConsumers() if ($ok) { while ($row = $query->fetch(\PDO::FETCH_ASSOC)) { $row = array_change_key_case($row); - $key = empty($row['consumer_key']) ? $row['consumer_key256'] : $row['consumer_key']; - $consumer = new LTI\ToolConsumer($key, $this); - $consumer->setRecordId(intval($row['consumer_pk'])); - $consumer->name = $row['name']; - $consumer->secret = $row['secret']; - $consumer->ltiVersion = $row['lti_version']; - $consumer->signatureMethod = $row['signature_method']; - $consumer->consumerName = $row['consumer_name']; - $consumer->consumerVersion = $row['consumer_version']; - $consumer->consumerGuid = $row['consumer_guid']; - $consumer->profile = json_decode($row['profile']); - $consumer->toolProxy = $row['tool_proxy']; + $platform = Platform::fromConsumerKey($row['consumer_key'], $this); + $platform->setRecordId(intval($row['consumer_pk'])); + $platform->name = $row['name']; + $platform->secret = $row['secret']; + $platform->platformId = $row['platform_id']; + $platform->clientId = $row['client_id']; + $platform->deploymentId = $row['deployment_id']; + $platform->rsaKey = $row['public_key']; + $platform->ltiVersion = $row['lti_version']; + $platform->signatureMethod = $row['signature_method']; + $platform->consumerName = $row['consumer_name']; + $platform->consumerVersion = $row['consumer_version']; + $platform->consumerGuid = $row['consumer_guid']; + $platform->profile = json_decode($row['profile']); + $platform->toolProxy = $row['tool_proxy']; $settings = json_decode($row['settings'], true); if (!is_array($settings)) { $settings = @unserialize($row['settings']); // check for old serialized setting @@ -367,24 +417,25 @@ public function getToolConsumers() if (!is_array($settings)) { $settings = array(); } - $consumer->setSettings($settings); - $consumer->protected = (intval($row['protected']) === 1); - $consumer->enabled = (intval($row['enabled']) === 1); - $consumer->enableFrom = null; + $platform->setSettings($settings); + $platform->protected = (intval($row['protected']) === 1); + $platform->enabled = (intval($row['enabled']) === 1); + $platform->enableFrom = null; if (!is_null($row['enable_from'])) { - $consumer->enableFrom = strtotime($row['enable_from']); + $platform->enableFrom = strtotime($row['enable_from']); } - $consumer->enableUntil = null; + $platform->enableUntil = null; if (!is_null($row['enable_until'])) { - $consumer->enableUntil = strtotime($row['enable_until']); + $platform->enableUntil = strtotime($row['enable_until']); } - $consumer->lastAccess = null; + $platform->lastAccess = null; if (!is_null($row['last_access'])) { - $consumer->lastAccess = strtotime($row['last_access']); + $platform->lastAccess = strtotime($row['last_access']); } - $consumer->created = strtotime($row['created']); - $consumer->updated = strtotime($row['updated']); - $consumers[] = $consumer; + $platform->created = strtotime($row['created']); + $platform->updated = strtotime($row['updated']); + $this->fixPlatformSettings($platform, false); + $consumers[] = $platform; } } @@ -416,7 +467,7 @@ public function loadContext($context) "FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' ' . 'WHERE (consumer_pk = :cid) AND (lti_context_id = :ctx)'; $query = $this->db->prepare($sql); - $query->bindValue('cid', $context->getConsumer()->getRecordId(), \PDO::PARAM_INT); + $query->bindValue('cid', $context->getPlatform()->getRecordId(), \PDO::PARAM_INT); $query->bindValue('ctx', $context->ltiContextId, \PDO::PARAM_STR); } $ok = $this->executeQuery($sql, $query); @@ -427,7 +478,7 @@ public function loadContext($context) if ($ok) { $row = array_change_key_case($row); $context->setRecordId(intval($row['context_pk'])); - $context->setConsumerId(intval($row['consumer_pk'])); + $context->setPlatformId(intval($row['consumer_pk'])); $context->title = $row['title']; $context->ltiContextId = $row['lti_context_id']; $context->type = $row['type']; @@ -459,7 +510,7 @@ public function saveContext($context) $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $settingsValue = json_encode($context->getSettings()); $id = $context->getRecordId(); - $consumer_pk = $context->getConsumer()->getRecordId(); + $consumer_pk = $context->getPlatform()->getRecordId(); if (empty($id)) { $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' (consumer_pk, title, ' . 'lti_context_id, type, settings, created, updated) ' . @@ -585,8 +636,8 @@ public function loadResourceLink($resourceLink) $this->dbTableNamePrefix . static::CONTEXT_TABLE_NAME . ' c ON r.context_pk = c.context_pk ' . ' WHERE ((r.consumer_pk = :id1) OR (c.consumer_pk = :id2)) AND (lti_resource_link_id = :rlid)'; $query = $this->db->prepare($sql); - $query->bindValue('id1', $resourceLink->getConsumer()->getRecordId(), \PDO::PARAM_INT); - $query->bindValue('id2', $resourceLink->getConsumer()->getRecordId(), \PDO::PARAM_INT); + $query->bindValue('id1', $resourceLink->getPlatform()->getRecordId(), \PDO::PARAM_INT); + $query->bindValue('id2', $resourceLink->getPlatform()->getRecordId(), \PDO::PARAM_INT); $query->bindValue('rlid', $resourceLink->getId(), \PDO::PARAM_STR); } $ok = $this->executeQuery($sql, $query); @@ -604,9 +655,9 @@ public function loadResourceLink($resourceLink) $resourceLink->setContextId(null); } if (!is_null($row['consumer_pk'])) { - $resourceLink->setConsumerId(intval($row['consumer_pk'])); + $resourceLink->setPlatformId(intval($row['consumer_pk'])); } else { - $resourceLink->setConsumerId(null); + $resourceLink->setPlatformId(null); } $resourceLink->title = $row['title']; $resourceLink->ltiResourceLinkId = $row['lti_resource_link_id']; @@ -657,7 +708,7 @@ public function saveResourceLink($resourceLink) $consumerId = null; $contextId = $resourceLink->getContextId(); } else { - $consumerId = $resourceLink->getConsumer()->getRecordId(); + $consumerId = $resourceLink->getPlatform()->getRecordId(); $contextId = null; } if (empty($resourceLink->primaryResourceLinkId)) { @@ -844,13 +895,13 @@ public function getSharesResourceLink($resourceLink) $sql = 'SELECT c.consumer_name, r.resource_link_pk, r.title, r.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' r ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' c ON r.consumer_pk = c.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' c ON r.consumer_pk = c.consumer_pk ' . 'WHERE (r.primary_resource_link_pk = :id1) ' . 'UNION ' . 'SELECT c2.consumer_name, r2.resource_link_pk, r2.title, r2.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' r2 ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' x ON r2.context_pk = x.context_pk ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' c2 ON x.consumer_pk = c2.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' c2 ON x.consumer_pk = c2.consumer_pk ' . 'WHERE (r2.primary_resource_link_pk = :id2) ' . 'ORDER BY consumer_name, title'; $query = $this->db->prepare($sql); @@ -872,17 +923,17 @@ public function getSharesResourceLink($resourceLink) } ### -### ConsumerNonce methods +### PlatformNonce methods ### /** * Load nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully loaded */ - public function loadConsumerNonce($nonce) + public function loadPlatformNonce($nonce) { // Delete any expired nonce values $now = date("{$this->dateFormat} {$this->timeFormat}", time()); @@ -892,7 +943,7 @@ public function loadConsumerNonce($nonce) $this->executeQuery($sql, $query); // Load the nonce - $id = $nonce->getConsumer()->getRecordId(); + $id = $nonce->getPlatform()->getRecordId(); $value = $nonce->getValue(); $sql = "SELECT value T FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE (consumer_pk = :id) AND (value = :value)'; $query = $this->db->prepare($sql); @@ -912,13 +963,13 @@ public function loadConsumerNonce($nonce) /** * Save nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully saved */ - public function saveConsumerNonce($nonce) + public function savePlatformNonce($nonce) { - $id = $nonce->getConsumer()->getRecordId(); + $id = $nonce->getPlatform()->getRecordId(); $value = $nonce->getValue(); $expires = date("{$this->dateFormat} {$this->timeFormat}", $nonce->expires); $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' (consumer_pk, value, expires) VALUES (:id, :value, :expires)'; @@ -931,6 +982,109 @@ public function saveConsumerNonce($nonce) return $ok; } + /** + * Delete nonce object. + * + * @param PlatformNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully deleted + */ + public function deletePlatformNonce($nonce) + { + $sql = "DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = :id) AND (value = :value)'; + $query = $this->db->prepare($sql); + $id = $nonce->getPlatform()->getRecordId(); + $query->bindValue('id', $id, \PDO::PARAM_STR); + $value = $nonce->getValue(); + $query->bindValue('value', $value, \PDO::PARAM_STR); + $ok = $this->executeQuery($sql, $query); + + return $ok; + } + +### +### AccessToken methods +### + + /** + * Load access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the nonce object was successfully loaded + */ + public function loadAccessToken($accessToken) + { + $ok = false; + + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $sql = "SELECT scopes, token, expires, created, updated FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = :consumer_pk)'; + $query = $this->db->prepare($sql); + $query->bindValue('consumer_pk', $consumer_pk, \PDO::PARAM_INT); + if ($this->executeQuery($sql, $query, false)) { + $row = $query->fetch(\PDO::FETCH_ASSOC); + if ($row !== false) { + $row = array_change_key_case($row); + $scopes = json_decode($row['scopes'], true); + if (!is_array($scopes)) { + $scopes = array(); + } + $accessToken->scopes = $scopes; + $accessToken->token = $row['token']; + $accessToken->expires = strtotime($row['expires']); + $accessToken->created = strtotime($row['created']); + $accessToken->updated = strtotime($row['updated']); + $ok = true; + } + } + + return $ok; + } + + /** + * Save access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the access token object was successfully saved + */ + public function saveAccessToken($accessToken) + { + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $scopes = json_encode($accessToken->scopes, JSON_UNESCAPED_SLASHES); + $token = $accessToken->token; + $expires = date("{$this->dateFormat} {$this->timeFormat}", $accessToken->expires); + $time = time(); + $now = date("{$this->dateFormat} {$this->timeFormat}", $time); + if (empty($accessToken->created)) { + $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + '(consumer_pk, scopes, token, expires, created, updated) ' . + 'VALUES (:consumer_pk, :scopes, :token, :expires, :created, :updated)'; + $query = $this->db->prepare($sql); + $query->bindValue('consumer_pk', $consumer_pk, \PDO::PARAM_INT); + $query->bindValue('scopes', $scopes, \PDO::PARAM_STR); + $query->bindValue('token', $token, \PDO::PARAM_STR); + $query->bindValue('expires', $expires, \PDO::PARAM_STR); + $query->bindValue('created', $now, \PDO::PARAM_STR); + $query->bindValue('updated', $now, \PDO::PARAM_STR); + } else { + $sql = 'UPDATE ' . $this->dbTableNamePrefix . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'SET scopes = :scopes, token = :token, expires = :expires, updated = :updated ' . + 'WHERE consumer_pk = :consumer_pk'; + $query = $this->db->prepare($sql); + $query->bindValue('scopes', $scopes, \PDO::PARAM_STR); + $query->bindValue('token', $token, \PDO::PARAM_STR); + $query->bindValue('expires', $expires, \PDO::PARAM_STR); + $query->bindValue('updated', $now, \PDO::PARAM_STR); + $query->bindValue('consumer_pk', $consumer_pk, \PDO::PARAM_INT); + } + $ok = $this->executeQuery($sql, $query); + + return $ok; + } + ### ### ResourceLinkShareKey methods ### @@ -983,12 +1137,8 @@ public function loadResourceLinkShareKey($shareKey) */ public function saveResourceLinkShareKey($shareKey) { - if ($shareKey->autoApprove) { - $approve = 1; - } else { - $approve = 0; - } $id = $shareKey->getId(); + $autoApprove = ($shareKey->autoApprove) ? 1 : 0; $expires = date("{$this->dateFormat} {$this->timeFormat}", $shareKey->expires); $sql = "INSERT INTO {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' ' . '(share_key_id, resource_link_pk, auto_approve, expires) ' . @@ -996,7 +1146,7 @@ public function saveResourceLinkShareKey($shareKey) $query = $this->db->prepare($sql); $query->bindValue('id', $id, \PDO::PARAM_STR); $query->bindValue('prlid', $shareKey->resourceLinkId, \PDO::PARAM_INT); - $query->bindValue('approve', $approve, \PDO::PARAM_INT); + $query->bindValue('approve', $autoApprove, \PDO::PARAM_INT); $query->bindValue('expires', $expires, \PDO::PARAM_STR); $ok = $this->executeQuery($sql, $query); @@ -1048,7 +1198,7 @@ public function loadUserResult($userresult) $query->bindValue('id', $id, \PDO::PARAM_INT); } else { $id = $userresult->getResourceLink()->getRecordId(); - $uid = $userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY); + $uid = $userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY); $sql = 'SELECT user_result_pk, resource_link_pk, lti_user_id, lti_result_sourcedid, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'WHERE (resource_link_pk = :id) AND (lti_user_id = :u_id)'; @@ -1090,7 +1240,7 @@ public function saveUserResult($userresult) 'VALUES (:rlid, :u_id, :sourcedid, :created, :updated)'; $query = $this->db->prepare($sql); $query->bindValue('rlid', $userresult->getResourceLink()->getRecordId(), \PDO::PARAM_INT); - $query->bindValue('u_id', $userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY), \PDO::PARAM_STR); + $query->bindValue('u_id', $userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY), \PDO::PARAM_STR); $query->bindValue('sourcedid', $userresult->ltiResultSourcedId, \PDO::PARAM_STR); $query->bindValue('created', $now, \PDO::PARAM_STR); $query->bindValue('updated', $now, \PDO::PARAM_STR); diff --git a/src/DataConnector/DataConnector_pdo_oci.php b/src/DataConnector/DataConnector_pdo_oci.php index 74b3536..42f9983 100644 --- a/src/DataConnector/DataConnector_pdo_oci.php +++ b/src/DataConnector/DataConnector_pdo_oci.php @@ -2,15 +2,6 @@ namespace ceLTIc\LTI\DataConnector; -use ceLTIc\LTI; -use ceLTIc\LTI\ConsumerNonce; -use ceLTIc\LTI\Context; -use ceLTIc\LTI\ResourceLink; -use ceLTIc\LTI\ResourceLinkShare; -use ceLTIc\LTI\ResourceLinkShareKey; -use ceLTIc\LTI\ToolConsumer; -use ceLTIc\LTI\UserResult; - /** * Class to represent an LTI Data Connector for PDO variations for Oracle connections * @@ -20,6 +11,7 @@ */ class DataConnector_pdo_oci extends DataConnector_pdo { + /** * Array of identity field sequence names * diff --git a/src/DataConnector/DataConnector_pdo_pgsql.php b/src/DataConnector/DataConnector_pdo_pgsql.php index ff0dabe..b54207d 100644 --- a/src/DataConnector/DataConnector_pdo_pgsql.php +++ b/src/DataConnector/DataConnector_pdo_pgsql.php @@ -3,9 +3,6 @@ namespace ceLTIc\LTI\DataConnector; use ceLTIc\LTI; -use ceLTIc\LTI\Context; -use ceLTIc\LTI\ToolConsumer; -use PDO; /** * Class to represent an LTI Data Connector for PDO variations for PostgreSQL connections diff --git a/src/DataConnector/DataConnector_pg.php b/src/DataConnector/DataConnector_pg.php index 880112f..7ff8011 100644 --- a/src/DataConnector/DataConnector_pg.php +++ b/src/DataConnector/DataConnector_pg.php @@ -3,12 +3,12 @@ namespace ceLTIc\LTI\DataConnector; use ceLTIc\LTI; -use ceLTIc\LTI\ConsumerNonce; +use ceLTIc\LTI\PlatformNonce; use ceLTIc\LTI\Context; use ceLTIc\LTI\ResourceLink; use ceLTIc\LTI\ResourceLinkShare; use ceLTIc\LTI\ResourceLinkShareKey; -use ceLTIc\LTI\ToolConsumer; +use ceLTIc\LTI\Platform; use ceLTIc\LTI\UserResult; use ceLTIc\LTI\Util; @@ -27,192 +27,231 @@ class DataConnector_pg extends DataConnector { ### -### ToolConsumer methods +### Platform methods ### /** - * Load tool consumer object. + * Load platform object. * - * @param ToolConsumer $consumer ToolConsumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully loaded + * @return bool True if the platform object was successfully loaded */ - public function loadToolConsumer($consumer) + public function loadPlatform($platform) { $ok = false; - if (!is_null($consumer->getRecordId())) { - $sql = sprintf('SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + if (!is_null($platform->getRecordId())) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - "WHERE consumer_pk = %d", $consumer->getRecordId()); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE consumer_pk = %d', $platform->getRecordId()); + } elseif (!empty($platform->platformId)) { + if (empty($platform->clientId)) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) ' . + 'GROUP BY platform_id, client_id', $this->escape($platform->platformId)); + } elseif (empty($platform->deploymentId)) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) AND (client_id = %s)', $this->escape($platform->platformId), + $this->escape($platform->clientId)); + } else { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) AND (client_id = %s) AND (deployment_id = %s)', $this->escape($platform->platformId), + $this->escape($platform->clientId), $this->escape($platform->deploymentId)); + } } else { - $key256 = static::getConsumerKey($consumer->getKey()); - $sql = sprintf('SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - "WHERE consumer_key256 = %s", $this->escape($key256)); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + "WHERE consumer_key = %s", $this->escape($platform->getKey())); } $rsConsumer = $this->executeQuery($sql); if ($rsConsumer) { - while ($row = pg_fetch_object($rsConsumer)) { - if (empty($key256) || empty($row->consumer_key) || ($consumer->getKey() === $row->consumer_key)) { - $consumer->setRecordId(intval($row->consumer_pk)); - $consumer->name = $row->name; - $consumer->setkey(empty($row->consumer_key) ? $row->consumer_key256 : $row->consumer_key); - $consumer->secret = $row->secret; - $consumer->ltiVersion = $row->lti_version; - $consumer->signatureMethod = $row->signature_method; - $consumer->consumerName = $row->consumer_name; - $consumer->consumerVersion = $row->consumer_version; - $consumer->consumerGuid = $row->consumer_guid; - $consumer->profile = json_decode($row->profile); - $consumer->toolProxy = $row->tool_proxy; - $settings = json_decode($row->settings, true); - if (!is_array($settings)) { - $settings = @unserialize($row->settings); // check for old serialized setting - } - if (!is_array($settings)) { - $settings = array(); - } - $consumer->setSettings($settings); - $consumer->protected = $row->protected; - $consumer->enabled = $row->enabled; - $consumer->enableFrom = null; - if (!is_null($row->enable_from)) { - $consumer->enableFrom = strtotime($row->enable_from); - } - $consumer->enableUntil = null; - if (!is_null($row->enable_until)) { - $consumer->enableUntil = strtotime($row->enable_until); - } - $consumer->lastAccess = null; - if (!is_null($row->last_access)) { - $consumer->lastAccess = strtotime($row->last_access); - } - $consumer->created = strtotime($row->created); - $consumer->updated = strtotime($row->updated); - $ok = true; - break; + $row = pg_fetch_object($rsConsumer); + if ($row) { + $platform->setRecordId(intval($row->consumer_pk)); + $platform->name = $row->name; + $platform->setkey($row->consumer_key); + $platform->secret = $row->secret; + $platform->platformId = $row->platform_id; + $platform->clientId = $row->client_id; + $platform->deploymentId = $row->deployment_id; + $platform->rsaKey = $row->public_key; + $platform->ltiVersion = $row->lti_version; + $platform->signatureMethod = $row->signature_method; + $platform->consumerName = $row->consumer_name; + $platform->consumerVersion = $row->consumer_version; + $platform->consumerGuid = $row->consumer_guid; + $platform->profile = json_decode($row->profile); + $platform->toolProxy = $row->tool_proxy; + $settings = json_decode($row->settings, true); + if (!is_array($settings)) { + $settings = @unserialize($row->settings); // check for old serialized setting } + if (!is_array($settings)) { + $settings = array(); + } + $platform->setSettings($settings); + $platform->protected = $row->protected; + $platform->enabled = $row->enabled; + $platform->enableFrom = null; + if (!is_null($row->enable_from)) { + $platform->enableFrom = strtotime($row->enable_from); + } + $platform->enableUntil = null; + if (!is_null($row->enable_until)) { + $platform->enableUntil = strtotime($row->enable_until); + } + $platform->lastAccess = null; + if (!is_null($row->last_access)) { + $platform->lastAccess = strtotime($row->last_access); + } + $platform->created = strtotime($row->created); + $platform->updated = strtotime($row->updated); + $this->fixPlatformSettings($platform, false); + $ok = true; } - pg_free_result($rsConsumer); } return $ok; } /** - * Save tool consumer object. + * Save platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully saved + * @return bool True if the platform object was successfully saved */ - public function saveToolConsumer($consumer) + public function savePlatform($platform) { - $id = $consumer->getRecordId(); - $key = $consumer->getKey(); - $key256 = static::getConsumerKey($key); - if ($key === $key256) { - $key = null; - } - $protected = ($consumer->protected) ? 'true' : 'false'; - $enabled = ($consumer->enabled) ? 'true' : 'false'; - $profile = (!empty($consumer->profile)) ? json_encode($consumer->profile) : null; - $settingsValue = json_encode($consumer->getSettings()); + $id = $platform->getRecordId(); + $protected = ($platform->protected) ? 'true' : 'false'; + $enabled = ($platform->enabled) ? 'true' : 'false'; + $profile = (!empty($platform->profile)) ? json_encode($platform->profile) : null; + $this->fixPlatformSettings($platform, true); + $settingsValue = json_encode($platform->getSettings()); + $this->fixPlatformSettings($platform, false); $time = time(); $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $from = null; - if (!is_null($consumer->enableFrom)) { - $from = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableFrom); + if (!is_null($platform->enableFrom)) { + $from = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableFrom); } $until = null; - if (!is_null($consumer->enableUntil)) { - $until = date("{$this->dateFormat} {$this->timeFormat}", $consumer->enableUntil); + if (!is_null($platform->enableUntil)) { + $until = date("{$this->dateFormat} {$this->timeFormat}", $platform->enableUntil); } $last = null; - if (!is_null($consumer->lastAccess)) { - $last = date($this->dateFormat, $consumer->lastAccess); + if (!is_null($platform->lastAccess)) { + $last = date($this->dateFormat, $platform->lastAccess); } if (empty($id)) { - $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' (consumer_key256, consumer_key, name, ' . - 'secret, lti_version, signature_method, consumer_name, consumer_version, consumer_guid, profile, ' . - 'tool_proxy, settings, protected, enabled, ' . + $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' (consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated) ' . - 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)', $this->escape($key256), - $this->escape($key), $this->escape($consumer->name), $this->escape($consumer->secret), - $this->escape($consumer->ltiVersion), $this->escape($consumer->signatureMethod), - $this->escape($consumer->consumerName), $this->escape($consumer->consumerVersion), - $this->escape($consumer->consumerGuid), $this->escape($profile), $this->escape($consumer->toolProxy), + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)', + $this->escape($platform->getKey()), $this->escape($platform->name), $this->escape($platform->secret), + $this->escape($platform->platformId), $this->escape($platform->clientId), $this->escape($platform->deploymentId), + $this->escape($platform->rsaKey), $this->escape($platform->ltiVersion), $this->escape($platform->signatureMethod), + $this->escape($platform->consumerName), $this->escape($platform->consumerVersion), + $this->escape($platform->consumerGuid), $this->escape($profile), $this->escape($platform->toolProxy), $this->escape($settingsValue), $protected, $enabled, $this->escape($from), $this->escape($until), $this->escape($last), $this->escape($now), $this->escape($now)); } else { - $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' SET ' . - 'consumer_key256 = %s, consumer_key = %s, ' . - 'name = %s, secret= %s, lti_version = %s, signature_method = %s, consumer_name = %s, consumer_version = %s, consumer_guid = %s, ' . + $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' SET ' . + 'consumer_key = %s, name = %s, secret= %s, ' . + 'platform_id = %s, client_id = %s, deployment_id = %s, public_key = %s, ' . + 'lti_version = %s, signature_method = %s, ' . + 'consumer_name = %s, consumer_version = %s, consumer_guid = %s, ' . 'profile = %s, tool_proxy = %s, settings = %s, ' . 'protected = %s, enabled = %s, enable_from = %s, enable_until = %s, last_access = %s, updated = %s ' . - 'WHERE consumer_pk = %d', $this->escape($key256), $this->escape($key), $this->escape($consumer->name), - $this->escape($consumer->secret), $this->escape($consumer->ltiVersion), $this->escape($consumer->signatureMethod), - $this->escape($consumer->consumerName), $this->escape($consumer->consumerVersion), - $this->escape($consumer->consumerGuid), $this->escape($profile), $this->escape($consumer->toolProxy), - $this->escape($settingsValue), $protected, $enabled, $this->escape($from), $this->escape($until), - $this->escape($last), $this->escape($now), $consumer->getRecordId()); + 'WHERE consumer_pk = %d', $this->escape($platform->getKey()), $this->escape($platform->name), + $this->escape($platform->secret), $this->escape($platform->platformId), $this->escape($platform->clientId), + $this->escape($platform->deploymentId), $this->escape($platform->rsaKey), $this->escape($platform->ltiVersion), + $this->escape($platform->signatureMethod), $this->escape($platform->consumerName), + $this->escape($platform->consumerVersion), $this->escape($platform->consumerGuid), $this->escape($profile), + $this->escape($platform->toolProxy), $this->escape($settingsValue), $protected, $enabled, $this->escape($from), + $this->escape($until), $this->escape($last), $this->escape($now), $platform->getRecordId()); } $ok = $this->executeQuery($sql); if ($ok) { if (empty($id)) { - $consumer->setRecordId($this->insert_id()); - $consumer->created = $time; + $platform->setRecordId($this->insert_id()); + $platform->created = $time; } - $consumer->updated = $time; + $platform->updated = $time; } return $ok; } /** - * Delete tool consumer object. + * Delete platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully deleted + * @return bool True if the platform object was successfully deleted */ - public function deleteToolConsumer($consumer) + public function deletePlatform($platform) { +// Delete any access token value for this consumer + $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' WHERE consumer_pk = %d', + $platform->getRecordId()); + $this->executeQuery($sql); + // Delete any nonce values for this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE consumer_pk = %d', - $consumer->getRecordId()); + $platform->getRecordId()); $this->executeQuery($sql); // Delete any outstanding share keys for resource links for this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' ' . "WHERE resource_link_pk IN (SELECT resource_link_pk FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' ' . - 'WHERE consumer_pk = %d)', $consumer->getRecordId()); + 'WHERE consumer_pk = %d)', $platform->getRecordId()); $this->executeQuery($sql); // Delete any outstanding share keys for resource links for contexts in this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' ' . "WHERE resource_link_pk IN (SELECT resource_link_pk FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk WHERE c.consumer_pk = %d)', - $consumer->getRecordId()); + $platform->getRecordId()); $this->executeQuery($sql); // Delete any users in resource links for this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . "WHERE resource_link_pk IN (SELECT resource_link_pk FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' ' . - 'WHERE consumer_pk = %d)', $consumer->getRecordId()); + 'WHERE consumer_pk = %d)', $platform->getRecordId()); $this->executeQuery($sql); // Delete any users in resource links for contexts in this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . "WHERE resource_link_pk IN (SELECT resource_link_pk FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk WHERE c.consumer_pk = %d)', - $consumer->getRecordId()); + $platform->getRecordId()); $this->executeQuery($sql); // Update any resource links for which this consumer is acting as a primary resource link @@ -220,7 +259,7 @@ public function deleteToolConsumer($consumer) 'SET primary_resource_link_pk = NULL, share_approved = NULL ' . 'WHERE primary_resource_link_pk IN ' . "(SELECT resource_link_pk FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' ' . - 'WHERE consumer_pk = %d)', $consumer->getRecordId()); + 'WHERE consumer_pk = %d)', $platform->getRecordId()); $ok = $this->executeQuery($sql); // Update any resource links for contexts in which this consumer is acting as a primary resource link @@ -229,68 +268,73 @@ public function deleteToolConsumer($consumer) 'WHERE primary_resource_link_pk IN ' . "(SELECT rl.resource_link_pk FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d)', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d)', $platform->getRecordId()); $ok = $this->executeQuery($sql); // Delete any resource links for this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' ' . - 'WHERE consumer_pk = %d', $consumer->getRecordId()); + 'WHERE consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any resource links for contexts in this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' ' . 'WHERE context_pk IN (' . "SELECT context_pk FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' ' . 'WHERE consumer_pk = %d)', - $consumer->getRecordId()); + $platform->getRecordId()); $this->executeQuery($sql); // Delete any contexts for this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' ' . - 'WHERE consumer_pk = %d', $consumer->getRecordId()); + 'WHERE consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete consumer - $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - 'WHERE consumer_pk = %d', $consumer->getRecordId()); + $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); if ($ok) { - $consumer->initialize(); + $platform->initialize(); } return $ok; } /** - * Load all tool consumers from the database. + * Load all platforms from the database. * - * @return ToolConsumer[] An array of the ToolConsumer objects + * @return Platform[] An array of the Platform objects */ - public function getToolConsumers() + public function getPlatforms() { $consumers = array(); - $sql = 'SELECT consumer_pk, consumer_key256, consumer_key, name, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . - 'profile, tool_proxy, settings, ' . - 'protected, enabled, enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + $sql = 'SELECT consumer_pk, consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'ORDER BY name'; $rsConsumers = $this->executeQuery($sql); if ($rsConsumers) { while ($row = pg_fetch_object($rsConsumers)) { - $key = empty($row->consumer_key) ? $row->consumer_key256 : $row->consumer_key; - $consumer = new ToolConsumer($key, $this); - $consumer->setRecordId(intval($row->consumer_pk)); - $consumer->name = $row->name; - $consumer->secret = $row->secret; - $consumer->ltiVersion = $row->lti_version; - $consumer->signatureMethod = $row->signature_method; - $consumer->consumerName = $row->consumer_name; - $consumer->consumerVersion = $row->consumer_version; - $consumer->consumerGuid = $row->consumer_guid; - $consumer->profile = json_decode($row->profile); - $consumer->toolProxy = $row->tool_proxy; + $platform = new Platform($this); + $platform->setRecordId(intval($row->consumer_pk)); + $platform->name = $row->name; + $platform->setKey($row->consumer_key); + $platform->secret = $row->secret; + $platform->platformId = $row->platform_id; + $platform->clientId = $row->client_id; + $platform->deploymentId = $row->deployment_id; + $platform->rsaKey = $row->public_key; + $platform->ltiVersion = $row->lti_version; + $platform->signatureMethod = $row->signature_method; + $platform->consumerName = $row->consumer_name; + $platform->consumerVersion = $row->consumer_version; + $platform->consumerGuid = $row->consumer_guid; + $platform->profile = json_decode($row->profile); + $platform->toolProxy = $row->tool_proxy; $settings = json_decode($row->settings, true); if (!is_array($settings)) { $settings = @unserialize($row->settings); // check for old serialized setting @@ -298,24 +342,25 @@ public function getToolConsumers() if (!is_array($settings)) { $settings = array(); } - $consumer->setSettings($settings); - $consumer->protected = (intval($row->protected) === 1); - $consumer->enabled = (intval($row->enabled) === 1); - $consumer->enableFrom = null; + $platform->setSettings($settings); + $platform->protected = (intval($row->protected) === 1); + $platform->enabled = (intval($row->enabled) === 1); + $platform->enableFrom = null; if (!is_null($row->enable_from)) { - $consumer->enableFrom = strtotime($row->enable_from); + $platform->enableFrom = strtotime($row->enable_from); } - $consumer->enableUntil = null; + $platform->enableUntil = null; if (!is_null($row->enable_until)) { - $consumer->enableUntil = strtotime($row->enable_until); + $platform->enableUntil = strtotime($row->enable_until); } - $consumer->lastAccess = null; + $platform->lastAccess = null; if (!is_null($row->last_access)) { - $consumer->lastAccess = strtotime($row->last_access); + $platform->lastAccess = strtotime($row->last_access); } - $consumer->created = strtotime($row->created); - $consumer->updated = strtotime($row->updated); - $consumers[] = $consumer; + $platform->created = strtotime($row->created); + $platform->updated = strtotime($row->updated); + $this->fixPlatformSettings($platform, false); + $consumers[] = $platform; } pg_free_result($rsConsumers); } @@ -344,15 +389,15 @@ public function loadContext($context) } else { $sql = sprintf('SELECT context_pk, consumer_pk, title, lti_context_id, type, settings, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' ' . - 'WHERE (consumer_pk = %d) AND (lti_context_id = %s)', $context->getConsumer()->getRecordId(), + 'WHERE (consumer_pk = %d) AND (lti_context_id = %s)', $context->getPlatform()->getRecordId(), $this->escape($context->ltiContextId)); } - $rs_context = $this->executeQuery($sql); - if ($rs_context) { - $row = pg_fetch_object($rs_context); + $rsContext = $this->executeQuery($sql); + if ($rsContext) { + $row = pg_fetch_object($rsContext); if ($row) { $context->setRecordId(intval($row->context_pk)); - $context->setConsumerId(intval($row->consumer_pk)); + $context->setPlatformId(intval($row->consumer_pk)); $context->title = $row->title; $context->ltiContextId = $row->lti_context_id; $context->type = $row->type; @@ -386,7 +431,7 @@ public function saveContext($context) $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $settingsValue = json_encode($context->getSettings()); $id = $context->getRecordId(); - $consumer_pk = $context->getConsumer()->getRecordId(); + $consumer_pk = $context->getPlatform()->getRecordId(); if (empty($id)) { $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' (consumer_pk, title, ' . 'lti_context_id, type, settings, created, updated) ' . @@ -489,7 +534,7 @@ public function loadResourceLink($resourceLink) "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' r LEFT OUTER JOIN ' . $this->dbTableNamePrefix . static::CONTEXT_TABLE_NAME . ' c ON r.context_pk = c.context_pk ' . ' WHERE ((r.consumer_pk = %d) OR (c.consumer_pk = %d)) AND (lti_resource_link_id = %s)', - $resourceLink->getConsumer()->getRecordId(), $resourceLink->getConsumer()->getRecordId(), + $resourceLink->getPlatform()->getRecordId(), $resourceLink->getPlatform()->getRecordId(), $this->escape($resourceLink->getId())); } $rsResourceLink = $this->executeQuery($sql); @@ -503,9 +548,9 @@ public function loadResourceLink($resourceLink) $resourceLink->setContextId(null); } if (!is_null($row->consumer_pk)) { - $resourceLink->setConsumerId(intval($row->consumer_pk)); + $resourceLink->setPlatformId(intval($row->consumer_pk)); } else { - $resourceLink->setConsumerId(null); + $resourceLink->setPlatformId(null); } $resourceLink->title = $row->title; $resourceLink->ltiResourceLinkId = $row->lti_resource_link_id; @@ -563,7 +608,7 @@ public function saveResourceLink($resourceLink) $consumerId = 'NULL'; $contextId = strval($resourceLink->getContextId()); } else { - $consumerId = strval($resourceLink->getConsumer()->getRecordId()); + $consumerId = strval($resourceLink->getPlatform()->getRecordId()); $contextId = 'NULL'; } $id = $resourceLink->getRecordId(); @@ -702,13 +747,13 @@ public function getSharesResourceLink($resourceLink) $sql = sprintf('SELECT c.consumer_name, r.resource_link_pk, r.title, r.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' AS r ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' AS c ON r.consumer_pk = c.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' AS c ON r.consumer_pk = c.consumer_pk ' . 'WHERE (r.primary_resource_link_pk = %d) ' . 'UNION ' . 'SELECT c2.consumer_name, r2.resource_link_pk, r2.title, r2.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' AS r2 ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' AS x ON r2.context_pk = x.context_pk ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' AS c2 ON x.consumer_pk = c2.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' AS c2 ON x.consumer_pk = c2.consumer_pk ' . 'WHERE (r2.primary_resource_link_pk = %d) ' . 'ORDER BY consumer_name, title', $resourceLink->getRecordId(), $resourceLink->getRecordId()); $rsShare = $this->executeQuery($sql); @@ -727,17 +772,17 @@ public function getSharesResourceLink($resourceLink) } ### -### ConsumerNonce methods +### PlatformNonce methods ### /** * Load nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully loaded */ - public function loadConsumerNonce($nonce) + public function loadPlatformNonce($nonce) { $ok = false; @@ -748,10 +793,10 @@ public function loadConsumerNonce($nonce) // Load the nonce $sql = sprintf("SELECT value AS T FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE (consumer_pk = %d) AND (value = %s)', - $nonce->getConsumer()->getRecordId(), $this->escape($nonce->getValue())); - $rs_nonce = $this->executeQuery($sql, false); - if ($rs_nonce) { - if (pg_fetch_object($rs_nonce)) { + $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue())); + $rsNonce = $this->executeQuery($sql, false); + if ($rsNonce) { + if (pg_fetch_object($rsNonce)) { $ok = true; } } @@ -762,15 +807,100 @@ public function loadConsumerNonce($nonce) /** * Save nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully saved */ - public function saveConsumerNonce($nonce) + public function savePlatformNonce($nonce) { $expires = date("{$this->dateFormat} {$this->timeFormat}", $nonce->expires); $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . " (consumer_pk, value, expires) VALUES (%d, %s, %s)", - $nonce->getConsumer()->getRecordId(), $this->escape($nonce->getValue()), $this->escape($expires)); + $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue()), $this->escape($expires)); + $ok = $this->executeQuery($sql); + + return $ok; + } + + /** + * Delete nonce object. + * + * @param PlatformNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully deleted + */ + public function deletePlatformNonce($nonce) + { + $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE (consumer_pk = %d) AND (value = %s)', + $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue())); + $ok = $this->executeQuery($sql); + + return $ok; + } + +### +### AccessToken methods +### + + /** + * Load access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the nonce object was successfully loaded + */ + public function loadAccessToken($accessToken) + { + $ok = false; + + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $sql = sprintf('SELECT scopes, token, expires, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = %d)', $consumer_pk); + $rsAccessToken = $this->executeQuery($sql, false); + if ($rsAccessToken) { + $row = pg_fetch_object($rsAccessToken); + if ($row) { + $scopes = json_decode($row->scopes, true); + if (!is_array($scopes)) { + $scopes = array(); + } + $accessToken->scopes = $scopes; + $accessToken->token = $row->token; + $accessToken->expires = strtotime($row->expires); + $accessToken->created = strtotime($row->created); + $accessToken->updated = strtotime($row->updated); + $ok = true; + } + } + + return $ok; + } + + /** + * Save access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the access token object was successfully saved + */ + public function saveAccessToken($accessToken) + { + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $scopes = json_encode($accessToken->scopes, JSON_UNESCAPED_SLASHES); + $token = $accessToken->token; + $expires = date("{$this->dateFormat} {$this->timeFormat}", $accessToken->expires); + $time = time(); + $now = date("{$this->dateFormat} {$this->timeFormat}", $time); + if (empty($accessToken->created)) { + $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + '(consumer_pk, scopes, token, expires, created, updated) ' . + 'VALUES (%d, %s, %s, %s, %s, %s)', $consumer_pk, $this->escape($scopes), $this->escape($token), + $this->escape($expires), $this->escape($now), $this->escape($now)); + } else { + $sql = sprintf('UPDATE ' . $this->dbTableNamePrefix . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'SET scopes = %s, token = %s, expires = %s, updated = %s WHERE consumer_pk = %d', $this->escape($scopes), + $this->escape($token), $this->escape($expires), $this->escape($now), $consumer_pk); + } $ok = $this->executeQuery($sql); return $ok; @@ -880,11 +1010,11 @@ public function loadUserResult($userresult) $sql = sprintf('SELECT user_result_pk, resource_link_pk, lti_user_id, lti_result_sourcedid, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'WHERE (resource_link_pk = %d) AND (lti_user_id = %s)', $userresult->getResourceLink()->getRecordId(), - $this->escape($userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY))); + $this->escape($userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY))); } - $rsUser = $this->executeQuery($sql); - if ($rsUser) { - $row = pg_fetch_object($rsUser); + $rsUserResult = $this->executeQuery($sql); + if ($rsUserResult) { + $row = pg_fetch_object($rsUserResult); if ($row) { $userresult->setRecordId(intval($row->user_result_pk)); $userresult->setResourceLinkId(intval($row->resource_link_pk)); @@ -914,8 +1044,8 @@ public function saveUserResult($userresult) $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' (resource_link_pk, ' . 'lti_user_id, lti_result_sourcedid, created, updated) ' . 'VALUES (%d, %s, %s, %s, %s)', $userresult->getResourceLink()->getRecordId(), - $this->escape($userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY)), - $this->escape($userresult->ltiResultSourcedId), $this->escape($now), $this->escape($now)); + $this->escape($userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY)), $this->escape($userresult->ltiResultSourcedId), + $this->escape($now), $this->escape($now)); } else { $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'SET lti_result_sourcedid = %s, updated = %s ' . diff --git a/src/DataConnector/DataConnector_sqlsrv.php b/src/DataConnector/DataConnector_sqlsrv.php index 8202310..dabab63 100644 --- a/src/DataConnector/DataConnector_sqlsrv.php +++ b/src/DataConnector/DataConnector_sqlsrv.php @@ -3,12 +3,12 @@ namespace ceLTIc\LTI\DataConnector; use ceLTIc\LTI; -use ceLTIc\LTI\ConsumerNonce; +use ceLTIc\LTI\PlatformNonce; use ceLTIc\LTI\Context; use ceLTIc\LTI\ResourceLink; use ceLTIc\LTI\ResourceLinkShare; use ceLTIc\LTI\ResourceLinkShareKey; -use ceLTIc\LTI\ToolConsumer; +use ceLTIc\LTI\Platform; use ceLTIc\LTI\UserResult; use ceLTIc\LTI\Util; @@ -27,173 +27,212 @@ class DataConnector_sqlsrv extends DataConnector { ### -### ToolConsumer methods +### Platform methods ### /** - * Load tool consumer object. + * Load platform object. * - * @param ToolConsumer $consumer ToolConsumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully loaded + * @return bool True if the platform object was successfully loaded */ - public function loadToolConsumer($consumer) + public function loadPlatform($platform) { $ok = false; - if (!is_null($consumer->getRecordId())) { - $sql = sprintf('SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + if (!is_null($platform->getRecordId())) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - "WHERE consumer_pk = %d", $consumer->getRecordId()); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + "WHERE consumer_pk = %d", $platform->getRecordId()); + } elseif (!empty($platform->platformId)) { + if (empty($platform->clientId)) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) ' . + 'GROUP BY platform_id, client_id', $this->escape($platform->platformId)); + } elseif (empty($platform->deploymentId)) { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) AND (client_id = %s)', $this->escape($platform->platformId), + $this->escape($platform->clientId)); + } else { + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + 'WHERE (platform_id = %s) AND (client_id = %s) AND (deployment_id = %s)', $this->escape($platform->platformId), + $this->escape($platform->clientId), $this->escape($platform->deploymentId)); + } } else { - $key256 = static::getConsumerKey($consumer->getKey()); - $sql = sprintf('SELECT consumer_pk, name, consumer_key256, consumer_key, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . + $sql = sprintf('SELECT consumer_pk, name, consumer_key, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . - "WHERE consumer_key256 = %s", $this->escape($key256)); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . + "WHERE consumer_key = %s", $this->escape($platform->getKey())); } $rsConsumer = $this->executeQuery($sql); if ($rsConsumer) { - while ($row = sqlsrv_fetch_object($rsConsumer)) { - if (empty($key256) || empty($row->consumer_key) || ($consumer->getKey() === $row->consumer_key)) { - $consumer->setRecordId(intval($row->consumer_pk)); - $consumer->name = $row->name; - $consumer->setkey(empty($row->consumer_key) ? $row->consumer_key256 : $row->consumer_key); - $consumer->secret = $row->secret; - $consumer->ltiVersion = $row->lti_version; - $consumer->signatureMethod = $row->signature_method; - $consumer->consumerName = $row->consumer_name; - $consumer->consumerVersion = $row->consumer_version; - $consumer->consumerGuid = $row->consumer_guid; - $consumer->profile = json_decode($row->profile); - $consumer->toolProxy = $row->tool_proxy; - $settings = json_decode($row->settings, true); - if (!is_array($settings)) { - $settings = @unserialize($row->settings); // check for old serialized setting - } - if (!is_array($settings)) { - $settings = array(); - } - $consumer->setSettings($settings); - $consumer->protected = (intval($row->protected) === 1); - $consumer->enabled = (intval($row->enabled) === 1); - $consumer->enableFrom = null; - if (!is_null($row->enable_from)) { - $consumer->enableFrom = date_timestamp_get($row->enable_from); - } - $consumer->enableUntil = null; - if (!is_null($row->enable_until)) { - $consumer->enableUntil = date_timestamp_get($row->enable_until); - } - $consumer->lastAccess = null; - if (!is_null($row->last_access)) { - $consumer->lastAccess = date_timestamp_get($row->last_access); - } - $consumer->created = date_timestamp_get($row->created); - $consumer->updated = date_timestamp_get($row->updated); - $ok = true; - break; + $row = sqlsrv_fetch_object($rsConsumer); + if ($row) { + $platform->setRecordId(intval($row->consumer_pk)); + $platform->name = $row->name; + $platform->setkey($row->consumer_key); + $platform->secret = $row->secret; + $platform->platformId = $row->platform_id; + $platform->clientId = $row->client_id; + $platform->deploymentId = $row->deployment_id; + $platform->rsaKey = $row->public_key; + $platform->ltiVersion = $row->lti_version; + $platform->signatureMethod = $row->signature_method; + $platform->consumerName = $row->consumer_name; + $platform->consumerVersion = $row->consumer_version; + $platform->consumerGuid = $row->consumer_guid; + $platform->profile = json_decode($row->profile); + $platform->toolProxy = $row->tool_proxy; + $settings = json_decode($row->settings, true); + if (!is_array($settings)) { + $settings = @unserialize($row->settings); // check for old serialized setting } + if (!is_array($settings)) { + $settings = array(); + } + $platform->setSettings($settings); + $platform->protected = (intval($row->protected) === 1); + $platform->enabled = (intval($row->enabled) === 1); + $platform->enableFrom = null; + if (!is_null($row->enable_from)) { + $platform->enableFrom = date_timestamp_get($row->enable_from); + } + $platform->enableUntil = null; + if (!is_null($row->enable_until)) { + $platform->enableUntil = date_timestamp_get($row->enable_until); + } + $platform->lastAccess = null; + if (!is_null($row->last_access)) { + $platform->lastAccess = date_timestamp_get($row->last_access); + } + $platform->created = date_timestamp_get($row->created); + $platform->updated = date_timestamp_get($row->updated); + $this->fixPlatformSettings($platform, false); + $ok = true; } - sqlsrv_free_stmt($rsConsumer); } return $ok; } /** - * Save tool consumer object. + * Save platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully saved + * @return bool True if the platform object was successfully saved */ - public function saveToolConsumer($consumer) + public function savePlatform($platform) { - $id = $consumer->getRecordId(); - $key = $consumer->getKey(); - $key256 = static::getConsumerKey($key); - if ($key === $key256) { - $key = null; - } - $protected = ($consumer->protected) ? 1 : 0; - $enabled = ($consumer->enabled) ? 1 : 0; - $profile = (!empty($consumer->profile)) ? json_encode($consumer->profile) : null; - $settingsValue = json_encode($consumer->getSettings()); + $id = $platform->getRecordId(); + $protected = ($platform->protected) ? 1 : 0; + $enabled = ($platform->enabled) ? 1 : 0; + $profile = (!empty($platform->profile)) ? json_encode($platform->profile) : null; + $this->fixPlatformSettings($platform, true); + $settingsValue = json_encode($platform->getSettings()); + $this->fixPlatformSettings($platform, false); $time = time(); $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $from = null; - if (!is_null($consumer->enableFrom)) { - $from = date_format($consumer->enableFrom, "{$this->dateFormat} {$this->timeFormat}"); + if (!is_null($platform->enableFrom)) { + $from = date_format($platform->enableFrom, "{$this->dateFormat} {$this->timeFormat}"); } $until = null; - if (!is_null($consumer->enableUntil)) { - $until = date_format($consumer->enableUntil, "{$this->dateFormat} {$this->timeFormat}"); + if (!is_null($platform->enableUntil)) { + $until = date_format($platform->enableUntil, "{$this->dateFormat} {$this->timeFormat}"); } $last = null; - if (!is_null($consumer->lastAccess)) { - $last = date_format($consumer->lastAccess, $this->dateFormat); + if (!is_null($platform->lastAccess)) { + $last = date_format($platform->lastAccess, $this->dateFormat); } if (empty($id)) { - $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' (consumer_key256, consumer_key, name, ' . - 'secret, lti_version, signature_method, consumer_name, consumer_version, consumer_guid, profile, ' . - 'tool_proxy, settings, protected, enabled, ' . + $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' (consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . 'enable_from, enable_until, last_access, created, updated) ' . - 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %d, %d, %s, %s, %s, %s, %s)', $this->escape($key256), - $this->escape($key), $this->escape($consumer->name), $this->escape($consumer->secret), - $this->escape($consumer->ltiVersion), $this->escape($consumer->signatureMethod), - $this->escape($consumer->consumerName), $this->escape($consumer->consumerVersion), - $this->escape($consumer->consumerGuid), $this->escape($profile), $this->escape($consumer->toolProxy), + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %d, %d, %s, %s, %s, %s, %s)', + $this->escape($platform->getKey()), $this->escape($platform->name), $this->escape($platform->secret), + $this->escape($platform->platformId), $this->escape($platform->clientId), $this->escape($platform->deploymentId), + $this->escape($platform->rsaKey), $this->escape($platform->ltiVersion), $this->escape($platform->signatureMethod), + $this->escape($platform->consumerName), $this->escape($platform->consumerVersion), + $this->escape($platform->consumerGuid), $this->escape($profile), $this->escape($platform->toolProxy), $this->escape($settingsValue), $protected, $enabled, $this->escape($from), $this->escape($until), $this->escape($last), $this->escape($now), $this->escape($now)); } else { - $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' SET ' . - 'consumer_key256 = %s, consumer_key = %s, ' . - 'name = %s, secret= %s, lti_version = %s, signature_method = %s, consumer_name = %s, consumer_version = %s, consumer_guid = %s, ' . + $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' SET ' . + 'consumer_key = %s, name = %s, secret= %s, ' . + 'platform_id = %s, client_id = %s, deployment_id = %s, public_key = %s, ' . + 'lti_version = %s, signature_method = %s, ' . + 'consumer_name = %s, consumer_version = %s, consumer_guid = %s, ' . 'profile = %s, tool_proxy = %s, settings = %s, ' . 'protected = %d, enabled = %d, enable_from = %s, enable_until = %s, last_access = %s, updated = %s ' . - 'WHERE consumer_pk = %d', $this->escape($key256), $this->escape($key), $this->escape($consumer->name), - $this->escape($consumer->secret), $this->escape($consumer->ltiVersion), $this->escape($consumer->signatureMethod), - $this->escape($consumer->consumerName), $this->escape($consumer->consumerVersion), - $this->escape($consumer->consumerGuid), $this->escape($profile), $this->escape($consumer->toolProxy), - $this->escape($settingsValue), $protected, $enabled, $this->escape($from), $this->escape($until), - $this->escape($last), $this->escape($now), $consumer->getRecordId()); + 'WHERE consumer_pk = %d', $this->escape($platform->getKey()), $this->escape($platform->name), + $this->escape($platform->secret), $this->escape($platform->platformId), $this->escape($platform->clientId), + $this->escape($platform->deploymentId), $this->escape($platform->rsaKey), $this->escape($platform->ltiVersion), + $this->escape($platform->signatureMethod), $this->escape($platform->consumerName), + $this->escape($platform->consumerVersion), $this->escape($platform->consumerGuid), $this->escape($profile), + $this->escape($platform->toolProxy), $this->escape($settingsValue), $protected, $enabled, $this->escape($from), + $this->escape($until), $this->escape($last), $this->escape($now), $platform->getRecordId()); } $ok = $this->executeQuery($sql); if ($ok) { if (empty($id)) { - $consumer->setRecordId($this->insert_id()); - $consumer->created = $time; + $platform->setRecordId($this->insert_id()); + $platform->created = $time; } - $consumer->updated = $time; + $platform->updated = $time; } return $ok; } /** - * Delete tool consumer object. + * Delete platform object. * - * @param ToolConsumer $consumer Consumer object + * @param Platform $platform Platform object * - * @return bool True if the tool consumer object was successfully deleted + * @return bool True if the platform object was successfully deleted */ - public function deleteToolConsumer($consumer) + public function deletePlatform($platform) { +// Delete any access token value for this consumer + $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' WHERE consumer_pk = %d', + $platform->getRecordId()); + $this->executeQuery($sql); + // Delete any nonce values for this consumer $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE consumer_pk = %d', - $consumer->getRecordId()); + $platform->getRecordId()); $this->executeQuery($sql); // Delete any outstanding share keys for resource links for this consumer $sql = sprintf('DELETE sk ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' sk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON sk.resource_link_pk = rl.resource_link_pk ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any outstanding share keys for resource links for contexts in this consumer @@ -201,14 +240,14 @@ public function deleteToolConsumer($consumer) "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' sk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON sk.resource_link_pk = rl.resource_link_pk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any users in resource links for this consumer $sql = sprintf('DELETE u ' . "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' u ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON u.resource_link_pk = rl.resource_link_pk ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any users in resource links for contexts in this consumer @@ -216,7 +255,7 @@ public function deleteToolConsumer($consumer) "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' u ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON u.resource_link_pk = rl.resource_link_pk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Update any resource links for which this consumer is acting as a primary resource link @@ -224,7 +263,7 @@ public function deleteToolConsumer($consumer) 'SET prl.primary_resource_link_pk = NULL, prl.share_approved = NULL ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' prl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON prl.primary_resource_link_pk = rl.resource_link_pk ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); // Update any resource links for contexts in which this consumer is acting as a primary resource link @@ -233,71 +272,76 @@ public function deleteToolConsumer($consumer) "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' prl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ON prl.primary_resource_link_pk = rl.resource_link_pk ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); // Delete any resource links for this consumer $sql = sprintf('DELETE rl ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ' . - 'WHERE rl.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE rl.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any resource links for contexts in this consumer $sql = sprintf('DELETE rl ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' rl ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ON rl.context_pk = c.context_pk ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete any contexts for this consumer $sql = sprintf('DELETE c ' . "FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' c ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $this->executeQuery($sql); // Delete consumer $sql = sprintf('DELETE c ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' c ' . - 'WHERE c.consumer_pk = %d', $consumer->getRecordId()); + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' c ' . + 'WHERE c.consumer_pk = %d', $platform->getRecordId()); $ok = $this->executeQuery($sql); if ($ok) { - $consumer->initialize(); + $platform->initialize(); } return $ok; } /** - * Load all tool consumers from the database. + * Load all platforms from the database. * - * @return ToolConsumer[] An array of the ToolConsumer objects + * @return Platform[] An array of the Platform objects */ - public function getToolConsumers() + public function getPlatforms() { $consumers = array(); - $sql = 'SELECT consumer_pk, consumer_key256, consumer_key, name, secret, lti_version, ' . - 'signature_method, consumer_name, consumer_version, consumer_guid, ' . - 'profile, tool_proxy, settings, ' . - 'protected, enabled, enable_from, enable_until, last_access, created, updated ' . - "FROM {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' ' . + $sql = 'SELECT consumer_pk, consumer_key, name, secret, ' . + 'platform_id, client_id, deployment_id, public_key, ' . + 'lti_version, signature_method, consumer_name, consumer_version, consumer_guid, ' . + 'profile, tool_proxy, settings, protected, enabled, ' . + 'enable_from, enable_until, last_access, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' ' . 'ORDER BY name'; $rsConsumers = $this->executeQuery($sql); if ($rsConsumers) { while ($row = sqlsrv_fetch_object($rsConsumers)) { - $key = empty($row->consumer_key) ? $row->consumer_key256 : $row->consumer_key; - $consumer = new ToolConsumer($key, $this); - $consumer->setRecordId(intval($row->consumer_pk)); - $consumer->name = $row->name; - $consumer->secret = $row->secret; - $consumer->ltiVersion = $row->lti_version; - $consumer->signatureMethod = $row->signature_method; - $consumer->consumerName = $row->consumer_name; - $consumer->consumerVersion = $row->consumer_version; - $consumer->consumerGuid = $row->consumer_guid; - $consumer->profile = json_decode($row->profile); - $consumer->toolProxy = $row->tool_proxy; + $platform = new Platform($this); + $platform->setRecordId(intval($row->consumer_pk)); + $platform->name = $row->name; + $platform->setKey($row->consumer_key); + $platform->secret = $row->secret; + $platform->platformId = $row->platform_id; + $platform->clientId = $row->client_id; + $platform->deploymentId = $row->deployment_id; + $platform->rsaKey = $row->public_key; + $platform->ltiVersion = $row->lti_version; + $platform->signatureMethod = $row->signature_method; + $platform->consumerName = $row->consumer_name; + $platform->consumerVersion = $row->consumer_version; + $platform->consumerGuid = $row->consumer_guid; + $platform->profile = json_decode($row->profile); + $platform->toolProxy = $row->tool_proxy; $settings = json_decode($row->settings, true); if (!is_array($settings)) { $settings = @unserialize($row->settings); // check for old serialized setting @@ -305,24 +349,25 @@ public function getToolConsumers() if (!is_array($settings)) { $settings = array(); } - $consumer->setSettings($settings); - $consumer->protected = (intval($row->protected) === 1); - $consumer->enabled = (intval($row->enabled) === 1); - $consumer->enableFrom = null; + $platform->setSettings($settings); + $platform->protected = (intval($row->protected) === 1); + $platform->enabled = (intval($row->enabled) === 1); + $platform->enableFrom = null; if (!is_null($row->enable_from)) { - $consumer->enableFrom = date_timestamp_get($row->enable_from); + $platform->enableFrom = date_timestamp_get($row->enable_from); } - $consumer->enableUntil = null; + $platform->enableUntil = null; if (!is_null($row->enable_until)) { - $consumer->enableUntil = date_timestamp_get($row->enable_until); + $platform->enableUntil = date_timestamp_get($row->enable_until); } - $consumer->lastAccess = null; + $platform->lastAccess = null; if (!is_null($row->last_access)) { - $consumer->lastAccess = date_timestamp_get($row->last_access); + $platform->lastAccess = date_timestamp_get($row->last_access); } - $consumer->created = date_timestamp_get($row->created); - $consumer->updated = date_timestamp_get($row->updated); - $consumers[] = $consumer; + $platform->created = date_timestamp_get($row->created); + $platform->updated = date_timestamp_get($row->updated); + $this->fixPlatformSettings($platform, false); + $consumers[] = $platform; } sqlsrv_free_stmt($rsConsumers); } @@ -351,15 +396,15 @@ public function loadContext($context) } else { $sql = sprintf('SELECT context_pk, consumer_pk, title, lti_context_id, type, settings, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' ' . - 'WHERE (consumer_pk = %d) AND (lti_context_id = %s)', $context->getConsumer()->getRecordId(), + 'WHERE (consumer_pk = %d) AND (lti_context_id = %s)', $context->getPlatform()->getRecordId(), $this->escape($context->ltiContextId)); } - $rs_context = $this->executeQuery($sql); - if ($rs_context) { - $row = sqlsrv_fetch_object($rs_context); + $rsContext = $this->executeQuery($sql); + if ($rsContext) { + $row = sqlsrv_fetch_object($rsContext); if ($row) { $context->setRecordId(intval($row->context_pk)); - $context->setConsumerId(intval($row->consumer_pk)); + $context->setPlatformId(intval($row->consumer_pk)); $context->title = $row->title; $context->ltiContextId = $row->lti_context_id; $context->type = $row->type; @@ -393,7 +438,7 @@ public function saveContext($context) $now = date("{$this->dateFormat} {$this->timeFormat}", $time); $settingsValue = json_encode($context->getSettings()); $id = $context->getRecordId(); - $consumer_pk = $context->getConsumer()->getRecordId(); + $consumer_pk = $context->getPlatform()->getRecordId(); if (empty($id)) { $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' (consumer_pk, title, ' . 'lti_context_id, type, settings, created, updated) ' . @@ -500,7 +545,7 @@ public function loadResourceLink($resourceLink) "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' r LEFT OUTER JOIN ' . $this->dbTableNamePrefix . static::CONTEXT_TABLE_NAME . ' c ON r.context_pk = c.context_pk ' . ' WHERE ((r.consumer_pk = %d) OR (c.consumer_pk = %d)) AND (lti_resource_link_id = %s)', - $resourceLink->getConsumer()->getRecordId(), $resourceLink->getConsumer()->getRecordId(), + $resourceLink->getPlatform()->getRecordId(), $resourceLink->getPlatform()->getRecordId(), $this->escape($resourceLink->getId())); } $rsResourceLink = $this->executeQuery($sql); @@ -514,9 +559,9 @@ public function loadResourceLink($resourceLink) $resourceLink->setContextId(null); } if (!is_null($row->consumer_pk)) { - $resourceLink->setConsumerId(intval($row->consumer_pk)); + $resourceLink->setPlatformId(intval($row->consumer_pk)); } else { - $resourceLink->setConsumerId(null); + $resourceLink->setPlatformId(null); } $resourceLink->title = $row->title; $resourceLink->ltiResourceLinkId = $row->lti_resource_link_id; @@ -574,7 +619,7 @@ public function saveResourceLink($resourceLink) $consumerId = 'NULL'; $contextId = strval($resourceLink->getContextId()); } else { - $consumerId = strval($resourceLink->getConsumer()->getRecordId()); + $consumerId = strval($resourceLink->getPlatform()->getRecordId()); $contextId = 'NULL'; } $id = $resourceLink->getRecordId(); @@ -713,13 +758,13 @@ public function getSharesResourceLink($resourceLink) $sql = sprintf('SELECT c.consumer_name, r.resource_link_pk, r.title, r.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' AS r ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' AS c ON r.consumer_pk = c.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' AS c ON r.consumer_pk = c.consumer_pk ' . 'WHERE (r.primary_resource_link_pk = %d) ' . 'UNION ' . 'SELECT c2.consumer_name, r2.resource_link_pk, r2.title, r2.share_approved ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_TABLE_NAME . ' AS r2 ' . "INNER JOIN {$this->dbTableNamePrefix}" . static::CONTEXT_TABLE_NAME . ' AS x ON r2.context_pk = x.context_pk ' . - "INNER JOIN {$this->dbTableNamePrefix}" . static::CONSUMER_TABLE_NAME . ' AS c2 ON x.consumer_pk = c2.consumer_pk ' . + "INNER JOIN {$this->dbTableNamePrefix}" . static::PLATFORM_TABLE_NAME . ' AS c2 ON x.consumer_pk = c2.consumer_pk ' . 'WHERE (r2.primary_resource_link_pk = %d) ' . 'ORDER BY consumer_name, title', $resourceLink->getRecordId(), $resourceLink->getRecordId()); $rsShare = $this->executeQuery($sql); @@ -738,17 +783,17 @@ public function getSharesResourceLink($resourceLink) } ### -### ConsumerNonce methods +### PlatformNonce methods ### /** * Load nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully loaded */ - public function loadConsumerNonce($nonce) + public function loadPlatformNonce($nonce) { $ok = false; @@ -758,11 +803,11 @@ public function loadConsumerNonce($nonce) $this->executeQuery($sql); // Load the nonce - $sql = sprintf("SELECT value AS T FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' WHERE (consumer_pk = %d) AND (value = %s)', - $nonce->getConsumer()->getRecordId(), $this->escape($nonce->getValue())); - $rs_nonce = $this->executeQuery($sql, false); - if ($rs_nonce) { - if (sqlsrv_fetch_object($rs_nonce)) { + $sql = sprintf("SELECT value AS T FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = %d) AND (value = %s)', $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue())); + $rsNonce = $this->executeQuery($sql, false); + if ($rsNonce) { + if (sqlsrv_fetch_object($rsNonce)) { $ok = true; } } @@ -773,15 +818,100 @@ public function loadConsumerNonce($nonce) /** * Save nonce object. * - * @param ConsumerNonce $nonce Nonce object + * @param PlatformNonce $nonce Nonce object * * @return bool True if the nonce object was successfully saved */ - public function saveConsumerNonce($nonce) + public function savePlatformNonce($nonce) { $expires = date("{$this->dateFormat} {$this->timeFormat}", $nonce->expires); $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . " (consumer_pk, value, expires) VALUES (%d, %s, %s)", - $nonce->getConsumer()->getRecordId(), $this->escape($nonce->getValue()), $this->escape($expires)); + $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue()), $this->escape($expires)); + $ok = $this->executeQuery($sql); + + return $ok; + } + + /** + * Delete nonce object. + * + * @param PlatformNonce $nonce Nonce object + * + * @return bool True if the nonce object was successfully deleted + */ + public function deletePlatformNonce($nonce) + { + $sql = sprintf("DELETE FROM {$this->dbTableNamePrefix}" . static::NONCE_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = %d) AND (value = %s)', $nonce->getPlatform()->getRecordId(), $this->escape($nonce->getValue())); + $ok = $this->executeQuery($sql); + + return $ok; + } + +### +### AccessToken methods +### + + /** + * Load access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the nonce object was successfully loaded + */ + public function loadAccessToken($accessToken) + { + $ok = false; + + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $sql = sprintf('SELECT scopes, token, expires, created, updated ' . + "FROM {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'WHERE (consumer_pk = %d)', $consumer_pk); + $rsAccessToken = $this->executeQuery($sql, false); + if ($rsAccessToken) { + $row = sqlsrv_fetch_object($rsAccessToken); + if ($row) { + $scopes = json_decode($row->scopes, true); + if (!is_array($scopes)) { + $scopes = array(); + } + $accessToken->scopes = $scopes; + $accessToken->token = $row->token; + $accessToken->expires = date_timestamp_get($row->expires); + $accessToken->created = date_timestamp_get($row->created); + $accessToken->updated = date_timestamp_get($row->updated); + $ok = true; + } + } + + return $ok; + } + + /** + * Save access token object. + * + * @param AccessToken $accessToken Access token object + * + * @return bool True if the access token object was successfully saved + */ + public function saveAccessToken($accessToken) + { + $consumer_pk = $accessToken->getPlatform()->getRecordId(); + $scopes = json_encode($accessToken->scopes, JSON_UNESCAPED_SLASHES); + $token = $accessToken->token; + $expires = date("{$this->dateFormat} {$this->timeFormat}", $accessToken->expires); + $time = time(); + $now = date("{$this->dateFormat} {$this->timeFormat}", $time); + if (empty($accessToken->created)) { + $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + '(consumer_pk, scopes, token, expires, created, updated) ' . + 'VALUES (%d, %s, %s, %s, %s, %s)', $consumer_pk, $this->escape($scopes), $this->escape($token), + $this->escape($expires), $this->escape($now), $this->escape($now)); + } else { + $sql = sprintf('UPDATE ' . $this->dbTableNamePrefix . static::ACCESS_TOKEN_TABLE_NAME . ' ' . + 'SET scopes = %s, token = %s, expires = %s, updated = %s WHERE consumer_pk = %d', $this->escape($scopes), + $this->escape($token), $this->escape($expires), $this->escape($now), $consumer_pk); + } $ok = $this->executeQuery($sql); return $ok; @@ -808,7 +938,6 @@ public function loadResourceLinkShareKey($shareKey) $this->executeQuery($sql); // Load share key -// $id = $this->escape_string($this->db, $shareKey->getId()); $id = $shareKey->getId(); $sql = 'SELECT resource_link_pk, auto_approve, expires ' . "FROM {$this->dbTableNamePrefix}" . static::RESOURCE_LINK_SHARE_KEY_TABLE_NAME . ' ' . @@ -892,11 +1021,11 @@ public function loadUserResult($userresult) $sql = sprintf('SELECT user_result_pk, resource_link_pk, lti_user_id, lti_result_sourcedid, created, updated ' . "FROM {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'WHERE (resource_link_pk = %d) AND (lti_user_id = %s)', $userresult->getResourceLink()->getRecordId(), - $this->escape($userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY))); + $this->escape($userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY))); } - $rsUser = $this->executeQuery($sql); - if ($rsUser) { - $row = sqlsrv_fetch_object($rsUser); + $rsUserResult = $this->executeQuery($sql); + if ($rsUserResult) { + $row = sqlsrv_fetch_object($rsUserResult); if ($row) { $userresult->setRecordId(intval($row->user_result_pk)); $userresult->setResourceLinkId(intval($row->resource_link_pk)); @@ -926,8 +1055,8 @@ public function saveUserResult($userresult) $sql = sprintf("INSERT INTO {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' (resource_link_pk, ' . 'lti_user_id, lti_result_sourcedid, created, updated) ' . 'VALUES (%d, %s, %s, %s, %s)', $userresult->getResourceLink()->getRecordId(), - $this->escape($userresult->getId(LTI\ToolProvider::ID_SCOPE_ID_ONLY)), - $this->escape($userresult->ltiResultSourcedId), $this->escape($now), $this->escape($now)); + $this->escape($userresult->getId(LTI\Tool::ID_SCOPE_ID_ONLY)), $this->escape($userresult->ltiResultSourcedId), + $this->escape($now), $this->escape($now)); } else { $sql = sprintf("UPDATE {$this->dbTableNamePrefix}" . static::USER_RESULT_TABLE_NAME . ' ' . 'SET lti_result_sourcedid = %s, updated = %s ' . diff --git a/src/HTTPMessage.php b/src/HTTPMessage.php index 3547d8f..855f0c3 100644 --- a/src/HTTPMessage.php +++ b/src/HTTPMessage.php @@ -2,7 +2,7 @@ namespace ceLTIc\LTI; -use ceLTIc\LTI\Http\HttpMessage; +use ceLTIc\LTI\Http; /** * Class to represent an HTTP message request @@ -14,7 +14,7 @@ * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 */ -class HTTPMessage extends HttpMessage +class HTTPMessage extends Http\HttpMessage { /** @@ -28,7 +28,7 @@ class HTTPMessage extends HttpMessage function __construct($url, $method = 'GET', $params = null, $header = null) { parent::__construct($url, $method, $params, $header); - Util::log('Class ceLTIc\LTI\HTTPMessage has been deprecated; please use ceLTIc\LTI\Http\HttpMessage instead.', true); + Util::logDebug('Class ceLTIc\LTI\HTTPMessage has been deprecated; please use ceLTIc\LTI\Http\HttpMessage instead.', true); } } diff --git a/src/Http/HttpMessage.php b/src/Http/HttpMessage.php index 0602859..3be7790 100644 --- a/src/Http/HttpMessage.php +++ b/src/Http/HttpMessage.php @@ -162,12 +162,13 @@ public static function getHttpClient() public function send() { $client = self::getHttpClient(); - $this->ok = false; if (empty($client)) { + $this->ok = false; $message = 'No HTTP client interface is available'; $this->error = $message; Util::logError($message, true); } elseif (empty($this->url)) { + $this->ok = false; $message = 'No URL provided for HTTP request'; $this->error = $message; Util::logError($message, true); diff --git a/src/Jwt/ClientInterface.php b/src/Jwt/ClientInterface.php new file mode 100644 index 0000000..01a9660 --- /dev/null +++ b/src/Jwt/ClientInterface.php @@ -0,0 +1,116 @@ + + * @copyright SPV Software Products + * @license GNU Lesser General Public License, version 3 () + */ +interface ClientInterface +{ + + /** + * Check if a JWT is defined. + * + * @return bool True if a JWT is defined + */ + public function hasJwt(); + + /** + * Load a JWT from a string. + * + * @param string $jwtString JWT string + * + * @return bool True if the JWT was successfully loaded + */ + public function load($jwtString); + + /** + * Check whether a JWT has a header with the specified name. + * + * @param string $name Header name + * + * @return bool True if the JWT has a header of the specified name + */ + public function hasHeader($name); + + /** + * Get the value of the header with the specified name. + * + * @param string $name Header name + * @param string $defaultValue Default value + * + * @return string The value of the header with the specified name, or the default value if it does not exist + */ + public function getHeader($name, $defaultValue = null); + + /** + * Get the value of the headers. + * + * @return array The value of the headers + */ + public function getHeaders(); + + /** + * Check whether a JWT has a claim with the specified name. + * + * @param string $name Claim name + * + * @return bool True if the JWT has a claim of the specified name + */ + public function hasClaim($name); + + /** + * Get the value of the claim with the specified name. + * + * @param string $name Claim name + * @param string $defaultValue Default value + * + * @return string The value of the claim with the specified name, or the default value if it does not exist + */ + public function getClaim($name, $defaultValue = null); + + /** + * Get the value of the payload. + * + * @return array The value of the payload + */ + public function getPayload(); + + /** + * Verify the signature of the JWT. + * + * @param string $publicKey Public key of issuer + * @param string $jku JSON Web Key URL of issuer (optional) + * + * @return bool True if the JWT has a valid signature + */ + public function verify($publicKey, $jku = null); + + /** + * Sign the JWT. + * + * @param array $payload Payload + * @param string $signatureMethod Signature method + * @param string $privateKey Private key in PEM format + * @param string $kid Key ID (optional) + * @param string $jku JSON Web Key URL (optional) * + * + * @return string Signed JWT + */ + public static function sign($payload, $signatureMethod, $privateKey, $kid = null, $jku = null); + + /** + * Get the public JWKS from a private key. + * + * @param string $privateKey Private key in PEM format + * @param string $signatureMethod Signature method + * @param string $kid Key ID (optional) + * + * @return array JWKS keys + */ + public static function getJWKS($privateKey, $signatureMethod, $kid); +} diff --git a/src/Jwt/FirebaseClient.php b/src/Jwt/FirebaseClient.php new file mode 100644 index 0000000..2345de9 --- /dev/null +++ b/src/Jwt/FirebaseClient.php @@ -0,0 +1,256 @@ + + * @copyright SPV Software Products + * @license GNU Lesser General Public License, version 3 () + */ +class FirebaseClient implements ClientInterface +{ + + private $jwtString = null; + private $jwtHeaders = null; + private $jwtPayload = null; + + /** + * Check if a JWT is defined. + * + * @return bool True if a JWT is defined + */ + public function hasJwt() + { + return !empty($this->jwtString); + } + + /** + * Load a JWT from a string. + * + * @param string $jwtString JWT string + * + * @return bool True if the JWT was successfully loaded + */ + public function load($jwtString) + { + $sections = explode('.', $jwtString); + $ok = count($sections) === 3; + if ($ok) { + $headers = json_decode(JWT::urlsafeB64Decode($sections[0])); + $payload = json_decode(JWT::urlsafeB64Decode($sections[1])); + $ok = !is_null($headers) && !is_null($payload); + } + if ($ok) { + $this->jwtString = $jwtString; + $this->jwtHeaders = $headers; + $this->jwtPayload = $payload; + } else { + $this->jwtString = null; + $this->jwtHeaders = null; + $this->jwtPayload = null; + } + + return $ok; + } + + /** + * Check whether a JWT has a header with the specified name. + * + * @param string $name Header name + * + * @return bool True if the JWT has a header of the specified name + */ + public function hasHeader($name) + { + return !empty($this->jwtHeaders) && isset($this->jwtHeaders->{$name}); + } + + /** + * Get the value of the header with the specified name. + * + * @param string $name Header name + * @param string $defaultValue Default value + * + * @return string The value of the header with the specified name, or the default value if it does not exist + */ + public function getHeader($name, $defaultValue = null) + { + if ($this->hasHeader($name)) { + $value = $this->jwtHeaders->{$name}; + } else { + $value = $defaultValue; + } + + return $value; + } + + /** + * Get the value of the headers. + * + * @return array The value of the headers + */ + public function getHeaders() + { + return $this->jwtHeaders; + } + + /** + * Check whether a JWT has a claim with the specified name. + * + * @param string $name Claim name + * + * @return bool True if the JWT has a claim of the specified name + */ + public function hasClaim($name) + { + return !empty($this->jwtPayload) && isset($this->jwtPayload->{$name}); + } + + /** + * Get the value of the claim with the specified name. + * + * @param string $name Claim name + * @param string $defaultValue Default value + * + * @return string|array The value of the claim with the specified name, or the default value if it does not exist + */ + public function getClaim($name, $defaultValue = null) + { + if ($this->hasClaim($name)) { + $value = $this->jwtPayload->{$name}; + } else { + $value = $defaultValue; + } + if (is_object($value)) { + $value = (array) $value; + } + + return $value; + } + + /** + * Get the value of the payload. + * + * @return array The value of the payload + */ + public function getPayload() + { + return $this->jwtPayload; + } + + /** + * Verify the signature of the JWT. + * + * @param string $publicKey Public key of issuer + * @param string $jku JSON Web Key URL of issuer (optional) + * + * @return bool True if the JWT has a valid signature + */ + public function verify($publicKey, $jku = null) + { + $ok = false; + $hasPublicKey = !empty($publicKey); + if ($hasPublicKey) { + if (is_string($publicKey)) { + $json = json_decode($publicKey, true); + if (!is_null($json)) { + try { + $jwks = array('keys' => array($json)); + $publicKey = JWK::parseKeySet($jwks); + } catch (\Exception $e) { + + } + } + } + } elseif (!empty($jku)) { + $publicKey = $this->fetchPublicKey($jku); + } + $retry = false; + do { + try { + JWT::decode($this->jwtString, $publicKey, array('RS256', 'RS384', 'RS512')); + $ok = true; + } catch (\Exception $e) { + Util::logError($e->getMessage()); + if (!$retry && $hasPublicKey && !empty($jku)) { + $retry = true; + $publicKey = $this->fetchPublicKey($jku); + } + } + } while (!$ok && $retry); + + return $ok; + } + + /** + * Sign the JWT. + * + * @param array $payload Payload + * @param string $signatureMethod Signature method + * @param string $privateKey Private key in PEM format + * @param string $kid Key ID (optional) + * @param string $jku JSON Web Key URL (optional) * + * + * @return string Signed JWT + */ + public static function sign($payload, $signatureMethod, $privateKey, $kid = null, $jku = null) + { + return JWT::encode($payload, $privateKey, $signatureMethod, $kid); + } + + /** + * Get the public JWKS from a private key. + * + * @param string $privateKey Private key in PEM format + * @param string $signatureMethod Signature method + * @param string $kid Key ID (optional) + * + * @return array JWKS keys + */ + public static function getJWKS($privateKey, $signatureMethod, $kid) + { + $res = openssl_pkey_get_private($privateKey); + $details = openssl_pkey_get_details($res); + $keys['keys'][] = [ + 'kty' => 'RSA', + 'n' => JWT::urlsafeB64Encode($details['rsa']['n']), + 'e' => JWT::urlsafeB64Encode($details['rsa']['e']), + 'kid' => $kid, + 'alg' => $signatureMethod, + 'use' => 'sig' + ]; + + return $keys; + } + +### +### PRIVATE METHOD +### + + /** + * Fetch the public keys from a URL. + * + * @param string $jku Endpoint for retrieving JSON web keys + * + * @return array Array of keys + */ + private function fetchPublicKey($jku) + { + $publicKey = array(); + $http = new HttpMessage($jku); + if ($http->send()) { + $keys = json_decode($http->response, true); + $publicKey = JWK::parseKeySet($keys); + } + + return $publicKey; + } + +} diff --git a/src/Jwt/Jwt.php b/src/Jwt/Jwt.php new file mode 100644 index 0000000..b933f07 --- /dev/null +++ b/src/Jwt/Jwt.php @@ -0,0 +1,61 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class Jwt +{ + + /** + * Allow use of jku header in JWT. + * + * @var bool $allowJkuHeader + */ + public static $allowJkuHeader = false; + + /** + * The client used to handle JWTs. + * + * @var ClientInterface $jwtClient + */ + private static $jwtClient; + + /** + * Class constructor. + */ + function __construct() + { + + } + + /** + * Set the JWT client to use for handling JWTs. + * + * @param Jwt\ClientInterface|null $jwtClient + */ + public static function setJwtClient($jwtClient = null) + { + self::$jwtClient = $jwtClient; + } + + /** + * Get the JWT client to use for handling JWTs. If one is not set, a default client is created. + * + * @return ClientInterface|null The JWT client + */ + public static function getJwtClient() + { + if (!self::$jwtClient) { + self::$jwtClient = new FirebaseClient(); + } + + return self::$jwtClient; + } + +} diff --git a/src/Jwt/SpomkyLabsClient.php b/src/Jwt/SpomkyLabsClient.php new file mode 100644 index 0000000..8231e24 --- /dev/null +++ b/src/Jwt/SpomkyLabsClient.php @@ -0,0 +1,263 @@ + + * @copyright SPV Software Products + * @license GNU Lesser General Public License, version 3 () + */ +class SpomkyLabsClient implements ClientInterface +{ + + private $jwt = null; + + /** + * Check if a JWT is defined. + * + * @return bool True if a JWT is defined + */ + public function hasJwt() + { + return !empty($this->jwt); + } + + /** + * Load a JWT from a string. + * + * @param string $jwtString JWT string + * + * @return bool True if the JWT was successfully loaded + */ + public function load($jwtString) + { + $loader = new Jose\Loader(); + $this->jwt = $loader->load($jwtString); + } + + /** + * Check whether a JWT has a header with the specified name. + * + * @param string $name Header name + * + * @return bool True if the JWT has a header of the specified name + */ + public function hasHeader($name) + { + return $this->jwt->getSignature(0)->hasProtectedHeader($name); + } + + /** + * Get the value of the header with the specified name. + * + * @param string $name Header name + * @param string $defaultValue Default value + * + * @return string The value of the header with the specified name, or the default value if it does not exist + */ + public function getHeader($name, $defaultValue = null) + { + try { + $value = $this->jwt->getSignature(0)->getProtectedHeader($name); + } catch (\Exception $e) { + $value = $defaultValue; + } + + return $value; + } + + /** + * Get the value of the headers. + * + * @return array The value of the headers + */ + public function getHeaders() + { + return $this->jwt->getSignature(0)->getProtectedHeaders(); + } + + /** + * Check whether a JWT has a claim with the specified name. + * + * @param string $name Claim name + * + * @return bool True if the JWT has a claim of the specified name + */ + public function hasClaim($name) + { + return $this->jwt->hasClaim($name); + } + + /** + * Get the value of the claim with the specified name. + * + * @param string $name Claim name + * @param string $defaultValue Default value + * + * @return string The value of the claim with the specified name, or the default value if it does not exist + */ + public function getClaim($name, $defaultValue = null) + { + try { + $value = $this->jwt->getClaim($name); + $value = json_decode(json_encode($value)); + } catch (\Exception $e) { + $value = $defaultValue; + } + + return $value; + } + + /** + * Get the value of the payload. + * + * @return array The value of the payload + */ + public function getPayload() + { + return $this->jwt->getPayload(); + } + + /** + * Verify the signature of the JWT. + * + * @param string $publicKey Public key of issuer + * @param string $jku JSON Web Key URL of issuer (optional) + * + * @return bool True if the JWT has a valid signature + */ + public function verify($publicKey, $jku = null) + { + $ok = false; + $hasPublicKey = !empty($publicKey); + $retry = false; + do { + try { + $verifier = new Jose\Verifier(['RS256', 'RS384', 'RS512']); + switch ($this->getHeader('alg')) { + case 'RS256': + case 'RS384': + case 'RS512': + if ((Jwt::$allowJkuHeader && $this->hasHeader('jku')) || (!empty($jku) && empty($publicKey))) { + if (Jwt::$allowJkuHeader && $this->hasHeader('jku')) { + $jwks = JWKFactory::createFromJKU($this->getHeader('jku'), true, null, 86400, true); + } else { + $jwks = JWKFactory::createFromJKU($jku, true, null, 86400, true); + } + $verifier->verifyWithKeySet($this->jwt, $jwks); + $jwk = $jwks->selectKey('sig', $this->getHeader('alg'), ['kid' => $this->getHeader('kid')]); + } else { + $json = json_decode($publicKey, true); + if (is_null($json)) { + $jwk = JWKFactory::createFromKey($publicKey, null, + [ + 'alg' => $this->getHeader('alg'), + 'use' => 'sig' + ] + ); + } else { + $jwk = new JWK($json); + } + $verifier->verifyWithKey($this->jwt, $jwk); + } + break; + } + $jwk = $jwk->toPublic(); + $rsa = new KeyConverter\RSAKey($jwk); + $rsa->toPEM(); + $ok = true; + } catch (\Exception $e) { + Util::logError($e->getMessage()); + if (!$retry && $hasPublicKey && !empty($jku)) { + $retry = true; + $publicKey = null; + } + } + } while (!$ok && $retry); + + return $ok; + } + + /** + * Sign the JWT. + * + * @param array $payload Payload + * @param string $signatureMethod Signature method + * @param string $privateKey Private key in PEM format + * @param string $kid Key ID (optional) + * @param string $jku JSON Web Key URL (optional) * + * + * @return string Signed JWT + */ + public static function sign($payload, $signatureMethod, $privateKey, $kid = null, $jku = null) + { + switch ($signatureMethod) { + case 'RS512': + $sig = new Signature\RS512(); + break; + case 'RS384': + $sig = new Signature\RS384(); + break; + default: + $signatureMethod = 'RS256'; + $sig = new Signature\RS256(); + break; + } + try { + $jwk = JWKFactory::createFromKey($privateKey, null, + [ + 'alg' => $signatureMethod, + 'use' => 'sig' + ] + ); + } catch (\Exception $e) { + $ok = false; + } + $headers = ['typ' => 'JWT', 'alg' => $signatureMethod]; + if (!empty($kid)) { + $headers['kid'] = $kid; + if (!empty($jku)) { + $headers['jku'] = $jku; + } + } + $signer = new \Jose\Signer([$sig]); + $jwt_creator = new \Jose\JWTCreator($signer); + + return $jwt_creator->sign($payload, $headers, $jwk); + } + + /** + * Get the public JWKS from a private key. + * + * @param string $privateKey Private key in PEM format + * @param string $signatureMethod Signature method + * @param string $kid Key ID (optional) + * + * @return array JWKS keys + */ + public static function getJWKS($privateKey, $signatureMethod, $kid) + { + $keys['keys'] = array(); + try { + $jwk = JWKFactory::createFromKey($privateKey, null, ['alg' => $signatureMethod, 'kid' => $kid, 'use' => 'sig']); + $jwk = $jwk->toPublic(); + $rsa = new KeyConverter\RSAKey($jwk); + $rsa = $rsa::toPublic($rsa); + $keys['keys'][] = $rsa->toArray(); + } catch (Exception $e) { + + } + + return $keys; + } + +} diff --git a/src/LineItem.php b/src/LineItem.php index 1da90fd..99bb954 100644 --- a/src/LineItem.php +++ b/src/LineItem.php @@ -71,48 +71,48 @@ class LineItem public $endpoint = null; /** - * Tool Consumer for this line item. + * Platform for this line item. * - * @var ToolConsumer|null $consumer + * @var Platform|null $platform */ - private $consumer = null; + private $platform = null; /** * Class constructor. * - * @param ToolConsumer $consumer ToolConsumer object - * @param string $label Label - * @param int $pointsPossible Points possible value + * @param Platform $platform Platform object + * @param string $label Label + * @param int $pointsPossible Points possible value */ - public function __construct($consumer, $label, $pointsPossible) + public function __construct($platform, $label, $pointsPossible) { - $this->consumer = $consumer; + $this->platform = $platform; $this->label = $label; $this->pointsPossible = $pointsPossible; } /** - * Get Tool Consumer. + * Get platform. * - * @return ToolConsumer ToolConsumer object for this line item. + * @return Platform Platform object for this line item. */ - public function getConsumer() + public function getPlatform() { - return $this->consumer; + return $this->platform; } /** - * Save the line item to the tool consumer. + * Save the line item to the platform. * * @return bool True if successful */ public function save() { - $service = new Service\LineItem($this->consumer, $this->endpoint); + $service = new Service\LineItem($this->platform, $this->endpoint); $http = $this->send('PUT', null, Service\LineItem::toJson($lineItem)); $ok = $http->ok; if ($ok && !empty($http->responseJson)) { - $lineItem = Service\LineItem::toLineItem($this->consumer, $http->responseJson); + $lineItem = Service\LineItem::toLineItem($this->platform, $http->responseJson); foreach (get_object_vars($lineItem) as $key => $value) { $this->$key = $value; } @@ -122,13 +122,13 @@ public function save() } /** - * Delete the line item on the tool consumer. + * Delete the line item on the platform. * * @return bool True if successful */ public function delete() { - $service = new Service\LineItem($this->consumer, $this->endpoint); + $service = new Service\LineItem($this->platform, $this->endpoint); $http = $service->send('DELETE'); return $http->ok; @@ -143,7 +143,7 @@ public function delete() */ public function getOutcomes($limit = null) { - $resultService = new Service\Result($this->consumer, $this->endpoint); + $resultService = new Service\Result($this->platform, $this->endpoint); return $resultService->getAll(); } @@ -156,7 +156,7 @@ public function getOutcomes($limit = null) */ public function readOutcome($user) { - $resultService = new Service\Result($this->consumer, $this->endpoint); + $resultService = new Service\Result($this->platform, $this->endpoint); return $resultService->get($user); } @@ -170,7 +170,7 @@ public function readOutcome($user) */ public function submitOutcome($ltiOutcome, $user) { - $scoreService = new Service\Score($this->consumer, $this->endpoint); + $scoreService = new Service\Score($this->platform, $this->endpoint); return $scoreService->submit($ltiOutcome, $user); } @@ -184,21 +184,21 @@ public function submitOutcome($ltiOutcome, $user) public function deleteOutcome($user) { $ltiOutcome = new Outcome(); - $scoreService = new Service\Score($this->consumer, $this->endpoint); + $scoreService = new Service\Score($this->platform, $this->endpoint); return $scoreService->submit($ltiOutcome, $user); } /** * Retrieve a line item definition. * - * @param ToolConsumer $consumer ToolConsumer object - * @param string $endpoint ID value + * @param Platform $platform Platform object + * @param string $endpoint ID value * * @return LineItem|bool LineItem object or false on error */ - public static function fromEndpoint($consumer, $endpoint) + public static function fromEndpoint($platform, $endpoint) { - return Service\LineItem::getLineItem($consumer, $endpoint); + return Service\LineItem::getLineItem($platform, $endpoint); } } diff --git a/src/MediaType/ResourceHandler.php b/src/MediaType/ResourceHandler.php index 0f8529f..2652a96 100644 --- a/src/MediaType/ResourceHandler.php +++ b/src/MediaType/ResourceHandler.php @@ -2,7 +2,7 @@ namespace ceLTIc\LTI\MediaType; -use ceLTIc\LTI\ToolProvider; +use ceLTIc\LTI\Tool; use ceLTIc\LTI\Profile; /** @@ -18,10 +18,10 @@ class ResourceHandler /** * Class constructor. * - * @param ToolProvider $toolProvider Tool Provider object + * @param Tool $tool Tool object * @param Profile\ResourceHandler $resourceHandler Profile resource handler object */ - function __construct($toolProvider, $resourceHandler) + function __construct($tool, $resourceHandler) { $this->resource_type = new \stdClass; $this->resource_type->code = $resourceHandler->item->id; @@ -39,11 +39,11 @@ function __construct($toolProvider, $resourceHandler) $this->icon_info[] = $icon_info; $this->message = array(); foreach ($resourceHandler->requiredMessages as $message) { - $this->message[] = new Message($message, $toolProvider->consumer->profile->capability_offered); + $this->message[] = new Message($message, $tool->platform->profile->capability_offered); } foreach ($resourceHandler->optionalMessages as $message) { - if (in_array($message->type, $toolProvider->consumer->profile->capability_offered)) { - $this->message[] = new Message($message, $toolProvider->consumer->profile->capability_offered); + if (in_array($message->type, $tool->platform->profile->capability_offered)) { + $this->message[] = new Message($message, $tool->platform->profile->capability_offered); } } } diff --git a/src/MediaType/SecurityContract.php b/src/MediaType/SecurityContract.php index ecac9fd..d1e1483 100644 --- a/src/MediaType/SecurityContract.php +++ b/src/MediaType/SecurityContract.php @@ -2,7 +2,7 @@ namespace ceLTIc\LTI\MediaType; -use ceLTIc\LTI\ToolProvider; +use ceLTIc\LTI\Tool; /** * Class to represent an LTI Security Contract document @@ -17,13 +17,13 @@ class SecurityContract /** * Class constructor. * - * @param ToolProvider $toolProvider Tool Provider instance + * @param Tool $tool Tool instance * @param string $secret Shared secret */ - function __construct($toolProvider, $secret) + function __construct($tool, $secret) { $tcContexts = array(); - foreach ($toolProvider->consumer->profile->{'@context'} as $context) { + foreach ($tool->platform->profile->{'@context'} as $context) { if (is_object($context)) { $tcContexts = array_merge(get_object_vars($context), $tcContexts); } @@ -31,9 +31,9 @@ function __construct($toolProvider, $secret) $this->shared_secret = $secret; $toolServices = array(); - foreach ($toolProvider->requiredServices as $requiredService) { + foreach ($tool->requiredServices as $requiredService) { foreach ($requiredService->formats as $format) { - $service = $toolProvider->findService($format, $requiredService->actions); + $service = $tool->findService($format, $requiredService->actions); if (($service !== false) && !array_key_exists($service->{'@id'}, $toolServices)) { $id = $service->{'@id'}; $parts = explode(':', $id, 2); @@ -50,9 +50,9 @@ function __construct($toolProvider, $secret) } } } - foreach ($toolProvider->optionalServices as $optionalService) { + foreach ($tool->optionalServices as $optionalService) { foreach ($optionalService->formats as $format) { - $service = $toolProvider->findService($format, $optionalService->actions); + $service = $tool->findService($format, $optionalService->actions); if (($service !== false) && !array_key_exists($service->{'@id'}, $toolServices)) { $id = $service->{'@id'}; $parts = explode(':', $id, 2); diff --git a/src/MediaType/ToolProfile.php b/src/MediaType/ToolProfile.php index 5fdb0b8..73bda37 100644 --- a/src/MediaType/ToolProfile.php +++ b/src/MediaType/ToolProfile.php @@ -2,7 +2,7 @@ namespace ceLTIc\LTI\MediaType; -use ceLTIc\LTI\ToolProvider; +use ceLTIc\LTI\Tool; /** * Class to represent an LTI Tool Profile @@ -19,71 +19,71 @@ class ToolProfile /** * Class constructor. * - * @param ToolProvider $toolProvider Tool Provider object + * @param Tool $tool Tool object */ - function __construct($toolProvider) + function __construct($tool) { $this->lti_version = 'LTI-2p0'; - if (!empty($toolProvider->product)) { + if (!empty($tool->product)) { $this->product_instance = new \stdClass; } - if (!empty($toolProvider->product->id)) { - $this->product_instance->guid = $toolProvider->product->id; + if (!empty($tool->product->id)) { + $this->product_instance->guid = $tool->product->id; } - if (!empty($toolProvider->product->name)) { + if (!empty($tool->product->name)) { $this->product_instance->product_info = new \stdClass; $this->product_instance->product_info->product_name = new \stdClass; - $this->product_instance->product_info->product_name->default_value = $toolProvider->product->name; + $this->product_instance->product_info->product_name->default_value = $tool->product->name; $this->product_instance->product_info->product_name->key = 'tool.name'; } - if (!empty($toolProvider->product->description)) { + if (!empty($tool->product->description)) { $this->product_instance->product_info->description = new \stdClass; - $this->product_instance->product_info->description->default_value = $toolProvider->product->description; + $this->product_instance->product_info->description->default_value = $tool->product->description; $this->product_instance->product_info->description->key = 'tool.description'; } - if (!empty($toolProvider->product->url)) { - $this->product_instance->guid = $toolProvider->product->url; + if (!empty($tool->product->url)) { + $this->product_instance->guid = $tool->product->url; } - if (!empty($toolProvider->product->version)) { - $this->product_instance->product_info->product_version = $toolProvider->product->version; + if (!empty($tool->product->version)) { + $this->product_instance->product_info->product_version = $tool->product->version; } - if (!empty($toolProvider->vendor)) { + if (!empty($tool->vendor)) { $this->product_instance->product_info->product_family = new \stdClass; $this->product_instance->product_info->product_family->vendor = new \stdClass; - if (!empty($toolProvider->product->id)) { - $this->product_instance->product_info->product_family->code = $toolProvider->product->id; + if (!empty($tool->product->id)) { + $this->product_instance->product_info->product_family->code = $tool->product->id; } } - if (!empty($toolProvider->vendor->id)) { - $this->product_instance->product_info->product_family->vendor->code = $toolProvider->vendor->id; + if (!empty($tool->vendor->id)) { + $this->product_instance->product_info->product_family->vendor->code = $tool->vendor->id; } - if (!empty($toolProvider->vendor->name)) { + if (!empty($tool->vendor->name)) { $this->product_instance->product_info->product_family->vendor->vendor_name = new \stdClass; - $this->product_instance->product_info->product_family->vendor->vendor_name->default_value = $toolProvider->vendor->name; + $this->product_instance->product_info->product_family->vendor->vendor_name->default_value = $tool->vendor->name; $this->product_instance->product_info->product_family->vendor->vendor_name->key = 'tool.vendor.name'; } - if (!empty($toolProvider->vendor->description)) { + if (!empty($tool->vendor->description)) { $this->product_instance->product_info->product_family->vendor->description = new \stdClass; - $this->product_instance->product_info->product_family->vendor->description->default_value = $toolProvider->vendor->description; + $this->product_instance->product_info->product_family->vendor->description->default_value = $tool->vendor->description; $this->product_instance->product_info->product_family->vendor->description->key = 'tool.vendor.description'; } - if (!empty($toolProvider->vendor->url)) { - $this->product_instance->product_info->product_family->vendor->website = $toolProvider->vendor->url; + if (!empty($tool->vendor->url)) { + $this->product_instance->product_info->product_family->vendor->website = $tool->vendor->url; } - if (!empty($toolProvider->vendor->timestamp)) { + if (!empty($tool->vendor->timestamp)) { $this->product_instance->product_info->product_family->vendor->timestamp = date('Y-m-d\TH:i:sP', - $toolProvider->vendor->timestamp); + $tool->vendor->timestamp); } $this->resource_handler = array(); - foreach ($toolProvider->resourceHandlers as $resourceHandler) { - $this->resource_handler[] = new ResourceHandler($toolProvider, $resourceHandler); + foreach ($tool->resourceHandlers as $resourceHandler) { + $this->resource_handler[] = new ResourceHandler($tool, $resourceHandler); } - if (!empty($toolProvider->baseUrl)) { + if (!empty($tool->baseUrl)) { $this->base_url_choice = array(); $this->base_url_choice[] = new \stdClass; - $this->base_url_choice[0]->default_base_url = $toolProvider->baseUrl; + $this->base_url_choice[0]->default_base_url = $tool->baseUrl; } } diff --git a/src/MediaType/ToolProxy.php b/src/MediaType/ToolProxy.php index 2055b1a..2e41b49 100644 --- a/src/MediaType/ToolProxy.php +++ b/src/MediaType/ToolProxy.php @@ -2,7 +2,7 @@ namespace ceLTIc\LTI\MediaType; -use ceLTIc\LTI\ToolProvider; +use ceLTIc\LTI\Tool; use ceLTIc\LTI\Profile\ServiceDefinition; /** @@ -18,11 +18,11 @@ class ToolProxy /** * Class constructor. * - * @param ToolProvider $toolProvider Tool Provider object + * @param Tool $tool Tool object * @param ServiceDefinition $toolProxyService Tool Proxy service - * @param string $secret Shared secret + * @param string $secret Shared secret */ - function __construct($toolProvider, $toolProxyService, $secret) + function __construct($tool, $toolProxyService, $secret) { $contexts = array(); @@ -30,9 +30,9 @@ function __construct($toolProvider, $toolProxyService, $secret) $this->{'@type'} = 'ToolProxy'; $this->{'@id'} = "{$toolProxyService->endpoint}"; $this->lti_version = 'LTI-2p0'; - $this->tool_consumer_profile = $toolProvider->consumer->profile->{'@id'}; - $this->tool_profile = new ToolProfile($toolProvider); - $this->security_contract = new SecurityContract($toolProvider, $secret); + $this->tool_consumer_profile = $tool->platform->profile->{'@id'}; + $this->tool_profile = new ToolProfile($tool); + $this->security_contract = new SecurityContract($tool, $secret); } } diff --git a/src/OAuthDataStore.php b/src/OAuthDataStore.php index c9cca84..525b529 100644 --- a/src/OAuthDataStore.php +++ b/src/OAuthDataStore.php @@ -17,20 +17,20 @@ class OAuthDataStore extends OAuth\OAuthDataStore { /** - * Tool Provider object. + * Tool object. * - * @var ToolProvider|null $toolProvider + * @var Tool|null $tool */ - private $toolProvider = null; + private $tool = null; /** * Class constructor. * - * @param ToolProvider $toolProvider Tool_Provider object + * @param Tool $tool Tool object */ - public function __construct($toolProvider) + public function __construct($tool) { - $this->toolProvider = $toolProvider; + $this->tool = $tool; } /** @@ -42,7 +42,7 @@ public function __construct($toolProvider) */ function lookup_consumer($consumerKey) { - return new OAuthConsumer($this->toolProvider->consumer->getKey(), $this->toolProvider->consumer->secret); + return new OAuthConsumer($this->tool->platform->getKey(), $this->tool->platform->secret); } /** @@ -71,13 +71,13 @@ function lookup_token($consumer, $tokenType, $token) */ function lookup_nonce($consumer, $token, $value, $timestamp) { - $nonce = new ConsumerNonce($this->toolProvider->consumer, $value); + $nonce = new PlatformNonce($this->tool->platform, $value); $ok = !$nonce->load(); if ($ok) { $ok = $nonce->save(); } if (!$ok) { - $this->toolProvider->reason = 'Invalid nonce.'; + $this->tool->reason = 'Invalid nonce.'; } return !$ok; diff --git a/src/Outcome.php b/src/Outcome.php index 6ff9e1a..8f5c9a6 100644 --- a/src/Outcome.php +++ b/src/Outcome.php @@ -107,8 +107,8 @@ class Outcome /** * Class constructor. * - * @param string $value Outcome value (optional, default is none) - * @param string $pointsPossible Points possible value (optional, default is 1) + * @param mixed $value Outcome value (optional, default is none) + * @param int $pointsPossible Points possible value (optional, default is none) * @param string $activityProgress Activity progress (optional, default is 'Completed') * @param string $gradingProgress Grading progress (optional, default is 'FullyGraded') */ diff --git a/src/Platform.php b/src/Platform.php new file mode 100644 index 0000000..16eeeda --- /dev/null +++ b/src/Platform.php @@ -0,0 +1,666 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class Platform +{ + use System; + + /** + * Local name of platform. + * + * @var string|null $name + */ + public $name = null; + + /** + * Platform ID. + * + * @var string $platformId + */ + public $platformId = null; + + /** + * Client ID. + * + * @var string $clientId + */ + public $clientId = null; + + /** + * Deployment ID. + * + * @var string $deploymentId + */ + public $deploymentId = null; + + /** + * Authorization server ID. + * + * @var string $authorizationServerId + */ + public $authorizationServerId = null; + + /** + * Login authentication URL. + * + * @var string $authenticationUrl + */ + public $authenticationUrl = null; + + /** + * Access Token service URL. + * + * @var string $accessTokenUrl + */ + public $accessTokenUrl = null; + + /** + * LTI version (as reported by last platform connection). + * + * @var string|null $ltiVersion + */ + public $ltiVersion = null; + + /** + * Name of platform (as reported by last platform connection). + * + * @var string|null $consumerName + */ + public $consumerName = null; + + /** + * Pplatform version (as reported by last platform connection). + * + * @var string|null $consumerVersion + */ + public $consumerVersion = null; + + /** + * The platform profile data. + * + * @var object|null $profile + */ + public $profile = null; + + /** + * Platform GUID (as reported by first platform connection). + * + * @var string|null $consumerGuid + */ + public $consumerGuid = null; + + /** + * Optional CSS path (as reported by last platform connection). + * + * @var string|null $cssPath + */ + public $cssPath = null; + + /** + * Access token to authorize service requests. + * + * @var AccessToken|null $accessToken + */ + private $accessToken = null; + + /** + * Get the authorization access token + * + * @return AccessToken Access token + */ + public function getAccessToken() + { + return $this->accessToken; + } + + /** + * Set the authorization access token + * + * @param AccessToken $accessToken Access token + */ + public function setAccessToken($accessToken) + { + $this->accessToken = $accessToken; + } + + /** + * Whether the platform instance is protected by matching the consumer_guid value in incoming requests. + * + * @var bool $protected + */ + public $protected = false; + + /** + * Whether the platform instance is enabled to accept incoming connection requests. + * + * @var bool $enabled + */ + public $enabled = false; + + /** + * Timestamp from which the the platform instance is enabled to accept incoming connection requests. + * + * @var int|null $enableFrom + */ + public $enableFrom = null; + + /** + * Timestamp until which the platform instance is enabled to accept incoming connection requests. + * + * @var int|null $enableUntil + */ + public $enableUntil = null; + + /** + * Timestamp for date of last connection from this platform. + * + * @var int|null $lastAccess + */ + public $lastAccess = null; + + /** + * Default scope to use when generating an Id value for a user. + * + * @var int $idScope + */ + public $idScope = Tool::ID_SCOPE_ID_ONLY; + + /** + * Default email address (or email domain) to use when no email address is provided for a user. + * + * @var string $defaultEmail + */ + public $defaultEmail = ''; + + /** + * HttpMessage object for last service request. + * + * @var HttpMessage|null $lastServiceRequest + */ + public $lastServiceRequest = null; + + /** + * Timestamp for when the object was created. + * + * @var int|null $created + */ + public $created = null; + + /** + * Timestamp for when the object was last updated. + * + * @var int|null $updated + */ + public $updated = null; + + /** + * Platform ID value. + * + * @var int|null $id + */ + private $id = null; + + /** + * Setting values (LTI parameters, custom parameters and local parameters). + * + * @var array $settings + */ + private $settings = null; + + /** + * Whether the settings value have changed since last saved. + * + * @var bool $settingsChanged + */ + private $settingsChanged = false; + + /** + * Class constructor. + * + * @param DataConnector $dataConnector A data connector object + */ + public function __construct($dataConnector = null) + { + $this->initialize(); + if (empty($dataConnector)) { + $dataConnector = DataConnector::getDataConnector(); + } + $this->dataConnector = $dataConnector; + } + + /** + * Initialise the platform. + */ + public function initialize() + { + $this->id = null; + $this->key = null; + $this->name = null; + $this->secret = null; + $this->signatureMethod = 'HMAC-SHA1'; + $this->encryptionMethod = null; + $this->rsaKey = null; + $this->kid = null; + $this->jku = null; + $this->platformId = null; + $this->clientId = null; + $this->deploymentId = null; + $this->ltiVersion = null; + $this->consumerName = null; + $this->consumerVersion = null; + $this->consumerGuid = null; + $this->profile = null; + $this->toolProxy = null; + $this->settings = array(); + $this->protected = false; + $this->enabled = false; + $this->enableFrom = null; + $this->enableUntil = null; + $this->lastAccess = null; + $this->idScope = Tool::ID_SCOPE_ID_ONLY; + $this->defaultEmail = ''; + $this->created = null; + $this->updated = null; + } + + /** + * Initialise the platform. + * + * Synonym for initialize(). + */ + public function initialise() + { + $this->initialize(); + } + + /** + * Save the platform to the database. + * + * @return bool True if the object was successfully saved + */ + public function save() + { + return $this->dataConnector->savePlatform($this); + } + + /** + * Delete the platform from the database. + * + * @return bool True if the object was successfully deleted + */ + public function delete() + { + return $this->dataConnector->deletePlatform($this); + } + + /** + * Get the platform record ID. + * + * @return int|null Platform record ID value + */ + public function getRecordId() + { + return $this->id; + } + + /** + * Sets the platform record ID. + * + * @param int $id Platform record ID value + */ + public function setRecordId($id) + { + $this->id = $id; + } + + /** + * Get the platform ID. + * + * The ID will be the consumer key if one exists, otherwise a concatenation of the platform/client/deployment IDs + * + * @return string Platform ID value + */ + public function getId() + { + if (!empty($this->key)) { + $id = $this->key; + } elseif (!empty($this->platformId)) { + $id = $this->platformId; + if (!empty($this->clientId)) { + $id .= '/' . $this->clientId; + } + if (!empty($this->deploymentId)) { + $id .= '#' . $this->deploymentId; + } + } else { + $id = null; + } + + return $id; + } + + /** + * Get platform family code (as reported by last platform connection). + * + * @return string Family code + */ + public function getFamilyCode() + { + $familyCode = ''; + if (!empty($this->consumerVersion)) { + list($familyCode, $version) = explode('-', $this->consumerVersion, 2); + } + + return $familyCode; + } + + /** + * Get the data connector. + * + * @return DataConnector|null Data connector object or string + */ + public function getDataConnector() + { + return $this->dataConnector; + } + + /** + * Is the platform available to accept launch requests? + * + * @return bool True if the platform is enabled and within any date constraints + */ + public function getIsAvailable() + { + $ok = $this->enabled; + + $now = time(); + if ($ok && !is_null($this->enableFrom)) { + $ok = $this->enableFrom <= $now; + } + if ($ok && !is_null($this->enableUntil)) { + $ok = $this->enableUntil > $now; + } + + return $ok; + } + + /** + * Get a setting value. + * + * @param string $name Name of setting + * @param string $default Value to return if the setting does not exist (optional, default is an empty string) + * + * @return string Setting value + */ + public function getSetting($name, $default = '') + { + if (array_key_exists($name, $this->settings)) { + $value = $this->settings[$name]; + } else { + $value = $default; + } + + return $value; + } + + /** + * Set a setting value. + * + * @param string $name Name of setting + * @param string $value Value to set, use an empty value to delete a setting (optional, default is null) + */ + public function setSetting($name, $value = null) + { + $old_value = $this->getSetting($name); + if ($value !== $old_value) { + if (!empty($value)) { + $this->settings[$name] = $value; + } else { + unset($this->settings[$name]); + } + $this->settingsChanged = true; + } + } + + /** + * Get an array of all setting values. + * + * @return array Associative array of setting values + */ + public function getSettings() + { + return $this->settings; + } + + /** + * Set an array of all setting values. + * + * @param array $settings Associative array of setting values + */ + public function setSettings($settings) + { + $this->settings = $settings; + } + + /** + * Save setting values. + * + * @return bool True if the settings were successfully saved + */ + public function saveSettings() + { + if ($this->settingsChanged) { + $ok = $this->save(); + } else { + $ok = true; + } + + return $ok; + } + + /** + * Check if the Tool Settings service is supported. + * + * @return bool True if this platform supports the Tool Settings service + */ + public function hasToolSettingsService() + { + $has = !empty($this->getSetting('custom_system_setting_url')); + if (!$has) { + $has = self::hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode(), $this); + } + return $has; + } + + /** + * Get Tool Settings. + * + * @param bool $simple True if all the simple media type is to be used (optional, default is true) + * + * @return mixed The array of settings if successful, otherwise false + */ + public function getToolSettings($simple = true) + { + $ok = false; + $settings = array(); + if (!empty($this->getSetting('custom_system_setting_url'))) { + $url = $this->getSetting('custom_system_setting_url'); + $service = new Service\ToolSettings($this, $url, $simple); + $settings = $service->get(); + $this->lastServiceRequest = $service->getHttpMessage(); + $ok = $settings !== false; + } + if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode(), $this)) { + $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode()); + $hook = new $className($this); + $settings = $hook->getToolSettings($simple); + } + + return $settings; + } + + /** + * Set Tool Settings. + * + * @param array $settings An associative array of settings (optional, default is none) + * + * @return bool True if action was successful, otherwise false + */ + public function setToolSettings($settings = array()) + { + $ok = false; + if (!empty($this->getSetting('custom_system_setting_url'))) { + $url = $this->getSetting('custom_system_setting_url'); + $service = new Service\ToolSettings($this, $url); + $ok = $service->set($settings); + $this->lastServiceRequest = $service->getHttpMessage(); + } + if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode(), $this)) { + $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode()); + $hook = new $className($this); + $ok = $hook->setToolSettings($settings); + } + + return $ok; + } + + /** + * Check if the Access Token service is supported. + * + * @return bool True if this platform supports the Access Token service + */ + public function hasAccessTokenService() + { + $has = !empty($this->getSetting('custom_oauth2_access_token_url')); + if (!$has) { + $has = self::hasConfiguredApiHook(self::$ACCESS_TOKEN_SERVICE_HOOK, $this->getFamilyCode(), $this); + } + return $has; + } + + /** + * Get the message parameters + * + * @return array The message parameter array + */ + public function getMessageParameters() + { + if ($this->ok && is_null($this->messageParameters)) { + $this->parseMessage(); + } + + return $this->messageParameters; + } + + /** + * Process an incoming request + */ + public function handleRequest() + { + Util::logRequest(); + if ($this->ok) { + $this->getMessageParameters(); + if ($this->ok) { + $this->ok = $this->authenticate(); + } + if (!$this->ok) { + Util::logError("Request failed with reason: '{$this->reason}'"); + } + } + } + + /** + * Load the platform from the database by its consumer key. + * + * @param string $key Consumer key + * @param DataConnector $dataConnector A data connector object + * @param bool $autoEnable true if the platform is to be enabled automatically (optional, default is false) + * + * @return Platform The platform object + */ + public static function fromConsumerKey($key = null, $dataConnector = null, $autoEnable = false) + { + $platform = new Platform($dataConnector); + $platform->key = $key; + if (!empty($dataConnector)) { + $ok = $dataConnector->loadPlatform($platform); + if ($ok && $autoEnable) { + $platform->enabled = true; + } + } + + return $platform; + } + + /** + * Load the platform from the database by its platform, client and deployment IDs. + * + * @param string $platformId The platform ID + * @param string $clientId The client ID + * @param string $deploymentId The deployment ID + * @param DataConnector $dataConnector A data connector object + * @param bool $autoEnable True if the platform is to be enabled automatically (optional, default is false) + * + * @return Platform The platform object + */ + public static function fromPlatformId($platformId, $clientId, $deploymentId, $dataConnector = null, $autoEnable = false) + { + $platform = new Platform($dataConnector); + $platform->platformId = $platformId; + $platform->clientId = $clientId; + $platform->deploymentId = $deploymentId; + if ($dataConnector->loadPlatform($platform)) { + if ($autoEnable) { + $this->enabled = true; + } + } + + return $platform; + } + + /** + * Load the platform from the database by its record ID. + * + * @param string $id The platform record ID + * @param DataConnector $dataConnector A data connector object + * + * @return Platform The platform object + */ + public static function fromRecordId($id, $dataConnector) + { + $platform = new Platform($dataConnector); + $platform->setRecordId($id); + $dataConnector->loadPlatform($platform); + + return $platform; + } + +### +### PRIVATE METHODS +### + + /** + * Check the authenticity of the LTI launch request. + * + * The platform, resource link and user objects will be initialised if the request is valid. + * + * @return bool True if the request has been successfully validated. + */ + private function authenticate() + { + return $this->verifySignature(); + } + +} diff --git a/src/PlatformNonce.php b/src/PlatformNonce.php new file mode 100644 index 0000000..31ca7f7 --- /dev/null +++ b/src/PlatformNonce.php @@ -0,0 +1,113 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class PlatformNonce +{ + + /** + * Maximum age nonce values will be retained for (in minutes). + */ + const MAX_NONCE_AGE = 30; // in minutes + + /** + * Maximum length which can be stored. + * + * Characters are removed from the beginning of the value when too long. + * + * @var int $maximumLength + */ + public static $maximumLength = 50; + + /** + * Timestamp for when the nonce value expires. + * + * @var int|null $expires + */ + public $expires = null; + + /** + * Platform to which this nonce applies. + * + * @var Platform|null $platform + */ + private $platform = null; + + /** + * Nonce value. + * + * @var string|null $value + */ + private $value = null; + + /** + * Class constructor. + * + * @param Platform $platform Platform object + * @param string $value Nonce value (optional, default is null) + */ + public function __construct($platform, $value = null) + { + $this->platform = $platform; + $this->value = substr($value, -self::$maximumLength); + $this->expires = time() + (self::MAX_NONCE_AGE * 60); + } + + /** + * Load a nonce value from the database. + * + * @return bool True if the nonce value was successfully loaded + */ + public function load() + { + return $this->platform->getDataConnector()->loadPlatformNonce($this); + } + + /** + * Save a nonce value in the database. + * + * @return bool True if the nonce value was successfully saved + */ + public function save() + { + return $this->platform->getDataConnector()->savePlatformNonce($this); + } + + /** + * Delete a nonce value in the database. + * + * @return bool True if the nonce value was successfully deleted + */ + public function delete() + { + return $this->platform->getDataConnector()->deletePlatformNonce($this); + } + + /** + * Get platform. + * + * @return Platform Platform for this nonce + */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Get outcome value. + * + * @return string Outcome value + */ + public function getValue() + { + return $this->value; + } + +} diff --git a/src/Profile/Message.php b/src/Profile/Message.php index 0d5a3f6..aefb667 100644 --- a/src/Profile/Message.php +++ b/src/Profile/Message.php @@ -21,7 +21,7 @@ class Message public $type = null; /** - * Path to send message request to (used in conjunction with a base URL for the Tool Provider). + * Path to send message request to (used in conjunction with a base URL for the Tool). * * @var string|null $path */ diff --git a/src/ResourceLink.php b/src/ResourceLink.php index e3332de..761c7df 100644 --- a/src/ResourceLink.php +++ b/src/ResourceLink.php @@ -10,7 +10,7 @@ use DOMElement; /** - * Class to represent a tool consumer resource link + * Class to represent a platform resource link * * @author Stephen P Vickers * @copyright SPV Software Products @@ -95,14 +95,14 @@ class ResourceLink public $ltiResourceLinkId = null; /** - * UserResult group sets (null if the consumer does not support the groups enhancement) + * UserResult group sets (null if the platform does not support the groups enhancement) * * @var array|null $groupSets */ public $groupSets = null; /** - * UserResult groups (null if the consumer does not support the groups enhancement) + * UserResult groups (null if the platform does not support the groups enhancement) * * @var array|null $groups */ @@ -144,7 +144,7 @@ class ResourceLink public $extResponseHeaders = null; /** - * Consumer key value for resource link being shared (if any). + * Primary key value for resource link being shared (if any). * * @var string|null $primaryResourceLinkId */ @@ -179,18 +179,18 @@ class ResourceLink private $id = null; /** - * Tool Consumer for this resource link. + * Platform for this resource link. * - * @var ToolConsumer|null $consumer + * @var Platform|null $platform */ - private $consumer = null; + private $platform = null; /** - * Tool Consumer ID for this resource link. + * Platform ID for this resource link. * - * @var int|null $consumerId + * @var int|null $platformId */ - private $consumerId = null; + private $platformId = null; /** * Context for this resource link. @@ -302,40 +302,85 @@ public function delete() /** * Get tool consumer. * + * @deprecated Use getPlatform() instead + * @see Context::getPlatform() + * * @return ToolConsumer Tool consumer object for this resource link. */ public function getConsumer() { - if (is_null($this->consumer)) { - if (!is_null($this->context) || !is_null($this->contextId)) { - $this->consumer = $this->getContext()->getConsumer(); - } else { - $this->consumer = ToolConsumer::fromRecordId($this->consumerId, $this->getDataConnector()); - } - } - - return $this->consumer; + Util::logDebug('Method ceLTIc\LTI\ResourceLink::getConsumer() has been deprecated; please use ceLTIc\LTI\ResourceLink::getPlatform() instead.', + true); + return $this->getPlatform(); } /** * Get tool consumer ID. * + * @deprecated Use getPlatformId() instead + * @see Context::getPlatformId() + * * @return int|null Tool Consumer ID for this resource link. */ public function getConsumerId() { - return $this->consumerId; + Util::logDebug('Method ceLTIc\LTI\ResourceLink::getConsumerId() has been deprecated; please use ceLTIc\LTI\ResourceLink::getPlatformId() instead.', + true); + return $this->getPlatformId(); } /** * Set tool consumer ID. * + * @deprecated Use setPlatformId() instead + * @see Context::setPlatformId() + * * @param int $consumerId Tool Consumer ID for this resource link. */ public function setConsumerId($consumerId) { - $this->consumer = null; - $this->consumerId = $consumerId; + Util::logDebug('Method ceLTIc\LTI\ResourceLink::setConsumerId() has been deprecated; please use ceLTIc\LTI\ResourceLink::setPlatformId() instead.', + true); + $this->setPlatformId($consumerId); + } + + /** + * Get platform. + * + * @return Platform Platform object for this resource link. + */ + public function getPlatform() + { + if (is_null($this->platform)) { + if (!is_null($this->context) || !is_null($this->contextId)) { + $this->platform = $this->getContext()->getPlatform(); + } else { + $this->platform = Platform::fromRecordId($this->platformId, $this->getDataConnector()); + } + } + + return $this->platform; + } + + /** + * Get platform ID. + * + * @return int|null Platform ID for this resource link. + */ + public function getPlatformId() + { + return $this->platformId; + } + + /** + * Set platform ID. + * + * @param int $platformId Platform ID for this resource link. + */ + public function setPlatformId($platformId) + { + $this->platform = null; + $this->platformId = $platformId; } /** @@ -391,13 +436,13 @@ public function setContextId($contextId) } /** - * Get tool consumer key. + * Get consumer key. * * @return string Consumer key value for this resource link. */ public function getKey() { - return $this->getConsumer()->getKey(); + return $this->getPlatform()->getKey(); } /** @@ -437,6 +482,13 @@ public function setRecordId($id) */ public function getDataConnector() { + if (empty($this->dataConnector)) { + $this->getPlatform(); + if (!empty($this->platform)) { + $this->dataConnector = $this->platform->getDataConnector(); + } + } + return $this->dataConnector; } @@ -524,7 +576,7 @@ public function hasOutcomesService() $has = !empty($this->getSetting('ext_ims_lis_basic_outcome_url')) || !empty($this->getSetting('lis_outcome_service_url')) || (!empty($this->getSetting('custom_lineitems_url')) && !empty($this->getSetting('custom_lineitem_url'))); if (!$has) { - $has = self::hasConfiguredApiHook(self::$OUTCOMES_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this); + $has = self::hasConfiguredApiHook(self::$OUTCOMES_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this); } return $has; } @@ -544,9 +596,8 @@ public function hasMembershipsService() $has = !empty($this->getSetting('ext_ims_lis_memberships_url')); } if (!$has) { - $has = self::hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this); + $has = self::hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this); } - return $has; } @@ -579,17 +630,17 @@ public function hasLineItemService() * * @param int $action The action type constant * @param Outcome $ltiOutcome Outcome object - * @param UserResult $userresult UserResult object + * @param UserResult $userResult UserResult object * * @return string|bool Outcome value read or true if the request was successfully processed */ - public function doOutcomesService($action, $ltiOutcome, $userresult) + public function doOutcomesService($action, $ltiOutcome, $userResult) { $response = false; $this->extResponse = ''; // Lookup service details from the source resource link appropriate to the user (in case the destination is being shared) - $sourceResourceLink = $userresult->getResourceLink(); - $sourcedId = $userresult->ltiResultSourcedId; + $sourceResourceLink = $userResult->getResourceLink(); + $sourcedId = $userResult->ltiResultSourcedId; // Use LTI 1.1 service in preference to extension service if it is available $urlAGS = $sourceResourceLink->getSetting('custom_lineitem_url'); @@ -637,7 +688,7 @@ public function doOutcomesService($action, $ltiOutcome, $userresult) if ($urlAGS) { switch ($action) { case self::EXT_READ: - $ltiOutcome = $this->doResultService($userresult, $urlAGS); + $ltiOutcome = $this->doResultService($userResult, $urlAGS); $response = !empty($ltiOutcome); break; case self::EXT_DELETE: @@ -645,7 +696,7 @@ public function doOutcomesService($action, $ltiOutcome, $userresult) $ltiOutcome->activityProgress = 'Initialized'; $ltiOutcome->gradingProgress = 'NotReady'; case self::EXT_WRITE: - $response = $this->doScoreService($ltiOutcome, $userresult, $urlAGS); + $response = $this->doScoreService($ltiOutcome, $userResult, $urlAGS); break; } } elseif ($urlLTI11) { @@ -720,10 +771,11 @@ public function doOutcomesService($action, $ltiOutcome, $userresult) $response = ''; } } - if (($response === false) && $this->hasConfiguredApiHook(self::$OUTCOMES_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this)) { - $className = $this->getApiHook(self::$OUTCOMES_SERVICE_HOOK, $this->getConsumer()->getFamilyCode()); + if (($response === false) && $this->hasConfiguredApiHook(self::$OUTCOMES_SERVICE_HOOK, + $this->getPlatform()->getFamilyCode(), $this)) { + $className = $this->getApiHook(self::$OUTCOMES_SERVICE_HOOK, $this->getPlatform()->getFamilyCode()); $hook = new $className($this); - $response = $hook->doOutcomesService($action, $ltiOutcome, $userresult); + $response = $hook->doOutcomesService($action, $ltiOutcome, $userResult); } return $response; @@ -743,6 +795,8 @@ public function doOutcomesService($action, $ltiOutcome, $userresult) */ public function doMembershipsService($withGroups = false) { + Util::logDebug('Method ceLTIc\LTI\ResourceLink::doMembershipsService() has been deprecated; please use ceLTIc\LTI\ResourceLink::getMemberships() instead.', + true); return $this->getMemberships($withGroups); } @@ -813,7 +867,7 @@ public function hasToolSettingsService() { $has = !empty($this->getSetting('custom_link_setting_url')); if (!$has) { - $has = self::hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this); + $has = self::hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this); } return $has; } @@ -837,8 +891,8 @@ public function getToolSettings($mode = Service\ToolSettings::MODE_CURRENT_LEVEL $this->lastServiceRequest = $service->getHttpMessage(); $ok = $settings !== false; } - if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this)) { - $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode()); + if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this)) { + $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode()); $hook = new $className($this); $settings = $hook->getToolSettings($mode, $simple); } @@ -847,7 +901,7 @@ public function getToolSettings($mode = Service\ToolSettings::MODE_CURRENT_LEVEL } /** - * Perform a Tool Settings service request. + * Set Tool Settings. * * @param array $settings An associative array of settings (optional, default is none) * @@ -862,8 +916,8 @@ public function setToolSettings($settings = array()) $ok = $service->set($settings); $this->lastServiceRequest = $service->getHttpMessage(); } - if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this)) { - $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode()); + if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this)) { + $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode()); $hook = new $className($this); $ok = $hook->setToolSettings($settings); } @@ -881,6 +935,8 @@ public function setToolSettings($settings = array()) */ public function hasMembershipService() { + Util::logDebug('Method ceLTIc\LTI\ResourceLink::hasMembershipService() has been deprecated; please use ceLTIc\LTI\ResourceLink::hasMembershipsService() instead.', + true); return $this->hasMembershipsService(); } @@ -894,6 +950,8 @@ public function hasMembershipService() */ public function getMembership() { + Util::logDebug('Method ceLTIc\LTI\ResourceLink::getMembership() has been deprecated; please use ceLTIc\LTI\ResourceLink::getMemberships() instead.', + true); return $this->getMemberships(); } @@ -911,14 +969,14 @@ public function getMemberships($withGroups = false) $hasLtiservice = !empty($this->getContextId()) && (!empty($this->getContext()->getSetting('custom_context_memberships_url')) || !empty($this->getContext()->getSetting('custom_context_memberships_v2_url'))); $hasExtService = !empty($this->getSetting('ext_ims_lis_memberships_url')); - $hasApiHook = $this->hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode(), $this); + $hasApiHook = $this->hasConfiguredApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode(), $this); if ($hasLtiservice && (!$withGroups || (!$hasExtService && !$hasApiHook))) { if (!empty($this->getContext()->getSetting('custom_context_memberships_v2_url'))) { $url = $this->getContext()->getSetting('custom_context_memberships_v2_url'); - $format = Service\Membership::MEMBERSHIPS_MEDIA_TYPE_NRPS; + $format = Service\Membership::MEDIA_TYPE_MEMBERSHIPS_NRPS; } else { $url = $this->getContext()->getSetting('custom_context_memberships_url'); - $format = Service\Membership::MEMBERSHIPS_MEDIA_TYPE_V1; + $format = Service\Membership::MEDIA_TYPE_MEMBERSHIPS_V1; } $service = new Service\Membership($this, $url, $format); $userResults = $service->get(); @@ -965,11 +1023,11 @@ public function getMemberships($withGroups = false) // Set the user email $email = (isset($members[$i]['person_contact_email_primary'])) ? $members[$i]['person_contact_email_primary'] : ''; - $userresult->setEmail($email, $this->getConsumer()->defaultEmail); + $userresult->setEmail($email, $this->getPlatform()->defaultEmail); // Set the user roles if (isset($members[$i]['roles'])) { - $userresult->roles = ToolProvider::parseRoles($members[$i]['roles']); + $userresult->roles = Tool::parseRoles($members[$i]['roles']); } // Set the user groups @@ -1016,22 +1074,22 @@ public function getMemberships($withGroups = false) $ok = $userResults !== false; } if (!$ok && $hasApiHook) { - $className = $this->getApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getConsumer()->getFamilyCode()); + $className = $this->getApiHook(self::$MEMBERSHIPS_SERVICE_HOOK, $this->getPlatform()->getFamilyCode()); $hook = new $className($this); $userResults = $hook->getMemberships($withGroups); $ok = $userResults !== false; } if ($ok) { - $oldUsers = $this->getUserResultSourcedIDs(true, ToolProvider::ID_SCOPE_RESOURCE); + $oldUsers = $this->getUserResultSourcedIDs(true, Tool::ID_SCOPE_RESOURCE); foreach ($userResults as $userresult) { // If a result sourcedid is provided save the user if (!empty($userresult->ltiResultSourcedId)) { $userresult->save(); } // Remove old user (if it exists) - unset($oldUsers[$userresult->getId(ToolProvider::ID_SCOPE_RESOURCE)]); + unset($oldUsers[$userresult->getId(Tool::ID_SCOPE_RESOURCE)]); } -// Delete any old users which were not in the latest list from the tool consumer +// Delete any old users which were not in the latest list from the platform foreach ($oldUsers as $id => $userresult) { $userresult->delete(); } @@ -1047,7 +1105,7 @@ public function getMemberships($withGroups = false) * It may also be optionally indexed by the user ID of a specified scope. * * @param bool $localOnly True if only users from this resource link are to be returned, not users from shared resource links (optional, default is false) - * @param int $idScope Scope to use for ID values (optional, default is null for consumer default) + * @param int $idScope Scope to use for ID values (optional, default is null for platform default) * * @return UserResult[] Array of UserResult objects */ @@ -1133,7 +1191,7 @@ public function getOutcomes($limit = null) $this->lastServiceRequest = null; $url = $this->getSetting('custom_lineitem_url'); if (!empty($url)) { - $resultService = new Service\Result($this->getConsumer(), $url); + $resultService = new Service\Result($this->getPlatform(), $url); $outcomes = $resultService->getAll($limit); $http = $resultService->getHttpMessage(); $this->extResponse = $http->response; @@ -1149,6 +1207,9 @@ public function getOutcomes($limit = null) /** * Class constructor from consumer. * + * @deprecated Use fromPlatform() instead + * @see ResourceLink::fromPlatform() + * * @param ToolConsumer $consumer Consumer object * @param string $ltiResourceLinkId Resource link ID value * @param string $tempId Temporary Resource link ID value (optional, default is null) @@ -1156,10 +1217,26 @@ public function getOutcomes($limit = null) * @return ResourceLink */ public static function fromConsumer($consumer, $ltiResourceLinkId, $tempId = null) + { + Util::logDebug('Method ceLTIc\LTI\ResourceLink::fromConsumer() has been deprecated; please use ceLTIc\LTI\ResourceLink::fromPlatform() instead.', + true); + return self::fromPlatform($consumer, $ltiResourceLinkId, $tempId); + } + + /** + * Class constructor from platform. + * + * @param Platform $platform Platform object + * @param string $ltiResourceLinkId Resource link ID value + * @param string $tempId Temporary Resource link ID value (optional, default is null) + * + * @return ResourceLink + */ + public static function fromPlatform($platform, $ltiResourceLinkId, $tempId = null) { $resourceLink = new ResourceLink(); - $resourceLink->consumer = $consumer; - $resourceLink->dataConnector = $consumer->getDataConnector(); + $resourceLink->platform = $platform; + $resourceLink->dataConnector = $platform->getDataConnector(); $resourceLink->ltiResourceLinkId = $ltiResourceLinkId; if (!empty($ltiResourceLinkId)) { $resourceLink->load(); @@ -1315,7 +1392,7 @@ private function checkValueType($ltiOutcome, $supportedTypes = null) } /** - * Send a service request to the tool consumer. + * Send an unofficial LTI service request to the platform. * * @param string $type Message type value * @param string $url URL to send request to @@ -1332,9 +1409,15 @@ private function doService($type, $url, $params) $this->extResponseHeaders = ''; $this->lastServiceRequest = null; if (!empty($url)) { - $params = $this->getConsumer()->signParameters($url, $type, $this->getConsumer()->ltiVersion, $params); -// Connect to tool consumer - $http = new HttpMessage($url, 'POST', $params); + $params['lti_version'] = $this->getPlatform()->ltiVersion; + $params['lti_message_type'] = $type; + $signed = $this->getPlatform()->addSignature($url, $params, 'POST', 'application/x-www-form-urlencoded'); +// Connect to platform + if (is_array($signed)) { + $http = new HttpMessage($url, 'POST', $signed); + } else { + $http = new HttpMessage($url, 'POST', $params, $signed); + } // Parse XML response if ($http->send()) { $this->extResponse = $http->response; @@ -1375,7 +1458,7 @@ private function doResultService($userResult, $url) $this->extResponseHeaders = ''; $this->lastServiceRequest = null; if (!empty($url)) { - $resultService = new Service\Result($this->getConsumer(), $url); + $resultService = new Service\Result($this->getPlatform(), $url); $outcome = $resultService->get($userResult); $http = $resultService->getHttpMessage(); $this->extResponse = $http->response; @@ -1406,7 +1489,7 @@ private function doScoreService($ltiOutcome, $userResult, $url) $this->extResponseHeaders = ''; $this->lastServiceRequest = null; if (!empty($url)) { - $scoreService = new Service\Score($this->getConsumer(), $url); + $scoreService = new Service\Score($this->getPlatform(), $url); $scoreService->submit($ltiOutcome, $userResult); $http = $scoreService->getHttpMessage(); $this->extResponse = $http->response; @@ -1421,7 +1504,7 @@ private function doScoreService($ltiOutcome, $userResult, $url) } /** - * Send a service request to the tool consumer. + * Send an LTI 1.1 service request to the platform. * * @param string $type Message type value * @param string $url URL to send request to @@ -1456,9 +1539,8 @@ private function doLTI11Service($type, $url, $xml) EOD; // Add message signature - $consumer = $this->getConsumer(); - $header = $consumer->addSignature($url, $xmlRequest, 'POST', 'application/xml'); -// Connect to tool consumer + $header = $this->getPlatform()->addSignature($url, $xmlRequest, 'POST', 'application/xml'); +// Connect to platform $http = new HttpMessage($url, 'POST', $xmlRequest, $header); // Parse XML response if ($http->send()) { @@ -1493,7 +1575,7 @@ private function getLineItemService() { $url = $this->getSetting('custom_lineitems_url'); if (!empty($url)) { - $lineItemService = new Service\LineItem($this->getConsumer(), $url); + $lineItemService = new Service\LineItem($this->getPlatform(), $url); } else { $lineItemService = false; } diff --git a/src/ResourceLinkShare.php b/src/ResourceLinkShare.php index 5fc8bc4..c339b38 100644 --- a/src/ResourceLinkShare.php +++ b/src/ResourceLinkShare.php @@ -3,7 +3,7 @@ namespace ceLTIc\LTI; /** - * Class to represent a tool consumer resource link share + * Class to represent a platform resource link share * * @author Stephen P Vickers * @copyright SPV Software Products diff --git a/src/ResourceLinkShareKey.php b/src/ResourceLinkShareKey.php index 014648f..7a51654 100644 --- a/src/ResourceLinkShareKey.php +++ b/src/ResourceLinkShareKey.php @@ -5,7 +5,7 @@ use ceLTIc\LTI\DataConnector\DataConnector; /** - * Class to represent a tool consumer resource link share key + * Class to represent a platform resource link share key * * @author Stephen P Vickers * @copyright SPV Software Products @@ -141,7 +141,7 @@ public function save() } else { $this->length = max(min($this->length, self::MAX_SHARE_KEY_LENGTH), self::MIN_SHARE_KEY_LENGTH); } - $this->id = DataConnector::getRandomString($this->length); + $this->id = Util::getRandomString($this->length); } return $this->dataConnector->saveResourceLinkShareKey($this); diff --git a/src/Service/AssignmentGrade.php b/src/Service/AssignmentGrade.php index aeeb5d4..3ded3dd 100644 --- a/src/Service/AssignmentGrade.php +++ b/src/Service/AssignmentGrade.php @@ -15,15 +15,14 @@ class AssignmentGrade extends Service /** * Class constructor. * - * @param ToolConsumer $consumer Tool consumer object for this service request + * @param Platform $platform Platform object for this service request * @param string $endpoint Service endpoint - * @param string $mediaType Media type of message body * @param string $path Path (optional) */ - public function __construct($consumer, $endpoint, $mediaType, $path = '') + public function __construct($platform, $endpoint, $path = '') { $endpoint = self::addPath($endpoint, $path); - parent::__construct($consumer, $endpoint, $mediaType); + parent::__construct($platform, $endpoint); } /** diff --git a/src/Service/LineItem.php b/src/Service/LineItem.php index 632da9f..292554f 100644 --- a/src/Service/LineItem.php +++ b/src/Service/LineItem.php @@ -3,7 +3,7 @@ namespace ceLTIc\LTI\Service; use ceLTIc\LTI; -use ceLTIc\LTI\ToolConsumer; +use ceLTIc\LTI\Platform; /** * Class to implement the Line Item service @@ -45,14 +45,14 @@ class LineItem extends AssignmentGrade /** * Class constructor. * - * @param ToolConsumer $consumer ToolConsumer object for this service request + * @param Platform $platform Platform object for this service request * @param string $endpoint Service endpoint * @param int|null $limit Limit of lineitems to be returned per request, null for all */ - public function __construct($consumer, $endpoint, $limit = null) + public function __construct($platform, $endpoint, $limit = null) { $this->limit = $limit; - parent::__construct($consumer, $endpoint, null); + parent::__construct($platform, $endpoint); $this->scope = self::$SCOPE_READONLY; } @@ -95,7 +95,7 @@ public function getAll($ltiResourceLinkId = null, $resourceId = null, $tag = nul if ($http->ok) { if (!empty($http->responseJson)) { foreach ($http->responseJson as $lineItem) { - $lineItems[] = self::toLineItem($this->getConsumer(), $lineItem); + $lineItems[] = self::toLineItem($this->getPlatform(), $lineItem); } } if (!empty($limit) && preg_match('/\<([^\>]+)\>; *rel=[\"next\"|next]/', $http->responseHeaders, $matches)) { @@ -126,7 +126,7 @@ public function createLineItem($lineItem) $this->scope = self::$SCOPE_READONLY; $ok = $http->ok && !empty($http->responseJson); if ($ok) { - $newLineItem = self::toLineItem($this->getConsumer(), $http->responseJson); + $newLineItem = self::toLineItem($this->getPlatform(), $http->responseJson); foreach (get_object_vars($newLineItem) as $key => $value) { $lineItem->$key = $value; } @@ -138,18 +138,18 @@ public function createLineItem($lineItem) /** * Retrieve a line item. * - * @param ToolConsumer $consumer ToolConsumer object for this service request + * @param Platform $platform Platform object for this service request * @param string $endpoint Line item endpoint * * @return LTI\\LineItem|bool LineItem object, or false on error */ - public static function getLineItem($consumer, $endpoint) + public static function getLineItem($platform, $endpoint) { - $service = new self($consumer, $endpoint); + $service = new self($platform, $endpoint); $service->mediaType = self::$MEDIA_TYPE_LINE_ITEM; $http = $service->send('GET'); if ($http->ok && !empty($http->responseJson)) { - $lineItem = self::toLineItem($consumer, $http->responseJson); + $lineItem = self::toLineItem($platform, $http->responseJson); } else { $lineItem = false; } @@ -164,15 +164,15 @@ public static function getLineItem($consumer, $endpoint) /** * Create a line item from a JSON object. * - * @param ToolConsumer $consumer Tool consumer object for this service request + * @param Platform $platform Platform object for this service request * @param object $json JSON object to convert * * @return LTI\\LineItem|null LineItem object, or null on error */ - private static function toLineItem($consumer, $json) + private static function toLineItem($platform, $json) { if (!empty($json->id) && !empty($json->label) && !empty($json->scoreMaximum)) { - $lineItem = new LTI\LineItem($consumer, $json->label, $json->scoreMaximum); + $lineItem = new LTI\LineItem($platform, $json->label, $json->scoreMaximum); if (!empty($json->id)) { $lineItem->endpoint = $json->id; } diff --git a/src/Service/Membership.php b/src/Service/Membership.php index 53039fc..d47dc19 100644 --- a/src/Service/Membership.php +++ b/src/Service/Membership.php @@ -19,19 +19,24 @@ class Membership extends Service /** * Media type for version 1 of Memberships service. */ - const MEMBERSHIPS_MEDIA_TYPE_V1 = 'application/vnd.ims.lis.v2.membershipcontainer+json'; + const MEDIA_TYPE_MEMBERSHIPS_V1 = 'application/vnd.ims.lis.v2.membershipcontainer+json'; /** * Media type for Names and Role Provisioning service. */ - const MEMBERSHIPS_MEDIA_TYPE_NRPS = 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json'; + const MEDIA_TYPE_MEMBERSHIPS_NRPS = 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json'; + + /** + * Access scope. + */ + public static $SCOPE = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; /** * The object to which the memberships apply (ResourceLink or Context). * * @var Context|ResourceLink $source */ - private $source; + private $source = null; /** * Class constructor. @@ -40,10 +45,12 @@ class Membership extends Service * @param string $endpoint Service endpoint * @param string $format Format to request */ - public function __construct($source, $endpoint, $format = self::MEMBERSHIPS_MEDIA_TYPE_V1) + public function __construct($source, $endpoint, $format = self::MEDIA_TYPE_MEMBERSHIPS_V1) { - $consumer = $source->getConsumer(); - parent::__construct($consumer, $endpoint, $format); + $platform = $source->getPlatform(); + parent::__construct($platform, $endpoint); + $this->scope = self::$SCOPE; + $this->mediaType = $format; $this->source = $source; } @@ -74,7 +81,7 @@ public function get($role = null, $limit = 0) } else { $userResults = array(); if ($isLink) { - $oldUsers = $this->source->getUserResultSourcedIDs(true, LTI\ToolProvider::ID_SCOPE_RESOURCE); + $oldUsers = $this->source->getUserResultSourcedIDs(true, LTI\Tool::ID_SCOPE_RESOURCE); } if (isset($http->responseJson->pageOf) && isset($http->responseJson->pageOf->membershipSubject) && isset($http->responseJson->pageOf->membershipSubject->membership)) { @@ -111,12 +118,12 @@ public function get($role = null, $limit = 0) // Set the user email $email = (isset($member->email)) ? $member->email : ''; - $userresult->setEmail($email, $this->source->getConsumer()->defaultEmail); + $userresult->setEmail($email, $this->source->getPlatform()->defaultEmail); // Set the user roles if (isset($membership->role)) { $roles = $this->parseContextsInArray($http->responseJson->{'@context'}, $membership->role); - $userresult->roles = LTI\ToolProvider::parseRoles($roles, LTI\ToolProvider::LTI_VERSION2); + $userresult->roles = LTI\Tool::parseRoles($roles, LTI\Util::LTI_VERSION2); } // If a result sourcedid is provided save the user @@ -164,7 +171,7 @@ public function get($role = null, $limit = 0) // Remove old user (if it exists) if ($isLink) { - unset($oldUsers[$userresult->getId(LTI\ToolProvider::ID_SCOPE_RESOURCE)]); + unset($oldUsers[$userresult->getId(LTI\Tool::ID_SCOPE_RESOURCE)]); } } } elseif (isset($http->responseJson->members)) { @@ -189,11 +196,11 @@ public function get($role = null, $limit = 0) // Set the user email $email = (isset($member->email)) ? $member->email : ''; - $userresult->setEmail($email, $this->source->getConsumer()->defaultEmail); + $userresult->setEmail($email, $this->source->getPlatform()->defaultEmail); // Set the user roles if (isset($member->roles)) { - $userresult->roles = LTI\ToolProvider::parseRoles($member->roles, LTI\ToolProvider::LTI_VERSION2); + $userresult->roles = LTI\Tool::parseRoles($member->roles, LTI\Util::LTI_VERSION2); } // If a result sourcedid is provided save the user @@ -237,12 +244,12 @@ public function get($role = null, $limit = 0) // Remove old user (if it exists) if ($isLink) { - unset($oldUsers[$userresult->getId(LTI\ToolProvider::ID_SCOPE_RESOURCE)]); + unset($oldUsers[$userresult->getId(LTI\Tool::ID_SCOPE_RESOURCE)]); } } } -/// Delete any old users which were not in the latest list from the tool consumer +/// Delete any old users which were not in the latest list from the platform if ($isLink) { foreach ($oldUsers as $id => $userresult) { $userresult->delete(); diff --git a/src/Service/Result.php b/src/Service/Result.php index 20bfb67..7508dc1 100644 --- a/src/Service/Result.php +++ b/src/Service/Result.php @@ -16,9 +16,9 @@ class Result extends AssignmentGrade { /** - * Media type. + * Access scope. */ - const RESULT_CONTAINER_MEDIA_TYPE = 'application/vnd.ims.lis.v2.resultcontainer+json'; + public static $SCOPE = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'; /** * Limit on size of container to be returned from requests. @@ -30,18 +30,20 @@ class Result extends AssignmentGrade /** * Class constructor. * - * @param ToolConsumer $consumer Tool consumer object for this service request + * @param Platform $platform Platform object for this service request * @param string $endpoint Service endpoint * @param int|null $limit Limit of results to be returned per request, null for all */ - public function __construct($consumer, $endpoint, $limit = null) + public function __construct($platform, $endpoint, $limit = null) { $this->limit = $limit; - parent::__construct($consumer, $endpoint, self::RESULT_CONTAINER_MEDIA_TYPE, '/results'); + parent::__construct($platform, $endpoint, '/results'); + $this->scope = self::$SCOPE; + $this->mediaType = 'application/vnd.ims.lis.v2.resultcontainer+json'; } /** - * Retrieve all user outcome for a lineitem. + * Retrieve all outcomes for a line item. * * @param int|null $limit Limit of results to be returned, null for service default * @@ -100,6 +102,10 @@ public function get($user) } } +### +### PRIVATE METHOD +### + private static function getOutcome($json) { $outcome = new Outcome(); diff --git a/src/Service/Score.php b/src/Service/Score.php index 51dd302..4565fd1 100644 --- a/src/Service/Score.php +++ b/src/Service/Score.php @@ -15,19 +15,21 @@ class Score extends AssignmentGrade { /** - * Media type. + * Access scope. */ - const SCORE_MEDIA_TYPE = 'application/vnd.ims.lis.v1.score+json'; + public static $SCOPE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score'; /** * Class constructor. * - * @param ToolConsumer $consumer Tool consumer object for this service request + * @param Tool $tool Tool object for this service request * @param string $endpoint Service endpoint */ - public function __construct($consumer, $endpoint) + public function __construct($tool, $endpoint) { - parent::__construct($consumer, $endpoint, self::SCORE_MEDIA_TYPE, '/scores'); + parent::__construct($tool, $endpoint, '/scores'); + $this->scope = self::$SCOPE; + $this->mediaType = 'application/vnd.ims.lis.v1.score+json'; } /** diff --git a/src/Service/Service.php b/src/Service/Service.php index 5767256..fc1f580 100644 --- a/src/Service/Service.php +++ b/src/Service/Service.php @@ -2,7 +2,8 @@ namespace ceLTIc\LTI\Service; -use ceLTIc\LTI; +use ceLTIc\LTI\AccessToken; +use ceLTIc\LTI\Platform; use ceLTIc\LTI\ToolConsumer; use ceLTIc\LTI\Http\HttpMessage; @@ -28,21 +29,28 @@ class Service * * @var string $endpoint */ - protected $endpoint; + protected $endpoint = null; + + /** + * Service access scope. + * + * @var string $scope + */ + protected $scope = null; /** * Media type of message body. * * @var string $mediaType */ - protected $mediaType; + protected $mediaType = null; /** - * Tool Consumer for this service request. + * Platform for this service request. * - * @var ToolConsumer $consumer + * @var Platform $platform */ - private $consumer; + private $platform = null; /** * HttpMessage object for last service request. @@ -54,25 +62,48 @@ class Service /** * Class constructor. * - * @param ToolConsumer $consumer Tool consumer object for this service request + * @param Platform $platform Platform object for this service request * @param string $endpoint Service endpoint - * @param string $mediaType Media type of message body */ - public function __construct($consumer, $endpoint, $mediaType) + public function __construct($platform, $endpoint) { - $this->consumer = $consumer; + $this->platform = $platform; $this->endpoint = $endpoint; - $this->mediaType = $mediaType; } /** * Get tool consumer. * + * @deprecated Use getPlatform() instead + * @see Service::getPlatform() + * * @return ToolConsumer Consumer for this service */ public function getConsumer() { - return $this->consumer; + Util::logDebug('Method ceLTIc\LTI\Service::getConsumer() has been deprecated; please use ceLTIc\LTI\Service::getPlatform() instead.', + true); + return $this->getPlatform(); + } + + /** + * Get platform. + * + * @return Platform Platform for this service + */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Get access scope. + * + * @return string Access scope + */ + public function getScope() + { + return $this->scope; } /** @@ -98,19 +129,36 @@ public function send($method, $parameters = array(), $body = null) $sep = '&'; } } - if (!$this->unsigned) { - $header = $this->consumer->signServiceRequest($url, $method, $this->mediaType, $body); - } else { - $header = null; - } - -// Connect to tool consumer - $this->http = new HttpMessage($url, $method, $body, $header); + $header = null; + $retry = !$this->platform->useOAuth1(); + do { + if (!$this->unsigned) { + $accessToken = $this->platform->getAccessToken(); + if (!$this->platform->useOAuth1()) { + if (empty($accessToken)) { + $accessToken = new AccessToken($this->platform); + $this->platform->setAccessToken($accessToken); + } + if (!$accessToken->hasScope($this->scope)) { + $accessToken->get($this->scope); + $retry = false; + } + } + $header = $this->platform->signServiceRequest($url, $method, $this->mediaType, $body); + } +// Connect to platform + $this->http = new HttpMessage($url, $method, $body, $header); // Parse JSON response - if ($this->http->send() && !empty($this->http->response)) { - $this->http->responseJson = json_decode($this->http->response); - $this->http->ok = !is_null($this->http->responseJson); - } + if ($this->http->send() && !empty($this->http->response)) { + $this->http->responseJson = json_decode($this->http->response); + $this->http->ok = !is_null($this->http->responseJson); + } + $retry = $retry && !$this->http->ok; + if ($retry) { // Invalidate existing token to force a new one to be obtained + $accessToken->expires = time(); + $this->platform->setAccessToken($accessToken); + } + } while ($retry); return $this->http; } diff --git a/src/Service/ToolSettings.php b/src/Service/ToolSettings.php index 5f34a14..25315ec 100644 --- a/src/Service/ToolSettings.php +++ b/src/Service/ToolSettings.php @@ -2,7 +2,7 @@ namespace ceLTIc\LTI\Service; -use ceLTIc\LTI\ToolConsumer; +use ceLTIc\LTI\Platform; use ceLTIc\LTI\Context; use ceLTIc\LTI\ResourceLink; @@ -31,6 +31,11 @@ class ToolSettings extends Service */ const MODE_DISTINCT_NAMES = 3; + /** + * Access scope. + */ + public static $SCOPE = 'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting'; + /** * Names of LTI parameters to be retained in the consumer settings property. * @@ -41,9 +46,9 @@ class ToolSettings extends Service 'LtiLink' => 'link'); /** - * The object to which the settings apply (ResourceLink, Context or ToolConsumer). + * The object to which the settings apply (ResourceLink, Context or Platform). * - * @var ToolConsumer|Context|ResourceLink $source + * @var Platform|Context|ResourceLink $source */ private $source; @@ -57,23 +62,24 @@ class ToolSettings extends Service /** * Class constructor. * - * @param ToolConsumer|Context|ResourceLink $source The object to which the settings apply (ResourceLink, Context or ToolConsumer) - * @param string $endpoint Service endpoint - * @param bool $simple True if the simple media type is to be used (optional, default is true) + * @param Platform|Context|ResourceLink $source The object to which the settings apply (ResourceLink, Context or Platform) + * @param string $endpoint Service endpoint + * @param bool $simple True if the simple media type is to be used (optional, default is true) */ public function __construct($source, $endpoint, $simple = true) { - if (is_a($source, 'ceLTIc\LTI\ToolConsumer')) { - $consumer = $source; + if (is_a($source, 'ceLTIc\LTI\Platform')) { + $platform = $source; } else { - $consumer = $source->getConsumer(); + $platform = $source->getPlatform(); } + parent::__construct($platform, $endpoint); + $this->scope = self::$SCOPE; if ($simple) { - $mediaType = 'application/vnd.ims.lti.v2.toolsettings.simple+json'; + $this->mediaType = 'application/vnd.ims.lti.v2.toolsettings.simple+json'; } else { - $mediaType = 'application/vnd.ims.lti.v2.toolsettings+json'; + $this->mediaType = 'application/vnd.ims.lti.v2.toolsettings+json'; } - parent::__construct($consumer, $endpoint, $mediaType); $this->source = $source; $this->simple = $simple; } @@ -120,7 +126,7 @@ public function get($mode = self::MODE_CURRENT_LEVEL) public function set($settings) { if (!$this->simple) { - if (is_a($this->source, 'ToolConsumer')) { + if (is_a($this->source, 'Platform')) { $type = 'ToolProxy'; } elseif (is_a($this->source, 'Context')) { $type = 'ToolProxyBinding'; diff --git a/src/System.php b/src/System.php new file mode 100644 index 0000000..2296dae --- /dev/null +++ b/src/System.php @@ -0,0 +1,914 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +trait System +{ + + /** + * True if the last request was successful. + * + * @var bool $ok + */ + public $ok = true; + + /** + * Shared secret. + * + * @var string|null $secret + */ + public $secret = null; + + /** + * Method used for signing messages. + * + * @var string $signatureMethod + */ + public $signatureMethod = 'HMAC-SHA1'; + + /** + * Algorithm used for encrypting messages. + * + * @var string $encryptionMethod + */ + public $encryptionMethod = ''; + + /** + * Data connector object. + * + * @var DataConnector|null $dataConnector + */ + public $dataConnector = null; + + /** + * RSA key in PEM or JSON format. + * + * Set to the private key for signing outgoing messages and service requests, and to the public key + * for verifying incoming messages and service requests. + * + * @var string|null $rsaKey + */ + public $rsaKey = null; + + /** + * Scopes to request when obtaining an access token. + * + * @var array $requiredScopes + */ + public $requiredScopes = array(); + + /** + * Key ID. + * + * @var string|null $kid + */ + public $kid = null; + + /** + * Endpoint for public key. + * + * @var string|null $jku + */ + public $jku = null; + + /** + * Error message for last request processed. + * + * @var string|null $reason + */ + public $reason = null; + + /** + * Details for error message relating to last request processed. + * + * @var array $details + */ + public $details = array(); + + /** + * Whether debug level messages are to be reported. + * + * @var bool $debugMode + */ + public $debugMode = false; + + /** + * JWT object, if any. + * + * @var JWS|null $jwt + */ + protected $jwt = null; + + /** + * Raw message parameters. + * + * @var array $rawParameters + */ + protected $rawParameters = null; + + /** + * LTI message parameters. + * + * @var array|null $messageParameters + */ + protected $messageParameters = null; + + /** + * Consumer key/client ID value. + * + * @var string|null $key + */ + private $key = null; + + /** + * Get the consumer key. + * + * @return string Consumer key value + */ + public function getKey() + { + return $this->key; + } + + /** + * Set the consumer key. + * + * @param string $key Consumer key value + */ + public function setKey($key) + { + $this->key = $key; + } + + /** + * Check whether a JWT exists + * + * @return bool True if a JWT exists + */ + public function hasJwt() + { + return !empty($this->jwt) && $this->jwt->hasJwt(); + } + + /** + * Get the JWT + * + * @return ClientInterface The JWT + */ + public function getJwt() + { + return $this->jwt; + } + + /** + * Get the raw POST parameters + * + * @return array The POST parameter array + */ + public function getRawParameters() + { + if (is_null($this->rawParameters)) { + $this->rawParameters = OAuth\OAuthUtil::parse_parameters(file_get_contents(OAuth\OAuthRequest::$POST_INPUT)); + } + + return $this->rawParameters; + } + + /** + * Get the message claims + * + * @param bool $fullyQualified True if claims should be fully qualified rather than grouped (default is false) + * + * @return array The message claim array + */ + public function getMessageClaims($fullyQualified = false) + { + $messageClaims = null; + if (!is_null($this->messageParameters)) { + $messageParameters = $this->messageParameters; + if (!empty($messageParameters['lti_message_type']) && array_key_exists($messageParameters['lti_message_type'], + Util::MESSAGE_TYPE_MAPPING)) { + $messageParameters['lti_message_type'] = Util::MESSAGE_TYPE_MAPPING[$messageParameters['lti_message_type']]; + } + if (!empty($messageParameters['accept_media_types'])) { + $mediaTypes = explode(',', $messageParameters['accept_media_types']); + $types = array(); + if (!empty($messageParameters['accept_types'])) { + $types = explode(',', $messageParameters['accept_types']); + } + foreach ($mediaTypes as $mediaType) { + if ($mediaType === Item::LTI_LINK_MEDIA_TYPE) { + unset($mediaTypes[array_search(Item::LTI_LINK_MEDIA_TYPE, $mediaTypes)]); + $types[] = Item::TYPE_LTI_LINK; + } elseif (substr($mediaType, 0, 6) === 'image/') { + $types[] = 'image'; + } elseif (substr($mediaType, 0, 6) === 'text/html') { + $types[] = 'html'; + } elseif (substr($mediaType, 0, 6) === '*/*') { + $types[] = 'html'; + $types[] = 'file'; + $types[] = 'link'; + } else { + $types[] = 'file'; + } + } + $messageParameters['accept_media_types'] = implode(',', $mediaTypes); + $types = array_unique($types); + $messageParameters['accept_types'] = implode(',', $types); + } + $messageClaims = array(); + foreach ($messageParameters as $key => $value) { + $ok = true; + if (array_key_exists($key, Util::JWT_CLAIM_MAPPING)) { + $mapping = Util::JWT_CLAIM_MAPPING[$key]; + if (isset($mapping['isObject']) && $mapping['isObject']) { + $value = json_decode($value); + } elseif (isset($mapping['isArray']) && $mapping['isArray']) { + $value = explode(',', $value); + sort($value); + } elseif (isset($mapping['isBoolean']) && $mapping['isBoolean']) { + $value = $value === 'true'; + } + $group = ''; + $claim = Util::JWT_CLAIM_PREFIX; + if (!empty($mapping['suffix'])) { + $claim .= "-{$mapping['suffix']}"; + } + $claim .= '/claim/'; + if (is_null($mapping['group'])) { + $claim = $mapping['claim']; + } elseif (empty($mapping['group'])) { + $claim .= $mapping['claim']; + } else { + $group = $claim . $mapping['group']; + $claim = $mapping['claim']; + } + } elseif (substr($key, 0, 7) === 'custom_') { + $group = Util::JWT_CLAIM_PREFIX . '/claim/custom'; + $claim = substr($key, 7); + } elseif (substr($key, 0, 4) === 'ext_') { + $group = Util::JWT_CLAIM_PREFIX . '/claim/ext'; + $claim = substr($key, 4); + } else { + $ok = false; + } + if ($ok) { + if ($fullyQualified) { + if (empty($group)) { + $messageClaims[$claim] = $value; + } else { + $messageClaims["{$group}/{$claim}"] = $value; + } + } elseif (empty($group)) { + $messageClaims[$claim] = $value; + } else { + $messageClaims[$group][$claim] = $value; + } + } + } + } + + return $messageClaims; + } + + /** + * Get an array of fully qualified user roles + * + * @param mixed $roles Comma-separated list of roles or array of roles + * @param string $ltiVersion LTI version (default is LTI-1p0) + * + * @return array Array of roles + */ + public static function parseRoles($roles, $ltiVersion = Util::LTI_VERSION1) + { + if (!is_array($roles)) { + $roles = explode(',', $roles); + } + $parsedRoles = array(); + foreach ($roles as $role) { + $role = trim($role); + if (!empty($role)) { + if ($ltiVersion === Util::LTI_VERSION1) { + if ((substr($role, 0, 4) !== 'urn:') && + (substr($role, 0, 7) !== 'http://') && (substr($role, 0, 8) !== 'https://')) { + $role = 'urn:lti:role:ims/lis/' . $role; + } + } elseif ((substr($role, 0, 7) !== 'http://') && (substr($role, 0, 8) !== 'https://')) { + $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . $role; + } + $parsedRoles[] = $role; + } + } + + return $parsedRoles; + } + + /** + * Add the signature to an LTI message. + * + * @param string $url URL for message request + * @param string $type LTI message type + * @param string $version LTI version + * @param array $params Message parameters + * + * @return array|string Array of signed message parameters or request headers + */ + public function signParameters($url, $type, $version, $params) + { + if (!empty($url)) { +// Add standard parameters + $params['lti_version'] = $version; + $params['lti_message_type'] = $type; +// Add signature + $params = $this->addSignature($url, $params, 'POST', 'application/x-www-form-urlencoded'); + } + + return $params; + } + + /** + * Generates the headers for an LTI service request. + * + * @param string $url URL for message request + * @param string $method HTTP method + * @param string $type Media type + * @param string $data Data being passed in request body (optional) + * + * @return string Headers to include with service request + */ + public function signServiceRequest($url, $method, $type, $data = null) + { + $header = ''; + if (!empty($url)) { + $header = $this->addSignature($url, $data, $method, $type); + } + + return $header; + } + + /** + * Perform a service request + * + * @param object $service Service object to be executed + * @param string $method HTTP action + * @param string $format Media type + * @param mixed $data Array of parameters or body string + * + * @return HttpMessage HTTP object containing request and response details + */ + public function doServiceRequest($service, $method, $format, $data) + { + $header = $this->addSignature($service->endpoint, $data, $method, $format); + +// Connect to platform + $http = new HttpMessage($service->endpoint, $method, $data, $header); +// Parse JSON response + if ($http->send() && !empty($http->response)) { + $http->responseJson = json_decode($http->response); + $http->ok = !is_null($http->responseJson); + } + + return $http; + } + + /** + * Determine whether this consumer is using the OAuth 1 security model. + * + * @return bool True if OAuth 1 security model should be used + */ + public function useOAuth1() + { + return empty($this->signatureMethod) || (substr($this->signatureMethod, 0, 2) !== 'RS'); + } + + /** + * Add the signature to an array of message parameters or to a header string. + * + * @param string $endpoint URL to which message is being sent + * @param mixed $data Data to be passed + * @param string $method HTTP method + * @param string|null $type Content type of data being passed + * @param string|null $nonce Nonce value for JWT + * @param string|null $hash OAuth body hash value + * @param int|null $timestamp Timestamp + * + * @return mixed Array of signed message parameters or header string + */ + public function addSignature($endpoint, $data, $method = 'POST', $type = null, $nonce = '', $hash = null, $timestamp = null) + { + if ($this->useOAuth1()) { + return $this->addOAuth1Signature($endpoint, $data, $method, $type, $hash, $timestamp); + } else { + return $this->addJWTSignature($endpoint, $data, $method, $type, $nonce, $timestamp); + } + } + + /** + * Verify the signature of a message. + * + * @return bool True if the signature is valid + */ + public function verifySignature() + { + $ok = false; + if ($this instanceof Tool) { + $platform = $this->platform; + } else { + $platform = $this; + } + if (isset($this->messageParameters['oauth_signature_method'])) { + $this->signatureMethod = $this->messageParameters['oauth_signature_method']; + if ($this instanceof Tool) { + $this->platform->signatureMethod = $this->messageParameters['oauth_signature_method']; + } + } + if (empty($this->jwt) || empty($this->jwt->hasJwt())) { // OAuth-signed message + try { + $store = new OAuthDataStore($this); + $server = new OAuth\OAuthServer($store); + $method = new OAuth\OAuthSignatureMethod_HMAC_SHA224(); + $server->add_signature_method($method); + $method = new OAuth\OAuthSignatureMethod_HMAC_SHA256(); + $server->add_signature_method($method); + $method = new OAuth\OAuthSignatureMethod_HMAC_SHA384(); + $server->add_signature_method($method); + $method = new OAuth\OAuthSignatureMethod_HMAC_SHA512(); + $server->add_signature_method($method); + $method = new OAuth\OAuthSignatureMethod_HMAC_SHA1(); + $server->add_signature_method($method); + $request = OAuth\OAuthRequest::from_request(); + $server->verify_request($request); + $ok = true; + } catch (\Exception $e) { + if (empty($this->reason)) { + $oauthConsumer = new OAuth\OAuthConsumer($platform->getKey(), $platform->secret); + $signature = $request->build_signature($method, $oauthConsumer, false); + if ($this->debugMode) { + $this->reason = $e->getMessage(); + } + if (empty($this->reason)) { + $this->reason = 'OAuth signature check failed - perhaps an incorrect secret or timestamp.'; + } + $this->details[] = 'Current timestamp: ' . time(); + $this->details[] = "Expected signature: {$signature}"; + $this->details[] = "Base string: {$request->base_string}"; + } + } + } else { // JWT-signed message + $nonce = new PlatformNonce($platform, $this->jwt->getClaim('nonce')); + $ok = !$nonce->load(); + if ($ok) { + $ok = $nonce->save(); + } + if (!$ok) { + $this->reason = 'Invalid nonce.'; + } elseif (!empty($platform->rsaKey) || !empty($platform->jku) || Jwt::$allowJkuHeader) { + unset($this->messageParameters['oauth_consumer_key']); + $ok = $this->jwt->verify($platform->rsaKey, $platform->jku); + if (!$ok) { + $this->reason = 'JWT signature check failed - perhaps an invalid public key or timestamp'; + } + } else { + $ok = false; + $this->reason = 'Unbale to verify JWT signature as neither a public key nor a JSON Web Key URL is specified'; + } + } + + return $ok; + } + +### +### PRIVATE METHODS +### + + /** + * Parse the message + */ + private function parseMessage() + { + if (is_null($this->messageParameters)) { + $this->getRawParameters(); + if (isset($this->rawParameters['id_token']) || isset($this->rawParameters['JWT'])) { // JWT-signed message + try { + $this->jwt = Jwt::getJwtClient(); + if (isset($this->rawParameters['id_token'])) { + $this->jwt->load($this->rawParameters['id_token']); + } else { + $this->jwt->load($this->rawParameters['JWT']); + } + $this->ok = $this->jwt->hasClaim('iss') && $this->jwt->hasClaim('aud') && + $this->jwt->hasClaim(Util::JWT_CLAIM_PREFIX . '/claim/deployment_id'); + if ($this->ok) { + $iss = $this->jwt->getClaim('iss'); + $aud = $this->jwt->getClaim('aud'); + $deploymentId = $this->jwt->getClaim(Util::JWT_CLAIM_PREFIX . '/claim/deployment_id'); + $this->ok = !empty($iss) && !empty($aud) && !empty($deploymentId); + if (!$this->ok) { + $this->reason = 'iss, aud and/or deployment_id claim is empty'; + } elseif (is_array($aud)) { + if ($this->jwt->hasClaim('azp')) { + $this->ok = !empty($this->jwt->getClaim('azp')); + if (!$this->ok) { + $this->reason = 'azp claim is empty'; + } else { + $this->ok = in_array($this->jwt->getClaim('azp'), $aud); + if ($this->ok) { + $aud = $this->jwt->getClaim('azp'); + } else { + $this->reason = 'azp claim value is not included in aud claim'; + } + } + } else { + $aud = $aud[0]; + $this->ok = !empty($aud); + if (!$this->ok) { + $this->reason = 'First element of aud claim is empty'; + } + } + } elseif ($this->jwt->hasClaim('azp')) { + $this->ok = $this->jwt->getClaim('azp') === $aud; + if (!$this->ok) { + $this->reason = 'aud claim does not match the azp claim'; + } + } + if ($this->ok) { + $this->platform = Platform::fromPlatformId($iss, $aud, $deploymentId, $this->dataConnector); + if (isset($this->rawParameters['id_token'])) { + $this->ok = !empty($this->rawParameters['state']); + if ($this->ok) { + $nonce = new PlatformNonce($this->platform, $this->rawParameters['state']); + $this->ok = $nonce->load(); + if ($this->ok) { + $this->ok = $nonce->delete(); + } + } + } + if ($this->ok) { + $this->platform->platformId = $this->jwt->getClaim('iss'); + $this->messageParameters = array(); + $this->messageParameters['oauth_consumer_key'] = $aud; + $this->messageParameters['oauth_signature_method'] = $this->jwt->getHeader('alg'); + $this->parseClaims(); + } else { + $this->reason = 'state parameter is invalid or missing'; + } + } + } else { + $this->reason = 'iss, aud and/or deployment_id claim not found'; + } + } catch (\Exception $e) { + $this->ok = false; + $this->reason = 'Message does not contain a valid JWT'; + } + } elseif (isset($this->rawParameters['error'])) { // Error with JWT-signed message + $this->ok = false; + $this->reason = $this->rawParameters['error']; + } else { // OAuth + if (isset($this->rawParameters['oauth_consumer_key'])) { + $this->platform = Platform::fromConsumerKey($this->rawParameters['oauth_consumer_key'], $this->dataConnector); + } + $this->messageParameters = $this->rawParameters; + } + } + } + + /** + * Parse the claims + */ + private function parseClaims() + { + foreach (Util::JWT_CLAIM_MAPPING as $key => $mapping) { + $claim = Util::JWT_CLAIM_PREFIX; + if (!empty($mapping['suffix'])) { + $claim .= "-{$mapping['suffix']}"; + } + $claim .= '/claim/'; + if (is_null($mapping['group'])) { + $claim = $mapping['claim']; + } elseif (empty($mapping['group'])) { + $claim .= $mapping['claim']; + } else { + $claim .= $mapping['group']; + } + if ($this->jwt->hasClaim($claim)) { + $value = null; + if (empty($mapping['group'])) { + $value = $this->jwt->getClaim($claim); + } else { + $group = $this->jwt->getClaim($claim); + if (is_array($group) && array_key_exists($mapping['claim'], $group)) { + $value = $group[$mapping['claim']]; + } elseif (is_object($group) && isset($group->{$mapping['claim']})) { + $value = $group->{$mapping['claim']}; + } + } + if (!is_null($value)) { + if (isset($mapping['isArray']) && $mapping['isArray']) { + if (!is_array($value)) { + $this->ok = false; + } else { + $value = implode(',', $value); + } + } elseif (isset($mapping['isObject']) && $mapping['isObject']) { + $value = json_encode($value); + } elseif (isset($mapping['isBoolean']) && $mapping['isBoolean']) { + $value = $value ? 'true' : 'false'; + } + } + if (!is_null($value) && is_string($value)) { + $this->messageParameters[$key] = $value; + } + } + } + if (!empty($this->messageParameters['lti_message_type']) && + in_array($this->messageParameters['lti_message_type'], array_values(Util::MESSAGE_TYPE_MAPPING))) { + $this->messageParameters['lti_message_type'] = array_search($this->messageParameters['lti_message_type'], + Util::MESSAGE_TYPE_MAPPING); + } + if (!empty($this->messageParameters['accept_types'])) { + $types = explode(',', $this->messageParameters['accept_types']); + $mediaTypes = array(); + if (!empty($this->messageParameters['accept_media_types'])) { + $mediaTypes = explode(',', $this->messageParameters['accept_media_types']); + } + if (in_array(Item::TYPE_LTI_LINK, $types)) { + $mediaTypes[] = Item::LTI_LINK_MEDIA_TYPE; + } + if (in_array('html', $types) && !in_array('*/*', $mediaTypes)) { + $mediaTypes[] = 'text/html'; + } + if (in_array('image', $types) && !in_array('*/*', $mediaTypes)) { + $mediaTypes[] = 'image/*'; + } + $mediaTypes = array_unique($mediaTypes); + $this->messageParameters['accept_media_types'] = implode(',', $mediaTypes); + } + $claim = Util::JWT_CLAIM_PREFIX . '/claim/custom'; + if ($this->jwt->hasClaim($claim)) { + $custom = $this->jwt->getClaim($claim); + if (!is_array($custom) && !is_object($custom)) { + $this->ok = false; + } else { + foreach ($custom as $key => $value) { + $this->messageParameters["custom_{$key}"] = $value; + } + } + } + $claim = Util::JWT_CLAIM_PREFIX . '/claim/ext'; + if ($this->jwt->hasClaim($claim)) { + $ext = $this->jwt->getClaim($claim); + if (!is_array($ext) && !is_object($ext)) { + $this->ok = false; + } else { + foreach ($ext as $key => $value) { + $this->messageParameters["ext_{$key}"] = $value; + } + } + } + } + + /** + * Add the OAuth 1 signature to an array of message parameters or to a header string. + * + * @param string $endpoint URL to which message is being sent + * @param mixed $data Data to be passed + * @param string $method HTTP method + * @param string|null $type Content type of data being passed + * @param string|null $hash OAuth body hash value + * @param int|null $timestamp Timestamp + * + * @return string[]|string Array of signed message parameters or header string + */ + private function addOAuth1Signature($endpoint, $data, $method, $type, $hash, $timestamp) + { + $params = array(); + if (is_array($data)) { + $params = $data; + $params['oauth_callback'] = 'about:blank'; + } +// Check for query parameters which need to be included in the signature + $queryString = parse_url($endpoint, PHP_URL_QUERY); + $queryParams = OAuth\OAuthUtil::parse_parameters($queryString); + $params = array_merge_recursive($queryParams, $params); + + if (!is_array($data)) { + if (empty($hash)) { // Calculate body hash + switch ($this->signatureMethod) { + case 'HMAC-SHA224': + $hash = base64_encode(hash('sha224', $data, true)); + break; + case 'HMAC-SHA256': + $hash = base64_encode(hash('sha256', $data, true)); + break; + case 'HMAC-SHA384': + $hash = base64_encode(hash('sha384', $data, true)); + break; + case 'HMAC-SHA512': + $hash = base64_encode(hash('sha512', $data, true)); + break; + default: + $hash = base64_encode(sha1($data, true)); + break; + } + } + $params['oauth_body_hash'] = $hash; + } + if (!empty($timestamp)) { + $params['oauth_timestamp'] = $timestamp; + } + +// Add OAuth signature + switch ($this->signatureMethod) { + case 'HMAC-SHA224': + $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA224(); + break; + case 'HMAC-SHA256': + $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA256(); + break; + case 'HMAC-SHA384': + $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA384(); + break; + case 'HMAC-SHA512': + $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA512(); + break; + default: + $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA1(); + break; + } + $oauthConsumer = new OAuth\OAuthConsumer($this->key, $this->secret, null); + $oauthReq = OAuth\OAuthRequest::from_consumer_and_token($oauthConsumer, null, $method, $endpoint, $params); + $oauthReq->sign_request($hmacMethod, $oauthConsumer, null); + if (!is_array($data)) { + $header = $oauthReq->to_header(); + if (empty($data)) { + if (!empty($type)) { + $header .= "\nAccept: {$type}"; + } + } elseif (isset($type)) { + $header .= "\nContent-Type: {$type}"; + $header .= "\nContent-Length: " . strlen($data); + } + return $header; + } else { + $params = $oauthReq->get_parameters(); + foreach ($queryParams as $key => $value) { + if (!is_array($value)) { + if (!is_array($params[$key])) { + if ($params[$key] === $value) { + unset($params[$key]); + } + } else { + $params[$key] = array_diff($params[$key], array($value)); + } + } else { + foreach ($value as $element) { + $params[$key] = array_diff($params[$key], array($value)); + } + } + } + return $params; + } + } + + /** + * Add the JWT signature to an array of message parameters or to a header string. + * + * @param string $endpoint URL to which message is being sent + * @param mixed $data Data to be passed + * @param string $method HTTP method + * @param string|null $type Content type of data being passed + * @param string|null $nonce Nonce value for JWT + * @param int|null $timestamp Timestamp + * + * @return string[]|string Array of signed message parameters or header string + */ + private function addJWTSignature($endpoint, $data, $method, $type, $nonce, $timestamp) + { + $ok = false; + if (is_array($data)) { + if (empty($nonce)) { + $nonce = Util::getRandomString(32); + } + if (!array_key_exists('grant_type', $data)) { + $this->messageParameters = $data; + $payload = $this->getMessageClaims(); + $ok = count($payload) > 2; + if ($ok) { + if ($this instanceof Tool) { + $privateKey = $this->rsaKey; + $kid = $this->kid; + $jku = $this->jku; + if (!empty($this->baseUrl)) { + $payload['iss'] = $this->baseUrl; + } else { + $payload['iss'] = $this->platform->platformId; + } + $payload['aud'] = array($this->platform->platformId); + $payload['azp'] = $this->platform->platformId; + $payload[Util::JWT_CLAIM_PREFIX . '/claim/deployment_id'] = $this->platform->deploymentId; + $paramName = 'JWT'; + } else { + if (!empty(Tool::$defaultTool)) { + $privateKey = Tool::$defaultTool->rsaKey; + $kid = Tool::$defaultTool->kid; + $jku = Tool::$defaultTool->jku; + } else { + $privateKey = $this->rsaKey; + $kid = $this->kid; + $jku = $this->jku; + } + $payload['iss'] = $this->platformId; + $payload['aud'] = array($this->key); + $payload['azp'] = $this->key; + $payload[Util::JWT_CLAIM_PREFIX . '/claim/deployment_id'] = $this->deploymentId; + $paramName = 'id_token'; + } + $payload['nonce'] = $nonce; + $payload[Util::JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint; + } + } else { + $ok = true; + if ($this instanceof Tool) { + $iss = $this->baseUrl; + $authorizationId = $this->platform->authorizationServerId; + $privateKey = $this->rsaKey; + $kid = $this->kid; + $jku = $this->jku; + } else { + if (!empty(Tool::$defaultTool)) { + $iss = Tool::$defaultTool->baseUrl; + $privateKey = Tool::$defaultTool->rsaKey; + $kid = Tool::$defaultTool->kid; + $jku = Tool::$defaultTool->jku; + } else { + $iss = $this->platformId; + $privateKey = $this->rsaKey; + $kid = $this->kid; + $jku = $this->jku; + } + $authorizationId = $this->authorizationServerId; + } + $payload['iss'] = $iss; + $payload['sub'] = $this->key; + if (empty($authorizationId)) { + $authorizationId = $endpoint; + } + $payload['aud'] = array($authorizationId); + $payload['jti'] = $nonce; + $params = $data; + $paramName = 'client_assertion'; + } + } + if ($ok) { + if (empty($timestamp)) { + $timestamp = time(); + } + $payload['iat'] = $timestamp; + $payload['exp'] = $timestamp + 60; + try { + $jwt = Jwt::getJwtClient(); + $params[$paramName] = $jwt::sign($payload, $this->signatureMethod, $privateKey, $kid, $jku); + } catch (\Exception $e) { + $params = array(); + } + + return $params; + } else { + $header = ''; + if ($this instanceof Tool) { + $accessToken = $this->platform->accessToken; + } else { + $accessToken = $this->accessToken; + } + if (!is_null($accessToken)) { + $header = "Authorization: Bearer {$accessToken->token}"; + } + if (empty($data) && ($method !== 'DELETE')) { + if (!empty($type)) { + $header .= "\nAccept: {$type}"; + } + } elseif (isset($type)) { + $header .= "\nContent-Type: {$type}"; + if (!empty($data) && is_string($data)) { + $header .= "\nContent-Length: " . strlen($data); + } + } + + return $header; + } + } + +} diff --git a/src/Tool.php b/src/Tool.php new file mode 100644 index 0000000..f08668a --- /dev/null +++ b/src/Tool.php @@ -0,0 +1,1441 @@ + + * @copyright SPV Software Products + * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 + */ +class Tool +{ + use System; + use ApiHook; + + /** + * Default connection error message. + */ + const CONNECTION_ERROR_MESSAGE = 'Sorry, there was an error connecting you to the application.'; + + /** + * Use ID value only. + */ + const ID_SCOPE_ID_ONLY = 0; + + /** + * Prefix an ID with the consumer key. + */ + const ID_SCOPE_GLOBAL = 1; + + /** + * Prefix the ID with the consumer key and context ID. + */ + const ID_SCOPE_CONTEXT = 2; + + /** + * Prefix the ID with the consumer key and resource ID. + */ + const ID_SCOPE_RESOURCE = 3; + + /** + * Character used to separate each element of an ID. + */ + const ID_SCOPE_SEPARATOR = ':'; + + /** + * Names of LTI parameters to be retained in the consumer settings property. + */ + private static $LTI_CONSUMER_SETTING_NAMES = array('custom_tc_profile_url', 'custom_system_setting_url', 'custom_oauth2_access_token_url'); + + /** + * Names of LTI parameters to be retained in the context settings property. + */ + private static $LTI_CONTEXT_SETTING_NAMES = array('custom_context_setting_url', + 'custom_context_memberships_url', 'custom_context_memberships_v2_url', 'custom_lineitems_url' + ); + + /** + * Names of LTI parameters to be retained in the resource link settings property. + */ + private static $LTI_RESOURCE_LINK_SETTING_NAMES = array('lis_result_sourcedid', 'lis_outcome_service_url', + 'ext_ims_lis_basic_outcome_url', 'ext_ims_lis_resultvalue_sourcedids', + 'ext_ims_lis_memberships_id', 'ext_ims_lis_memberships_url', + 'ext_ims_lti_tool_setting', 'ext_ims_lti_tool_setting_id', 'ext_ims_lti_tool_setting_url', + 'custom_link_setting_url', 'custom_link_memberships_url', + 'custom_lineitems_url', 'custom_lineitem_url' + ); + + /** + * Names of LTI parameters to be retained even when not passed. + */ + private static $LTI_RETAIN_SETTING_NAMES = array('custom_lineitem_url'); + + /** + * Names of LTI custom parameter substitution variables (or capabilities) and their associated default message parameter names. + */ + private static $CUSTOM_SUBSTITUTION_VARIABLES = array('User.id' => 'user_id', + 'User.image' => 'user_image', + 'User.username' => 'username', + 'User.scope.mentor' => 'role_scope_mentor', + 'Membership.role' => 'roles', + 'Person.sourcedId' => 'lis_person_sourcedid', + 'Person.name.full' => 'lis_person_name_full', + 'Person.name.family' => 'lis_person_name_family', + 'Person.name.given' => 'lis_person_name_given', + 'Person.email.primary' => 'lis_person_contact_email_primary', + 'Context.id' => 'context_id', + 'Context.type' => 'context_type', + 'Context.title' => 'context_title', + 'Context.label' => 'context_label', + 'CourseOffering.sourcedId' => 'lis_course_offering_sourcedid', + 'CourseSection.sourcedId' => 'lis_course_section_sourcedid', + 'CourseSection.label' => 'context_label', + 'CourseSection.title' => 'context_title', + 'ResourceLink.id' => 'resource_link_id', + 'ResourceLink.title' => 'resource_link_title', + 'ResourceLink.description' => 'resource_link_description', + 'Result.sourcedId' => 'lis_result_sourcedid', + 'BasicOutcome.url' => 'lis_outcome_service_url', + 'ToolConsumerProfile.url' => 'custom_tc_profile_url', + 'ToolProxy.url' => 'tool_proxy_url', + 'ToolProxy.custom.url' => 'custom_system_setting_url', + 'ToolProxyBinding.custom.url' => 'custom_context_setting_url', + 'LtiLink.custom.url' => 'custom_link_setting_url', + 'LineItems.url' => 'custom_lineitems_url', + 'LineItem.url' => 'custom_lineitem_url', + 'ToolProxyBinding.memberships.url' => 'custom_context_memberships_url', + 'ToolProxyBinding.nrps.url' => 'custom_context_memberships_v2_url', + 'LtiLink.memberships.url' => 'custom_link_memberships_url' + ); + + /** + * Tool consumer object. + * + * @deprecated Use Tool::$platform instead + * @see platform + * + * @var ToolConsumer|null $consumer + */ + public $consumer = null; + + /** + * Platform object. + * + * @var Platform|null $platform + */ + public $platform = null; + + /** + * Return URL provided by platform. + * + * @var string|null $returnUrl + */ + public $returnUrl = null; + + /** + * UserResult object. + * + * @var UserResult|null $userResult + */ + public $userResult = null; + + /** + * Resource link object. + * + * @var ResourceLink|null $resourceLink + */ + public $resourceLink = null; + + /** + * Context object. + * + * @var Context|null $context + */ + public $context = null; + + /** + * Default email domain. + * + * @var string $defaultEmail + */ + public $defaultEmail = ''; + + /** + * Scope to use for user IDs. + * + * @var int $idScope + */ + public $idScope = self::ID_SCOPE_ID_ONLY; + + /** + * Whether shared resource link arrangements are permitted. + * + * @var bool $allowSharing + */ + public $allowSharing = false; + + /** + * Message for last request processed + * + * @var string $message + */ + public $message = self::CONNECTION_ERROR_MESSAGE; + + /** + * Base URL for tool service + * + * @var string|null $baseUrl + */ + public $baseUrl = null; + + /** + * Vendor details + * + * @var Item|null $vendor + */ + public $vendor = null; + + /** + * Product details + * + * @var Item|null $product + */ + public $product = null; + + /** + * Services required by Tool + * + * @var array|null $requiredServices + */ + public $requiredServices = null; + + /** + * Optional services used by Tool + * + * @var array|null $optionalServices + */ + public $optionalServices = null; + + /** + * Resource handlers for Tool + * + * @var array|null $resourceHandlers + */ + public $resourceHandlers = null; + + /** + * Default tool for use with service requests + * + * @var Tool|null $defaultTool + */ + public static $defaultTool = null; + + /** + * URL to redirect user to on successful completion of the request. + * + * @var string|null $redirectUrl + */ + protected $redirectUrl = null; + + /** + * Media types accepted by the platform. + * + * @var array|null $mediaTypes + */ + protected $mediaTypes = null; + + /** + * Document targets accepted by the platform. + * + * @var array|null $documentTargets + */ + protected $documentTargets = null; + + /** + * Default HTML to be displayed on a successful completion of the request. + * + * @var string|null $output + */ + protected $output = null; + + /** + * HTML to be displayed on an unsuccessful completion of the request and no return URL is available. + * + * @var string|null $errorOutput + */ + protected $errorOutput = null; + + /** + * LTI parameter constraints for auto validation checks. + * + * @var array|null $constraints + */ + private $constraints = null; + + /** + * Class constructor + * + * @param DataConnector $dataConnector Object containing a database connection object + */ + function __construct($dataConnector = null) + { + $this->consumer = &$this->platform; + $this->constraints = array(); + if (empty($dataConnector)) { + $dataConnector = DataConnector::getDataConnector(); + } + $this->dataConnector = $dataConnector; + $this->vendor = new Profile\Item(); + $this->product = new Profile\Item(); + $this->requiredServices = array(); + $this->optionalServices = array(); + $this->resourceHandlers = array(); + } + + /** + * Get the message parameters + * + * @return array The message parameter array + */ + public function getMessageParameters() + { + if (is_null($this->messageParameters)) { + $this->parseMessage(); +// Set debug mode + if (!$this->debugMode) { + $this->debugMode = (isset($this->messageParameters['custom_debug']) && + (strtolower($this->messageParameters['custom_debug']) === 'true')); + if ($this->debugMode) { + $currentLogLevel = Util::$logLevel; + Util::$logLevel = Util::LOGLEVEL_DEBUG; + if ($currentLogLevel < Util::LOGLEVEL_INFO) { + Util::logRequest(); + } + } + } +// Set return URL if available + if (!empty($this->messageParameters['launch_presentation_return_url'])) { + $this->returnUrl = $this->messageParameters['launch_presentation_return_url']; + } elseif (!empty($this->messageParameters['content_item_return_url'])) { + $this->returnUrl = $this->messageParameters['content_item_return_url']; + } + } + + return $this->messageParameters; + } + + /** + * Process an incoming request + */ + public function handleRequest() + { + if ($this->debugMode) { + Util::$logLevel = Util::LOGLEVEL_DEBUG; + } + if (!empty($_REQUEST['iss'])) { // Initiate login request + Util::logRequest(); + if (empty($_REQUEST['login_hint'])) { + $this->ok = false; + $this->reason = 'Missing login_hint parameter'; + } elseif (empty($_REQUEST['target_link_uri'])) { + $this->ok = false; + $this->reason = 'Missing target_link_uri parameter'; + } else { + $hint = isset($_REQUEST['lti_message_hint']) ? $_REQUEST['lti_message_hint'] : null; + $this->ok = $this->sendAuthenticationRequest($hint); + } + } else { // LTI message + $this->getMessageParameters(); + if (!empty($this->platform) && $this->platform->debugMode) { + Util::$logLevel = Util::LOGLEVEL_DEBUG; + } + Util::logRequest(); + if ($this->ok && $this->authenticate()) { + if (empty($this->output)) { + $this->doCallback(); + } + } + } + if (!$this->ok) { + Util::logError("Request failed with reason: '{$this->reason}'"); + } + $this->result(); + } + + /** + * Add a parameter constraint to be checked on launch + * + * @param string $name Name of parameter to be checked + * @param bool $required True if parameter is required (optional, default is true) + * @param int $maxLength Maximum permitted length of parameter value (optional, default is null) + * @param array $messageTypes Array of message types to which the constraint applies (optional, default is all) + */ + public function setParameterConstraint($name, $required = true, $maxLength = null, $messageTypes = null) + { + $name = trim($name); + if (strlen($name) > 0) { + $this->constraints[$name] = array('required' => $required, 'max_length' => $maxLength, 'messages' => $messageTypes); + } + } + + /** + * Get an array of defined tool consumers + * + * @deprecated Use getPlatforms() instead + * @see Tool::getPlatforms() + * + * @return array Array of ToolConsumer objects + */ + public function getConsumers() + { + Util::logDebug('Method ceLTIc\LTI\Tool::getConsumers() has been deprecated; please use ceLTIc\LTI\Tool::getPlatforms() instead.', + true); + return $this->getPlatforms(); + } + + /** + * Get an array of defined platforms + * + * @return array Array of Platform objects + */ + public function getPlatforms() + { + return $this->dataConnector->getPlatforms(); + } + + /** + * Find an offered service based on a media type and HTTP action(s) + * + * @param string $format Media type required + * @param array $methods Array of HTTP actions required + * + * @return object The service object + */ + public function findService($format, $methods) + { + $found = false; + $services = $this->platform->profile->service_offered; + if (is_array($services)) { + $n = -1; + foreach ($services as $service) { + $n++; + if (!is_array($service->format) || !in_array($format, $service->format)) { + continue; + } + $missing = array(); + foreach ($methods as $method) { + if (!is_array($service->action) || !in_array($method, $service->action)) { + $missing[] = $method; + } + } + $methods = $missing; + if (count($methods) <= 0) { + $found = $service; + break; + } + } + } + + return $found; + } + + /** + * Send the tool proxy to the platform + * + * @return bool True if the tool proxy was accepted + */ + public function doToolProxyService() + { +// Create tool proxy + $toolProxyService = $this->findService('application/vnd.ims.lti.v2.toolproxy+json', array('POST')); + $secret = Util::getRandomString(12); + $toolProxy = new MediaType\ToolProxy($this, $toolProxyService, $secret); + $http = $this->platform->doServiceRequest($toolProxyService, 'POST', 'application/vnd.ims.lti.v2.toolproxy+json', + json_encode($toolProxy)); + $ok = $http->ok && ($http->status === 201) && !empty($http->responseJson->tool_proxy_guid); + if ($ok) { + $this->platform->setKey($http->responseJson->tool_proxy_guid); + $this->platform->secret = $toolProxy->security_contract->shared_secret; + $this->platform->toolProxy = json_encode($toolProxy); + $this->platform->save(); + } + + return $ok; + } + + /** + * Generate a web page containing an auto-submitted form of parameters. + * + * @deprecated Use Util::sendForm() instead + * @see Util::sendForm() + * + * @param string $url URL to which the form should be submitted + * @param array $params Array of form parameters + * @param string $target Name of target (optional) + * + * @return string + */ + public static function sendForm($url, $params, $target = '') + { + Util::logDebug('Method ceLTIc\LTI\Tool::sendForm() has been deprecated; please use ceLTIc\LTI\Util::sendForm() instead.', + true); + Util::sendForm($url, $params, $target); + } + +### +### PROTECTED METHODS +### + + /** + * Process a valid launch request + * + * @return bool True if no error + */ + protected function onLaunch() + { + $this->onError(); + } + + /** + * Process a valid configure request + * + * @return bool True if no error + */ + protected function onConfigure() + { + $this->onError(); + } + + /** + * Process a valid dashboard request + * + * @return bool True if no error + */ + protected function onDashboard() + { + $this->onError(); + } + + /** + * Process a valid content-item request + * + * @return bool True if no error + */ + protected function onContentItem() + { + $this->onError(); + } + + /** + * Process a valid tool proxy registration request + * + * @return bool True if no error + */ + protected function onRegister() + { + $this->onError(); + } + + /** + * Process a response to an invalid request + * + * @return bool True if no further error processing required + */ + protected function onError() + { + $this->ok = false; + } + +### +### PRIVATE METHODS +### + + /** + * Call any callback function for the requested action. + * + * This function may set the redirect_url and output properties. + * + * @param string|null $method Name of method to be called (optional) + */ + private function doCallback($method = null) + { + $callback = $method; + if (is_null($callback)) { + $callback = Util::$METHOD_NAMES[$this->messageParameters['lti_message_type']]; + } + if (method_exists($this, $callback)) { + $this->$callback(); + } elseif (is_null($method) && $this->ok) { + $this->ok = false; + $this->reason = "Message type not supported: {$this->messageParameters['lti_message_type']}"; + } + if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) { + $this->platform->save(); + } + } + + /** + * Perform the result of an action. + * + * This function may redirect the user to another URL rather than returning a value. + * + * @return string Output to be displayed (redirection, or display HTML or message) + */ + private function result() + { + if (!$this->ok) { + $this->onError(); + } + if (!$this->ok) { +// If not valid, return an error message to the platform if a return URL is provided + if (!empty($this->returnUrl)) { + $errorUrl = $this->returnUrl; + if (strpos($errorUrl, '?') === false) { + $errorUrl .= '?'; + } else { + $errorUrl .= '&'; + } + if ($this->debugMode && !is_null($this->reason)) { + $errorUrl .= 'lti_errormsg=' . urlencode("Debug error: $this->reason"); + } else { + $errorUrl .= 'lti_errormsg=' . urlencode($this->message); + if (!is_null($this->reason)) { + $errorUrl .= '<i_errorlog=' . urlencode("Debug error: $this->reason"); + } + } + if (!is_null($this->platform) && isset($this->messageParameters['lti_message_type']) && + ($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest')) { + $formParams = array(); + if (isset($this->messageParameters['data'])) { + $formParams['data'] = $this->messageParameters['data']; + } + $version = (isset($this->messageParameters['lti_version'])) ? $this->messageParameters['lti_version'] : Util::LTI_VERSION1; + $formParams = $this->signParameters($errorUrl, 'ContentItemSelection', $version, $formParams); + $page = Util::sendForm($errorUrl, $formParams); + echo $page; + } else { + header("Location: {$errorUrl}"); + } + exit; + } else { + if (!is_null($this->errorOutput)) { + echo $this->errorOutput; + } elseif ($this->debugMode && !empty($this->reason)) { + echo "Debug error: {$this->reason}"; + } else { + echo "Error: {$this->message}"; + } + exit; + } + } elseif (!is_null($this->redirectUrl)) { + header("Location: {$this->redirectUrl}"); + exit; + } elseif (!is_null($this->output)) { + echo $this->output; + exit; + } + } + + /** + * Check the authenticity of the LTI launch request. + * + * The platform, resource link and user objects will be initialised if the request is valid. + * + * @return bool True if the request has been successfully validated. + */ + private function authenticate() + { +// Get the platform + $doSavePlatform = false; + $this->ok = $_SERVER['REQUEST_METHOD'] === 'POST'; + if (!$this->ok) { + $this->reason = 'LTI messages must use HTTP POST'; + } elseif (!empty($this->jwt) && !empty($this->jwt->hasJwt())) { + $this->ok = false; + if (is_null($this->messageParameters['oauth_consumer_key']) || (strlen($this->messageParameters['oauth_consumer_key']) <= 0)) { + $this->reason = 'Missing iss claim'; + } elseif (empty($this->jwt->getClaim('iat', ''))) { + $this->reason = 'Missing iat claim'; + } elseif (empty($this->jwt->getClaim('exp', ''))) { + $this->reason = 'Missing exp claim'; + } elseif (intval($this->jwt->getClaim('iat')) > intval($this->jwt->getClaim('exp'))) { + $this->reason = 'iat claim must not have a value greater than exp claim'; + } elseif (empty($this->jwt->getClaim('nonce', ''))) { + $this->reason = 'Missing nonce claim'; + } else { + $this->ok = true; + } + } +// Check all required launch parameters + if ($this->ok) { + $this->ok = isset($this->messageParameters['lti_message_type']) && array_key_exists($this->messageParameters['lti_message_type'], + Util::$METHOD_NAMES); + if (!$this->ok) { + $this->reason = 'Invalid or missing lti_message_type parameter.'; + } + } + if ($this->ok) { + $this->ok = isset($this->messageParameters['lti_version']) && in_array($this->messageParameters['lti_version'], + Util::$LTI_VERSIONS); + if (!$this->ok) { + $this->reason = 'Invalid or missing lti_version parameter.'; + } + } + if ($this->ok) { + if ($this->messageParameters['lti_message_type'] === 'basic-lti-launch-request') { + $this->ok = isset($this->messageParameters['resource_link_id']) && (strlen(trim($this->messageParameters['resource_link_id'])) > 0); + if (!$this->ok) { + $this->reason = 'Missing resource link ID.'; + } + } elseif ($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') { + if (isset($this->messageParameters['accept_media_types']) && (strlen(trim($this->messageParameters['accept_media_types'])) > 0)) { + $mediaTypes = array_filter(explode(',', str_replace(' ', '', $this->messageParameters['accept_media_types'])), + 'strlen'); + $mediaTypes = array_unique($mediaTypes); + $this->ok = count($mediaTypes) > 0; + if (!$this->ok) { + $this->reason = 'No accept_media_types found.'; + } else { + $this->mediaTypes = $mediaTypes; + } + } + if ($this->ok && !empty($this->jwt) && $this->jwt->hasJwt()) { + if (isset($this->messageParameters['accept_types']) && (strlen(trim($this->messageParameters['accept_types'])) > 0)) { + $types = array_filter(explode(',', str_replace(' ', '', $this->messageParameters['accept_types'])), 'strlen'); + $types = array_unique($types); + $this->ok = count($types) > 0; + if (!$this->ok) { + $this->reason = 'No accept_types found.'; + } else { + $this->mediaTypes = $types; + } + } + } elseif ($this->ok) { + $this->ok = !empty($this->messageParameters['accept_media_types']); + if (!$this->ok) { + $this->reason = 'No accept_media_types found.'; + } + } + if (!$this->ok) { + $this->reason = 'No accept_media_types found.'; + } + if ($this->ok) { + if (isset($this->messageParameters['accept_types']) && (strlen(trim($this->messageParameters['accept_types'])) > 0)) { + $acceptTypes = array_filter(explode(',', str_replace(' ', '', $this->messageParameters['accept_types'])), + 'strlen'); + $acceptTypes = array_unique($acceptTypes); + $this->ok = count($acceptTypes) > 0; + if ($this->ok) { + $this->acceptTypes = $acceptTypes; + } + } else { + $this->ok = empty($this->jwt) || !$this->jwt->hasJwt(); + } + if (!$this->ok) { + $this->reason = 'No accept_types found.'; + } + } + if ($this->ok) { + if (isset($this->messageParameters['accept_presentation_document_targets']) && + (strlen(trim($this->messageParameters['accept_presentation_document_targets'])) > 0)) { + $documentTargets = array_filter(explode(',', + str_replace(' ', '', $this->messageParameters['accept_presentation_document_targets'])), 'strlen'); + $documentTargets = array_unique($documentTargets); + $this->ok = count($documentTargets) > 0; + if (!$this->ok) { + $this->reason = 'Missing or empty accept_presentation_document_targets parameter.'; + } else { + foreach ($documentTargets as $documentTarget) { + $this->ok = $this->checkValue($documentTarget, + array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none'), + 'Invalid value in accept_presentation_document_targets parameter: %s.'); + if (!$this->ok) { + break; + } + } + if ($this->ok) { + $this->documentTargets = $documentTargets; + } + } + } else { + $this->ok = false; + $this->reason = ''; + } + } + if ($this->ok) { + $this->ok = !empty($this->messageParameters['content_item_return_url']); + if (!$this->ok) { + $this->reason = 'Missing content_item_return_url parameter.'; + } + } + } elseif ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest') { + $this->ok = ((isset($this->messageParameters['reg_key']) && (strlen(trim($this->messageParameters['reg_key'])) > 0)) && (isset($this->messageParameters['reg_password']) && (strlen(trim($this->messageParameters['reg_password'])) > + 0)) && (isset($this->messageParameters['tc_profile_url']) && (strlen(trim($this->messageParameters['tc_profile_url'])) > + 0)) && (isset($this->messageParameters['launch_presentation_return_url']) && (strlen(trim($this->messageParameters['launch_presentation_return_url'])) > 0))); + if ($this->debugMode && !$this->ok) { + $this->reason = 'Missing message parameters.'; + } + } + } + $now = time(); +// Check consumer key + if ($this->ok && ($this->messageParameters['lti_message_type'] !== 'ToolProxyRegistrationRequest')) { + $this->ok = isset($this->messageParameters['oauth_consumer_key']); + if (!$this->ok) { + $this->reason = 'Missing consumer key.'; + } + if ($this->ok) { + $this->ok = !is_null($this->platform->created); + if (!$this->ok) { + $this->reason = 'Invalid consumer key: ' . $this->messageParameters['oauth_consumer_key']; + } + } + if ($this->ok) { + if ($this->messageParameters['oauth_signature_method'] !== $this->platform->signatureMethod) { + $this->platform->signatureMethod = $this->messageParameters['oauth_signature_method']; + $doSavePlatform = true; + } + $today = date('Y-m-d', $now); + if (is_null($this->platform->lastAccess)) { + $doSavePlatform = true; + } else { + $last = date('Y-m-d', $this->platform->lastAccess); + $doSavePlatform = $doSavePlatform || ($last !== $today); + } + $this->platform->lastAccess = $now; + $this->ok = $this->verifySignature(); + } + if ($this->ok) { + if ($this->platform->protected) { + if (!is_null($this->platform->consumerGuid)) { + $this->ok = empty($this->messageParameters['tool_consumer_instance_guid']) || ($this->platform->consumerGuid === $this->messageParameters['tool_consumer_instance_guid']); + if (!$this->ok) { + $this->reason = 'Request is from an invalid platform.'; + } + } else { + $this->ok = isset($this->messageParameters['tool_consumer_instance_guid']); + if (!$this->ok) { + $this->reason = 'A platform GUID must be included in the launch request.'; + } + } + } + if ($this->ok) { + $this->ok = $this->platform->enabled; + if (!$this->ok) { + $this->reason = 'Platform has not been enabled by the tool.'; + } + } + if ($this->ok) { + $this->ok = is_null($this->platform->enableFrom) || ($this->platform->enableFrom <= $now); + if ($this->ok) { + $this->ok = is_null($this->platform->enableUntil) || ($this->platform->enableUntil > $now); + if (!$this->ok) { + $this->reason = 'Platform access has expired.'; + } + } else { + $this->reason = 'Platform access is not yet available.'; + } + } + } +// Validate other message parameter values + if ($this->ok) { + if ($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') { + if (isset($this->messageParameters['accept_unsigned'])) { + $this->ok = $this->checkValue($this->messageParameters['accept_unsigned'], array('true', 'false'), + 'Invalid value for accept_unsigned parameter: %s.'); + } + if ($this->ok && isset($this->messageParameters['accept_multiple'])) { + $this->ok = $this->checkValue($this->messageParameters['accept_multiple'], array('true', 'false'), + 'Invalid value for accept_multiple parameter: %s.'); + } + if ($this->ok && isset($this->messageParameters['accept_copy_advice'])) { + $this->ok = $this->checkValue($this->messageParameters['accept_copy_advice'], array('true', 'false'), + 'Invalid value for accept_copy_advice parameter: %s.'); + } + if ($this->ok && isset($this->messageParameters['auto_create'])) { + $this->ok = $this->checkValue($this->messageParameters['auto_create'], array('true', 'false'), + 'Invalid value for auto_create parameter: %s.'); + } + if ($this->ok && isset($this->messageParameters['can_confirm'])) { + $this->ok = $this->checkValue($this->messageParameters['can_confirm'], array('true', 'false'), + 'Invalid value for can_confirm parameter: %s.'); + } + } elseif (isset($this->messageParameters['launch_presentation_document_target'])) { + $this->ok = $this->checkValue($this->messageParameters['launch_presentation_document_target'], + array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay'), + 'Invalid value for launch_presentation_document_target parameter: %s.'); + } + } + } + + if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) { + $this->ok = $this->messageParameters['lti_version'] === Util::LTI_VERSION2; + if (!$this->ok) { + $this->reason = 'Invalid lti_version parameter'; + } + if ($this->ok) { + $url = $this->messageParameters['tc_profile_url']; + if (strpos($url, '?') === false) { + $url .= '?'; + } else { + $url .= '&'; + } + $url .= 'lti_version=' . Util::LTI_VERSION2; + $http = new HttpMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json'); + $this->ok = $http->send(); + if (!$this->ok) { + $this->reason = 'Platform profile not accessible.'; + } else { + $tcProfile = json_decode($http->response); + $this->ok = !is_null($tcProfile); + if (!$this->ok) { + $this->reason = 'Invalid JSON in platform profile.'; + } + } + } +// Check for required capabilities + if ($this->ok) { + $this->platform = Platform::fromConsumerKey($this->messageParameters['reg_key'], $this->dataConnector); + $this->platform->profile = $tcProfile; + $capabilities = $this->platform->profile->capability_offered; + $missing = array(); + foreach ($this->resourceHandlers as $resourceHandler) { + foreach ($resourceHandler->requiredMessages as $message) { + if (!in_array($message->type, $capabilities)) { + $missing[$message->type] = true; + } + } + } + foreach ($this->constraints as $name => $constraint) { + if ($constraint['required']) { + if (empty(array_intersect($capabilities, + array_keys(array_intersect(self::$CUSTOM_SUBSTITUTION_VARIABLES, array($name)))))) { + $missing[$name] = true; + } + } + } + if (!empty($missing)) { + ksort($missing); + $this->reason = 'Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\''; + $this->ok = false; + } + } +// Check for required services + if ($this->ok) { + foreach ($this->requiredServices as $service) { + foreach ($service->formats as $format) { + if (!$this->findService($format, $service->actions)) { + if ($this->ok) { + $this->reason = 'Required service(s) not offered - '; + $this->ok = false; + } else { + $this->reason .= ', '; + } + $this->reason .= "'{$format}' [" . implode(', ', $service->actions) . ']'; + } + } + } + } + if ($this->ok) { + if ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest') { + $this->platform->profile = $tcProfile; + $this->platform->secret = $this->messageParameters['reg_password']; + $this->platform->ltiVersion = $this->messageParameters['lti_version']; + $this->platform->name = $tcProfile->product_instance->service_owner->service_owner_name->default_value; + $this->platform->consumerName = $this->platform->name; + $this->platform->consumerVersion = "{$tcProfile->product_instance->product_info->product_family->code}-{$tcProfile->product_instance->product_info->product_version}"; + $this->platform->consumerGuid = $tcProfile->product_instance->guid; + $this->platform->enabled = true; + $this->platform->protected = true; + $doSavePlatform = true; + } + } + } elseif ($this->ok && !empty($this->messageParameters['custom_tc_profile_url']) && empty($this->platform->profile)) { + $url = $this->messageParameters['custom_tc_profile_url']; + if (strpos($url, '?') === false) { + $url .= '?'; + } else { + $url .= '&'; + } + $url .= 'lti_version=' . $this->messageParameters['lti_version']; + $http = new HttpMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json'); + if ($http->send()) { + $tcProfile = json_decode($http->response); + if (!is_null($tcProfile)) { + $this->platform->profile = $tcProfile; + $doSavePlatform = true; + } + } + } + + if ($this->ok) { + +// Check if a relaunch is being requested + if (isset($this->messageParameters['relaunch_url'])) { + if (empty($this->messageParameters['platform_state'])) { + $this->ok = false; + $this->reason = 'Missing or empty platform_state parameter'; + } else { + $this->sendRelaunchRequest(); + } + } else { + +// Validate message parameter constraints + $invalidParameters = array(); + foreach ($this->constraints as $name => $constraint) { + if (empty($constraint['messages']) || in_array($this->messageParameters['lti_message_type'], + $constraint['messages'])) { + $ok = true; + if ($constraint['required']) { + if (!isset($this->messageParameters[$name]) || (strlen(trim($this->messageParameters[$name])) <= 0)) { + $invalidParameters[] = "{$name} (missing)"; + $ok = false; + } + } + if ($ok && !is_null($constraint['max_length']) && isset($this->messageParameters[$name])) { + if (strlen(trim($this->messageParameters[$name])) > $constraint['max_length']) { + $invalidParameters[] = "{$name} (too long)"; + } + } + } + } + if (count($invalidParameters) > 0) { + $this->ok = false; + if (empty($this->reason)) { + $this->reason = 'Invalid parameter(s): ' . implode(', ', $invalidParameters) . '.'; + } + } + + if ($this->ok) { + +// Set the request context + $contextId = ''; + if ($this->hasConfiguredApiHook(self::$CONTEXT_ID_HOOK, $this->platform->getFamilyCode(), $this)) { + $className = $this->getApiHook(self::$CONTEXT_ID_HOOK, $this->platform->getFamilyCode()); + $tpHook = new $className($this); + $contextId = $tpHook->getContextId(); + } + if (empty($contextId) && isset($this->messageParameters['context_id'])) { + $contextId = trim($this->messageParameters['context_id']); + } + if (!empty($contextId)) { + $this->context = Context::fromPlatform($this->platform, $contextId); + $title = ''; + if (isset($this->messageParameters['context_title'])) { + $title = trim($this->messageParameters['context_title']); + } + if (empty($title)) { + $title = "Course {$this->context->getId()}"; + } + $this->context->title = $title; + if (isset($this->messageParameters['context_type'])) { + $this->context->type = trim($this->messageParameters['context_type']); + } + } + +// Set the request resource link + if (isset($this->messageParameters['resource_link_id'])) { + $contentItemId = ''; + if (isset($this->messageParameters['custom_content_item_id'])) { + $contentItemId = $this->messageParameters['custom_content_item_id']; + } + if (empty($this->context)) { + $this->resourceLink = ResourceLink::fromPlatform($this->platform, + trim($this->messageParameters['resource_link_id']), $contentItemId); + } else { + $this->resourceLink = ResourceLink::fromContext($this->context, + trim($this->messageParameters['resource_link_id']), $contentItemId); + } + $title = ''; + if (isset($this->messageParameters['resource_link_title'])) { + $title = trim($this->messageParameters['resource_link_title']); + } + if (empty($title)) { + $title = "Resource {$this->resourceLink->getId()}"; + } + $this->resourceLink->title = $title; +// Delete any existing custom parameters + foreach ($this->platform->getSettings() as $name => $value) { + if ((strpos($name, 'custom_') === 0) && (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES))) { + $this->platform->setSetting($name); + $doSavePlatform = true; + } + } + if (!empty($this->context)) { + foreach ($this->context->getSettings() as $name => $value) { + if ((strpos($name, 'custom_') === 0) && (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES))) { + $this->context->setSetting($name); + } + } + } + foreach ($this->resourceLink->getSettings() as $name => $value) { + if ((strpos($name, 'custom_') === 0) && (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES))) { + $this->resourceLink->setSetting($name); + } + } +// Save LTI parameters + foreach (self::$LTI_CONSUMER_SETTING_NAMES as $name) { + if (isset($this->messageParameters[$name])) { + $this->platform->setSetting($name, $this->messageParameters[$name]); + } else if (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES)) { + $this->platform->setSetting($name); + } + } + if (!empty($this->context)) { + foreach (self::$LTI_CONTEXT_SETTING_NAMES as $name) { + if (isset($this->messageParameters[$name])) { + $this->context->setSetting($name, $this->messageParameters[$name]); + } else if (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES)) { + $this->context->setSetting($name); + } + } + } + foreach (self::$LTI_RESOURCE_LINK_SETTING_NAMES as $name) { + if (isset($this->messageParameters[$name])) { + $this->resourceLink->setSetting($name, $this->messageParameters[$name]); + } else if (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES)) { + $this->resourceLink->setSetting($name); + } + } +// Save other custom parameters at all levels + foreach ($this->messageParameters as $name => $value) { + if ((strpos($name, 'custom_') === 0) && !in_array($name, + array_merge(self::$LTI_CONSUMER_SETTING_NAMES, self::$LTI_CONTEXT_SETTING_NAMES, + self::$LTI_RESOURCE_LINK_SETTING_NAMES))) { + $this->platform->setSetting($name, $value); + if (!empty($this->context)) { + $this->context->setSetting($name, $value); + } + $this->resourceLink->setSetting($name, $value); + } + } + } + +// Set the user instance + $userId = ''; + if ($this->hasConfiguredApiHook(self::$USER_ID_HOOK, $this->platform->getFamilyCode(), $this)) { + $className = $this->getApiHook(self::$USER_ID_HOOK, $this->platform->getFamilyCode()); + $tpHook = new $className($this); + $userId = $tpHook->getUserId(); + } + if (empty($userId) && isset($this->messageParameters['user_id'])) { + $userId = trim($this->messageParameters['user_id']); + } + + $this->userResult = UserResult::fromResourceLink($this->resourceLink, $userId); + +// Set the user name + $firstname = (isset($this->messageParameters['lis_person_name_given'])) ? $this->messageParameters['lis_person_name_given'] : ''; + $lastname = (isset($this->messageParameters['lis_person_name_family'])) ? $this->messageParameters['lis_person_name_family'] : ''; + $fullname = (isset($this->messageParameters['lis_person_name_full'])) ? $this->messageParameters['lis_person_name_full'] : ''; + $this->userResult->setNames($firstname, $lastname, $fullname); + +// Set the sourcedId + if (isset($this->messageParameters['lis_person_sourcedid'])) { + $this->userResult->sourcedId = $this->messageParameters['lis_person_sourcedid']; + } + +// Set the username + if (isset($this->messageParameters['ext_username'])) { + $this->userResult->username = $this->messageParameters['ext_username']; + } elseif (isset($this->messageParameters['ext_user_username'])) { + $this->userResult->username = $this->messageParameters['ext_user_username']; + } elseif (isset($this->messageParameters['custom_username'])) { + $this->userResult->username = $this->messageParameters['custom_username']; + } elseif (isset($this->messageParameters['custom_user_username'])) { + $this->userResult->username = $this->messageParameters['custom_user_username']; + } + +// Set the user email + $email = (isset($this->messageParameters['lis_person_contact_email_primary'])) ? $this->messageParameters['lis_person_contact_email_primary'] : ''; + $this->userResult->setEmail($email, $this->defaultEmail); + +// Set the user image URI + if (isset($this->messageParameters['user_image'])) { + $this->userResult->image = $this->messageParameters['user_image']; + } + +// Set the user roles + if (isset($this->messageParameters['roles'])) { + $this->userResult->roles = self::parseRoles($this->messageParameters['roles'], + $this->messageParameters['lti_version']); + } + +// Initialise the platform and check for changes + $this->platform->defaultEmail = $this->defaultEmail; + if ($this->platform->ltiVersion !== $this->messageParameters['lti_version']) { + $this->platform->ltiVersion = $this->messageParameters['lti_version']; + $doSavePlatform = true; + } + if (isset($this->messageParameters['deployment_id'])) { + $this->platform->deploymentId = $this->messageParameters['deployment_id']; + } + if (isset($this->messageParameters['tool_consumer_instance_name'])) { + if ($this->platform->consumerName !== $this->messageParameters['tool_consumer_instance_name']) { + $this->platform->consumerName = $this->messageParameters['tool_consumer_instance_name']; + $doSavePlatform = true; + } + } + if (isset($this->messageParameters['tool_consumer_info_product_family_code'])) { + $version = $this->messageParameters['tool_consumer_info_product_family_code']; + if (isset($this->messageParameters['tool_consumer_info_version'])) { + $version .= "-{$this->messageParameters['tool_consumer_info_version']}"; + } +// do not delete any existing consumer version if none is passed + if ($this->platform->consumerVersion !== $version) { + $this->platform->consumerVersion = $version; + $doSavePlatform = true; + } + } elseif (isset($this->messageParameters['ext_lms']) && ($this->platform->consumerName !== $this->messageParameters['ext_lms'])) { + $this->platform->consumerVersion = $this->messageParameters['ext_lms']; + $doSavePlatform = true; + } + if (isset($this->messageParameters['tool_consumer_instance_guid'])) { + if (is_null($this->platform->consumerGuid)) { + $this->platform->consumerGuid = $this->messageParameters['tool_consumer_instance_guid']; + $doSavePlatform = true; + } elseif (!$this->platform->protected) { + $doSavePlatform = ($this->platform->consumerGuid !== $this->messageParameters['tool_consumer_instance_guid']); + if ($doSavePlatform) { + $this->platform->consumerGuid = $this->messageParameters['tool_consumer_instance_guid']; + } + } + } + if (isset($this->messageParameters['launch_presentation_css_url'])) { + if ($this->platform->cssPath !== $this->messageParameters['launch_presentation_css_url']) { + $this->platform->cssPath = $this->messageParameters['launch_presentation_css_url']; + $doSavePlatform = true; + } + } elseif (isset($this->messageParameters['ext_launch_presentation_css_url']) && ($this->platform->cssPath !== $this->messageParameters['ext_launch_presentation_css_url'])) { + $this->platform->cssPath = $this->messageParameters['ext_launch_presentation_css_url']; + $doSavePlatform = true; + } elseif (!empty($this->platform->cssPath)) { + $this->platform->cssPath = null; + $doSavePlatform = true; + } + } + +// Persist changes to platform + if ($doSavePlatform) { + $this->platform->save(); + } + + if ($this->ok) { + +// Persist changes to cpntext + if (isset($this->context)) { + $this->context->save(); + } + + if (isset($this->resourceLink)) { +// Persist changes to resource link + $this->resourceLink->save(); + +// Persist changes to user instnce + $this->userResult->setResourceLinkId($this->resourceLink->getRecordId()); + if (isset($this->messageParameters['lis_result_sourcedid'])) { + if ($this->userResult->ltiResultSourcedId !== $this->messageParameters['lis_result_sourcedid']) { + $this->userResult->ltiResultSourcedId = $this->messageParameters['lis_result_sourcedid']; + $this->userResult->save(); + } + } elseif (!empty($this->userResult->ltiResultSourcedId)) { + $this->userResult->ltiResultSourcedId = ''; + $this->userResult->save(); + } + +// Check if a share arrangement is in place for this resource link + $this->ok = $this->checkForShare(); + } + } + } + } + + return $this->ok; + } + + /** + * Check if a share arrangement is in place. + * + * @return bool True if no error is reported + */ + private function checkForShare() + { + $ok = true; + $doSaveResourceLink = true; + + $id = $this->resourceLink->primaryResourceLinkId; + + $shareRequest = isset($this->messageParameters['custom_share_key']) && !empty($this->messageParameters['custom_share_key']); + if ($shareRequest) { + if (!$this->allowSharing) { + $ok = false; + $this->reason = 'Your sharing request has been refused because sharing is not being permitted.'; + } else { +// Check if this is a new share key + $shareKey = new ResourceLinkShareKey($this->resourceLink, $this->messageParameters['custom_share_key']); + if (!is_null($shareKey->resourceLinkId)) { +// Update resource link with sharing primary resource link details + $id = $shareKey->resourceLinkId; + $ok = ($id !== $this->resourceLink->getRecordId()); + if ($ok) { + $this->resourceLink->primaryResourceLinkId = $id; + $this->resourceLink->shareApproved = $shareKey->autoApprove; + $ok = $this->resourceLink->save(); + if ($ok) { + $doSaveResourceLink = false; + $this->userResult->getResourceLink()->primaryResourceLinkId = $id; + $this->userResult->getResourceLink()->shareApproved = $shareKey->autoApprove; + $this->userResult->getResourceLink()->updated = time(); +// Remove share key + $shareKey->delete(); + } else { + $this->reason = 'An error occurred initialising your share arrangement.'; + } + } else { + $this->reason = 'It is not possible to share your resource link with yourself.'; + } + } + if ($ok) { + $ok = !is_null($id); + if (!$ok) { + $this->reason = 'You have requested to share a resource link but none is available.'; + } else { + $ok = (!is_null($this->userResult->getResourceLink()->shareApproved) && $this->userResult->getResourceLink()->shareApproved); + if (!$ok) { + $this->reason = 'Your share request is waiting to be approved.'; + } + } + } + } + } else { +// Check no share is in place + $ok = is_null($id); + if (!$ok) { + $this->reason = 'You have not requested to share a resource link but an arrangement is currently in place.'; + } + } + +// Look up primary resource link + if ($ok && !is_null($id)) { + $resourceLink = ResourceLink::fromRecordId($id, $this->dataConnector); + $ok = !is_null($resourceLink->created); + if ($ok) { + if ($doSaveResourceLink) { + $this->resourceLink->save(); + } + $this->resourceLink = $resourceLink; + } else { + $this->reason = 'Unable to load resource link being shared.'; + } + } + + return $ok; + } + + /** + * Generate a form to perform an authentication request. + * + * @return bool True if form was generated + */ + private function sendAuthenticationRequest($hint) + { + + $clientId = null; + if (isset($_REQUEST['client_id'])) { + $clientId = $_REQUEST['client_id']; + } + $deploymentId = null; + if (isset($_REQUEST['lti_deployment_id'])) { + $deploymentId = $_REQUEST['lti_deployment_id']; + } + $this->platform = Platform::fromPlatformId($_REQUEST['iss'], $clientId, $deploymentId, $this->dataConnector); + if ($this->platform->debugMode) { + Util::$logLevel = Util::LOGLEVEL_DEBUG; + } + $ok = !is_null($this->platform) && !empty($this->platform->authenticationUrl); + if (!$ok) { + $this->reason = 'Platform not found or no platform authentication request URL'; + } else { + $oauthRequest = OAuth\OAuthRequest::from_request(); + do { + $nonce = new PlatformNonce($this->platform, Util::getRandomString()); + $ok = !$nonce->load(); + } while (!$ok); + $ok = $nonce->save(); + if ($ok) { + $params = array( + 'client_id' => $this->platform->clientId, + 'login_hint' => $_REQUEST['login_hint'], + 'nonce' => Util::getRandomString(32), + 'prompt' => 'none', + 'redirect_uri' => $oauthRequest->get_normalized_http_url(), + 'response_mode' => 'form_post', + 'response_type' => 'id_token', + 'scope' => 'openid', + 'state' => $nonce->getValue() + ); + if (!is_null($hint)) { + $params['lti_message_hint'] = $hint; + } + $this->output = Util::sendForm($this->platform->authenticationUrl, $params); + } else { + $this->reason = 'Unable to generate a state value'; + } + } + + return $ok; + } + + /** + * Generate a form to perform a relaunch request. + */ + private function sendRelaunchRequest() + { + do { + $nonce = new PlatformNonce($this->platform, Util::getRandomString()); + $ok = !$nonce->load(); + } while (!$ok); + $ok = $nonce->save(); + if ($ok) { + $params = array( + 'tool_state' => $nonce->getValue(), + 'platform_state' => $this->messageParameters['platform_state'] + ); + $params = $this->platform->addSignature($this->messageParameters['relaunch_url'], $params); + $this->output = Util::sendForm($this->messageParameters['relaunch_url'], $params); + } else { + $this->reason = 'Unable to generate a state value'; + } + } + + /** + * Validate a parameter value from an array of permitted values. + * + * @param mixed $value Value to be checked + * @param array $values Array of permitted values + * @param string $reason Reason to generate when the value is not permitted + * + * @return bool True if value is valid + */ + private function checkValue($value, $values, $reason) + { + $ok = in_array($value, $values); + if (!$ok && !empty($reason)) { + $this->reason = sprintf($reason, $value); + } + + return $ok; + } + +} diff --git a/src/ToolConsumer.php b/src/ToolConsumer.php index c936566..8c4cf47 100644 --- a/src/ToolConsumer.php +++ b/src/ToolConsumer.php @@ -2,731 +2,39 @@ namespace ceLTIc\LTI; -use ceLTIc\LTI\DataConnector; -use ceLTIc\LTI\Service; -use ceLTIc\LTI\Http\HttpMessage; -use ceLTIc\LTI\OAuth; +use ceLTIc\LTI\Platform; /** * Class to represent a tool consumer * + * @deprecated Use Platform instead + * @see Platform + * * @author Stephen P Vickers * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 */ -class ToolConsumer +class ToolConsumer extends Platform { - /** - * Local name of tool consumer. - * - * @var string|null $name - */ - public $name = null; - - /** - * Shared secret. - * - * @var string|null $secret - */ - public $secret = null; - - /** - * LTI version (as reported by last tool consumer connection). - * - * @var string|null $ltiVersion - */ - public $ltiVersion = null; - - /** - * Method used for signing messages. - * - * @var string $signatureMethod - */ - public $signatureMethod = 'HMAC-SHA1'; - - /** - * Name of tool consumer (as reported by last tool consumer connection). - * - * @var string|null $consumerName - */ - public $consumerName = null; - - /** - * Tool consumer version (as reported by last tool consumer connection). - * - * @var string|null $consumerVersion - */ - public $consumerVersion = null; - - /** - * The consumer profile data. - * - * @var object|null $profile - */ - public $profile = null; - - /** - * Tool consumer GUID (as reported by first tool consumer connection). - * - * @var string|null $consumerGuid - */ - public $consumerGuid = null; - - /** - * Optional CSS path (as reported by last tool consumer connection). - * - * @var string|null $cssPath - */ - public $cssPath = null; - - /** - * Whether the tool consumer instance is protected by matching the consumer_guid value in incoming requests. - * - * @var bool $protected - */ - public $protected = false; - - /** - * Whether the tool consumer instance is enabled to accept incoming connection requests. - * - * @var bool $enabled - */ - public $enabled = false; - - /** - * Timestamp from which the the tool consumer instance is enabled to accept incoming connection requests. - * - * @var int|null $enableFrom - */ - public $enableFrom = null; - - /** - * Timestamp until which the tool consumer instance is enabled to accept incoming connection requests. - * - * @var int|null $enableUntil - */ - public $enableUntil = null; - - /** - * Timestamp for date of last connection from this tool consumer. - * - * @var int|null $lastAccess - */ - public $lastAccess = null; - - /** - * Default scope to use when generating an Id value for a user. - * - * @var int $idScope - */ - public $idScope = ToolProvider::ID_SCOPE_ID_ONLY; - - /** - * Default email address (or email domain) to use when no email address is provided for a user. - * - * @var string $defaultEmail - */ - public $defaultEmail = ''; - - /** - * HttpMessage object for last service request. - * - * @var HttpMessage|null $lastServiceRequest - */ - public $lastServiceRequest = null; - - /** - * Timestamp for when the object was created. - * - * @var int|null $created - */ - public $created = null; - - /** - * Timestamp for when the object was last updated. - * - * @var int|null $updated - */ - public $updated = null; - - /** - * Consumer ID value. - * - * @var int|null $id - */ - private $id = null; - - /** - * Consumer key value. - * - * @var string|null $key - */ - private $key = null; - - /** - * Setting values (LTI parameters, custom parameters and local parameters). - * - * @var array $settings - */ - private $settings = null; - - /** - * Whether the settings value have changed since last saved. - * - * @var bool $settingsChanged - */ - private $settingsChanged = false; - - /** - * Data connector object or string. - * - * @var DataConnector|null $dataConnector - */ - private $dataConnector = null; - /** * Class constructor. * - * @param string $key Consumer key + * @param string $key Consumer key/client ID * @param DataConnector $dataConnector A data connector object - * @param bool $autoEnable true if the tool consumers is to be enabled automatically (optional, default is false) + * @param bool $autoEnable true if the tool consumer is to be enabled automatically (optional, default is false) */ public function __construct($key = null, $dataConnector = null, $autoEnable = false) { - $this->initialize(); - if (empty($dataConnector)) { - $dataConnector = DataConnector\DataConnector::getDataConnector(); - } - $this->dataConnector = $dataConnector; - if (!is_null($key) && (strlen($key) > 0)) { - $this->load($key, $autoEnable); - } else { - $this->secret = DataConnector\DataConnector::getRandomString(32); - } - } - - /** - * Initialise the tool consumer. - */ - public function initialize() - { - $this->id = null; - $this->key = null; - $this->name = null; - $this->secret = null; - $this->signatureMethod = 'HMAC-SHA1'; - $this->ltiVersion = null; - $this->consumerName = null; - $this->consumerVersion = null; - $this->consumerGuid = null; - $this->profile = null; - $this->toolProxy = null; - $this->settings = array(); - $this->protected = false; - $this->enabled = false; - $this->enableFrom = null; - $this->enableUntil = null; - $this->lastAccess = null; - $this->idScope = ToolProvider::ID_SCOPE_ID_ONLY; - $this->defaultEmail = ''; - $this->created = null; - $this->updated = null; - } - - /** - * Initialise the tool consumer. - * - * Synonym for initialize(). - */ - public function initialise() - { - $this->initialize(); - } - - /** - * Save the tool consumer to the database. - * - * @return bool True if the object was successfully saved - */ - public function save() - { - $ok = $this->dataConnector->saveToolConsumer($this); - if ($ok) { - $this->settingsChanged = false; - } - - return $ok; - } - - /** - * Delete the tool consumer from the database. - * - * @return bool True if the object was successfully deleted - */ - public function delete() - { - return $this->dataConnector->deleteToolConsumer($this); - } - - /** - * Get the tool consumer record ID. - * - * @return int|null Consumer record ID value - */ - public function getRecordId() - { - return $this->id; - } - - /** - * Sets the tool consumer record ID. - * - * @param int $id Consumer record ID value - */ - public function setRecordId($id) - { - $this->id = $id; - } - - /** - * Get the tool consumer key. - * - * @return string Consumer key value - */ - public function getKey() - { - return $this->key; - } - - /** - * Set the tool consumer key. - * - * @param string $key Consumer key value - */ - public function setKey($key) - { - $this->key = $key; - } - - /** - * Get tool consumer family code (as reported by last tool consumer connection). - * - * @return string Family code - */ - public function getFamilyCode() - { - $familyCode = ''; - if (!empty($this->consumerVersion)) { - list($familyCode, $version) = explode('-', $this->consumerVersion, 2); - } - - return $familyCode; - } - - /** - * Get the data connector. - * - * @return DataConnector|null Data connector object or string - */ - public function getDataConnector() - { - return $this->dataConnector; - } - - /** - * Is the consumer key available to accept launch requests? - * - * @return bool True if the consumer key is enabled and within any date constraints - */ - public function getIsAvailable() - { - $ok = $this->enabled; - - $now = time(); - if ($ok && !is_null($this->enableFrom)) { - $ok = $this->enableFrom <= $now; - } - if ($ok && !is_null($this->enableUntil)) { - $ok = $this->enableUntil > $now; - } - - return $ok; - } - - /** - * Get a setting value. - * - * @param string $name Name of setting - * @param string $default Value to return if the setting does not exist (optional, default is an empty string) - * - * @return string Setting value - */ - public function getSetting($name, $default = '') - { - if (array_key_exists($name, $this->settings)) { - $value = $this->settings[$name]; - } else { - $value = $default; - } - - return $value; - } - - /** - * Set a setting value. - * - * @param string $name Name of setting - * @param string $value Value to set, use an empty value to delete a setting (optional, default is null) - */ - public function setSetting($name, $value = null) - { - $old_value = $this->getSetting($name); - if ($value !== $old_value) { - if (!empty($value)) { - $this->settings[$name] = $value; - } else { - unset($this->settings[$name]); - } - $this->settingsChanged = true; - } - } - - /** - * Get an array of all setting values. - * - * @return array Associative array of setting values - */ - public function getSettings() - { - return $this->settings; - } - - /** - * Set an array of all setting values. - * - * @param array $settings Associative array of setting values - */ - public function setSettings($settings) - { - $this->settings = $settings; - } - - /** - * Save setting values. - * - * @return bool True if the settings were successfully saved - */ - public function saveSettings() - { - if ($this->settingsChanged) { - $ok = $this->save(); - } else { - $ok = true; - } - - return $ok; - } - - /** - * Check if the Tool Settings service is supported. - * - * @return bool True if this tool consumer supports the Tool Settings service - */ - public function hasToolSettingsService() - { - $has = !empty($this->getSetting('custom_system_setting_url')); - if (!$has) { - $has = self::hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode(), $this); - } - return $has; - } - - /** - * Get Tool Settings. - * - * @param bool $simple True if all the simple media type is to be used (optional, default is true) - * - * @return mixed The array of settings if successful, otherwise false - */ - public function getToolSettings($simple = true) - { - $ok = false; - $settings = array(); - if (!empty($this->getSetting('custom_system_setting_url'))) { - $url = $this->getSetting('custom_system_setting_url'); - $service = new Service\ToolSettings($this, $url, $simple); - $settings = $service->get(); - $this->lastServiceRequest = $service->getHttpMessage(); - $ok = $settings !== false; - } - if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode(), $this)) { - $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode()); - $hook = new $className($this); - $settings = $hook->getToolSettings($simple); - } - - return $settings; - } - - /** - * Perform a Tool Settings service request. - * - * @param array $settings An associative array of settings (optional, default is none) - * - * @return bool True if action was successful, otherwise false - */ - public function setToolSettings($settings = array()) - { - $ok = false; - if (!empty($this->getSetting('custom_system_setting_url'))) { - $url = $this->getSetting('custom_system_setting_url'); - $service = new Service\ToolSettings($this, $url); - $ok = $service->set($settings); - $this->lastServiceRequest = $service->getHttpMessage(); - } - if (!$ok && $this->hasConfiguredApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode(), $this)) { - $className = $this->getApiHook(self::$TOOL_SETTINGS_SERVICE_HOOK, $this->getFamilyCode()); - $hook = new $className($this); - $ok = $hook->setToolSettings($settings); - } - - return $ok; - } - - /** - * Add the signature to an LTI message. - * - * @param string $url URL for message request - * @param string $type LTI message type - * @param string $version LTI version - * @param array $params Message parameters - * - * @return array Array of signed message parameters - */ - public function signParameters($url, $type, $version, $params) - { - if (!empty($url)) { -// Add standard parameters - $params['lti_version'] = $version; - $params['lti_message_type'] = $type; -// Add signature - $params = $this->addSignature($url, $params, 'POST', $type); - } - - return $params; - } - - /** - * Generates the headers for an LTI service request. - * - * @param string $url URL for message request - * @param string $method HTTP method - * @param string $type Media type - * @param string $data Data being passed in request body (optional) - * - * @return string Headers to include with service request - */ - public function signServiceRequest($url, $method, $type, $data = null) - { - $header = ''; - if (!empty($url)) { - $header = $this->addSignature($url, $data, $method, $type); - } - - return $header; - } - - /** - * Add the signature to an array of message parameters or to a header string. - * - * @param string $endpoint URL to which message is being sent - * @param mixed $data Data to be passed - * @param string $method HTTP method - * @param string|null $type Content type of data being passed - * - * @return mixed Array of signed message parameters or header string - */ - public function addSignature($endpoint, $data, $method = 'POST', $type = null) - { - switch ($this->signatureMethod) { - case 'HMAC-SHA1': - case 'HMAC-SHA224': - case 'HMAC-SHA256': - case 'HMAC-SHA384': - case 'HMAC-SHA512': - return $this->addOAuthSignature($endpoint, $data, $method, $type); - break; - default: - return $data; - break; - } - } - - /** - * Perform a service request - * - * @param object $service Service object to be executed - * @param string $method HTTP action - * @param string $format Media type - * @param mixed $data Array of parameters or body string - * - * @return HttpMessage HTTP object containing request and response details - */ - public function doServiceRequest($service, $method, $format, $data) - { - $header = $this->addSignature($service->endpoint, $data, $method, $format); - -// Connect to tool consumer - $http = new HttpMessage($service->endpoint, $method, $data, $header); -// Parse JSON response - if ($http->send() && !empty($http->response)) { - $http->responseJson = json_decode($http->response); - $http->ok = !is_null($http->responseJson); - } - - return $http; - } - - /** - * Load the tool consumer from the database by its record ID. - * - * @param string $id The consumer key record ID - * @param DataConnector $dataConnector Database connection object - * - * @return ToolConsumer The tool consumer object - */ - public static function fromRecordId($id, $dataConnector) - { - $toolConsumer = new ToolConsumer(null, $dataConnector); - - $toolConsumer->initialize(); - $toolConsumer->setRecordId($id); - if (!$dataConnector->loadToolConsumer($toolConsumer)) { - $toolConsumer->initialize(); - } - - return $toolConsumer; - } - -### -### PRIVATE METHODS -### - - /** - * Load the tool consumer from the database. - * - * @param string $key The consumer key value - * @param bool $autoEnable True if the consumer should be enabled (optional, default if false) - * - * @return bool True if the consumer was successfully loaded - */ - private function load($key, $autoEnable = false) - { - $this->key = $key; - $ok = $this->dataConnector->loadToolConsumer($this); - if (!$ok) { - $this->enabled = $autoEnable; - } - - return $ok; - } - - /** - * Add the OAuth signature to an array of message parameters or to a header string. - * - * @param string $endpoint URL to which message is being sent - * @param mixed $data Data to be passed - * @param string $method HTTP method - * @param string|null $type Content type of data being passed - * - * @return string[]|string Array of signed message parameters or header string - */ - private function addOAuthSignature($endpoint, $data, $method, $type) - { - $params = array(); - if (is_array($data)) { - $params = $data; - $params['oauth_callback'] = 'about:blank'; - } -// Check for query parameters which need to be included in the signature - $queryString = parse_url($endpoint, PHP_URL_QUERY); - $queryParams = OAuth\OAuthUtil::parse_parameters($queryString); - $params = array_merge_recursive($queryParams, $params); - - if (!is_array($data)) { -// Calculate body hash - switch ($this->signatureMethod) { - case 'HMAC-SHA224': - $hash = base64_encode(hash('sha224', $data, true)); - break; - case 'HMAC-SHA256': - $hash = base64_encode(hash('sha256', $data, true)); - break; - case 'HMAC-SHA384': - $hash = base64_encode(hash('sha384', $data, true)); - break; - case 'HMAC-SHA512': - $hash = base64_encode(hash('sha512', $data, true)); - break; - default: - $hash = base64_encode(sha1($data, true)); - break; - } - $params['oauth_body_hash'] = $hash; - } - -// Add OAuth signature - switch ($this->signatureMethod) { - case 'HMAC-SHA224': - $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA224(); - break; - case 'HMAC-SHA256': - $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA256(); - break; - case 'HMAC-SHA384': - $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA384(); - break; - case 'HMAC-SHA512': - $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA512(); - break; - default: - $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA1(); - break; - } - $oauthConsumer = new OAuth\OAuthConsumer($this->key, $this->secret, null); - $oauthReq = OAuth\OAuthRequest::from_consumer_and_token($oauthConsumer, null, $method, $endpoint, $params); - $oauthReq->sign_request($hmacMethod, $oauthConsumer, null); - if (!is_array($data)) { - $header = $oauthReq->to_header(); - if (empty($data)) { - if (!empty($type)) { - $header .= "\nAccept: {$type}"; - } - } elseif (isset($type)) { - $header .= "\nContent-Type: {$type}"; - $header .= "\nContent-Length: " . strlen($data); - } - return $header; - } else { - $params = $oauthReq->get_parameters(); - foreach ($queryParams as $key => $value) { - if (!is_array($value)) { - if (!is_array($params[$key])) { - if ($params[$key] === $value) { - unset($params[$key]); - } - } else { - $params[$key] = array_diff($params[$key], array($value)); - } - } else { - foreach ($value as $element) { - $params[$key] = array_diff($params[$key], array($value)); - } - } - } - return $params; + parent::__construct($dataConnector); + $platform = Platform::fromConsumerKey($key, $dataConnector, $autoEnable); + $this->setKey($key); + $this->setRecordId($platform->getRecordId()); + foreach (get_object_vars($platform) as $key => $value) { + $this->$key = $value; } + Util::logDebug('Class ceLTIc\LTI\ToolConsumer has been deprecated; please use ceLTIc\LTI\Platform::fromConsumerKey instead.', + true); } } diff --git a/src/ToolProvider.php b/src/ToolProvider.php index bf5edd6..72bdbf8 100644 --- a/src/ToolProvider.php +++ b/src/ToolProvider.php @@ -2,329 +2,38 @@ namespace ceLTIc\LTI; -use ceLTIc\LTI\DataConnector; -use ceLTIc\LTI\MediaType; -use ceLTIc\LTI\Profile; -use ceLTIc\LTI\Http\HttpMessage; -use ceLTIc\LTI\OAuth; -use ceLTIc\LTI\ApiHook\ApiHook; +use ceLTIc\LTI\Tool; +use ceLTIc\LTI\DataConnector\DataConnector; +use ceLTIc\LTI\Util; /** * Class to represent an LTI Tool Provider * + * @deprecated Use Tool instead + * @see Tool + * * @author Stephen P Vickers * @copyright SPV Software Products * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 */ -class ToolProvider +class ToolProvider extends Tool { - use ApiHook; - - /** - * Default connection error message. - */ - const CONNECTION_ERROR_MESSAGE = 'Sorry, there was an error connecting you to the application.'; /** * LTI version 1 for messages. + * + * @deprecated Use Util::LTI_VERSION1 instead + * @see Util::LTI_VERSION1 */ const LTI_VERSION1 = 'LTI-1p0'; /** * LTI version 2 for messages. - */ - const LTI_VERSION2 = 'LTI-2p0'; - - /** - * Use ID value only. - */ - const ID_SCOPE_ID_ONLY = 0; - - /** - * Prefix an ID with the consumer key. - */ - const ID_SCOPE_GLOBAL = 1; - - /** - * Prefix the ID with the consumer key and context ID. - */ - const ID_SCOPE_CONTEXT = 2; - - /** - * Prefix the ID with the consumer key and resource ID. - */ - const ID_SCOPE_RESOURCE = 3; - - /** - * Character used to separate each element of an ID. - */ - const ID_SCOPE_SEPARATOR = ':'; - - /** - * Permitted LTI versions for messages. - */ - private static $LTI_VERSIONS = array(self::LTI_VERSION1, self::LTI_VERSION2); - - /** - * List of supported message types and associated class methods. - */ - private static $METHOD_NAMES = array('basic-lti-launch-request' => 'onLaunch', - 'ConfigureLaunchRequest' => 'onConfigure', - 'DashboardRequest' => 'onDashboard', - 'ContentItemSelectionRequest' => 'onContentItem', - 'ToolProxyRegistrationRequest' => 'onRegister' - ); - - /** - * Names of LTI parameters to be retained in the consumer settings property. - */ - private static $LTI_CONSUMER_SETTING_NAMES = array('custom_tc_profile_url', 'custom_system_setting_url', 'custom_oauth2_access_token_url'); - - /** - * Names of LTI parameters to be retained in the context settings property. - */ - private static $LTI_CONTEXT_SETTING_NAMES = array('custom_context_setting_url', - 'custom_context_memberships_url', 'custom_context_memberships_v2_url', 'custom_lineitems_url'); - - /** - * Names of LTI parameters to be retained in the resource link settings property. - */ - private static $LTI_RESOURCE_LINK_SETTING_NAMES = array('lis_result_sourcedid', 'lis_outcome_service_url', - 'ext_ims_lis_basic_outcome_url', 'ext_ims_lis_resultvalue_sourcedids', - 'ext_ims_lis_memberships_id', 'ext_ims_lis_memberships_url', - 'ext_ims_lti_tool_setting', 'ext_ims_lti_tool_setting_id', 'ext_ims_lti_tool_setting_url', - 'custom_link_setting_url', 'custom_link_memberships_url', - 'custom_lineitems_url', 'custom_lineitem_url'); - - /** - * Names of LTI parameters to be retained even when not passed. - */ - private static $LTI_RETAIN_SETTING_NAMES = array('custom_lineitem_url'); - - /** - * Names of LTI custom parameter substitution variables (or capabilities) and their associated default message parameter names. - */ - private static $CUSTOM_SUBSTITUTION_VARIABLES = array('User.id' => 'user_id', - 'User.image' => 'user_image', - 'User.username' => 'username', - 'User.scope.mentor' => 'role_scope_mentor', - 'Membership.role' => 'roles', - 'Person.sourcedId' => 'lis_person_sourcedid', - 'Person.name.full' => 'lis_person_name_full', - 'Person.name.family' => 'lis_person_name_family', - 'Person.name.given' => 'lis_person_name_given', - 'Person.email.primary' => 'lis_person_contact_email_primary', - 'Context.id' => 'context_id', - 'Context.type' => 'context_type', - 'Context.title' => 'context_title', - 'Context.label' => 'context_label', - 'CourseOffering.sourcedId' => 'lis_course_offering_sourcedid', - 'CourseSection.sourcedId' => 'lis_course_section_sourcedid', - 'CourseSection.label' => 'context_label', - 'CourseSection.title' => 'context_title', - 'ResourceLink.id' => 'resource_link_id', - 'ResourceLink.title' => 'resource_link_title', - 'ResourceLink.description' => 'resource_link_description', - 'Result.sourcedId' => 'lis_result_sourcedid', - 'BasicOutcome.url' => 'lis_outcome_service_url', - 'ToolConsumerProfile.url' => 'custom_tc_profile_url', - 'ToolProxy.url' => 'tool_proxy_url', - 'ToolProxy.custom.url' => 'custom_system_setting_url', - 'ToolProxyBinding.custom.url' => 'custom_context_setting_url', - 'LtiLink.custom.url' => 'custom_link_setting_url', - 'LineItems.url' => 'custom_lineitems_url', - 'LineItem.url' => 'custom_lineitem_url', - 'ToolProxyBinding.memberships.url' => 'custom_context_memberships_url', - 'LtiLink.memberships.url' => 'custom_link_memberships_url'); - - /** - * True if the last request was successful. - * - * @var bool $ok - */ - public $ok = true; - - /** - * Tool Consumer object. - * - * @var ToolConsumer|null $consumer - */ - public $consumer = null; - - /** - * Return URL provided by tool consumer. - * - * @var string|null $returnUrl - */ - public $returnUrl = null; - - /** - * UserResult object. - * - * @var UserResult|null $userResult - */ - public $userResult = null; - - /** - * Resource link object. - * - * @var ResourceLink|null $resourceLink - */ - public $resourceLink = null; - - /** - * Context object. - * - * @var Context|null $context - */ - public $context = null; - - /** - * Data connector object. - * - * @var DataConnector|null $dataConnector - */ - public $dataConnector = null; - - /** - * Default email domain. - * - * @var string $defaultEmail - */ - public $defaultEmail = ''; - - /** - * Scope to use for user IDs. * - * @var int $idScope + * @deprecated Use Util::LTI_VERSION2 instead + * @see Util::LTI_VERSION2 */ - public $idScope = self::ID_SCOPE_ID_ONLY; - - /** - * Whether shared resource link arrangements are permitted. - * - * @var bool $allowSharing - */ - public $allowSharing = false; - - /** - * Message for last request processed - * - * @var string $message - */ - public $message = self::CONNECTION_ERROR_MESSAGE; - - /** - * Error message for last request processed. - * - * @var string|null $reason - */ - public $reason = null; - - /** - * Details for error message relating to last request processed. - * - * @var array $details - */ - public $details = array(); - - /** - * Base URL for tool provider service - * - * @var string|null $baseUrl - */ - public $baseUrl = null; - - /** - * Vendor details - * - * @var Item|null $vendor - */ - public $vendor = null; - - /** - * Product details - * - * @var Item|null $product - */ - public $product = null; - - /** - * Services required by Tool Provider - * - * @var array|null $requiredServices - */ - public $requiredServices = null; - - /** - * Optional services used by Tool Provider - * - * @var array|null $optionalServices - */ - public $optionalServices = null; - - /** - * Resource handlers for Tool Provider - * - * @var array|null $resourceHandlers - */ - public $resourceHandlers = null; - - /** - * URL to redirect user to on successful completion of the request. - * - * @var string|null $redirectUrl - */ - protected $redirectUrl = null; - - /** - * Media types accepted by the Tool Consumer. - * - * @var array|null $mediaTypes - */ - protected $mediaTypes = null; - - /** - * Document targets accepted by the Tool Consumer. - * - * @var array|null $documentTargets - */ - protected $documentTargets = null; - - /** - * Default HTML to be displayed on a successful completion of the request. - * - * @var string|null $output - */ - protected $output = null; - - /** - * HTML to be displayed on an unsuccessful completion of the request and no return URL is available. - * - * @var string|null $errorOutput - */ - protected $errorOutput = null; - - /** - * Whether debug messages explaining the cause of errors are to be returned to the tool consumer. - * - * @var bool $debugMode - */ - protected $debugMode = false; - - /** - * LTI message parameters. - * - * @var array|null $messageParameters - */ - protected $messageParameters = null; - - /** - * LTI parameter constraints for auto validation checks. - * - * @var array|null $constraints - */ - private $constraints = null; + const LTI_VERSION2 = 'LTI-2p0'; /** * Class constructor @@ -333,1110 +42,8 @@ class ToolProvider */ function __construct($dataConnector) { - $this->constraints = array(); - $this->dataConnector = $dataConnector; - $this->ok = !is_null($this->dataConnector); - $this->vendor = new Profile\Item(); - $this->product = new Profile\Item(); - $this->requiredServices = array(); - $this->optionalServices = array(); - $this->resourceHandlers = array(); - } - - /** - * Process an incoming request - */ - public function handleRequest() - { - if ($this->debugMode) { - Util::$logLevel = Util::LOGLEVEL_DEBUG; - } - Util::logRequest(); - if ($this->ok) { - $this->getMessageParameters(); - if ($this->authenticate()) { - if (empty($this->output)) { - $this->doCallback(); - } - } - if (!$this->ok) { - Util::logError("Request failed with reason: '{$this->reason}'"); - } - } - $this->result(); - } - - /** - * Add a parameter constraint to be checked on launch - * - * @param string $name Name of parameter to be checked - * @param bool $required True if parameter is required (optional, default is true) - * @param int $maxLength Maximum permitted length of parameter value (optional, default is null) - * @param array $messageTypes Array of message types to which the constraint applies (optional, default is all) - */ - public function setParameterConstraint($name, $required = true, $maxLength = null, $messageTypes = null) - { - $name = trim($name); - if (strlen($name) > 0) { - $this->constraints[$name] = array('required' => $required, 'max_length' => $maxLength, 'messages' => $messageTypes); - } - } - - /** - * Get an array of defined tool consumers - * - * @return array Array of ToolConsumer objects - */ - public function getConsumers() - { - return $this->dataConnector->getToolConsumers(); - } - - /** - * Find an offered service based on a media type and HTTP action(s) - * - * @param string $format Media type required - * @param array $methods Array of HTTP actions required - * - * @return object The service object - */ - public function findService($format, $methods) - { - $found = false; - $services = $this->consumer->profile->service_offered; - if (is_array($services)) { - $n = -1; - foreach ($services as $service) { - $n++; - if (!is_array($service->format) || !in_array($format, $service->format)) { - continue; - } - $missing = array(); - foreach ($methods as $method) { - if (!is_array($service->action) || !in_array($method, $service->action)) { - $missing[] = $method; - } - } - $methods = $missing; - if (count($methods) <= 0) { - $found = $service; - break; - } - } - } - - return $found; - } - - /** - * Send the tool proxy to the Tool Consumer - * - * @return bool True if the tool proxy was accepted - */ - public function doToolProxyService() - { -// Create tool proxy - $toolProxyService = $this->findService('application/vnd.ims.lti.v2.toolproxy+json', array('POST')); - $secret = DataConnector\DataConnector::getRandomString(12); - $toolProxy = new MediaType\ToolProxy($this, $toolProxyService, $secret); - $http = $this->consumer->doServiceRequest($toolProxyService, 'POST', 'application/vnd.ims.lti.v2.toolproxy+json', - json_encode($toolProxy)); - $ok = $http->ok && ($http->status == 201) && isset($http->responseJson->tool_proxy_guid) && (strlen($http->responseJson->tool_proxy_guid) > 0); - if ($ok) { - $this->consumer->setKey($http->responseJson->tool_proxy_guid); - $this->consumer->secret = $toolProxy->security_contract->shared_secret; - $this->consumer->toolProxy = json_encode($toolProxy); - $this->consumer->save(); - } - - return $ok; - } - - /** - * Get the message parameters - * - * @return array The message parameter array - */ - public function getMessageParameters() - { - if ($this->ok && is_null($this->messageParameters)) { - $this->messageParameters = OAuth\OAuthUtil::parse_parameters(file_get_contents(OAuth\OAuthRequest::$POST_INPUT)); - if (!empty($this->messageParameters['oauth_consumer_key'])) { - $this->consumer = new ToolConsumer($this->messageParameters['oauth_consumer_key'], $this->dataConnector); - } -// Set debug mode - if (!$this->debugMode) { - $this->debugMode = isset($this->messageParameters['custom_debug']) && (strtolower($this->messageParameters['custom_debug']) === 'true'); - if ($this->debugMode) { - $currentLogLevel = Util::$logLevel; - Util::$logLevel = Util::LOGLEVEL_DEBUG; - if ($currentLogLevel < Util::LOGLEVEL_INFO) { - Util::logRequest(); - } - } - } -// Set return URL if available - if (isset($this->messageParameters['launch_presentation_return_url'])) { - $this->returnUrl = $this->messageParameters['launch_presentation_return_url']; - } elseif (isset($this->messageParameters['content_item_return_url'])) { - $this->returnUrl = $this->messageParameters['content_item_return_url']; - } - } - - return $this->messageParameters; - } - - /** - * Get an array of fully qualified user roles - * - * @param mixed $roles Comma-separated list of roles or array of roles - * @param string $ltiVersion LTI version (default is LTI-1p0) - * - * @return array Array of roles - */ - public static function parseRoles($roles, $ltiVersion = self::LTI_VERSION1) - { - if (!is_array($roles)) { - $roles = explode(',', $roles); - } - $parsedRoles = array(); - foreach ($roles as $role) { - $role = trim($role); - if (!empty($role)) { - if ($ltiVersion === self::LTI_VERSION1) { - if (substr($role, 0, 4) !== 'urn:') { - $role = 'urn:lti:role:ims/lis/' . $role; - } - } elseif ((substr($role, 0, 7) !== 'http://') && (substr($role, 0, 8) !== 'https://')) { - $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . $role; - } - $parsedRoles[] = $role; - } - } - - return $parsedRoles; - } - - /** - * Generate a web page containing an auto-submitted form of parameters. - * - * @param string $url URL to which the form should be submitted - * @param array $params Array of form parameters - * @param string $target Name of target (optional) - * - * @return string - */ - public static function sendForm($url, $params, $target = '') - { - $page = <<< EOD - - -IMS LTI message - - - -
- -EOD; - foreach ($params as $key => $value) { - $key = htmlentities($key, ENT_COMPAT | ENT_HTML401, 'UTF-8'); - if (!is_array($value)) { - $value = htmlentities($value, ENT_COMPAT | ENT_HTML401, 'UTF-8'); - $page .= <<< EOD - - -EOD; - } else { - foreach ($value as $element) { - $element = htmlentities($element, ENT_COMPAT | ENT_HTML401, 'UTF-8'); - $page .= <<< EOD - - -EOD; - } - } - } - - $page .= <<< EOD -
- - -EOD; - - return $page; - } - -### -### PROTECTED METHODS -### - - /** - * Process a valid launch request - * - * @return bool True if no error - */ - protected function onLaunch() - { - $this->onError(); - } - - /** - * Process a valid configure request - * - * @return bool True if no error - */ - protected function onConfigure() - { - $this->onError(); - } - - /** - * Process a valid dashboard request - * - * @return bool True if no error - */ - protected function onDashboard() - { - $this->onError(); - } - - /** - * Process a valid content-item request - * - * @return bool True if no error - */ - protected function onContentItem() - { - $this->onError(); - } - - /** - * Process a valid tool proxy registration request - * - * @return bool True if no error - */ - protected function onRegister() - { - $this->onError(); - } - - /** - * Process a response to an invalid request - * - * @return bool True if no further error processing required - */ - protected function onError() - { - $this->ok = false; - } - -### -### PRIVATE METHODS -### - - /** - * Call any callback function for the requested action. - * - * This function may set the redirect_url and output properties. - * - * @param string|null $method Name of method to be called (optional) - */ - private function doCallback($method = null) - { - $callback = $method; - if (is_null($callback)) { - $callback = self::$METHOD_NAMES[$this->messageParameters['lti_message_type']]; - } - if (method_exists($this, $callback)) { - $this->$callback(); - } elseif (is_null($method) && $this->ok) { - $this->ok = false; - $this->reason = "Message type not supported: {$this->messageParameters['lti_message_type']}"; - } - if ($this->ok && ($this->messageParameters['lti_message_type'] == 'ToolProxyRegistrationRequest')) { - $this->consumer->save(); - } - } - - /** - * Perform the result of an action. - * - * This function may redirect the user to another URL rather than returning a value. - * - * @return string Output to be displayed (redirection, or display HTML or message) - */ - private function result() - { - if (!$this->ok) { - $this->onError(); - } - if (!$this->ok) { - -// If not valid, return an error message to the tool consumer if a return URL is provided - if (!empty($this->returnUrl)) { - $errorUrl = $this->returnUrl; - if (strpos($errorUrl, '?') === false) { - $errorUrl .= '?'; - } else { - $errorUrl .= '&'; - } - if ($this->debugMode && !is_null($this->reason)) { - $errorUrl .= 'lti_errormsg=' . urlencode("Debug error: $this->reason"); - } else { - $errorUrl .= 'lti_errormsg=' . urlencode($this->message); - if (!is_null($this->reason)) { - $errorUrl .= '<i_errorlog=' . urlencode("Debug error: $this->reason"); - } - } - if (!is_null($this->consumer) && isset($this->messageParameters['lti_message_type']) && (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') || - ($this->messageParameters['lti_message_type'] === 'LtiDeepLinkingRequest'))) { - $formParams = array(); - if (isset($this->messageParameters['data'])) { - $formParams['data'] = $this->messageParameters['data']; - } - $version = (isset($this->messageParameters['lti_version'])) ? $this->messageParameters['lti_version'] : self::LTI_VERSION1; - $formParams = $this->consumer->signParameters($errorUrl, 'ContentItemSelection', $version, $formParams); - $page = self::sendForm($errorUrl, $formParams); - echo $page; - } else { - header("Location: {$errorUrl}"); - } - exit; - } else { - if (!is_null($this->errorOutput)) { - echo $this->errorOutput; - } elseif ($this->debugMode && !empty($this->reason)) { - echo "Debug error: {$this->reason}"; - } else { - echo "Error: {$this->message}"; - } - } - } elseif (!is_null($this->redirectUrl)) { - header("Location: {$this->redirectUrl}"); - exit; - } elseif (!is_null($this->output)) { - echo $this->output; - exit; - } - } - - /** - * Check the authenticity of the LTI launch request. - * - * The consumer, resource link and user objects will be initialised if the request is valid. - * - * @return bool True if the request has been successfully validated. - */ - private function authenticate() - { -// Get the consumer - $doSaveConsumer = false; - $this->ok = $_SERVER['REQUEST_METHOD'] === 'POST'; - if (!$this->ok) { - $this->reason = 'Message should be an HTTP POST request'; - } -// Check all required launch parameters - if ($this->ok) { - $this->ok = isset($this->messageParameters['lti_message_type']) && array_key_exists($this->messageParameters['lti_message_type'], - self::$METHOD_NAMES); - if (!$this->ok) { - $this->reason = 'Invalid or missing lti_message_type parameter.'; - } - } - if ($this->ok) { - $this->ok = isset($this->messageParameters['lti_version']) && in_array($this->messageParameters['lti_version'], - self::$LTI_VERSIONS); - if (!$this->ok) { - $this->reason = 'Invalid or missing lti_version parameter.'; - } - } - if ($this->ok) { - if (($this->messageParameters['lti_message_type'] === 'basic-lti-launch-request') || ($this->messageParameters['lti_message_type'] === 'LtiResourceLinkRequest') || ($this->messageParameters['lti_message_type'] === 'DashboardRequest')) { - $this->ok = isset($this->messageParameters['resource_link_id']) && (strlen(trim($this->messageParameters['resource_link_id'])) > 0); - if (!$this->ok) { - $this->reason = 'Missing resource link ID.'; - } - } elseif (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') || ($this->messageParameters['lti_message_type'] === 'LtiDeepLinkingRequest')) { - if (isset($this->messageParameters['accept_media_types']) && (strlen(trim($this->messageParameters['accept_media_types'])) > 0)) { - $mediaTypes = array_filter(explode(',', str_replace(' ', '', $this->messageParameters['accept_media_types'])), - 'strlen'); - $mediaTypes = array_unique($mediaTypes); - $this->ok = count($mediaTypes) > 0; - if (!$this->ok) { - $this->reason = 'No accept_media_types found.'; - } else { - $this->mediaTypes = $mediaTypes; - } - } else { - $this->ok = false; - } - if ($this->ok && isset($this->messageParameters['accept_presentation_document_targets']) && (strlen(trim($this->messageParameters['accept_presentation_document_targets'])) > 0)) { - $documentTargets = array_filter(explode(',', - str_replace(' ', '', $this->messageParameters['accept_presentation_document_targets'])), 'strlen'); - $documentTargets = array_unique($documentTargets); - $this->ok = count($documentTargets) > 0; - if (!$this->ok) { - $this->reason = 'Missing or empty accept_presentation_document_targets parameter.'; - } else { - foreach ($documentTargets as $documentTarget) { - $this->ok = $this->checkValue($documentTarget, - array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none'), - 'Invalid value in accept_presentation_document_targets parameter: %s.'); - if (!$this->ok) { - break; - } - } - if ($this->ok) { - $this->documentTargets = $documentTargets; - } - } - } else { - $this->ok = false; - } - if ($this->ok) { - $this->ok = isset($this->messageParameters['content_item_return_url']) && (strlen(trim($this->messageParameters['content_item_return_url'])) > 0); - if (!$this->ok) { - $this->reason = 'Missing content_item_return_url parameter.'; - } - } - } elseif ($this->messageParameters['lti_message_type'] == 'ToolProxyRegistrationRequest') { - $this->ok = ((isset($this->messageParameters['reg_key']) && (strlen(trim($this->messageParameters['reg_key'])) > 0)) && - (isset($this->messageParameters['reg_password']) && (strlen(trim($this->messageParameters['reg_password'])) > 0)) && - (isset($this->messageParameters['tc_profile_url']) && (strlen(trim($this->messageParameters['tc_profile_url'])) > 0)) && - (isset($this->messageParameters['launch_presentation_return_url']) && (strlen(trim($this->messageParameters['launch_presentation_return_url'])) > 0))); - if ($this->debugMode && !$this->ok) { - $this->reason = 'Missing message parameters.'; - } - } - } - $now = time(); -// Check consumer key - if ($this->ok && ($this->messageParameters['lti_message_type'] != 'ToolProxyRegistrationRequest')) { - $this->ok = isset($this->messageParameters['oauth_consumer_key']); - if (!$this->ok) { - $this->reason = 'Missing consumer key.'; - } - if ($this->ok) { - $this->ok = !is_null($this->consumer->created); - if (!$this->ok) { - $this->reason = 'Invalid consumer key: ' . $this->messageParameters['oauth_consumer_key']; - } - } - if ($this->ok) { - $today = date('Y-m-d', $now); - if (is_null($this->consumer->lastAccess)) { - $doSaveConsumer = true; - } else { - $last = date('Y-m-d', $this->consumer->lastAccess); - $doSaveConsumer = $doSaveConsumer || ($last !== $today); - } - $this->consumer->lastAccess = $now; - $this->consumer->signatureMethod = isset($this->messageParameters['oauth_signature_method']) ? $this->messageParameters['oauth_signature_method'] : - $this->consumer->signatureMethod; - try { - $store = new OAuthDataStore($this); - $server = new OAuth\OAuthServer($store); - $method = new OAuth\OAuthSignatureMethod_HMAC_SHA224(); - $server->add_signature_method($method); - $method = new OAuth\OAuthSignatureMethod_HMAC_SHA256(); - $server->add_signature_method($method); - $method = new OAuth\OAuthSignatureMethod_HMAC_SHA384(); - $server->add_signature_method($method); - $method = new OAuth\OAuthSignatureMethod_HMAC_SHA512(); - $server->add_signature_method($method); - $method = new OAuth\OAuthSignatureMethod_HMAC_SHA1(); - $server->add_signature_method($method); - $request = OAuth\OAuthRequest::from_request(); - $res = $server->verify_request($request); - } catch (\Exception $e) { - $this->ok = false; - if (empty($this->reason)) { - $consumer = new OAuth\OAuthConsumer($this->consumer->getKey(), $this->consumer->secret); - $signature = $request->build_signature($method, $consumer, false); - if ($this->debugMode) { - $this->reason = $e->getMessage(); - } - if (empty($this->reason)) { - $this->reason = 'OAuth signature check failed - perhaps an incorrect secret or timestamp.'; - } - $this->details[] = 'Current timestamp: ' . time(); - $this->details[] = "Expected signature: {$signature}"; - $this->details[] = "Base string: {$request->base_string}"; - } - } - } - if ($this->ok) { - if ($this->consumer->protected) { - if (!is_null($this->consumer->consumerGuid)) { - $this->ok = empty($this->messageParameters['tool_consumer_instance_guid']) || - ($this->consumer->consumerGuid === $this->messageParameters['tool_consumer_instance_guid']); - if (!$this->ok) { - $this->reason = 'Request is from an invalid tool consumer.'; - } - } else { - $this->ok = isset($this->messageParameters['tool_consumer_instance_guid']); - if (!$this->ok) { - $this->reason = 'A tool consumer GUID must be included in the launch request.'; - } - } - } - if ($this->ok) { - $this->ok = $this->consumer->enabled; - if (!$this->ok) { - $this->reason = 'Tool consumer has not been enabled by the tool provider.'; - } - } - if ($this->ok) { - $this->ok = is_null($this->consumer->enableFrom) || ($this->consumer->enableFrom <= $now); - if ($this->ok) { - $this->ok = is_null($this->consumer->enableUntil) || ($this->consumer->enableUntil > $now); - if (!$this->ok) { - $this->reason = 'Tool consumer access has expired.'; - } - } else { - $this->reason = 'Tool consumer access is not yet available.'; - } - } - } - -// Validate other message parameter values - if ($this->ok) { - if (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') || ($this->messageParameters['lti_message_type'] === 'LtiDeepLinkingRequest')) { - if (isset($this->messageParameters['accept_unsigned'])) { - $this->ok = $this->checkValue($this->messageParameters['accept_unsigned'], array('true', 'false'), - 'Invalid value for accept_unsigned parameter: %s.'); - } - if ($this->ok && isset($this->messageParameters['accept_multiple'])) { - $this->ok = $this->checkValue($this->messageParameters['accept_multiple'], array('true', 'false'), - 'Invalid value for accept_multiple parameter: %s.'); - } - if ($this->ok && isset($this->messageParameters['accept_copy_advice'])) { - $this->ok = $this->checkValue($this->messageParameters['accept_copy_advice'], array('true', 'false'), - 'Invalid value for accept_copy_advice parameter: %s.'); - } - if ($this->ok && isset($this->messageParameters['auto_create'])) { - $this->ok = $this->checkValue($this->messageParameters['auto_create'], array('true', 'false'), - 'Invalid value for auto_create parameter: %s.'); - } - if ($this->ok && isset($this->messageParameters['can_confirm'])) { - $this->ok = $this->checkValue($this->messageParameters['can_confirm'], array('true', 'false'), - 'Invalid value for can_confirm parameter: %s.'); - } - } elseif (isset($this->messageParameters['launch_presentation_document_target'])) { - $this->ok = $this->checkValue($this->messageParameters['launch_presentation_document_target'], - array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay'), - 'Invalid value for launch_presentation_document_target parameter: %s.'); - } - } - } - - if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) { - $this->ok = $this->messageParameters['lti_version'] == self::LTI_VERSION2; - if (!$this->ok) { - $this->reason = 'Invalid lti_version parameter'; - } - if ($this->ok) { - $url = $this->messageParameters['tc_profile_url']; - if (strpos($url, '?') === false) { - $url .= '?'; - } else { - $url .= '&'; - } - $url .= 'lti_version=' . self::LTI_VERSION2; - $http = new HttpMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json'); - $this->ok = $http->send(); - if (!$this->ok) { - $this->reason = 'Tool consumer profile not accessible.'; - } else { - $tcProfile = json_decode($http->response); - $this->ok = !is_null($tcProfile); - if (!$this->ok) { - $this->reason = 'Invalid JSON in tool consumer profile.'; - } - } - } -// Check for required capabilities - if ($this->ok) { - $this->consumer = new ToolConsumer($this->messageParameters['reg_key'], $this->dataConnector); - $this->consumer->profile = $tcProfile; - $capabilities = $this->consumer->profile->capability_offered; - $missing = array(); - foreach ($this->resourceHandlers as $resourceHandler) { - foreach ($resourceHandler->requiredMessages as $message) { - if (!in_array($message->type, $capabilities)) { - $missing[$message->type] = true; - } - } - } - foreach ($this->constraints as $name => $constraint) { - if ($constraint['required']) { - if (empty(array_intersect($capabilities, - array_keys(array_intersect(self::$CUSTOM_SUBSTITUTION_VARIABLES, array($name)))))) { - $missing[$name] = true; - } - } - } - if (!empty($missing)) { - ksort($missing); - $this->reason = 'Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\''; - $this->ok = false; - } - } -// Check for required services - if ($this->ok) { - foreach ($this->requiredServices as $service) { - foreach ($service->formats as $format) { - if (!$this->findService($format, $service->actions)) { - if ($this->ok) { - $this->reason = 'Required service(s) not offered - '; - $this->ok = false; - } else { - $this->reason .= ', '; - } - $this->reason .= "'{$format}' [" . implode(', ', $service->actions) . ']'; - } - } - } - } - if ($this->ok) { - if ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest') { - $this->consumer->profile = $tcProfile; - $this->consumer->secret = $this->messageParameters['reg_password']; - $this->consumer->ltiVersion = $this->messageParameters['lti_version']; - $this->consumer->name = $tcProfile->product_instance->service_owner->service_owner_name->default_value; - $this->consumer->consumerName = $this->consumer->name; - $this->consumer->consumerVersion = "{$tcProfile->product_instance->product_info->product_family->code}-{$tcProfile->product_instance->product_info->product_version}"; - $this->consumer->consumerGuid = $tcProfile->product_instance->guid; - $this->consumer->enabled = true; - $this->consumer->protected = true; - $doSaveConsumer = true; - } - } - } elseif ($this->ok && !empty($this->messageParameters['custom_tc_profile_url']) && empty($this->consumer->profile)) { - $url = $this->messageParameters['custom_tc_profile_url']; - if (strpos($url, '?') === false) { - $url .= '?'; - } else { - $url .= '&'; - } - $url .= 'lti_version=' . $this->messageParameters['lti_version']; - $http = new HttpMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json'); - if ($http->send()) { - $tcProfile = json_decode($http->response); - if (!is_null($tcProfile)) { - $this->consumer->profile = $tcProfile; - $doSaveConsumer = true; - } - } - } - - if ($this->ok) { - -// Check if a relaunch is being requested - if (isset($this->messageParameters['relaunch_url'])) { - if (empty($this->messageParameters['platform_state'])) { - $this->ok = false; - $this->reason = 'Missing or empty platform_state parameter'; - } else { - $this->sendRelaunchRequest(); - } - } else { - -// Validate message parameter constraints - $invalidParameters = array(); - foreach ($this->constraints as $name => $constraint) { - if (empty($constraint['messages']) || in_array($this->messageParameters['lti_message_type'], $constraint['messages'])) { - $ok = true; - if ($constraint['required']) { - if (!isset($this->messageParameters[$name]) || (strlen(trim($this->messageParameters[$name])) <= 0)) { - $invalidParameters[] = "{$name} (missing)"; - $ok = false; - } - } - if ($ok && !is_null($constraint['max_length']) && isset($this->messageParameters[$name])) { - if (strlen(trim($this->messageParameters[$name])) > $constraint['max_length']) { - $invalidParameters[] = "{$name} (too long)"; - } - } - } - } - if (count($invalidParameters) > 0) { - $this->ok = false; - if (empty($this->reason)) { - $this->reason = 'Invalid parameter(s): ' . implode(', ', $invalidParameters) . '.'; - } - } - - if ($this->ok) { - -// Set the request context - $contextId = ''; - if ($this->hasConfiguredApiHook(self::$CONTEXT_ID_HOOK, $this->consumer->getFamilyCode(), $this)) { - $className = $this->getApiHook(self::$CONTEXT_ID_HOOK, $this->consumer->getFamilyCode()); - $tpHook = new $className($this); - $contextId = $tpHook->getContextId(); - } - if (empty($contextId) && isset($this->messageParameters['context_id'])) { - $contextId = trim($this->messageParameters['context_id']); - } - if (!empty($contextId)) { - $this->context = Context::fromConsumer($this->consumer, $contextId); - $title = ''; - if (isset($this->messageParameters['context_title'])) { - $title = trim($this->messageParameters['context_title']); - } - if (empty($title)) { - $title = "Course {$this->context->getId()}"; - } - $this->context->title = $title; - if (isset($this->messageParameters['context_type'])) { - $this->context->type = trim($this->messageParameters['context_type']); - } - } - -// Set the request resource link - if (isset($this->messageParameters['resource_link_id'])) { - $contentItemId = ''; - if (isset($this->messageParameters['custom_content_item_id'])) { - $contentItemId = $this->messageParameters['custom_content_item_id']; - } - if (empty($this->context)) { - $this->resourceLink = ResourceLink::fromConsumer($this->consumer, - trim($this->messageParameters['resource_link_id']), $contentItemId); - } else { - $this->resourceLink = ResourceLink::fromContext($this->context, - trim($this->messageParameters['resource_link_id']), $contentItemId); - } - $title = ''; - if (isset($this->messageParameters['resource_link_title'])) { - $title = trim($this->messageParameters['resource_link_title']); - } - if (empty($title)) { - $title = "Resource {$this->resourceLink->getId()}"; - } - $this->resourceLink->title = $title; -// Delete any existing custom parameters - foreach ($this->consumer->getSettings() as $name => $value) { - if ((strpos($name, 'custom_') === 0) && (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES))) { - $this->consumer->setSetting($name); - $doSaveConsumer = true; - } - } - if (!empty($this->context)) { - foreach ($this->context->getSettings() as $name => $value) { - if ((strpos($name, 'custom_') === 0) && (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES))) { - $this->context->setSetting($name); - } - } - } - foreach ($this->resourceLink->getSettings() as $name => $value) { - if ((strpos($name, 'custom_') === 0) && (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES))) { - $this->resourceLink->setSetting($name); - } - } -// Save LTI parameters - foreach (self::$LTI_CONSUMER_SETTING_NAMES as $name) { - if (isset($this->messageParameters[$name])) { - $this->consumer->setSetting($name, $this->messageParameters[$name]); - } else if (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES)) { - $this->consumer->setSetting($name); - } - } - if (!empty($this->context)) { - foreach (self::$LTI_CONTEXT_SETTING_NAMES as $name) { - if (isset($this->messageParameters[$name])) { - $this->context->setSetting($name, $this->messageParameters[$name]); - } else if (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES)) { - $this->context->setSetting($name); - } - } - } - foreach (self::$LTI_RESOURCE_LINK_SETTING_NAMES as $name) { - if (isset($this->messageParameters[$name])) { - $this->resourceLink->setSetting($name, $this->messageParameters[$name]); - } else if (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES)) { - $this->resourceLink->setSetting($name); - } - } -// Save other custom parameters at all levels - foreach ($this->messageParameters as $name => $value) { - if ((strpos($name, 'custom_') === 0) && - !in_array($name, - array_merge(self::$LTI_CONSUMER_SETTING_NAMES, self::$LTI_CONTEXT_SETTING_NAMES, - self::$LTI_RESOURCE_LINK_SETTING_NAMES))) { - $this->consumer->setSetting($name, $value); - if (!empty($this->context)) { - $this->context->setSetting($name, $value); - } - $this->resourceLink->setSetting($name, $value); - } - } - } - -// Set the user instance - $userId = ''; - if ($this->hasConfiguredApiHook(self::$USER_ID_HOOK, $this->consumer->getFamilyCode(), $this)) { - $className = $this->getApiHook(self::$USER_ID_HOOK, $this->consumer->getFamilyCode()); - $tpHook = new $className($this); - $userId = $tpHook->getUserId(); - } - if (empty($userId) && isset($this->messageParameters['user_id'])) { - $userId = trim($this->messageParameters['user_id']); - } - - $this->userResult = UserResult::fromResourceLink($this->resourceLink, $userId); - -// Set the user name - $firstname = (isset($this->messageParameters['lis_person_name_given'])) ? $this->messageParameters['lis_person_name_given'] : ''; - $lastname = (isset($this->messageParameters['lis_person_name_family'])) ? $this->messageParameters['lis_person_name_family'] : ''; - $fullname = (isset($this->messageParameters['lis_person_name_full'])) ? $this->messageParameters['lis_person_name_full'] : ''; - $this->userResult->setNames($firstname, $lastname, $fullname); - -// Set the sourcedId - if (isset($this->messageParameters['lis_person_sourcedid'])) { - $this->userResult->sourcedId = $this->messageParameters['lis_person_sourcedid']; - } - -// Set the username - if (isset($this->messageParameters['ext_username'])) { - $this->userResult->username = $this->messageParameters['ext_username']; - } elseif (isset($this->messageParameters['ext_user_username'])) { - $this->userResult->username = $this->messageParameters['ext_user_username']; - } elseif (isset($this->messageParameters['custom_username'])) { - $this->userResult->username = $this->messageParameters['custom_username']; - } elseif (isset($this->messageParameters['custom_user_username'])) { - $this->userResult->username = $this->messageParameters['custom_user_username']; - } - -// Set the user email - $email = (isset($this->messageParameters['lis_person_contact_email_primary'])) ? $this->messageParameters['lis_person_contact_email_primary'] : ''; - $this->userResult->setEmail($email, $this->defaultEmail); - -// Set the user image URI - if (isset($this->messageParameters['user_image'])) { - $this->userResult->image = $this->messageParameters['user_image']; - } - -// Set the user roles - if (isset($this->messageParameters['roles'])) { - $this->userResult->roles = self::parseRoles($this->messageParameters['roles'], $this->consumer->ltiVersion); - } - -// Initialise the consumer and check for changes - $this->consumer->defaultEmail = $this->defaultEmail; - if ($this->consumer->ltiVersion !== $this->messageParameters['lti_version']) { - $this->consumer->ltiVersion = $this->messageParameters['lti_version']; - $doSaveConsumer = true; - } - if (isset($this->messageParameters['tool_consumer_instance_name'])) { - if ($this->consumer->consumerName !== $this->messageParameters['tool_consumer_instance_name']) { - $this->consumer->consumerName = $this->messageParameters['tool_consumer_instance_name']; - $doSaveConsumer = true; - } - } - if (isset($this->messageParameters['tool_consumer_info_product_family_code'])) { - $version = $this->messageParameters['tool_consumer_info_product_family_code']; - if (isset($this->messageParameters['tool_consumer_info_version'])) { - $version .= "-{$this->messageParameters['tool_consumer_info_version'] - }"; - } -// do not delete any existing consumer version if none is passed - if ($this->consumer->consumerVersion !== $version) { - $this->consumer->consumerVersion = $version; - $doSaveConsumer = true; - } - } elseif (isset($this->messageParameters['ext_lms']) && ($this->consumer->consumerName !== $this->messageParameters['ext_lms'])) { - $this->consumer->consumerVersion = $this->messageParameters['ext_lms']; - $doSaveConsumer = true; - } - if (isset($this->messageParameters['tool_consumer_instance_guid'])) { - if (is_null($this->consumer->consumerGuid)) { - $this->consumer->consumerGuid = $this->messageParameters['tool_consumer_instance_guid']; - $doSaveConsumer = true; - } elseif (!$this->consumer->protected) { - $doSaveConsumer = ($this->consumer->consumerGuid !== $this->messageParameters['tool_consumer_instance_guid']); - if ($doSaveConsumer) { - $this->consumer->consumerGuid = $this->messageParameters['tool_consumer_instance_guid']; - } - } - } - if (isset($this->messageParameters['launch_presentation_css_url'])) { - if ($this->consumer->cssPath !== $this->messageParameters['launch_presentation_css_url']) { - $this->consumer->cssPath = $this->messageParameters['launch_presentation_css_url']; - $doSaveConsumer = true; - } - } elseif (isset($this->messageParameters['ext_launch_presentation_css_url']) && - ($this->consumer->cssPath !== $this->messageParameters['ext_launch_presentation_css_url'])) { - $this->consumer->cssPath = $this->messageParameters['ext_launch_presentation_css_url']; - $doSaveConsumer = true; - } elseif (!empty($this->consumer->cssPath)) { - $this->consumer->cssPath = null; - $doSaveConsumer = true; - } - } - -// Persist changes to consumer - if ($doSaveConsumer) { - $this->consumer->save(); - } - if ($this->ok) { - - if (isset($this->context)) { - $this->context->save(); - } - - if (isset($this->resourceLink)) { -// Persist changes to resource link - $this->resourceLink->save(); - -// Save the user instance - $this->userResult->setResourceLinkId($this->resourceLink->getRecordId()); - if (isset($this->messageParameters['lis_result_sourcedid'])) { - if ($this->userResult->ltiResultSourcedId !== $this->messageParameters['lis_result_sourcedid']) { - $this->userResult->ltiResultSourcedId = $this->messageParameters['lis_result_sourcedid']; - $this->userResult->save(); - } - } elseif (!empty($this->userResult->ltiResultSourcedId)) { - $this->userResult->ltiResultSourcedId = ''; - $this->userResult->save(); - } - -// Check if a share arrangement is in place for this resource link - $this->ok = $this->checkForShare(); - } - } - } - } - - return $this->ok; - } - - /** - * Check if a share arrangement is in place. - * - * @return bool True if no error is reported - */ - private function checkForShare() - { - $ok = true; - $doSaveResourceLink = true; - - $id = $this->resourceLink->primaryResourceLinkId; - - $shareRequest = isset($this->messageParameters['custom_share_key']) && !empty($this->messageParameters['custom_share_key']); - if ($shareRequest) { - if (!$this->allowSharing) { - $ok = false; - $this->reason = 'Your sharing request has been refused because sharing is not being permitted.'; - } else { -// Check if this is a new share key - $shareKey = new ResourceLinkShareKey($this->resourceLink, $this->messageParameters['custom_share_key']); - if (!is_null($shareKey->resourceLinkId)) { -// Update resource link with sharing primary resource link details - $id = $shareKey->resourceLinkId; - $ok = ($id != $this->resourceLink->getRecordId()); - if ($ok) { - $this->resourceLink->primaryResourceLinkId = $id; - $this->resourceLink->shareApproved = $shareKey->autoApprove; - $ok = $this->resourceLink->save(); - if ($ok) { - $doSaveResourceLink = false; - $this->userResult->getResourceLink()->primaryResourceLinkId = $id; - $this->userResult->getResourceLink()->shareApproved = $shareKey->autoApprove; - $this->userResult->getResourceLink()->updated = time(); -// Remove share key - $shareKey->delete(); - } else { - $this->reason = 'An error occurred initialising your share arrangement.'; - } - } else { - $this->reason = 'It is not possible to share your resource link with yourself.'; - } - } - if ($ok) { - $ok = !is_null($id); - if (!$ok) { - $this->reason = 'You have requested to share a resource link but none is available.'; - } else { - $ok = (!is_null($this->userResult->getResourceLink()->shareApproved) && $this->userResult->getResourceLink()->shareApproved); - if (!$ok) { - $this->reason = 'Your share request is waiting to be approved.'; - } - } - } - } - } else { -// Check no share is in place - $ok = is_null($id); - if (!$ok) { - $this->reason = 'You have not requested to share a resource link but an arrangement is currently in place.'; - } - } - -// Look up primary resource link - if ($ok && !is_null($id)) { - $resourceLink = ResourceLink::fromRecordId($id, $this->dataConnector); - $ok = !is_null($resourceLink->created); - if ($ok) { - if ($doSaveResourceLink) { - $this->resourceLink->save(); - } - $this->resourceLink = $resourceLink; - } else { - $this->reason = 'Unable to load resource link being shared.'; - } - } - - return $ok; - } - - /** - * Generate a form to perform a relaunch request. - */ - private function sendRelaunchRequest() - { - do { - $nonce = new ConsumerNonce($this->consumer, DataConnector\DataConnector::getRandomString()); - $ok = !$nonce->load(); - } while (!$ok); - $ok = $nonce->save(); - if ($ok) { - $params = array( - 'tool_state' => $nonce->getValue(), - 'platform_state' => $this->messageParameters['platform_state'] - ); - $params = $this->consumer->addSignature($this->messageParameters['relaunch_url'], $params); - $this->output = static::sendForm($this->messageParameters['relaunch_url'], $params); - } else { - $this->reason = 'Unable to generate a state value'; - } - } - - /** - * Validate a parameter value from an array of permitted values. - * - * @param mixed $value Value to be checked - * @param array $values Array of permitted values - * @param string $reason Reason to generate when the value is not permitted - * - * @return bool True if value is valid - */ - private function checkValue($value, $values, $reason) - { - $ok = in_array($value, $values); - if (!$ok && !empty($reason)) { - $this->reason = sprintf($reason, $value); - } - - return $ok; + Util::logDebug('Class ceLTIc\LTI\ToolProvider has been deprecated; please use ceLTIc\LTI\Tool instead.', true); + parent::__construct($dataConnector); } } diff --git a/src/User.php b/src/User.php index f5e3b7c..7bbf900 100644 --- a/src/User.php +++ b/src/User.php @@ -3,7 +3,7 @@ namespace ceLTIc\LTI; /** - * Class to represent a tool consumer user + * Class to represent a platform user * * @author Stephen P Vickers * @copyright SPV Software Products diff --git a/src/UserResult.php b/src/UserResult.php index 7ef6537..4c7b737 100644 --- a/src/UserResult.php +++ b/src/UserResult.php @@ -5,7 +5,7 @@ use ceLTIc\LTI\DataConnector; /** - * Class to represent a tool consumer user + * Class to represent a platform user * * @author Stephen P Vickers * @copyright SPV Software Products @@ -186,7 +186,7 @@ public function setDataConnector($dataConnector) } /** - * Get the user ID (which may be a compound of the tool consumer and resource link IDs). + * Get the user ID (which may be a compound of the platform and resource link IDs). * * @param int $idScope Scope to use for user ID (optional, default is null for consumer default setting) * @@ -196,28 +196,28 @@ public function getId($idScope = null) { if (empty($idScope)) { if (!is_null($this->resourceLink)) { - $idScope = $this->resourceLink->getConsumer()->idScope; + $idScope = $this->resourceLink->getPlatform()->idScope; } else { - $idScope = ToolProvider::ID_SCOPE_ID_ONLY; + $idScope = Tool::ID_SCOPE_ID_ONLY; } } switch ($idScope) { - case ToolProvider::ID_SCOPE_GLOBAL: - $id = $this->getResourceLink()->getKey() . ToolProvider::ID_SCOPE_SEPARATOR . $this->ltiUserId; + case Tool::ID_SCOPE_GLOBAL: + $id = $this->getResourceLink()->getConsumer()->getId() . Tool::ID_SCOPE_SEPARATOR . $this->ltiUserId; break; - case ToolProvider::ID_SCOPE_CONTEXT: - $id = $this->getResourceLink()->getKey(); + case Tool::ID_SCOPE_CONTEXT: + $id = $this->getResourceLink()->getConsumer()->getId(); if ($this->resourceLink->ltiContextId) { - $id .= ToolProvider::ID_SCOPE_SEPARATOR . $this->resourceLink->ltiContextId; + $id .= Tool::ID_SCOPE_SEPARATOR . $this->resourceLink->ltiContextId; } - $id .= ToolProvider::ID_SCOPE_SEPARATOR . $this->ltiUserId; + $id .= Tool::ID_SCOPE_SEPARATOR . $this->ltiUserId; break; - case ToolProvider::ID_SCOPE_RESOURCE: - $id = $this->getResourceLink()->getKey(); + case Tool::ID_SCOPE_RESOURCE: + $id = $this->getResourceLink()->getConsumer()->getId(); if ($this->resourceLink->ltiResourceLinkId) { - $id .= ToolProvider::ID_SCOPE_SEPARATOR . $this->resourceLink->ltiResourceLinkId; + $id .= Tool::ID_SCOPE_SEPARATOR . $this->resourceLink->ltiResourceLinkId; } - $id .= ToolProvider::ID_SCOPE_SEPARATOR . $this->ltiUserId; + $id .= Tool::ID_SCOPE_SEPARATOR . $this->ltiUserId; break; default: $id = $this->ltiUserId; diff --git a/src/Util.php b/src/Util.php index 4e8b3ab..0c74932 100644 --- a/src/Util.php +++ b/src/Util.php @@ -14,6 +14,98 @@ final class Util { + /** + * LTI version 1 for messages. + */ + const LTI_VERSION1 = 'LTI-1p0'; + + /** + * LTI version 1.3 for messages. + */ + const LTI_VERSION1P3 = '1.3.0'; + + /** + * LTI version 2 for messages. + */ + const LTI_VERSION2 = 'LTI-2p0'; + + /** + * Prefix for standard JWT message claims. + */ + const JWT_CLAIM_PREFIX = 'https://purl.imsglobal.org/spec/lti'; + + /** + * Mapping for standard message types. + */ + const MESSAGE_TYPE_MAPPING = array( + 'basic-lti-launch-request' => 'LtiResourceLinkRequest', + 'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest', + 'ContentItemSelection' => 'LtiDeepLinkingResponse' + ); + + /** + * Mapping for standard message parameters to JWT claim. + */ + const JWT_CLAIM_MAPPING = array( + 'accept_types' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_types', 'isArray' => true), + 'accept_copy_advice' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'copyAdvice', 'isBoolean' => true), + 'accept_media_types' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_media_types'), + 'accept_multiple' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_multiple', 'isBoolean' => true), + 'accept_presentation_document_targets' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_presentation_document_targets', 'isArray' => true), + 'accept_unsigned' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_unsigned', 'isBoolean' => true), + 'auto_create' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'auto_create', 'isBoolean' => true), + 'can_confirm' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'can_confirm'), // Not included in Deep Linking v2 spec + 'content_item_return_url' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'deep_link_return_url'), + 'content_items' => array('suffix' => 'dl', 'group' => '', 'claim' => 'content_items', 'isObject' => true), + 'data' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'data'), + 'text' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'text'), + 'title' => array('suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'title'), + 'lti_msg' => array('suffix' => 'dl', 'group' => '', 'claim' => 'msg'), + 'lti_errormsg' => array('suffix' => 'dl', 'group' => '', 'claim' => 'errormsg'), + 'lti_log' => array('suffix' => 'dl', 'group' => '', 'claim' => 'log'), + 'lti_errorlog' => array('suffix' => 'dl', 'group' => '', 'claim' => 'errorlog'), + 'context_id' => array('suffix' => '', 'group' => 'context', 'claim' => 'id'), + 'context_label' => array('suffix' => '', 'group' => 'context', 'claim' => 'label'), + 'context_title' => array('suffix' => '', 'group' => 'context', 'claim' => 'title'), + 'context_type' => array('suffix' => '', 'group' => 'context', 'claim' => 'type', 'isArray' => true), + 'lis_course_offering_sourcedid' => array('suffix' => '', 'group' => 'context', 'claim' => 'course_offering_sourcedid'), + 'lis_course_section_sourcedid' => array('suffix' => '', 'group' => 'context', 'claim' => 'course_section_sourcedid'), + 'launch_presentation_css_url' => array('suffix' => '', 'group' => 'launch_presentation', 'claim' => 'css_url'), + 'launch_presentation_document_target' => array('suffix' => '', 'group' => 'launch_presentation', 'claim' => 'document_target'), + 'launch_presentation_height' => array('suffix' => '', 'group' => 'launch_presentation', 'claim' => 'height'), + 'launch_presentation_locale' => array('suffix' => '', 'group' => 'launch_presentation', 'claim' => 'locale'), + 'launch_presentation_return_url' => array('suffix' => '', 'group' => 'launch_presentation', 'claim' => 'return_url'), + 'launch_presentation_width' => array('suffix' => '', 'group' => 'launch_presentation', 'claim' => 'width'), + 'lis_person_contact_email_primary' => array('suffix' => '', 'group' => null, 'claim' => 'email'), + 'lis_person_name_family' => array('suffix' => '', 'group' => null, 'claim' => 'family_name'), + 'lis_person_name_full' => array('suffix' => '', 'group' => null, 'claim' => 'name'), + 'lis_person_name_given' => array('suffix' => '', 'group' => null, 'claim' => 'given_name'), + 'lis_person_sourcedid' => array('suffix' => '', 'group' => null, 'claim' => 'sourced_id'), + 'user_id' => array('suffix' => '', 'group' => null, 'claim' => 'sub'), + 'user_image' => array('suffix' => '', 'group' => null, 'claim' => 'picture'), + 'roles' => array('suffix' => '', 'group' => '', 'claim' => 'roles', 'isArray' => true), + 'deployment_id' => array('suffix' => '', 'group' => '', 'claim' => 'deployment_id'), + 'lti_message_type' => array('suffix' => '', 'group' => '', 'claim' => 'message_type'), + 'lti_version' => array('suffix' => '', 'group' => '', 'claim' => 'version'), + 'resource_link_description' => array('suffix' => '', 'group' => 'resource_link', 'claim' => 'description'), + 'resource_link_id' => array('suffix' => '', 'group' => 'resource_link', 'claim' => 'id'), + 'resource_link_title' => array('suffix' => '', 'group' => 'resource_link', 'claim' => 'title'), + 'tool_consumer_info_product_family_code' => array('suffix' => '', 'group' => 'tool_platform', 'claim' => 'family_code'), + 'tool_consumer_info_version' => array('suffix' => '', 'group' => 'tool_platform', 'claim' => 'version'), + 'tool_consumer_instance_contact_email' => array('suffix' => '', 'group' => 'tool_platform', 'claim' => 'contact_email'), + 'tool_consumer_instance_description' => array('suffix' => '', 'group' => 'tool_platform', 'claim' => 'description'), + 'tool_consumer_instance_guid' => array('suffix' => '', 'group' => 'tool_platform', 'claim' => 'guid'), + 'tool_consumer_instance_name' => array('suffix' => '', 'group' => 'tool_platform', 'claim' => 'name'), + 'tool_consumer_instance_url' => array('suffix' => '', 'group' => 'tool_platform', 'claim' => 'url'), + 'tool_state' => array('suffix' => '', 'group' => 'tool', 'claim' => 'state'), + 'custom_context_memberships_v2_url' => array('suffix' => 'nrps', 'group' => 'namesroleservice', 'claim' => 'context_memberships_url'), + 'custom_lineitems_url' => array('suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'lineitems'), + 'custom_lineitem_url' => array('suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'lineitem'), + 'custom_ags_scopes' => array('suffix' => 'ags', 'group' => 'endpoint', 'claim' => 'scope', 'isArray' => true), + 'lis_outcome_service_url' => array('suffix' => 'bos', 'group' => 'basicoutcomesservice', 'claim' => 'lis_outcome_service_url'), + 'lis_result_sourcedid' => array('suffix' => 'bos', 'group' => 'basicoutcomesservice', 'claim' => 'lis_result_sourcedid') + ); + /** * No logging. */ @@ -34,6 +126,21 @@ final class Util */ const LOGLEVEL_DEBUG = 3; + /** + * Permitted LTI versions for messages. + */ + public static $LTI_VERSIONS = array(self::LTI_VERSION1, self::LTI_VERSION1P3, self::LTI_VERSION2); + + /** + * List of supported message types and associated class methods. + */ + public static $METHOD_NAMES = array('basic-lti-launch-request' => 'onLaunch', + 'ConfigureLaunchRequest' => 'onConfigure', + 'DashboardRequest' => 'onDashboard', + 'ContentItemSelectionRequest' => 'onContentItem', + 'ToolProxyRegistrationRequest' => 'onRegister' + ); + /** * Current logging level. * @@ -96,7 +203,7 @@ public static function logRequest() $message .= " with a body of:\n" . var_export($body, true); } } - self::log($message); + self::logInfo($message); } } @@ -126,4 +233,121 @@ public static function log($message, $showSource = false) error_log($message . $source); } + /** + * Generate a web page containing an auto-submitted form of parameters. + * + * @param string $url URL to which the form should be submitted + * @param array $params Array of form parameters + * @param string $target Name of target (optional) + * + * @return string + */ + public static function sendForm($url, $params, $target = '') + { + $page = <<< EOD + + +IMS LTI message + + + +
+ +EOD; + foreach ($params as $key => $value) { + $key = htmlentities($key, ENT_COMPAT | ENT_HTML401, 'UTF-8'); + if (!is_array($value)) { + $value = htmlentities($value, ENT_COMPAT | ENT_HTML401, 'UTF-8'); + $page .= <<< EOD + + +EOD; + } else { + foreach ($value as $element) { + $element = htmlentities($element, ENT_COMPAT | ENT_HTML401, 'UTF-8'); + $page .= <<< EOD + + +EOD; + } + } + } + + $page .= <<< EOD +
+ + +EOD; + + return $page; + } + + /** + * Redirect to a URL with query parameters. + * + * @param string $url URL to which the form should be submitted + * @param array $params Array of form parameters + * + * @return string + */ + public static function redirect($url, $params) + { + if (!empty($params)) { + if (strpos($url, '?') === false) { + $url .= '?'; + $sep = ''; + } else { + $sep = '&'; + } + foreach ($params as $key => $value) { + $key = urlencode($key); + if (!is_array($value)) { + $value = urlencode($value); + $url .= "{$sep}{$key}={$value}"; + $sep = '&'; + } else { + foreach ($value as $element) { + $element = urlencode($element); + $url .= "{$sep}{$key}={$element}"; + $sep = '&'; + } + } + } + } + + header("Location: {$url}"); + exit; + } + + /** + * Generate a random string. + * + * The generated string will only comprise letters (upper- and lower-case) and digits. + * + * @param int $length Length of string to be generated (optional, default is 8 characters) + * + * @return string Random string + */ + public static function getRandomString($length = 8) + { + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $value = ''; + $charsLength = strlen($chars) - 1; + + for ($i = 1; $i <= $length; $i++) { + $value .= $chars[rand(0, $charsLength)]; + } + + return $value; + } + }