--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Manages the creation and usage of access controlled links.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Nina Herrmann (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace repository_nextcloud;
+
+use context;
+use \core\oauth2\api;
+use \core\notification;
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/webdavlib.php');
+
+/**
+ * Manages the creation and usage of access controlled links.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Nina Herrmann (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class access_controlled_link_manager{
+ /**
+ * OCS client that uses the Open Collaboration Services REST API.
+ * @var ocs_client
+ */
+ protected $ocsclient;
+ /**
+ * ocsclient of the systemaccount.
+ * @var ocs_client
+ */
+ protected $systemocsclient;
+ /**
+ * Client to manage oauth2 features from the systemaccount.
+ * @var \core\oauth2\client
+ */
+ protected $systemoauthclient;
+ /**
+ * Client to manage webdav request from the systemaccount..
+ * @var \webdav_client
+ */
+ protected $systemwebdavclient;
+ /**
+ * Issuer from the oauthclient.
+ * @var \core\oauth2\issuer
+ */
+ protected $issuer;
+ /**
+ * Name of the related repository.
+ * @var string
+ */
+ protected $repositoryname;
+
+ /**
+ * Access_controlled_link_manager constructor.
+ * @param ocs_client $ocsclient
+ * @param \core\oauth2\client $systemoauthclient
+ * @param ocs_client $systemocsclient
+ * @param \core\oauth2\issuer $issuer
+ * @param string $repositoryname
+ * @throws configuration_exception
+ */
+ public function __construct($ocsclient, $systemoauthclient, $systemocsclient, $issuer, $repositoryname) {
+ $this->ocsclient = $ocsclient;
+ $this->systemoauthclient = $systemoauthclient;
+ $this->systemocsclient = $systemocsclient;
+
+ $this->repositoryname = $repositoryname;
+ $this->issuer = $issuer;
+ $this->systemwebdavclient = $this->create_system_dav();
+ }
+
+ /**
+ * Deletes the share of the systemaccount and a user. In case the share could not be deleted a notification is
+ * displayed.
+ * @param int $shareid Remote ID of the share to be deleted.
+ */
+ public function delete_share_dataowner_sysaccount($shareid) {
+ $shareid = (int) $shareid;
+ $deleteshareparams = [
+ 'share_id' => $shareid
+ ];
+ $deleteshareresponse = $this->ocsclient->call('delete_share', $deleteshareparams);
+ $xml = simplexml_load_string($deleteshareresponse);
+
+ if (empty($xml->meta->statuscode) || $xml->meta->statuscode != 100 ) {
+ notification::warning('You just shared a file with a access controlled link.
+ However, the share between you and the systemaccount could not be deleted and is still present in your instance.');
+ }
+ }
+
+ /**
+ * Creates a share between a user and the system account. If $username is set the sharing direction is system account -> user,
+ * otherwise user -> system account.
+ * @param string $path Remote path of the file that will be shared
+ * @param string $username optional when set the file is shared with the corresponding user otherwise with
+ * the systemaccount.
+ * @param bool $maywrite if false, only(!) read access is granted.
+ * @return array statuscode, shareid, and filetarget
+ * @throws request_exception
+ */
+ public function create_share_user_sysaccount($path, $username = null, $maywrite = false) {
+ $result = array();
+
+ if ($username != null) {
+ $shareusername = $username;
+ } else {
+ $systemaccount = \core\oauth2\api::get_system_account($this->issuer);
+ $shareusername = $systemaccount->get('username');
+ }
+ $permissions = ocs_client::SHARE_PERMISSION_READ;
+ if ($maywrite) {
+ // Add more privileges (write, reshare) if allowed for the given user.
+ $permissions |= ocs_client::SHARE_PERMISSION_ALL;
+ }
+ $createshareparams = [
+ 'path' => $path,
+ 'shareType' => ocs_client::SHARE_TYPE_USER,
+ 'publicUpload' => false,
+ 'shareWith' => $shareusername,
+ 'permissions' => $permissions,
+ ];
+
+ // File is now shared with the system account.
+ if ($username === null) {
+ $createshareresponse = $this->ocsclient->call('create_share', $createshareparams);
+ } else {
+ $createshareresponse = $this->systemocsclient->call('create_share', $createshareparams);
+ }
+ $xml = simplexml_load_string($createshareresponse);
+
+ $statuscode = (int)$xml->meta->statuscode;
+ if ($statuscode != 100 && $statuscode != 403) {
+ $details = get_string('filenotaccessed', 'repository_nextcloud');
+ throw new request_exception(get_string('request_exception',
+ 'repository_nextcloud', array('instance' => $this->repositoryname, 'errormessage' => $details)));
+ }
+ $result['shareid'] = (int)$xml->data->id;
+ $result['statuscode'] = $statuscode;
+ $result['filetarget'] = (string)$xml->data[0]->file_target;
+
+ return $result;
+ }
+
+ /** Copy or moves a file to a new path.
+ * @param string $srcpath source path
+ * @param string $dstpath
+ * @param string $operation move or copy
+ * @param \webdav_client $webdavclient needed when moving files.
+ * @return String Http-status of the request
+ * @throws configuration_exception
+ * @throws \coding_exception
+ * @throws \moodle_exception
+ * @throws \repository_nextcloud\request_exception
+ */
+ public function transfer_file_to_path($srcpath, $dstpath, $operation, $webdavclient = null) {
+ $this->systemwebdavclient->open();
+ $webdavendpoint = issuer_management::parse_endpoint_url('webdav', $this->issuer);
+
+ $srcpath = ltrim($srcpath, '/');
+ $sourcepath = $webdavendpoint['path'] . $srcpath;
+ $dstpath = ltrim($dstpath, '/');
+ $destinationpath = $webdavendpoint['path'] . $dstpath . '/' . $srcpath;
+
+ if ($operation === 'copy') {
+ $result = $this->systemwebdavclient->copy_file($sourcepath, $destinationpath, true);
+ } else if ($operation === 'move') {
+ $result = $webdavclient->move($sourcepath, $destinationpath, false);
+ if ($result == 412) {
+ // A file with that name already exists at that target. Find a unique location!
+ $increment = 0; // Will be appended to/inserted into the filename.
+ // Define the pattern that is used to insert the increment to the filename.
+ if (substr_count($srcpath, '.') === 0) {
+ // No file extension; append increment to the (sprintf-escaped) name.
+ $namepattern = str_replace('%', '%%', $destinationpath) . ' (%s)';
+ } else {
+ // Append the increment to the second-to-last component, which is presumably the one before the extension.
+ // Again, the original path is sprintf-escaped.
+ $components = explode('.', str_replace('%', '%%', $destinationpath));
+ $components[count($components) - 2] .= ' (%s)';
+ $namepattern = implode('.', $components);
+ }
+ }
+ while ($result == 412) {
+ $increment++;
+ $destinationpath = sprintf($namepattern, $increment);
+ $result = $webdavclient->move($sourcepath, $destinationpath, false);
+ }
+ }
+ $this->systemwebdavclient->close();
+ if (!($result == 201 || $result == 412)) {
+ $details = get_string('contactadminwith', 'repository_nextcloud',
+ 'A webdav request to ' . $operation . ' a file failed.');
+ throw new request_exception(array('instance' => $this->repositoryname, 'errormessage' => $details));
+ }
+ return $result;
+ }
+
+ /**
+ * Creates a unique folder path for the access controlled link.
+ * @param context $context
+ * @param string $component
+ * @param string $filearea
+ * @param string $itemid
+ * @return string $result full generated path.
+ * @throws request_exception If the folder path cannot be created.
+ */
+ public function create_folder_path_access_controlled_links($context, $component, $filearea, $itemid) {
+ global $CFG, $SITE;
+ // The fullpath to store the file is generated from the context.
+ $contextlist = array_reverse($context->get_parent_contexts(true));
+ $fullpath = '';
+ $allfolders = [];
+ foreach ($contextlist as $ctx) {
+ // Prepare human readable context folders names, making sure they are still unique within the site.
+ $prevlang = force_current_language($CFG->lang);
+ $foldername = $ctx->get_context_name();
+ force_current_language($prevlang);
+
+ if ($ctx->contextlevel === CONTEXT_SYSTEM) {
+ // Append the site short name to the root folder.
+ $foldername .= ' ('.$SITE->shortname.')';
+ // Append the relevant object id.
+ } else if ($ctx->instanceid) {
+ $foldername .= ' (id '.$ctx->instanceid.')';
+ } else {
+ // This does not really happen but just in case.
+ $foldername .= ' (ctx '.$ctx->id.')';
+ }
+
+ $foldername = clean_param($foldername, PARAM_FILE);
+ $allfolders[] = $foldername;
+ }
+
+ $allfolders[] = clean_param($component, PARAM_FILE);
+ $allfolders[] = clean_param($filearea, PARAM_FILE);
+ $allfolders[] = clean_param($itemid, PARAM_FILE);
+
+ // Extracts the end of the webdavendpoint.
+ $parsedwebdavurl = issuer_management::parse_endpoint_url('webdav', $this->issuer);
+ $webdavprefix = $parsedwebdavurl['path'];
+ $this->systemwebdavclient->open();
+ // Checks whether folder exist and creates non-existent folders.
+ foreach ($allfolders as $foldername) {
+ $fullpath .= '/' . $foldername;
+ $isdir = $this->systemwebdavclient->is_dir($webdavprefix . $fullpath);
+ // Folder already exist, continue.
+ if ($isdir === true) {
+ continue;
+ }
+ $response = $this->systemwebdavclient->mkcol($webdavprefix . $fullpath);
+
+ if ($response != 201) {
+ $this->systemwebdavclient->close();
+ $details = get_string('contactadminwith', 'repository_nextcloud',
+ "Folder path $fullpath could not be created in the system account.");
+ throw new request_exception(array('instance' => $this->repositoryname,
+ 'errormessage' => $details));
+ }
+ }
+ $this->systemwebdavclient->close();
+ return $fullpath;
+ }
+
+ /** Creates a new webdav_client for the system account.
+ * @return \webdav_client
+ * @throws configuration_exception
+ */
+ public function create_system_dav() {
+ $webdavendpoint = issuer_management::parse_endpoint_url('webdav', $this->issuer);
+
+ // Selects the necessary information (port, type, server) from the path to build the webdavclient.
+ $server = $webdavendpoint['host'];
+ if ($webdavendpoint['scheme'] === 'https') {
+ $webdavtype = 'ssl://';
+ $webdavport = 443;
+ } else if ($webdavendpoint['scheme'] === 'http') {
+ $webdavtype = '';
+ $webdavport = 80;
+ }
+
+ // Override default port, if a specific one is set.
+ if (isset($webdavendpoint['port'])) {
+ $webdavport = $webdavendpoint['port'];
+ }
+
+ // Authentication method is `bearer` for OAuth 2. Pass oauth client from which WebDAV obtains the token when needed.
+ $dav = new \webdav_client($server, '', '', 'bearer', $webdavtype,
+ $this->systemoauthclient->get_accesstoken()->token, $webdavendpoint['path']);
+
+ $dav->port = $webdavport;
+ $dav->debug = false;
+ return $dav;
+ }
+
+ /** Creates a folder to store access controlled links.
+ * @param string $controlledlinkfoldername
+ * @param \webdav_client $webdavclient
+ * @throws \coding_exception
+ * @throws configuration_exception
+ * @throws request_exception
+ */
+ public function create_storage_folder($controlledlinkfoldername, $webdavclient) {
+ $parsedwebdavurl = issuer_management::parse_endpoint_url('webdav', $this->issuer);
+ $webdavprefix = $parsedwebdavurl['path'];
+ // Checks whether folder exist and creates non-existent folders.
+ $webdavclient->open();
+ $isdir = $webdavclient->is_dir($webdavprefix . $controlledlinkfoldername);
+ // Folder already exist, continue.
+ if (!$isdir) {
+ $responsecreateshare = $webdavclient->mkcol($webdavprefix . $controlledlinkfoldername);
+
+ if ($responsecreateshare != 201) {
+ $webdavclient->close();
+ throw new request_exception(array('instance' => $this->repositoryname,
+ 'errormessage' => get_string('contactadminwith', 'repository_nextcloud',
+ 'The folder to store files in the user account could not be created.')));
+ }
+ }
+ $webdavclient->close();
+ }
+
+ /** Gets all shares from a path (the path is file specific) and extracts the share of a specific user. In case
+ * multiple shares exist the first one is taken. Multiple shares can only appear when shares are created outside
+ * of this plugin, therefore this case is not handled.
+ * @param string $path
+ * @param string $username
+ * @return \SimpleXMLElement
+ * @throws \moodle_exception
+ */
+ public function get_shares_from_path($path, $username) {
+ $ocsparams = [
+ 'path' => $path,
+ 'reshares' => true
+ ];
+
+ $getsharesresponse = $this->systemocsclient->call('get_shares', $ocsparams);
+ $xml = simplexml_load_string($getsharesresponse);
+ $validelement = array();
+ foreach ($fileid = $xml->data->element as $element) {
+ if ($element->share_with == $username) {
+ $validelement = $element;
+ break;
+ }
+ }
+ if (empty($validelement)) {
+ throw new request_exception(array('instance' => $this->repositoryname,
+ 'errormessage' => get_string('filenotaccessed', 'repository_nextcloud')));
+
+ }
+ return $validelement->id;
+ }
+
+ /** This method can only be used if the response is from a newly created share. In this case there is more information
+ * in the response. For a reference refer to
+ * https://docs.nextcloud.com/server/13/developer_manual/core/ocs-share-api.html#get-information-about-a-known-share.
+ * @param int $shareid
+ * @param string $username
+ * @return mixed the id of the share
+ * @throws \coding_exception
+ * @throws \repository_nextcloud\request_exception
+ */
+ public function get_share_information_from_shareid($shareid, $username) {
+ $ocsparams = [
+ 'share_id' => (int) $shareid
+ ];
+
+ $shareinformation = $this->ocsclient->call('get_information_of_share', $ocsparams);
+ $xml = simplexml_load_string($shareinformation);
+ foreach ($fileid = $xml->data->element as $element) {
+ if ($element->share_with == $username) {
+ $validelement = $element;
+ break;
+ }
+ }
+ if (empty($validelement)) {
+ throw new request_exception(array('instance' => $this->repositoryname,
+ 'errormessage' => get_string('filenotaccessed', 'repository_nextcloud')));
+
+ }
+ return (string) $validelement->file_target;
+ }
+
+ /**
+ * Find a file that has previously been shared with the system account.
+ * @param string $path Path to file in user context.
+ * @return array shareid: ID of share, filetarget: path to file in sys account.
+ * @throws request_exception If the share cannot be resolved.
+ */
+ public function find_share_in_sysaccount($path) {
+ $systemaccount = \core\oauth2\api::get_system_account($this->issuer);
+ $systemaccountuser = $systemaccount->get('username');
+
+ // Find out share ID from user files.
+ $ocsparams = [
+ 'path' => $path,
+ 'reshares' => true
+ ];
+
+ $getsharesresponse = $this->ocsclient->call('get_shares', $ocsparams);
+ $xml = simplexml_load_string($getsharesresponse);
+ $validelement = array();
+ foreach ($fileid = $xml->data->element as $element) {
+ if ($element->share_with == $systemaccountuser) {
+ $validelement = $element;
+ break;
+ }
+ }
+ if (empty($validelement)) {
+ throw new request_exception(array('instance' => $this->repositoryname,
+ 'errormessage' => get_string('filenotaccessed', 'repository_nextcloud')));
+ }
+ $shareid = (int) $validelement->id;
+
+ // Use share id to find file name in system account's context.
+ $ocsparams = [
+ 'share_id' => $shareid
+ ];
+
+ $shareinformation = $this->systemocsclient->call('get_information_of_share', $ocsparams);
+ $xml = simplexml_load_string($shareinformation);
+ foreach ($fileid = $xml->data->element as $element) {
+ if ($element->share_with == $systemaccountuser) {
+ $validfile = $element;
+ break;
+ }
+ }
+ if (empty($validfile)) {
+ throw new request_exception(array('instance' => $this->repositoryname,
+ 'errormessage' => get_string('filenotaccessed', 'repository_nextcloud')));
+
+ }
+ return [
+ 'shareid' => $shareid,
+ 'filetarget' => (string) $validfile->file_target
+ ];
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Exception for when client configuration data is missing.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace repository_nextcloud;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Exception for when client configuration data is missing.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class configuration_exception extends \moodle_exception {
+
+ /**
+ * This exception is used when the configuration of the plugin can not be processed or database entries are
+ * missing.
+ * @param string $hint optional param for additional information of the problem
+ * @param string $debuginfo detailed information how to fix problem
+ */
+ public function __construct($hint = '', $debuginfo = null) {
+ parent::__construct('configuration_exception', 'repository_nextcloud', '', $hint, $debuginfo);
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+//
+
+/**
+ * Provide static functions for creating and validating issuers.
+ *
+ * @package repository_nextcloud
+ * @copyright 2018 Jan Dageförde (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace repository_nextcloud;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Provide static functions for creating and validating issuers.
+ *
+ * @package repository_nextcloud
+ * @copyright 2018 Jan Dageförde (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class issuer_management {
+
+ /**
+ * Check if an issuer provides all endpoints that are required by repository_nextcloud.
+ * @param \core\oauth2\issuer $issuer An issuer.
+ * @return bool True, if all endpoints exist; false otherwise.
+ */
+ public static function is_valid_issuer(\core\oauth2\issuer $issuer) {
+ $endpointwebdav = false;
+ $endpointocs = false;
+ $endpointtoken = false;
+ $endpointauth = false;
+ $endpointuserinfo = false;
+ $endpoints = \core\oauth2\api::get_endpoints($issuer);
+ foreach ($endpoints as $endpoint) {
+ $name = $endpoint->get('name');
+ switch ($name) {
+ case 'webdav_endpoint':
+ $endpointwebdav = true;
+ break;
+ case 'ocs_endpoint':
+ $endpointocs = true;
+ break;
+ case 'token_endpoint':
+ $endpointtoken = true;
+ break;
+ case 'authorization_endpoint':
+ $endpointauth = true;
+ break;
+ case 'userinfo_endpoint':
+ $endpointuserinfo = true;
+ break;
+ }
+ }
+ return $endpointwebdav && $endpointocs && $endpointtoken && $endpointauth && $endpointuserinfo;
+ }
+
+ /**
+ * Returns the parsed url parts of an endpoint of an issuer.
+ * @param string $endpointname
+ * @param \core\oauth2\issuer $issuer
+ * @return array parseurl [scheme => https/http, host=>'hostname', port=>443, path=>'path']
+ * @throws configuration_exception if an endpoint is undefined
+ */
+ public static function parse_endpoint_url(string $endpointname, \core\oauth2\issuer $issuer): array {
+ $url = $issuer->get_endpoint_url($endpointname);
+ if (empty($url)) {
+ throw new configuration_exception(sprintf('Endpoint %s not defined.', $endpointname));
+ }
+ return parse_url($url);
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * REST interface to Nextcloud's implementation of Open Collaboration Services.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Jan Dageförde (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace repository_nextcloud;
+
+use core\oauth2\client;
+use core\oauth2\rest;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * REST interface to Nextcloud's implementation of Open Collaboration Services.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Jan Dageförde (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class ocs_client extends rest {
+
+ /**
+ * shareType=0 creates a private user share.
+ */
+ const SHARE_TYPE_USER = 0;
+
+ /**
+ * shareType=3 creates a public share.
+ */
+ const SHARE_TYPE_PUBLIC = 3;
+
+ /**
+ * permissions=1 gives read permission for a share.
+ */
+ const SHARE_PERMISSION_READ = 1;
+
+ /**
+ * permissions=1 gives read permission for a share.
+ */
+ const SHARE_PERMISSION_ALL = 31;
+
+ /**
+ * OCS endpoint as configured for the used issuer.
+ * @var \moodle_url
+ */
+ private $ocsendpoint;
+
+
+ /**
+ * Get endpoint URLs from the used issuer to use them in get_api_functions().
+ * @param client $oauthclient OAuth-authenticated Nextcloud client
+ * @throws configuration_exception Exception if critical endpoints are missing.
+ * @throws \moodle_exception when trying to construct a moodleurl
+ */
+ public function __construct(client $oauthclient) {
+ parent::__construct($oauthclient);
+
+ $issuer = $oauthclient->get_issuer();
+ $ocsendpoint = $issuer->get_endpoint_url('ocs');
+ if ($ocsendpoint === false) {
+ throw new configuration_exception('Endpoint ocs_endpoint not defined.');
+ }
+ $this->ocsendpoint = new \moodle_url($ocsendpoint);
+ if (empty($this->ocsendpoint->get_param('format'))) {
+ $this->ocsendpoint->params(array('format' => 'xml'));
+ }
+ }
+
+ /**
+ * Define relevant functions of the OCS API.
+ *
+ * Previously, the instruction to create a oauthclient recommended the user to enter the return format (format=xml).
+ * However, in this case the shareid is appended at the wrong place. Therefore, a new url is build which inserts the
+ * shareid at the suitable place for delete_share and get_information_of_share.
+ * create_share docs: https://docs.nextcloud.com/server/13/developer_manual/core/ocs-share-api.html#create-a-new-share
+ *
+ */
+ public function get_api_functions() {
+ return [
+ 'create_share' => [
+ 'endpoint' => $this->ocsendpoint->out(false),
+ 'method' => 'post',
+ 'args' => [
+ 'path' => PARAM_TEXT, // Could be PARAM_PATH, we really don't want to enforce a Moodle understanding of paths.
+ 'shareType' => PARAM_INT,
+ 'shareWith' => PARAM_TEXT, // Name of receiving user/group. Required if SHARE_TYPE_USER.
+ 'publicUpload' => PARAM_RAW, // Actually Boolean, but neither String-Boolean ('false') nor PARAM_BOOL (0/1).
+ 'permissions' => PARAM_INT,
+ 'shareWith' => PARAM_TEXT,
+ 'expireDate' => PARAM_TEXT
+ ],
+ 'response' => 'text/xml'
+ ],
+ 'delete_share' => [
+ 'endpoint' => $this->build_share_url(),
+ 'method' => 'delete',
+ 'args' => [
+ 'share_id' => PARAM_INT
+ ],
+ 'response' => 'text/xml'
+ ],
+ 'get_shares' => [
+ 'endpoint' => $this->ocsendpoint->out(false),
+ 'method' => 'get',
+ 'args' => [
+ 'path' => PARAM_TEXT,
+ 'reshares' => PARAM_RAW, // Returns not only the shares from the current user but all of the given file.
+ 'subfiles' => PARAM_RAW, // Returns all shares within a folder, given that path defines a folder.
+ ],
+ 'response' => 'text/xml'
+ ],
+ 'get_information_of_share' => [
+ 'endpoint' => $this->build_share_url(),
+ 'method' => 'get',
+ 'args' => [
+ 'share_id' => PARAM_INT
+ ],
+ 'response' => 'text/xml'
+ ],
+ ];
+ }
+
+ /**
+ * Private Function to return a url with the shareid in the path.
+ * @return string
+ */
+ private function build_share_url() {
+ // Out_omit_querystring() in combination with ocsendpoint->get_path() is not used since both function include
+ // /ocs/v1.php.
+ $shareurl = $this->ocsendpoint->get_scheme() . '://' . $this->ocsendpoint->get_host() . ':' .
+ $this->ocsendpoint->get_port() . $this->ocsendpoint->get_path() . '/{share_id}?' .
+ $this->ocsendpoint->get_query_string(false);
+ return $shareurl;
+ }
+
+ /**
+ * In POST requests, Moodle's REST API assumes that params are
+ * - transmitted as part of the URL or
+ * - expressed in JSON.
+ * Neither is true; we are passing an array to $functionargs which is then put into CURLOPT_POSTFIELDS.
+ * Curl assumes the content type to be `multipart/form-data` then, but the Moodle REST API tries to put
+ * a JSON content type. As a result, clients would fail.
+ * To make this less tedious to use, we assume that the params-as-array-in-$functionargs is the default for us.
+ *
+ * @param string $functionname Name of a function from get_api_functions()
+ * @param array $functionargs Request parameters
+ * @param bool|string $rawpost Optional param to include in the body of a post
+ * @param bool|string $contenttype Content type of the request body. Default: multipart/form-data if !$rawpost, JSON otherwise
+ * @return object|string
+ * @throws \coding_exception
+ * @throws \core\oauth2\rest_exception
+ */
+ public function call($functionname, $functionargs, $rawpost = false, $contenttype = false) {
+ if ($rawpost === false && $contenttype === false) {
+ return parent::call($functionname, $functionargs, false, 'multipart/form-data');
+ } else {
+ return parent::call($functionname, $functionargs, $rawpost, $contenttype);
+ }
+ }
+
+}
\ No newline at end of file
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy provider.
+ *
+ * @package repository_nextcloud
+ * @copyright 2018 Nina Herrmann (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace repository_nextcloud\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy provider implementing null_provider.
+ *
+ * @package repository_nextcloud
+ * @copyright 2018 Nina Herrmann (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+ /**
+ * Get the language string identifier with the component's language
+ * file to explain why this plugin stores no data.
+ *
+ * @return string
+ */
+ public static function get_reason() : string {
+ return 'privacy:metadata';
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Exception for when an OCS request fails
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Jan Dageförde (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace repository_nextcloud;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Exception for when an OCS request fails
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Jan Dageförde (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class request_exception extends \moodle_exception {
+
+ /**
+ * An OCS request has failed.
+ *
+ * @param string $hint optional param for additional information of the problem
+ * @param string $debuginfo detailed information how to fix problem
+ */
+ public function __construct($hint = '', $debuginfo = null) {
+ parent::__construct('request_exception', 'repository_nextcloud', '', $hint, $debuginfo);
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Capability definitions for Nextcloud repository.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+ 'repository/nextcloud:view' => array(
+ 'captype' => 'read',
+ 'contextlevel' => CONTEXT_MODULE,
+ 'archetypes' => array(
+ 'user' => CAP_ALLOW
+ )
+ )
+);
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Language strings' definition for Nextcloud repository.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+// General.
+$string['pluginname'] = 'Nextcloud';
+$string['configplugin'] = 'Nextcloud repository configuration';
+$string['nextcloud'] = 'Nextcloud';
+$string['nextcloud:view'] = 'View Nextcloud';
+$string['configplugin'] = 'Nextcloud configuration';
+$string['pluginname_help'] = 'Nextcloud repository';
+
+// Settings.
+$string['issuervalidation_without'] = 'You have not selected an Nextcloud server as the OAuth 2 issuer yet.';
+$string['issuervalidation_valid'] = 'Currently the {$a} issuer is active.';
+$string['issuervalidation_invalid'] = 'Currently the {$a} issuer is active, however it does not implement all necessary endpoints. The repository will not work.';
+$string['right_issuers'] = 'The following issuers implement the required endpoints: <br> {$a}';
+$string['no_right_issuers'] = 'None of the existing issuers implement all required endpoints. Please register an appropriate issuer.';
+$string['chooseissuer'] = 'Issuer';
+$string['chooseissuer_help'] = 'To add a new issuer visit the admin OAuth 2 services page. <br>
+For additional help with the OAuth 2 API please refer to the Moodle documentation.';
+$string['foldername'] = 'Name of folder created in Nextcloud users\' private space that holds all access controlled links';
+$string['foldername_help'] = 'To assure users find files shared with them, shares are saved into a specific folder. <br>
+This setting determines the name of the folder. It is recommended to chose a name associated with your Moodle instance.';
+$string['oauth2serviceslink'] = '<a href="{$a}" title="Link to OAuth 2 services configuration">OAuth 2 services configuration</a>';
+$string['privacy:metadata'] = 'The Nextcloud repository plugin neither stores any personal data nor transmits user data to the remote system.';
+$string['internal'] = 'Internal (files stored in Moodle)';
+$string['external'] = 'External (only links stored in Moodle)';
+$string['both'] = 'Internal and external';
+$string['supportedreturntypes'] = 'Supported files';
+$string['defaultreturntype'] = 'Default return type';
+$string['fileoptions'] = 'The types and defaults for returned files is configurable here. Note that all files linked externally will be updated so that the owner is the Moodle system account.';
+
+// Exceptions.
+$string['configuration_exception'] = 'An error in the configuration of the OAuth 2 client occurred: {$a}';
+$string['request_exception'] = 'A request to {$a->instance} has failed. {$a->errormessage}';
+$string['requestnotexecuted'] = 'The request could not be executed. If this happens frequently please contact the course or site administrator.';
+$string['notauthorized'] = 'You are not authorized to execute the demanded request. Please ensure you are authenticated with the right account.';
+$string['contactadminwith'] = 'The requested action could not be executed. In case this happens frequently please contact the side administrator with the following additional information:<br>"<i>{$a}</i>"';
+$string['cannotconnect'] = 'The user could not be authenticated, please log in and then upload the file.';
+$string['filenotaccessed'] = 'The requested file could not be accessed. Please check whether you have chosen a valid file and you are authenticated with the right account.';
+$string['couldnotmove'] = 'The requested file could not be moved in the {$a} folder.';
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Nextcloud repository plugin library.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or
+ */
+
+use repository_nextcloud\issuer_management;
+use repository_nextcloud\ocs_client;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/repository/lib.php');
+require_once($CFG->libdir . '/webdavlib.php');
+
+/**
+ * Nextcloud repository class.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class repository_nextcloud extends repository {
+ /**
+ * OAuth 2 client
+ * @var \core\oauth2\client
+ */
+ private $client = null;
+
+ /**
+ * OAuth 2 Issuer
+ * @var \core\oauth2\issuer
+ */
+ private $issuer = null;
+
+ /**
+ * Additional scopes needed for the repository. Currently, nextcloud does not actually support/use scopes, so
+ * this is intended as a hint at required functionality and will help declare future scopes.
+ */
+ const SCOPES = 'files ocs';
+
+ /**
+ * Webdav client which is used for webdav operations.
+ *
+ * @var \webdav_client
+ */
+ private $dav = null;
+
+ /**
+ * Basepath for WebDAV operations
+ * @var string
+ */
+ private $davbasepath;
+
+ /**
+ * OCS client that uses the Open Collaboration Services REST API.
+ * @var ocs_client
+ */
+ private $ocsclient;
+
+ /**
+ * @var oauth2_client System account client.
+ */
+ private $systemoauthclient = false;
+
+ /**
+ * OCS systemocsclient that uses the Open Collaboration Services REST API.
+ * @var ocs_client
+ */
+ private $systemocsclient = null;
+ /**
+ * Name of the folder for controlled links.
+ * @var string
+ */
+ private $controlledlinkfoldername;
+
+ /**
+ * repository_nextcloud constructor.
+ * @param int $repositoryid
+ * @param bool|int|stdClass $context
+ * @param array $options
+ */
+ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
+ parent::__construct($repositoryid, $context, $options);
+ try {
+ // Issuer from repository instance config.
+ $issuerid = $this->get_option('issuerid');
+ $this->issuer = \core\oauth2\api::get_issuer($issuerid);
+ } catch (dml_missing_record_exception $e) {
+ // A repository is marked as disabled when no issuer is present.
+ $this->disabled = true;
+ return;
+ }
+
+ try {
+ // Load the webdav endpoint and parse the basepath.
+ $webdavendpoint = issuer_management::parse_endpoint_url('webdav', $this->issuer);
+ // Get basepath without trailing slash, because future uses will come with a leading slash.
+ $basepath = $webdavendpoint['path'];
+ if (strlen($basepath) > 0 && substr($basepath, -1) === '/') {
+ $basepath = substr($basepath, 0, -1);
+ }
+ $this->davbasepath = $basepath;
+ } catch (\repository_nextcloud\configuration_exception $e) {
+ // A repository is marked as disabled when no webdav_endpoint is present
+ // or it fails to parse, because all operations concerning files
+ // rely on the webdav endpoint.
+ $this->disabled = true;
+ return;
+ }
+ $this->controlledlinkfoldername = $this->get_option('controlledlinkfoldername');
+
+ if (!$this->issuer) {
+ $this->disabled = true;
+ return;
+ } else if (!$this->issuer->get('enabled')) {
+ // In case the Issuer is not enabled, the repository is disabled.
+ $this->disabled = true;
+ return;
+ } else if (!issuer_management::is_valid_issuer($this->issuer)) {
+ // Check if necessary endpoints are present.
+ $this->disabled = true;
+ return;
+ }
+
+ $this->ocsclient = new ocs_client($this->get_user_oauth_client());
+ }
+
+ /**
+ * Get or initialise an oauth client for the system account.
+ * @return false|oauth2_client False if initialisation was unsuccessful, otherwise an initialised client.
+ */
+ private function get_system_oauth_client() {
+ if ($this->systemoauthclient === false) {
+ try {
+ $this->systemoauthclient = \core\oauth2\api::get_system_oauth_client($this->issuer);
+ } catch (\moodle_exception $e) {
+ $this->systemoauthclient = false;
+ }
+ }
+ return $this->systemoauthclient;
+ }
+
+ /**
+ * Get or initialise an ocs client for the system account.
+ * @return null|ocs_client Null if initialisation was unsuccessful, otherwise an initialised client.
+ */
+ private function get_system_ocs_client() {
+ if ($this->systemocsclient === null) {
+ try {
+ $systemoauth = $this->get_system_oauth_client();
+ if (!$systemoauth) {
+ return null;
+ }
+ $this->systemocsclient = new ocs_client($systemoauth);
+ } catch (\moodle_exception $e) {
+ $this->systemocsclient = null;
+ }
+ }
+ return $this->systemocsclient;
+ }
+
+ /**
+ * Initiates the webdav client.
+ * @throws \repository_nextcloud\configuration_exception If configuration is missing (endpoints).
+ */
+ private function initiate_webdavclient() {
+ if ($this->dav !== null) {
+ return $this->dav;
+ }
+
+ $webdavendpoint = issuer_management::parse_endpoint_url('webdav', $this->issuer);
+
+ // Selects the necessary information (port, type, server) from the path to build the webdavclient.
+ $server = $webdavendpoint['host'];
+ if ($webdavendpoint['scheme'] === 'https') {
+ $webdavtype = 'ssl://';
+ $webdavport = 443;
+ } else if ($webdavendpoint['scheme'] === 'http') {
+ $webdavtype = '';
+ $webdavport = 80;
+ }
+
+ // Override default port, if a specific one is set.
+ if (isset($webdavendpoint['port'])) {
+ $webdavport = $webdavendpoint['port'];
+ }
+
+ // Authentication method is `bearer` for OAuth 2. Pass token of authenticated client, too.
+ $this->dav = new \webdav_client($server, '', '', 'bearer', $webdavtype,
+ $this->get_user_oauth_client()->get_accesstoken()->token);
+
+ $this->dav->port = $webdavport;
+ $this->dav->debug = false;
+ return $this->dav;
+ }
+
+ /**
+ * This function does exactly the same as in the WebDAV repository. The only difference is, that
+ * the nextcloud OAuth2 client uses OAuth2 instead of Basic Authentication.
+ *
+ * @param string $reference relative path to the file.
+ * @param string $title title of the file.
+ * @return array|bool returns either the moodle path to the file or false.
+ */
+ public function get_file($reference, $title = '') {
+ // Normal file.
+ $reference = urldecode($reference);
+
+ // Prepare a file with an arbitrary name - cannot be $title because of special chars (cf. MDL-57002).
+ $path = $this->prepare_file(uniqid());
+ $this->initiate_webdavclient();
+ if (!$this->dav->open()) {
+ return false;
+ }
+ $this->dav->get_file($this->davbasepath . $reference, $path);
+ $this->dav->close();
+
+ return array('path' => $path);
+ }
+
+ /**
+ * This function does exactly the same as in the WebDAV repository. The only difference is, that
+ * the nextcloud OAuth2 client uses OAuth2 instead of Basic Authentication.
+ *
+ * @param string $path relative path to the directory or file.
+ * @param string $page page number (given multiple pages of elements).
+ * @return array directory properties.
+ */
+ public function get_listing($path='', $page = '') {
+ if (empty($path)) {
+ $path = '/';
+ }
+
+ $ret = $this->get_listing_prepare_response($path);
+
+ // Before any WebDAV method can be executed, a WebDAV client socket needs to be opened
+ // which connects to the server.
+ $this->initiate_webdavclient();
+ if (!$this->dav->open()) {
+ return $ret;
+ }
+
+ // Since the paths which are received from the PROPFIND WebDAV method are url encoded
+ // (because they depict actual web-paths), the received paths need to be decoded back
+ // for the plugin to be able to work with them.
+ $ls = $this->dav->ls($this->davbasepath . urldecode($path));
+ $this->dav->close();
+
+ // The method get_listing return all information about all child files/folders of the
+ // current directory. If no information was received, the directory must be empty.
+ if (!is_array($ls)) {
+ return $ret;
+ }
+
+ // Process WebDAV output and convert it into Moodle format.
+ $ret['list'] = $this->get_listing_convert_response($path, $ls);
+ return $ret;
+
+ }
+
+ /**
+ * Use OCS to generate a public share to the requested file.
+ * This method derives a download link from the public share URL.
+ *
+ * @param string $url relative path to the chosen file
+ * @return string the generated download link.
+ * @throws \repository_nextcloud\request_exception If nextcloud responded badly
+ *
+ */
+ public function get_link($url) {
+ $ocsparams = [
+ 'path' => $url,
+ 'shareType' => ocs_client::SHARE_TYPE_PUBLIC,
+ 'publicUpload' => false,
+ 'permissions' => ocs_client::SHARE_PERMISSION_READ
+ ];
+
+ $response = $this->ocsclient->call('create_share', $ocsparams);
+ $xml = simplexml_load_string($response);
+ $repositoryname = get_string('pluginname', 'repository_nextcloud');
+
+ if ($xml === false ) {
+ throw new \repository_nextcloud\request_exception(array('instance' => $repositoryname,
+ 'errormessage' => 'Invalid response'));
+ }
+
+ if ((string)$xml->meta->status !== 'ok') {
+ throw new \repository_nextcloud\request_exception(array('instance' => $repositoryname, 'errormessage' => sprintf(
+ '(%s) %s', $xml->meta->statuscode, $xml->meta->message)));
+ }
+
+ // Take the share link and convert it into a download link.
+ return ((string)$xml->data[0]->url) . '/download';
+ }
+
+ /**
+ * This method converts the source from the file picker (chosen by the user) into
+ * information, which will be received by methods that fetch files/references from
+ * the Nextcloud server.
+ *
+ * @param string $source source of the file, returned by repository as 'source' and received back from user (not cleaned)
+ * @return string file reference, ready to be stored
+ */
+ public function get_file_reference($source) {
+ $usefilereference = optional_param('usefilereference', false, PARAM_BOOL);
+
+ // A filereference is requested if an alias/shortcut shall be created, i.e. a FILE_REFERENCE option is selected.
+ // Therefore, generate and return a public link to the file.
+ if ($usefilereference) {
+ $reference = $this->get_link($source);
+ $filereturn = new stdClass();
+ $filereturn->type = 'FILE_REFERENCE';
+ $filereturn->link = $reference;
+ return json_encode($filereturn);
+ }
+
+ // Otherwise, the simple relative path to the file is enough.
+ return $source;
+ }
+
+ /** Called when a file is selected as a "access control link".
+ * Invoked at MOODLE/repository/repository_ajax.php
+ *
+ * This is called at the point the reference files are being copied from the draft area to the real area.
+ * What is done here is transfer ownership to the system user (by copying) then delete the intermediate share
+ * used for that. Finally update the reference to point to new file name.
+ *
+ * @param string $reference this reference is generated by repository::get_file_reference()
+ * @param context $context the target context for this new file.
+ * @param string $component the target component for this new file.
+ * @param string $filearea the target filearea for this new file.
+ * @param string $itemid the target itemid for this new file.
+ * @return string updated reference (final one before it's saved to db).
+ * @throws \repository_nextcloud\configuration_exception
+ * @throws \repository_nextcloud\request_exception
+ * @throws coding_exception
+ * @throws moodle_exception
+ * @throws repository_exception
+ */
+ public function reference_file_selected($reference, $context, $component, $filearea, $itemid) {
+ $source = json_decode($reference);
+
+ if (is_object($source)) {
+ if ($source->type != 'FILE_CONTROLLED_LINK') {
+ // Only access controlled links need special handling; we are done.
+ return $reference;
+ }
+ if (!empty($source->usesystem)) {
+ // If we already copied this file to the system account - we are done.
+ return $reference;
+ }
+ }
+
+ // Check this issuer is enabled.
+ if ($this->disabled || $this->get_system_oauth_client() === false || $this->get_system_ocs_client() === null) {
+ throw new repository_exception('cannotdownload', 'repository');
+ }
+
+ $linkmanager = new \repository_nextcloud\access_controlled_link_manager($this->ocsclient, $this->get_system_oauth_client(),
+ $this->get_system_ocs_client(), $this->issuer, $this->get_name());
+
+ // Get the current user.
+ $userauth = $this->get_user_oauth_client();
+ if ($userauth === false) {
+ $details = get_string('cannotconnect', 'repository_nextcloud');
+ throw new \repository_nextcloud\request_exception(array('instance' => $this->get_name(), 'errormessage' => $details));
+ }
+ // 1. Share the File with the system account.
+ $responsecreateshare = $linkmanager->create_share_user_sysaccount($reference);
+ if ($responsecreateshare['statuscode'] == 403) {
+ // File has already been shared previously => find file in system account and use that.
+ $responsecreateshare = $linkmanager->find_share_in_sysaccount($reference);
+ }
+
+ // 2. Create a unique path in the system account.
+ $createdfolder = $linkmanager->create_folder_path_access_controlled_links($context, $component, $filearea,
+ $itemid);
+
+ // 3. Copy File to the new folder path.
+ $linkmanager->transfer_file_to_path($responsecreateshare['filetarget'], $createdfolder, 'copy');
+
+ // 4. Delete the share.
+ $linkmanager->delete_share_dataowner_sysaccount($responsecreateshare['shareid']);
+
+ // Update the returned reference so that the stored_file in moodle points to the newly copied file.
+ $filereturn = new stdClass();
+ $filereturn->type = 'FILE_CONTROLLED_LINK';
+ $filereturn->link = $createdfolder . $responsecreateshare['filetarget'];
+ $filereturn->name = $reference;
+ $filereturn->usesystem = true;
+ $filereturn = json_encode($filereturn);
+
+ return $filereturn;
+ }
+
+ /**
+ * Repository method that serves the referenced file (created e.g. via get_link).
+ * All parameters are there for compatibility with superclass, but they are ignored.
+ *
+ * @param stored_file $storedfile
+ * @param int $lifetime (ignored)
+ * @param int $filter (ignored)
+ * @param bool $forcedownload (ignored)
+ * @param array $options (ignored)
+ * @throws \repository_nextcloud\configuration_exception
+ * @throws \repository_nextcloud\request_exception
+ * @throws coding_exception
+ * @throws moodle_exception
+ */
+ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
+ $repositoryname = $this->get_name();
+ $reference = json_decode($storedfile->get_reference());
+
+ if ($reference->type == 'FILE_REFERENCE') {
+ redirect($reference->link);
+ }
+
+ // 1. assure the client and user is logged in.
+ if (empty($this->client) || $this->get_system_oauth_client() === false || $this->get_system_ocs_client() === null) {
+ $details = get_string('contactadminwith', 'repository_nextcloud',
+ 'The OAuth client could not be connected.');
+ throw new \repository_nextcloud\request_exception(array('instance' => $repositoryname, 'errormessage' => $details));
+ }
+
+ if (!$this->client->is_logged_in()) {
+ $this->print_login_popup(['style' => 'margin-top: 250px']);
+ return;
+ }
+
+ // Determining writeability of file from the using context.
+ // Variable $info is null|\file_info. file_info::is_writable is only true if user may write for any reason.
+ $fb = get_file_browser();
+ $context = context::instance_by_id($storedfile->get_contextid(), MUST_EXIST);
+ $info = $fb->get_file_info($context,
+ $storedfile->get_component(),
+ $storedfile->get_filearea(),
+ $storedfile->get_itemid(),
+ $storedfile->get_filepath(),
+ $storedfile->get_filename());
+ $maywrite = !empty($info) && $info->is_writable();
+
+ $this->initiate_webdavclient();
+
+ // Create the a manager to handle steps.
+ $linkmanager = new \repository_nextcloud\access_controlled_link_manager($this->ocsclient, $this->get_system_oauth_client(),
+ $this->get_system_ocs_client(), $this->issuer, $repositoryname);
+
+ // 2. Check whether user has folder for files otherwise create it.
+ $linkmanager->create_storage_folder($this->controlledlinkfoldername, $this->dav);
+
+ $userinfo = $this->client->get_userinfo();
+ $username = $userinfo['username'];
+
+ // Creates a share between the systemaccount and the user.
+ $responsecreateshare = $linkmanager->create_share_user_sysaccount($reference->link, $username, $maywrite);
+
+ $statuscode = $responsecreateshare['statuscode'];
+
+ if ($statuscode == 403) {
+ $shareid = $linkmanager->get_shares_from_path($reference->link, $username);
+ } else if ($statuscode == 100) {
+ $filetarget = $linkmanager->get_share_information_from_shareid($responsecreateshare['shareid'], $username);
+ $copyresult = $linkmanager->transfer_file_to_path($filetarget, $this->controlledlinkfoldername,
+ 'move', $this->dav);
+ if (!($copyresult == 201 || $copyresult == 412)) {
+ throw new \repository_nextcloud\request_exception(array('instance' => $this->repositoryname,
+ 'errormessage' => get_string('couldnotmove', 'repository_nextcloud', $this->controlledlinkfoldername)));
+ }
+ $shareid = $responsecreateshare['shareid'];
+ } else if ($statuscode == 997) {
+ throw new \repository_nextcloud\request_exception(array('instance' => $repositoryname,
+ 'errormessage' => get_string('notauthorized', 'repository_nextcloud')));
+ } else {
+ $details = get_string('filenotaccessed', 'repository_nextcloud');
+ throw new \repository_nextcloud\request_exception(array('instance' => $repositoryname, 'errormessage' => $details));
+ }
+ $filetarget = $linkmanager->get_share_information_from_shareid((int)$shareid, $username);
+
+ // Obtain the file from Nextcloud using a Bearer token authenticated connection because we cannot perform a redirect here.
+ // The reason is that Nextcloud uses samesite cookie validation, i.e. a redirected request would not be authenticated.
+ // (Also the browser might use the session of a Nextcloud user that is different from the one that is known to Moodle.)
+ $filename = basename($filetarget);
+ $tmppath = make_request_directory() . '/' . $filename;
+ $this->dav->open();
+
+ // Concat webdav path with file path.
+ $webdavendpoint = issuer_management::parse_endpoint_url('webdav', $this->issuer);
+ $filetarget = ltrim($filetarget, '/');
+ $filetarget = $webdavendpoint['path'] . $filetarget;
+
+ // Write file into temp location.
+ if (!$this->dav->get_file($filetarget, $tmppath)) {
+ $this->dav->close();
+ throw new repository_exception('cannotdownload', 'repository');
+ }
+ $this->dav->close();
+
+ // Output the obtained file to the user and remove it from disk.
+ send_temp_file($tmppath, $filename);
+ }
+
+ /**
+ * Which return type should be selected by default.
+ *
+ * @return int
+ */
+ public function default_returntype() {
+ $setting = $this->get_option('defaultreturntype');
+ $supported = $this->get_option('supportedreturntypes');
+ if (($setting == FILE_INTERNAL && $supported !== 'external') || $supported === 'internal') {
+ return FILE_INTERNAL;
+ }
+ return FILE_CONTROLLED_LINK;
+ }
+
+ /**
+ * Return names of the general options.
+ * By default: no general option name.
+ *
+ * @return array
+ */
+ public static function get_type_option_names() {
+ return array();
+ }
+
+ /**
+ * Function which checks whether the user is logged in on the Nextcloud instance.
+ *
+ * @return bool false, if no Access Token is set or can be requested.
+ */
+ public function check_login() {
+ $client = $this->get_user_oauth_client();
+ return $client->is_logged_in();
+ }
+
+ /**
+ * Get a cached user authenticated oauth client.
+ * @param bool|moodle_url $overrideurl Use this url instead of the repo callback.
+ * @return \core\oauth2\client
+ */
+ protected function get_user_oauth_client($overrideurl = false) {
+ if ($this->client) {
+ return $this->client;
+ }
+ if ($overrideurl) {
+ $returnurl = $overrideurl;
+ } else {
+ $returnurl = new moodle_url('/repository/repository_callback.php');
+ $returnurl->param('callback', 'yes');
+ $returnurl->param('repo_id', $this->id);
+ $returnurl->param('sesskey', sesskey());
+ }
+ $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES);
+ return $this->client;
+ }
+
+
+ /**
+ * Prints a simple Login Button which redirects to an authorization window from Nextcloud.
+ *
+ * @return mixed login window properties.
+ * @throws coding_exception
+ */
+ public function print_login() {
+ $client = $this->get_user_oauth_client();
+ $loginurl = $client->get_login_url();
+ if ($this->options['ajax']) {
+ $ret = array();
+ $btn = new \stdClass();
+ $btn->type = 'popup';
+ $btn->url = $loginurl->out(false);
+ $ret['login'] = array($btn);
+ return $ret;
+ } else {
+ echo html_writer::link($loginurl, get_string('login', 'repository'),
+ array('target' => '_blank', 'rel' => 'noopener noreferrer'));
+ }
+ }
+
+ /**
+ * Deletes the held Access Token and prints the Login window.
+ *
+ * @return array login window properties.
+ */
+ public function logout() {
+ $client = $this->get_user_oauth_client();
+ $client->log_out();
+ return parent::logout();
+ }
+
+ /**
+ * Sets up access token after the redirection from Nextcloud.
+ */
+ public function callback() {
+ $client = $this->get_user_oauth_client();
+ // If an Access Token is stored within the client, it has to be deleted to prevent the addition
+ // of an Bearer authorization header in the request method.
+ $client->log_out();
+
+ // This will upgrade to an access token if we have an authorization code and save the access token in the session.
+ $client->is_logged_in();
+ }
+
+
+ /**
+ * Create an instance for this plug-in
+ *
+ * @param string $type the type of the repository
+ * @param int $userid the user id
+ * @param stdClass $context the context
+ * @param array $params the options for this instance
+ * @param int $readonly whether to create it readonly or not (defaults to not)
+ * @return mixed
+ * @throws dml_exception
+ * @throws required_capability_exception
+ */
+ public static function create($type, $userid, $context, $params, $readonly=0) {
+ require_capability('moodle/site:config', context_system::instance());
+ return parent::create($type, $userid, $context, $params, $readonly);
+ }
+
+
+ /**
+ * This method adds a select form and additional information to the settings form..
+ *
+ * @param \moodleform $mform Moodle form (passed by reference)
+ * @return bool|void
+ * @throws coding_exception
+ * @throws dml_exception
+ */
+ public static function instance_config_form($mform) {
+ if (!has_capability('moodle/site:config', context_system::instance())) {
+ $mform->addElement('static', null, '', get_string('nopermissions', 'error', get_string('configplugin',
+ 'repository_nextcloud')));
+ return false;
+ }
+
+ // Load configured issuers.
+ $issuers = core\oauth2\api::get_all_issuers();
+ $types = array();
+
+ // Validates which issuers implement the right endpoints. WebDav is necessary for Nextcloud.
+ $validissuers = [];
+ foreach ($issuers as $issuer) {
+ $types[$issuer->get('id')] = $issuer->get('name');
+ if (\repository_nextcloud\issuer_management::is_valid_issuer($issuer)) {
+ $validissuers[] = $issuer->get('name');
+ }
+ }
+
+ // Render the form.
+ $url = new \moodle_url('/admin/tool/oauth2/issuers.php');
+ $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_nextcloud', $url->out()));
+
+ $mform->addElement('select', 'issuerid', get_string('chooseissuer', 'repository_nextcloud'), $types);
+ $mform->addRule('issuerid', get_string('required'), 'required', null, 'issuer');
+ $mform->addHelpButton('issuerid', 'chooseissuer', 'repository_nextcloud');
+ $mform->setType('issuerid', PARAM_INT);
+
+ // All issuers that are valid are displayed seperately (if any).
+ if (count($validissuers) === 0) {
+ $mform->addElement('static', null, '', get_string('no_right_issuers', 'repository_nextcloud'));
+ } else {
+ $mform->addElement('static', null, '', get_string('right_issuers', 'repository_nextcloud',
+ implode(', ', $validissuers)));
+ }
+
+ $mform->addElement('text', 'controlledlinkfoldername', get_string('foldername', 'repository_nextcloud'));
+ $mform->addHelpButton('controlledlinkfoldername', 'foldername', 'repository_nextcloud');
+ $mform->setType('controlledlinkfoldername', PARAM_TEXT);
+ $mform->setDefault('controlledlinkfoldername', 'Moodlefiles');
+
+ $mform->addElement('static', null, '', get_string('fileoptions', 'repository_nextcloud'));
+ $choices = [
+ 'both' => get_string('both', 'repository_nextcloud'),
+ 'internal' => get_string('internal', 'repository_nextcloud'),
+ 'external' => get_string('external', 'repository_nextcloud'),
+ ];
+ $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_nextcloud'), $choices);
+
+ $choices = [
+ FILE_INTERNAL => get_string('internal', 'repository_nextcloud'),
+ FILE_CONTROLLED_LINK => get_string('external', 'repository_nextcloud'),
+ ];
+ $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_nextcloud'), $choices);
+ }
+
+ /**
+ * Save settings for repository instance
+ *
+ * @param array $options settings
+ * @return bool
+ */
+ public function set_option($options = array()) {
+ $options['issuerid'] = clean_param($options['issuerid'], PARAM_INT);
+ $options['controlledlinkfoldername'] = clean_param($options['controlledlinkfoldername'], PARAM_TEXT);
+
+ $ret = parent::set_option($options);
+ return $ret;
+ }
+
+ /**
+ * Names of the plugin settings
+ *
+ * @return array
+ */
+ public static function get_instance_option_names() {
+ return ['issuerid', 'controlledlinkfoldername',
+ 'defaultreturntype', 'supportedreturntypes'];
+ }
+
+ /**
+ * Method to define which file-types are supported (hardcoded can not be changed in Admin Menu)
+ * By default FILE_INTERNAL is supported. In case a system account is connected and an issuer exist,
+ * FILE_CONTROLLED_LINK is supported.
+ * FILE_INTERNAL - the file is uploaded/downloaded and stored directly within the Moodle file system.
+ * FILE_CONTROLLED_LINK - creates a copy of the file in Nextcloud from which private shares to permitted users will be
+ * created. The file itself can not be changed any longer by the owner.
+ * @return int return type bitmask supported
+ */
+ public function supported_returntypes() {
+ // We can only support access controlled links if the system account is connected.
+ $setting = $this->get_option('supportedreturntypes');
+ $sysisconnected = !empty($this->issuer) && $this->issuer->is_system_account_connected();
+ if ($setting === 'internal') {
+ return FILE_INTERNAL;
+ }
+ if ($setting === 'external') {
+ if ($sysisconnected) {
+ return FILE_CONTROLLED_LINK | FILE_REFERENCE | FILE_EXTERNAL;
+ }
+ return FILE_REFERENCE | FILE_EXTERNAL;
+ }
+ // Otherwise all of them are supported (controlled link only with system account).
+ if ($sysisconnected) {
+ return FILE_CONTROLLED_LINK | FILE_INTERNAL | FILE_REFERENCE | FILE_EXTERNAL;
+ }
+ return FILE_INTERNAL | FILE_REFERENCE | FILE_EXTERNAL;
+
+ }
+
+ /**
+ * Take the WebDAV `ls()' output and convert it into a format that Moodle's filepicker understands.
+ *
+ * @param string $dirpath Relative (urlencoded) path of the folder of interest.
+ * @param array $ls Output by WebDAV
+ * @return array Moodle-formatted list of directory contents; ready for use as $ret['list'] in get_listings
+ */
+ private function get_listing_convert_response($dirpath, $ls) {
+ global $OUTPUT;
+ $folders = array();
+ $files = array();
+ $parsedurl = issuer_management::parse_endpoint_url('webdav', $this->issuer);
+ $basepath = rtrim('/' . ltrim($parsedurl['path'], '/ '), '/ ');
+
+ foreach ($ls as $item) {
+ if (!empty($item['lastmodified'])) {
+ $item['lastmodified'] = strtotime($item['lastmodified']);
+ } else {
+ $item['lastmodified'] = null;
+ }
+
+ // Extracting object title from absolute path: First remove Nextcloud basepath.
+ $item['href'] = substr(urldecode($item['href']), strlen($basepath));
+ // Then remove relative path to current folder.
+ $title = substr($item['href'], strlen($dirpath));
+
+ if (!empty($item['resourcetype']) && $item['resourcetype'] == 'collection') {
+ // A folder.
+ if ($dirpath == $item['href']) {
+ // Skip "." listing.
+ continue;
+ }
+
+ $folders[strtoupper($title)] = array(
+ 'title' => rtrim($title, '/'),
+ 'thumbnail' => $OUTPUT->image_url(file_folder_icon(90))->out(false),
+ 'children' => array(),
+ 'datemodified' => $item['lastmodified'],
+ 'path' => $item['href']
+ );
+ } else {
+ // A file.
+ $size = !empty($item['getcontentlength']) ? $item['getcontentlength'] : '';
+ $files[strtoupper($title)] = array(
+ 'title' => $title,
+ 'thumbnail' => $OUTPUT->image_url(file_extension_icon($title, 90))->out(false),
+ 'size' => $size,
+ 'datemodified' => $item['lastmodified'],
+ 'source' => $item['href']
+ );
+ }
+ }
+ ksort($files);
+ ksort($folders);
+ return array_merge($folders, $files);
+ }
+ /**
+ * Print the login in a popup.
+ *
+ * @param array|null $attr Custom attributes to be applied to popup div.
+ */
+ private function print_login_popup($attr = null) {
+ global $OUTPUT;
+
+ $this->client = $this->get_user_oauth_client();
+ $url = new moodle_url($this->client->get_login_url());
+ $state = $url->get_param('state') . '&reloadparent=true';
+ $url->param('state', $state);
+
+ echo $OUTPUT->header();
+
+ $repositoryname = get_string('pluginname', 'repository_nextcloud');
+
+ $button = new single_button($url, get_string('logintoaccount', 'repository', $repositoryname),
+ 'post', true);
+ $button->add_action(new popup_action('click', $url, 'Login'));
+ $button->class = 'mdl-align';
+ $button = $OUTPUT->render($button);
+ echo html_writer::div($button, '', $attr);
+
+ echo $OUTPUT->footer();
+ }
+ /**
+ * Prepare response of get_listing; namely
+ * - defining setting elements,
+ * - filling in the parent path of the currently-viewed directory.
+ * @param string $path Relative path
+ * @return array ret array for use as get_listing's $ret
+ */
+ private function get_listing_prepare_response($path) {
+ $ret = [
+ // Fetch the list dynamically. An AJAX request is sent to the server as soon as the user opens a folder.
+ 'dynload' => true,
+ 'nosearch' => true, // Disable search.
+ 'nologin' => false, // Provide a login link because a user logs into his/her private Nextcloud storage.
+ 'path' => array([ // Contains all parent paths to the current path.
+ 'name' => $this->get_meta()->name,
+ 'path' => '',
+ ]),
+ 'defaultreturntype' => $this->default_returntype(),
+ 'manage' => $this->issuer->get('baseurl'), // Provide button to go into file management interface quickly.
+ 'list' => array(), // Contains all file/folder information and is required to build the file/folder tree.
+ ];
+
+ // If relative path is a non-top-level path, calculate all its parents' paths.
+ // This is used for navigation in the file picker.
+ if ($path != '/') {
+ $chunks = explode('/', trim($path, '/'));
+ $parent = '/';
+ // Every sub-path to the last part of the current path is a parent path.
+ foreach ($chunks as $chunk) {
+ $subpath = $parent . $chunk . '/';
+ $ret['path'][] = [
+ 'name' => urldecode($chunk),
+ 'path' => $subpath
+ ];
+ // Prepare next iteration.
+ $parent = $subpath;
+ }
+ }
+ return $ret;
+ }
+}
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 32 32"><rect rx="5" ry="5" height="32" width="32" fill="#0082c9"/><path style="text-decoration-color:#000;isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-indent:0;text-transform:none;text-decoration-style:solid" d="M16.023 9.342c-3.126 0-5.75 2.14-6.552 5.02-.7-1.54-2.24-2.632-4.03-2.632-2.436 0-4.44 2.004-4.44 4.44 0 2.438 2.003 4.442 4.44 4.442 1.79 0 3.33-1.092 4.03-2.632.802 2.88 3.427 5.02 6.553 5.02 3.11 0 5.72-2.117 6.538-4.972.713 1.512 2.23 2.584 4 2.584 2.437 0 4.44-2.004 4.44-4.442 0-2.437-2.004-4.44-4.44-4.44-1.77 0-3.288 1.072-4 2.584-.818-2.855-3.428-4.972-6.537-4.972zm0 2.607a4.203 4.203 0 0 1 4.223 4.22 4.203 4.203 0 0 1-4.223 4.223A4.203 4.203 0 0 1 11.8 16.17a4.203 4.203 0 0 1 4.223-4.22zM5.44 14.336c1.028 0 1.834.805 1.834 1.834a1.815 1.815 0 0 1-1.834 1.835 1.815 1.815 0 0 1-1.834-1.834c0-1.028.806-1.833 1.834-1.833zm21.12 0c1.027 0 1.833.805 1.834 1.834 0 1.03-.806 1.835-1.835 1.835a1.815 1.815 0 0 1-1.835-1.834c0-1.028.806-1.833 1.834-1.833z" fill="#fff" color="#000" white-space="normal"/></svg>
\ No newline at end of file
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the repository_nextcloud class.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use core\oauth2\system_account;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/webdavlib.php');
+require_once($CFG->dirroot . '/repository/nextcloud/tests/fixtures/testable_access_controlled_link_manager.php');
+
+/**
+ * Class repository_nextcloud_testcase
+ * @group repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class repository_nextcloud_access_controlled_link_manager_testcase extends advanced_testcase {
+
+ /** @var null|testable_access_controlled_link_manager a malleable variant of the access_controlled_link_manager. */
+ public $linkmanager = null;
+
+ /** @var null|\repository_nextcloud\ocs_client The ocs_client used to send requests. */
+ public $ocsmockclient = null;
+
+ /** @var null|\core\oauth2\client Mock oauth client for the system account. */
+ private $oauthsystemmock = null;
+
+ /** @var null|\core\oauth2\issuer which belongs to the repository_nextcloud object. */
+ public $issuer = null;
+
+ /**
+ * SetUp to create an repository instance.
+ */
+ protected function setUp() {
+ $this->resetAfterTest(true);
+
+ // Admin is necessary to create issuer object.
+ $this->setAdminUser();
+
+ $generator = $this->getDataGenerator()->get_plugin_generator('repository_nextcloud');
+ $this->issuer = $generator->test_create_issuer();
+ $generator->test_create_endpoints($this->issuer->get('id'));
+
+ // Mock clients.
+ $this->ocsmockclient = $this->getMockBuilder(repository_nextcloud\ocs_client::class
+ )->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $this->oauthsystemmock = $this->getMockBuilder(\core\oauth2\client::class
+ )->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $systemwebdavclient = $this->getMockBuilder(\webdav_client::class
+ )->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $systemocsclient = $systemocsclient = $this->getMockBuilder(repository_nextcloud\ocs_client::class
+ )->disableOriginalConstructor()->disableOriginalClone()->getMock();
+
+ // Pseudo system account user.
+ $this->systemaccountusername = 'pseudouser';
+ $record = new stdClass();
+ $record->issuerid = $this->issuer->get('id');
+ $record->refreshtoken = 'pseudotoken';
+ $record->grantedscopes = 'scopes';
+ $record->email = '';
+ $record->username = $this->systemaccountusername;
+ $systemaccount = new system_account(0, $record);
+ $systemaccount->create();
+
+ $this->linkmanager = new testable_access_controlled_link_manager($this->ocsmockclient,
+ $this->oauthsystemmock, $systemocsclient,
+ $this->issuer, 'Nextcloud', $systemwebdavclient);
+
+ }
+
+ /**
+ * Function to test the private function create_share_user_sysaccount.
+ */
+ public function test_create_share_user_sysaccount_user_shares() {
+ $params = [
+ 'path' => "/ambient.txt",
+ 'shareType' => \repository_nextcloud\ocs_client::SHARE_TYPE_USER,
+ 'publicUpload' => false,
+ 'shareWith' => $this->systemaccountusername,
+ 'permissions' => \repository_nextcloud\ocs_client::SHARE_PERMISSION_READ,
+ ];
+ $expectedresponse = <<<XML
+<?xml version="1.0"?>
+<ocs>
+ <meta>
+ <status>ok</status>
+ <statuscode>100</statuscode>
+ <message/>
+ </meta>
+ <data>
+ <id>207</id>
+ <share_type>0</share_type>
+ <uid_owner>user1</uid_owner>
+ <displayname_owner>user1</displayname_owner>
+ <permissions>19</permissions>
+ <stime>1511532198</stime>
+ <parent/>
+ <expiration/>
+ <token/>
+ <uid_file_owner>user1</uid_file_owner>
+ <displayname_file_owner>user1</displayname_file_owner>
+ <path>/ambient.txt</path>
+ <item_type>file</item_type>
+ <mimetype>text/plain</mimetype>
+ <storage_id>home::user1</storage_id>
+ <storage>3</storage>
+ <item_source>545</item_source>
+ <file_source>545</file_source>
+ <file_parent>20</file_parent>
+ <file_target>/ambient.txt</file_target>
+ <share_with>tech</share_with>
+ <share_with_displayname>tech</share_with_displayname>
+ <mail_send>0</mail_send>
+ </data>
+</ocs>
+XML;
+ $this->ocsmockclient->expects($this->once())->method('call')->with('create_share', $params)->will(
+ $this->returnValue($expectedresponse));
+
+ $result = $this->linkmanager->create_share_user_sysaccount("/ambient.txt");
+ $xml = simplexml_load_string($expectedresponse);
+ $expected = array();
+ $expected['statuscode'] = (int)$xml->meta->statuscode;
+ $expected['shareid'] = (int)$xml->data->id;
+ $expected['filetarget'] = (string)$xml->data[0]->file_target;
+ $this->assertEquals($expected, $result);
+ }
+ /**
+ * Test the delete_share_function. In case the request fails, the function throws an exception, however this
+ * can not be tested in phpUnit since it is javascript.
+ */
+ public function test_delete_share_dataowner_sysaccount() {
+ $shareid = 5;
+ $deleteshareparams = [
+ 'share_id' => $shareid
+ ];
+ $returnxml = <<<XML
+<?xml version="1.0"?>
+<ocs>
+ <meta>
+ <status>ok</status>
+ <statuscode>100</statuscode>
+ <message/>
+ </meta>
+ <data/>
+</ocs>
+XML;
+ $this->ocsmockclient->expects($this->once())->method('call')->with('delete_share', $deleteshareparams)->will(
+ $this->returnValue($returnxml));
+ $this->linkmanager->delete_share_dataowner_sysaccount($shareid, 'repository_nextcloud');
+
+ }
+
+ /**
+ * Function which test that create folder path does return the adequate results (path and success).
+ * Additionally mock checks whether the right params are passed to the corresponding functions.
+ */
+ public function test_create_folder_path_folders_are_not_created() {
+
+ $mocks = $this->set_up_mocks_for_create_folder_path(true, 'somename');
+ $this->set_private_property($mocks['mockclient'], 'systemwebdavclient', $this->linkmanager);
+ $result = $this->linkmanager->create_folder_path_access_controlled_links($mocks['mockcontext'], "mod_resource",
+ 'content', 0);
+ $this->assertEquals('/somename (ctx )/mod_resource/content/0', $result);
+ }
+ /**
+ * Function which test that create folder path does return the adequate results (path and success).
+ * Additionally mock checks whether the right params are passed to the corresponding functions.
+ */
+ public function test_create_folder_path_folders_are_created() {
+
+ // In Context is okay, number of context counts for number of iterations.
+ $mocks = $this->set_up_mocks_for_create_folder_path(false, 'somename/withslash', true, 201);
+ $this->set_private_property($mocks['mockclient'], 'systemwebdavclient', $this->linkmanager);
+ $result = $this->linkmanager->create_folder_path_access_controlled_links($mocks['mockcontext'], "mod_resource",
+ 'content', 0);
+ $this->assertEquals('/somenamewithslash (ctx )/mod_resource/content/0', $result);
+ }
+ /**
+ * Test whether the create_folder_path methode throws exception.
+ */
+ public function test_create_folder_path_folder_creation_fails() {
+
+ $mocks = $this->set_up_mocks_for_create_folder_path(false, 'somename', true, 400);
+ $this->set_private_property($mocks['mockclient'], 'systemwebdavclient', $this->linkmanager);
+ $this->expectException(\repository_nextcloud\request_exception::class);
+ $this->linkmanager->create_folder_path_access_controlled_links($mocks['mockcontext'], "mod_resource",
+ 'content', 0);
+ }
+
+ /**
+ * Helper function to generate mocks for testing create folder path.
+ * @param bool $returnisdir Return value mocking the result of invoking is_dir
+ * @param bool $returnestedcontext Name of the folder that is simulated to be checked/created
+ * @param bool $callmkcol Also mock creation of the folder
+ * @param int $returnmkcol Return value mocking the result of invoking mkcol
+ * @return array ['mockcontext' context_module mock, 'mockclient' => webdav client mock]
+ */
+ protected function set_up_mocks_for_create_folder_path($returnisdir, $returnestedcontext, $callmkcol = false,
+ $returnmkcol = 201) {
+ $mockcontext = $this->createMock(context_module::class);
+ $mockclient = $this->getMockBuilder(\webdav_client::class
+ )->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $parsedwebdavurl = parse_url($this->issuer->get_endpoint_url('webdav'));
+ $webdavprefix = $parsedwebdavurl['path'];
+ // Empty ctx 'id' expected because using code will not be able to access $ctx->id.
+ $cleanedcontextname = clean_param($returnestedcontext, PARAM_FILE);
+ $dirstring = $webdavprefix . '/' . $cleanedcontextname . ' (ctx )';
+ $mockclient->expects($this->atMost(4))->method('is_dir')->with($this->logicalOr(
+ $dirstring, $dirstring . '/mod_resource', $dirstring . '/mod_resource/content',
+ $dirstring . '/mod_resource/content/0'))->willReturn($returnisdir);
+ if ($callmkcol == true) {
+ $mockclient->expects($this->atMost(4))->method('mkcol')->willReturn($returnmkcol);
+ }
+ $mockcontext->method('get_parent_contexts')->willReturn(array('1' => $mockcontext));
+ $mockcontext->method('get_context_name')->willReturn($returnestedcontext);
+
+ return array('mockcontext' => $mockcontext, 'mockclient' => $mockclient);
+ }
+
+ /**
+ * Test whether the right methods from the webdavclient are called when the storage_folder is created.
+ * 1. Directory already exist -> no further action needed.
+ */
+ public function test_create_storage_folder_success() {
+ $mockwebdavclient = $this->createMock(\webdav_client::class);
+ $url = $this->issuer->get_endpoint_url('webdav');
+ $parsedwebdavurl = parse_url($url);
+ $webdavprefix = $parsedwebdavurl['path'];
+ $mockwebdavclient->expects($this->once())->method('open')->willReturn(true);
+ $mockwebdavclient->expects($this->once())->method('is_dir')->with($webdavprefix . 'myname')->willReturn(true);
+ $mockwebdavclient->expects($this->once())->method('close');
+ $this->linkmanager->create_storage_folder('myname', $mockwebdavclient);
+
+ }
+ /**
+ * Test whether the right methods from the webdavclient are called when the storage_folder is created.
+ * 2. Directory does not exist. It is created with mkcol and returns a success.
+ *
+ */
+ public function test_create_storage_folder_success_mkcol() {
+ $mockwebdavclient = $this->createMock(\webdav_client::class);
+ $url = $this->issuer->get_endpoint_url('webdav');
+ $parsedwebdavurl = parse_url($url);
+ $webdavprefix = $parsedwebdavurl['path'];
+ $mockwebdavclient->expects($this->once())->method('open')->willReturn(true);
+ $mockwebdavclient->expects($this->once())->method('is_dir')->with($webdavprefix . 'myname')->willReturn(false);
+ $mockwebdavclient->expects($this->once())->method('mkcol')->with($webdavprefix . 'myname')->willReturn(201);
+ $mockwebdavclient->expects($this->once())->method('close');
+
+ $this->linkmanager->create_storage_folder('myname', $mockwebdavclient);
+ }
+ /**
+ * Test whether the right methods from the webdavclient are called when the storage_folder is created.
+ * 3. Request to create Folder fails.
+ */
+ public function test_create_storage_folder_failure() {
+ $mockwebdavclient = $this->createMock(\webdav_client::class);
+ $url = $this->issuer->get_endpoint_url('webdav');
+ $parsedwebdavurl = parse_url($url);
+ $webdavprefix = $parsedwebdavurl['path'];
+ $mockwebdavclient->expects($this->once())->method('open')->willReturn(true);
+ $mockwebdavclient->expects($this->once())->method('is_dir')->with($webdavprefix . 'myname')->willReturn(false);
+ $mockwebdavclient->expects($this->once())->method('mkcol')->with($webdavprefix . 'myname')->willReturn(400);
+
+ $this->expectException(\repository_nextcloud\request_exception::class);
+ $this->linkmanager->create_storage_folder('myname', $mockwebdavclient);
+ }
+ /**
+ * Test whether the webdav client gets the right params and whether function differentiates between move and copy.
+ */
+ public function test_transfer_file_to_path_copyfile() {
+ // Initialize params.
+ $parsedwebdavurl = parse_url($this->issuer->get_endpoint_url('webdav'));
+ $webdavprefix = $parsedwebdavurl['path'];
+ $srcpath = 'sourcepath';
+ $dstpath = "destinationpath/another/path";
+
+ // Mock the Webdavclient and set expected methods.
+ $systemwebdavclientmock = $this->createMock(\webdav_client::class);
+ $systemwebdavclientmock->expects($this->once())->method('open')->willReturn(true);
+ $systemwebdavclientmock->expects($this->once())->method('copy_file')->with($webdavprefix . $srcpath,
+ $webdavprefix . $dstpath . '/' . $srcpath, true)->willReturn(201);
+ $this->set_private_property($systemwebdavclientmock, 'systemwebdavclient', $this->linkmanager);
+
+ // Call of function.
+ $result = $this->linkmanager->transfer_file_to_path($srcpath, $dstpath, 'copy');
+
+ $this->assertEquals(201, $result);
+ }
+ /**
+ * This function tests whether the function transfer_file_to_path() moves or copies a given file to a given path
+ * It tests whether the webdav_client gets the right parameter and whether function distinguishes between move and copy.
+ *
+ */
+ public function test_transfer_file_to_path_copyfile_movefile() {
+ // Initialize params.
+ $parsedwebdavurl = parse_url($this->issuer->get_endpoint_url('webdav'));
+ $webdavprefix = $parsedwebdavurl['path'];
+ $srcpath = 'sourcepath';
+ $dstpath = "destinationpath/another/path";
+
+ $systemwebdavclientmock = $this->createMock(\webdav_client::class);
+
+ $systemwebdavclientmock->expects($this->once())->method('open')->willReturn(true);
+ $this->set_private_property($systemwebdavclientmock, 'systemwebdavclient', $this->linkmanager);
+ $webdavclientmock = $this->createMock(\webdav_client::class);
+
+ $webdavclientmock->expects($this->once())->method('move')->with($webdavprefix . $srcpath,
+ $webdavprefix . $dstpath . '/' . $srcpath, false)->willReturn(201);
+ $result = $this->linkmanager->transfer_file_to_path($srcpath, $dstpath, 'move', $webdavclientmock);
+ $this->assertEquals(201, $result);
+ }
+
+ /**
+ * Test the get_shares_from path() function. This function extracts from an list of shares the share of a given user
+ * (the username is a parameter in the function call) and returns the id. The test firstly test whether the right fileid
+ * for user1 is extracted then for user2 and last but least whether an error is thrown if the user does not have a share.
+ * @throws moodle_exception
+ */
+ public function test_get_shares_from_path() {
+ $params = [
+ 'path' => '/Kernsystem/Kursbereich Miscellaneous/Kurs Example Course/Datei zet/mod_resource/content/0/picture.png',
+ 'reshares' => true
+ ];
+ $reference = new stdClass();
+ $reference->link = "/Kernsystem/Kursbereich Miscellaneous/Kurs Example Course/Datei zet/mod_resource/content/0/picture.png";
+ $reference->name = "f\u00fcrdennis.png";
+ $reference->usesystem = true;
+ $expectedresponse = <<<XML
+<?xml version="1.0"?>
+<ocs>
+ <meta>
+ <status>ok</status>
+ <statuscode>100</statuscode>
+ <message/>
+ </meta>
+ <data>
+ <element>
+ <id>292</id>
+ <share_type>0</share_type>
+ <uid_owner>tech</uid_owner>
+ <displayname_owner>tech</displayname_owner>
+ <permissions>19</permissions>
+ <stime>1515752494</stime>
+ <parent/>
+ <expiration/>
+ <token/>
+ <uid_file_owner>tech</uid_file_owner>
+ <displayname_file_owner>tech</displayname_file_owner>
+ <path>some/path/of/some/file.pdf</path>
+ <item_type>file</item_type>
+ <mimetype>image/png</mimetype>
+ <storage_id>home::tech</storage_id>
+ <storage>4</storage>
+ <item_source>1085</item_source>
+ <file_source>1085</file_source>
+ <file_parent>1084</file_parent>
+ <file_target>/fehler (3).png</file_target>
+ <share_with>user1</share_with>
+ <share_with_displayname>user1</share_with_displayname>
+ <mail_send>0</mail_send>
+ </element>
+ <element>
+ <id>293</id>
+ <share_type>0</share_type>
+ <uid_owner>tech</uid_owner>
+ <displayname_owner>tech</displayname_owner>
+ <permissions>19</permissions>
+ <stime>1515752494</stime>
+ <parent/>
+ <expiration/>
+ <token/>
+ <uid_file_owner>tech</uid_file_owner>
+ <displayname_file_owner>tech</displayname_file_owner>
+ <path>some/path/of/some/file.pdf</path>
+ <item_type>file</item_type>
+ <mimetype>image/png</mimetype>
+ <storage_id>home::tech</storage_id>
+ <storage>4</storage>
+ <item_source>1085</item_source>
+ <file_source>1085</file_source>
+ <file_parent>1084</file_parent>
+ <file_target>/fehler (3).png</file_target>
+ <share_with>user2</share_with>
+ <share_with_displayname>user2</share_with_displayname>
+ <mail_send>0</mail_send>
+ </element>
+ </data>
+</ocs>
+XML;
+ $this->set_private_property($this->ocsmockclient, 'systemocsclient', $this->linkmanager);
+
+ $this->ocsmockclient->expects($this->exactly(3))->method('call')->with('get_shares', $params)->will(
+ $this->returnValue($expectedresponse));
+ $xmlobjuser1 = (int) $this->linkmanager->get_shares_from_path($reference->link, 'user2');
+ $xmlobjuser2 = (int) $this->linkmanager->get_shares_from_path($reference->link, 'user1');
+
+ $this->assertEquals(293, $xmlobjuser1);
+ $this->assertEquals(292, $xmlobjuser2);
+
+ $this->expectException(\repository_nextcloud\request_exception::class);
+
+ $this->expectExceptionMessage('A request to Nextcloud has failed. The requested file could not be accessed. Please ' .
+ 'check whether you have chosen a valid file and you are authenticated with the right account.');
+ $this->linkmanager->get_shares_from_path($reference->link, 'user3');
+
+ }
+ /** Test whether the systemwebdav client is constructed correctly. Port is set to 443 in case of https, to 80 in
+ * case of http and exception is thrown when endpoint does not exist.
+ * @throws \repository_nextcloud\configuration_exception
+ * @throws coding_exception
+ */
+ public function test_create_system_dav() {
+ // Initialize mock and params.
+ $fakeaccesstoken = new stdClass();
+ $fakeaccesstoken->token = "fake access token";
+ // Use `atLeastOnce` instead of `exactly(2)` because it is only called a second time on dev systems that allow http://.
+ $this->oauthsystemmock->expects($this->atLeastOnce())->method('get_accesstoken')->willReturn($fakeaccesstoken);
+ $parsedwebdavurl = parse_url($this->issuer->get_endpoint_url('webdav'));
+
+ // Call function and create own client.
+ $dav = $this->linkmanager->create_system_dav();
+ $mydav = new \webdav_client($parsedwebdavurl['host'], '', '', 'bearer', 'ssl://',
+ "fake access token", $parsedwebdavurl['path']);
+ $mydav->port = 443;
+ $mydav->debug = false;
+ $this->assertEquals($mydav, $dav);
+
+ // Deletes the old webdav endpoint and ...
+ $this->delete_endpoints('webdav_endpoint');
+ // Creates a new one which requires different ports.
+ try {
+ $endpoint = new stdClass();
+ $endpoint->name = "webdav_endpoint";
+ $endpoint->url = 'http://www.default.test/webdav/index.php';
+ $endpoint->issuerid = $this->issuer->get('id');
+ \core\oauth2\api::create_endpoint($endpoint);
+
+ // Call function and create own client.
+ $dav = $this->linkmanager->create_system_dav();
+ $mydav = new \webdav_client($parsedwebdavurl['host'], '', '', 'bearer', '',
+ "fake access token");
+ $mydav->port = 80;
+ $mydav->debug = false;
+ $this->assertEquals($mydav, $dav);
+ } catch (core\invalid_persistent_exception $e) {
+ // In some cases Moodle does not allow to create http connections. In those cases the exception
+ // is catched here and the test are executed.
+ $this->expectException(\core\invalid_persistent_exception::class);
+ $this->linkmanager->create_system_dav();
+ } finally {
+
+ // Delte endpoints and ...
+ $this->delete_endpoints('webdav_endpoint');
+
+ // Do not insert new ones, therefore exception is thrown.
+ $this->expectException(\repository_nextcloud\configuration_exception::class);
+ $this->linkmanager->create_system_dav();
+ }
+ }
+
+ /**
+ * Tests the function get_share_information_from_shareid(). From a response with two element it is tested
+ * whether the right file_target is extracted and lastly it is checked whether an error is thrown in case no suitable
+ * element exists.
+ * @throws \repository_nextcloud\request_exception
+ * @throws coding_exception
+ */
+ public function test_get_share_information_from_shareid() {
+ $params303 = [
+ 'share_id' => 303,
+ ];
+ $params302 = [
+ 'share_id' => 302,
+ ];
+ $expectedresponse = <<<XML
+<?xml version="1.0"?>
+<ocs>
+ <meta>
+ <status>ok</status>
+ <statuscode>100</statuscode>
+ <message/>
+ </meta>
+ <data>
+ <element>
+ <id>302</id>
+ <share_type>0</share_type>
+ <uid_owner>tech</uid_owner>
+ <displayname_owner>tech</displayname_owner>
+ <permissions>19</permissions>
+ <stime>1516096325</stime>
+ <parent/>
+ <expiration/>
+ <token/>
+ <uid_file_owner>tech</uid_file_owner>
+ <displayname_file_owner>tech</displayname_file_owner>
+ <path>/some/target (2).png</path>
+ <item_type>file</item_type>
+ <mimetype>image/png</mimetype>
+ <storage_id>shared::/some/target.png</storage_id>
+ <storage>4</storage>
+ <item_source>1125</item_source>
+ <file_source>1125</file_source>
+ <file_parent>20</file_parent>
+ <file_target>/some/target.png</file_target>
+ <share_with>user1</share_with>
+ <share_with_displayname>user1</share_with_displayname>
+ <mail_send>0</mail_send>
+ </element>
+ <element>
+ <id>303</id>
+ <share_type>0</share_type>
+ <uid_owner>tech</uid_owner>
+ <displayname_owner>tech</displayname_owner>
+ <permissions>19</permissions>
+ <stime>1516096325</stime>
+ <parent/>
+ <expiration/>
+ <token/>
+ <uid_file_owner>tech</uid_file_owner>
+ <displayname_file_owner>tech</displayname_file_owner>
+ <path>/some/target (2).pdf</path>
+ <item_type>file</item_type>
+ <mimetype>image/png</mimetype>
+ <storage_id>shared::/some/target.pdf</storage_id>
+ <storage>4</storage>
+ <item_source>1125</item_source>
+ <file_source>1125</file_source>
+ <file_parent>20</file_parent>
+ <file_target>/some/target.pdf</file_target>
+ <share_with>user2</share_with>
+ <share_with_displayname>user1</share_with_displayname>
+ <mail_send>0</mail_send>
+ </element>
+ </data>
+</ocs>
+XML;
+ $this->set_private_property($this->ocsmockclient, 'systemocsclient', $this->linkmanager);
+
+ $this->ocsmockclient->expects($this->exactly(3))->method('call')->with('get_information_of_share',
+ $this->logicalOr($params303, $params302))->will($this->returnValue($expectedresponse));
+
+ // Test function for two different users. Setting the id is just a dummy value since always $expectedresponse ...
+ // ... is returned.
+ $filetarget = $this->linkmanager->get_share_information_from_shareid(303, 'user2');
+ $this->assertEquals('/some/target.pdf', $filetarget);
+
+ $filetarget = $this->linkmanager->get_share_information_from_shareid(302, 'user1');
+ $this->assertEquals('/some/target.png', $filetarget);
+
+ // Expect exception in case no suitable elemtn exist in the response.
+ $this->expectException(\repository_nextcloud\request_exception::class);
+ $this->expectExceptionMessage('A request to Nextcloud has failed. The requested file could not be accessed. Please ' .
+ 'check whether you have chosen a valid file and you are authenticated with the right account.');
+ $this->linkmanager->get_share_information_from_shareid(302, 'user3');
+ }
+
+ /**
+ * Helper method which inserts a value into a non-public field of an object.
+ *
+ * @param mixed $value mock value that will be inserted.
+ * @param string $propertyname name of the private property.
+ * @param object $class Instance that is being modified.
+ * @return ReflectionProperty the resulting reflection property.
+ */
+ protected function set_private_property($value, $propertyname, $class) {
+ $refclient = new ReflectionClass($class);
+ $private = $refclient->getProperty($propertyname);
+ $private->setAccessible(true);
+ $private->setValue($class, $value);
+ return $private;
+ }
+ /**
+ * Helper method which gets a value from a non-public field of an object.
+ *
+ * @param string $propertyname name of the private property.
+ * @param object $class Instance that is being modified.
+ * @return mixed the resulting value.
+ */
+ protected function get_private_property($propertyname, $class) {
+ $refclient = new ReflectionClass($class);
+ $private = $refclient->getProperty($propertyname);
+ $private->setAccessible(true);
+ $property = $private->getValue($private);
+ return $property;
+ }
+ /**
+ * Deletes all endpoint with the given name.
+ * @param string $endpointname
+ * @return array|null
+ * @throws moodle_exception
+ */
+ protected function delete_endpoints($endpointname) {
+ $endpoints = \core\oauth2\api::get_endpoints($this->issuer);
+ $arrayofids = array();
+ foreach ($endpoints as $endpoint) {
+ $name = $endpoint->get('name');
+ if ($name === $endpointname) {
+ $arrayofids[$endpoint->get('id')] = $endpoint->get('id');
+ }
+ }
+ if (empty($arrayofids)) {
+ return;
+ }
+ foreach ($arrayofids as $id) {
+ core\oauth2\api::delete_endpoint($id);
+ }
+ }
+
+}
\ No newline at end of file
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Test support class for testing access_controlled_link_manager.
+ *
+ * @package repository_nextcloud
+ * @copyright 2018 Nina Herrmann (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+use core\oauth2\client;
+use repository_nextcloud\access_controlled_link_manager;
+use repository_nextcloud\ocs_client;
+
+/**
+ * Test support class for testing access_controlled_link_manager.
+ *
+ * @package repository_nextcloud
+ * @copyright 2018 Nina Herrmann (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_access_controlled_link_manager extends access_controlled_link_manager {
+
+ /**
+ * Access_controlled_link_manager constructor.
+ * @param ocs_client $ocsclient
+ * @param client $systemoauthclient
+ * @param ocs_client $systemocsclient
+ * @param \core\oauth2\issuer $issuer
+ * @param string $repositoryname
+ * @param \webdav_client $systemdav
+ */
+ public function __construct($ocsclient, $systemoauthclient, $systemocsclient, \core\oauth2\issuer $issuer, $repositoryname,
+ $systemdav) {
+ $this->ocsclient = $ocsclient;
+ $this->systemoauthclient = $systemoauthclient;
+ $this->systemocsclient = $systemocsclient;
+ $this->repositoryname = $repositoryname;
+ $this->issuer = $issuer;
+ $this->systemwebdavclient = $systemdav;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data generator for repository plugin.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Data generator for repository plugin.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class repository_nextcloud_generator extends testing_repository_generator {
+
+ /**
+ * Creates an issuer and a user.
+ * @return \core\oauth2\issuer
+ */
+ public function test_create_issuer () {
+ $issuerdata = new stdClass();
+ $issuerdata->name = "Service";
+ $issuerdata->clientid = "Clientid";
+ $issuerdata->clientsecret = "Secret";
+ $issuerdata->loginscopes = "openid profile email";
+ $issuerdata->loginscopesoffline = "openid profile email";
+ $issuerdata->baseurl = "";
+ $issuerdata->image = "aswdf";
+
+ // Create the issuer.
+ $issuer = \core\oauth2\api::create_issuer($issuerdata);
+ return $issuer;
+ }
+
+ /**
+ * Creates the required endpoints.
+ * @param int $issuerid
+ * @return \core\oauth2\issuer
+ */
+ public function test_create_endpoints ($issuerid) {
+ $this->test_create_single_endpoint($issuerid, "ocs_endpoint");
+ $this->test_create_single_endpoint($issuerid, "authorization_endpoint");
+ $this->test_create_single_endpoint($issuerid, "webdav_endpoint", "https://www.default.test/webdav/index.php");
+ $this->test_create_single_endpoint($issuerid, "token_endpoint");
+ $this->test_create_single_endpoint($issuerid, "userinfo_endpoint");
+ }
+
+ /**
+ * Create a single endpoint.
+ *
+ * @param int $issuerid
+ * @param string $endpointtype
+ * @param string $url
+ * @return \core\oauth2\endpoint An instantiated endpoint
+ */
+ public function test_create_single_endpoint($issuerid, $endpointtype, $url="https://www.default.test") {
+ $endpoint = new stdClass();
+ $endpoint->name = $endpointtype;
+ $endpoint->url = $url;
+ $endpoint->issuerid = $issuerid;
+ $return = \core\oauth2\api::create_endpoint($endpoint);
+ return $return;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the repository_nextcloud class.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/repository/lib.php');
+require_once($CFG->libdir . '/webdavlib.php');
+
+/**
+ * Class repository_nextcloud_lib_testcase
+ * @group repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class repository_nextcloud_lib_testcase extends advanced_testcase {
+
+ /** @var null|\repository_nextcloud the repository_nextcloud object, which the tests are run on. */
+ private $repo = null;
+
+ /** @var null|\core\oauth2\issuer which belongs to the repository_nextcloud object.*/
+ private $issuer = null;
+
+ /**
+ * SetUp to create an repository instance.
+ */
+ protected function setUp() {
+ $this->resetAfterTest(true);
+
+ // Admin is neccessary to create api and issuer objects.
+ $this->setAdminUser();
+
+ /** @var repository_nextcloud_generator $generator */
+ $generator = $this->getDataGenerator()->get_plugin_generator('repository_nextcloud');
+ $this->issuer = $generator->test_create_issuer();
+
+ // Create Endpoints for issuer.
+ $generator->test_create_endpoints($this->issuer->get('id'));
+
+ // Params for the config form.
+ $reptype = $generator->create_type([
+ 'visible' => 1,
+ 'enableuserinstances' => 0,
+ 'enablecourseinstances' => 0,
+ ]);
+
+ $instance = $generator->create_instance([
+ 'issuerid' => $this->issuer->get('id'),
+ 'pluginname' => 'Nextcloud',
+ 'controlledlinkfoldername' => 'Moodlefiles',
+ 'supportedreturntypes' => 'both',
+ 'defaultreturntype' => FILE_INTERNAL,
+ ]);
+
+ // At last, create a repository_nextcloud object from the instance id.
+ $this->repo = new repository_nextcloud($instance->id);
+ $this->repo->options['typeid'] = $reptype->id;
+ $this->repo->options['sortorder'] = 1;
+ $this->resetAfterTest(true);
+ }
+
+ /**
+ * Checks the is_visible method in case the repository is set to hidden in the database.
+ */
+ public function test_is_visible_parent_false() {
+ global $DB;
+ $id = $this->repo->options['typeid'];
+
+ // Check, if the method returns false, when the repository is set to visible in the database
+ // and the client configuration data is complete.
+ $DB->update_record('repository', (object) array('id' => $id, 'visible' => 0));
+
+ $this->assertFalse($this->repo->is_visible());
+ }
+
+ /**
+ * Test whether the repo is disabled.
+ */
+ public function test_repo_creation() {
+ $issuerid = $this->repo->get_option('issuerid');
+
+ // Config saves the right id.
+ $this->assertEquals($this->issuer->get('id'), $issuerid);
+
+ // Function that is used in construct method returns the right id.
+ $constructissuer = \core\oauth2\api::get_issuer($issuerid);
+ $this->assertEquals($this->issuer->get('id'), $constructissuer->get('id'));
+
+ $this->assertEquals(true, $constructissuer->get('enabled'));
+ $this->assertFalse($this->repo->disabled);
+ }
+
+ /**
+ * Returns an array of endpoints or null.
+ * @param string $endpointname
+ * @return array|null
+ */
+ private function get_endpoint_id($endpointname) {
+ $endpoints = \core\oauth2\api::get_endpoints($this->issuer);
+ $id = array();
+ foreach ($endpoints as $endpoint) {
+ $name = $endpoint->get('name');
+ if ($name === $endpointname) {
+ $id[$endpoint->get('id')] = $endpoint->get('id');
+ }
+ }
+ if (empty($id)) {
+ return null;
+ }
+ return $id;
+ }
+ /**
+ * Test if repository is disabled when webdav_endpoint is deleted.
+ */
+ public function test_issuer_webdav() {
+ $idwebdav = $this->get_endpoint_id('webdav_endpoint');
+ if (!empty($idwebdav)) {
+ foreach ($idwebdav as $id) {
+ \core\oauth2\api::delete_endpoint($id);
+ }
+ }
+ $this->assertFalse(\repository_nextcloud\issuer_management::is_valid_issuer($this->issuer));
+ }
+ /**
+ * Test if repository is disabled when ocs_endpoint is deleted.
+ */
+ public function test_issuer_ocs() {
+ $idocs = $this->get_endpoint_id('ocs_endpoint');
+ if (!empty($idocs)) {
+ foreach ($idocs as $id) {
+ \core\oauth2\api::delete_endpoint($id);
+ }
+ }
+ $this->assertFalse(\repository_nextcloud\issuer_management::is_valid_issuer($this->issuer));
+ }
+
+ /**
+ * Test if repository is disabled when userinfo_endpoint is deleted.
+ */
+ public function test_issuer_userinfo() {
+ $idtoken = $this->get_endpoint_id('userinfo_endpoint');
+ if (!empty($idtoken)) {
+ foreach ($idtoken as $id) {
+ \core\oauth2\api::delete_endpoint($id);
+ }
+ }
+ $this->assertFalse(\repository_nextcloud\issuer_management::is_valid_issuer($this->issuer));
+ }
+
+ /**
+ * Test if repository is disabled when token_endpoint is deleted.
+ */
+ public function test_issuer_token() {
+ $idtoken = $this->get_endpoint_id('token_endpoint');
+ if (!empty($idtoken)) {
+ foreach ($idtoken as $id) {
+ \core\oauth2\api::delete_endpoint($id);
+ }
+ }
+ $this->assertFalse(\repository_nextcloud\issuer_management::is_valid_issuer($this->issuer));
+ }
+
+ /**
+ * Test if repository is disabled when auth_endpoint is deleted.
+ */
+ public function test_issuer_authorization() {
+ $idauth = $this->get_endpoint_id('authorization_endpoint');
+ if (!empty($idauth)) {
+ foreach ($idauth as $id) {
+ \core\oauth2\api::delete_endpoint($id);
+ }
+ }
+ $this->assertFalse(\repository_nextcloud\issuer_management::is_valid_issuer($this->issuer));
+ }
+ /**
+ * Test if repository throws an error when endpoint does not exist.
+ */
+ public function test_parse_endpoint_url_error() {
+ $this->expectException(\repository_nextcloud\configuration_exception::class);
+ \repository_nextcloud\issuer_management::parse_endpoint_url('notexisting', $this->issuer);
+ }
+ /**
+ * Test get_listing method with an example directory. Tests error cases.
+ */
+ public function test_get_listing_error() {
+ $ret = $this->get_initialised_return_array();
+ $this->setUser();
+ // WebDAV socket is not opened.
+ $mock = $this->createMock(\webdav_client::class);
+ $mock->expects($this->once())->method('open')->will($this->returnValue(false));
+ $private = $this->set_private_property($mock, 'dav');
+
+ $this->assertEquals($ret, $this->repo->get_listing('/'));
+
+ // Response is not an array.
+ $mock = $this->createMock(\webdav_client::class);
+ $mock->expects($this->once())->method('open')->will($this->returnValue(true));
+ $mock->expects($this->once())->method('ls')->will($this->returnValue('notanarray'));
+ $private->setValue($this->repo, $mock);
+
+ $this->assertEquals($ret, $this->repo->get_listing('/'));
+ }
+ /**
+ * Test get_listing method with an example directory. Tests the root directory.
+ */
+ public function test_get_listing_root() {
+ $this->setUser();
+ $ret = $this->get_initialised_return_array();
+
+ // This is the expected response from the ls method.
+ $response = array(
+ array(
+ 'href' => 'remote.php/webdav/',
+ 'lastmodified' => 'Thu, 08 Dec 2016 16:06:26 GMT',
+ 'resourcetype' => 'collection',
+ 'status' => 'HTTP/1.1 200 OK',
+ 'getcontentlength' => ''
+ ),
+ array(
+ 'href' => 'remote.php/webdav/Documents/',
+ 'lastmodified' => 'Thu, 08 Dec 2016 16:06:26 GMT',
+ 'resourcetype' => 'collection',
+ 'status' => 'HTTP/1.1 200 OK',
+ 'getcontentlength' => ''
+ ),
+ array(
+ 'href' => 'remote.php/webdav/welcome.txt',
+ 'lastmodified' => 'Thu, 08 Dec 2016 16:06:26 GMT',
+ 'status' => 'HTTP/1.1 200 OK',
+ 'getcontentlength' => '163'
+ )
+ );
+
+ // The expected result from the get_listing method in the repository_nextcloud class.
+ $ret['list'] = array(
+ 'DOCUMENTS/' => array(
+ 'title' => 'Documents',
+ 'thumbnail' => null,
+ 'children' => array(),
+ 'datemodified' => 1481213186,
+ 'path' => '/Documents/'
+ ),
+ 'WELCOME.TXT' => array(
+ 'title' => 'welcome.txt',
+ 'thumbnail' => null,
+ 'size' => '163',
+ 'datemodified' => 1481213186,
+ 'source' => '/welcome.txt'
+ )
+ );
+
+ // Valid response from the client.
+ $mock = $this->createMock(\webdav_client::class);
+ $mock->expects($this->once())->method('open')->will($this->returnValue(true));
+ $mock->expects($this->once())->method('ls')->will($this->returnValue($response));
+ $this->set_private_property($mock, 'dav');
+
+ $ls = $this->repo->get_listing('/');
+
+ // Those attributes can not be tested properly.
+ $ls['list']['DOCUMENTS/']['thumbnail'] = null;
+ $ls['list']['WELCOME.TXT']['thumbnail'] = null;
+
+ $this->assertEquals($ret, $ls);
+ }
+ /**
+ * Test get_listing method with an example directory. Tests a different directory than the root
+ * directory.
+ */
+ public function test_get_listing_directory() {
+ $ret = $this->get_initialised_return_array();
+ $this->setUser();
+
+ // An additional directory path has to be added to the 'path' field within the returned array.
+ $ret['path'][1] = array(
+ 'name' => 'dir',
+ 'path' => '/dir/'
+ );
+
+ // This is the expected response from the get_listing method in the Nextcloud client.
+ $response = array(
+ array(
+ 'href' => 'remote.php/webdav/dir/',
+ 'lastmodified' => 'Thu, 08 Dec 2016 16:06:26 GMT',
+ 'resourcetype' => 'collection',
+ 'status' => 'HTTP/1.1 200 OK',
+ 'getcontentlength' => ''
+ ),
+ array(
+ 'href' => 'remote.php/webdav/dir/Documents/',
+ 'lastmodified' => null,
+ 'resourcetype' => 'collection',
+ 'status' => 'HTTP/1.1 200 OK',
+ 'getcontentlength' => ''
+ ),
+ array(
+ 'href' => 'remote.php/webdav/dir/welcome.txt',
+ 'lastmodified' => 'Thu, 08 Dec 2016 16:06:26 GMT',
+ 'status' => 'HTTP/1.1 200 OK',
+ 'getcontentlength' => '163'
+ )
+ );
+
+ // The expected result from the get_listing method in the repository_nextcloud class.
+ $ret['list'] = array(
+ 'DOCUMENTS/' => array(
+ 'title' => 'Documents',
+ 'thumbnail' => null,
+ 'children' => array(),
+ 'datemodified' => null,
+ 'path' => '/dir/Documents/'
+ ),
+ 'WELCOME.TXT' => array(
+ 'title' => 'welcome.txt',
+ 'thumbnail' => null,
+ 'size' => '163',
+ 'datemodified' => 1481213186,
+ 'source' => '/dir/welcome.txt'
+ )
+ );
+
+ // Valid response from the client.
+ $mock = $this->createMock(\webdav_client::class);
+ $mock->expects($this->once())->method('open')->will($this->returnValue(true));
+ $mock->expects($this->once())->method('ls')->will($this->returnValue($response));
+ $this->set_private_property($mock, 'dav');
+
+ $ls = $this->repo->get_listing('/dir/');
+
+ // Can not be tested properly.
+ $ls['list']['DOCUMENTS/']['thumbnail'] = null;
+ $ls['list']['WELCOME.TXT']['thumbnail'] = null;
+
+ $this->assertEquals($ret, $ls);
+ }
+ /**
+ * Test the get_link method.
+ */
+ public function test_get_link_success() {
+ $mock = $this->getMockBuilder(\repository_nextcloud\ocs_client::class)->disableOriginalConstructor()->disableOriginalClone(
+ )->getMock();
+ $file = '/datei';
+ $expectedresponse = <<<XML
+<?xml version="1.0"?>
+<ocs>
+ <meta>
+ <status>ok</status>
+ <statuscode>100</statuscode>
+ <message/>
+ </meta>
+ <data>
+ <id>2</id>
+ <share_type>3</share_type>
+ <uid_owner>admin</uid_owner>
+ <displayname_owner>admin</displayname_owner>
+ <permissions>1</permissions>
+ <stime>1502883721</stime>
+ <parent/>
+ <expiration/>
+ <token>QXbqrJj8DcMaXen</token>
+ <uid_file_owner>admin</uid_file_owner>
+ <displayname_file_owner>admin</displayname_file_owner>
+ <path>/somefile</path>
+ <item_type>file</item_type>
+ <mimetype>application/pdf</mimetype>
+ <storage_id>home::admin</storage_id>
+ <storage>1</storage>
+ <item_source>6</item_source>
+ <file_source>6</file_source>
+ <file_parent>4</file_parent>
+ <file_target>/somefile</file_target>
+ <share_with/>
+ <share_with_displayname/>
+ <name/>
+ <url>https://www.default.test/somefile</url>
+ <mail_send>0</mail_send>
+ </data>
+</ocs>
+XML;
+ // Expected Parameters.
+ $ocsquery = [
+ 'path' => $file,
+ 'shareType' => \repository_nextcloud\ocs_client::SHARE_TYPE_PUBLIC,
+ 'publicUpload' => false,
+ 'permissions' => \repository_nextcloud\ocs_client::SHARE_PERMISSION_READ
+ ];
+
+ // With test whether mock is called with right parameters.
+ $mock->expects($this->once())->method('call')->with('create_share', $ocsquery)->will($this->returnValue($expectedresponse));
+ $this->set_private_property($mock, 'ocsclient');
+
+ // Method does extract the link from the xml format.
+ $this->assertEquals('https://www.default.test/somefile/download', $this->repo->get_link($file));
+ }
+
+ /**
+ * get_link can get OCS failure responses. Test that this is handled appropriately.
+ */
+ public function test_get_link_failure() {
+ $mock = $this->getMockBuilder(\repository_nextcloud\ocs_client::class)->disableOriginalConstructor()->disableOriginalClone(
+ )->getMock();
+ $file = '/datei';
+ $expectedresponse = <<<XML
+<?xml version="1.0"?>
+<ocs>
+ <meta>
+ <status>failure</status>
+ <statuscode>404</statuscode>
+ <message>Msg</message>
+ </meta>
+ <data/>
+</ocs>
+XML;
+ // Expected Parameters.
+ $ocsquery = [
+ 'path' => $file,
+ 'shareType' => \repository_nextcloud\ocs_client::SHARE_TYPE_PUBLIC,
+ 'publicUpload' => false,
+ 'permissions' => \repository_nextcloud\ocs_client::SHARE_PERMISSION_READ
+ ];
+
+ // With test whether mock is called with right parameters.
+ $mock->expects($this->once())->method('call')->with('create_share', $ocsquery)->will($this->returnValue($expectedresponse));
+ $this->set_private_property($mock, 'ocsclient');
+
+ // Suppress (expected) XML parse error... Nextcloud sometimes returns JSON on extremely bad errors.
+ libxml_use_internal_errors(true);
+
+ // Method get_link correctly raises an exception that contains error code and message.
+ $this->expectException(\repository_nextcloud\request_exception::class);
+ $this->expectExceptionMessage(get_string('request_exception', 'repository_nextcloud', array('instance' => 'Nextcloud',
+ 'errormessage' => sprintf('(%s) %s', '404', 'Msg'))));
+ $this->repo->get_link($file);
+ }
+
+ /**
+ * get_link can get OCS responses that are not actually XML. Test that this is handled appropriately.
+ */
+ public function test_get_link_problem() {
+ $mock = $this->getMockBuilder(\repository_nextcloud\ocs_client::class)->disableOriginalConstructor()->disableOriginalClone(
+ )->getMock();
+ $file = '/datei';
+ $expectedresponse = <<<JSON
+{"message":"CSRF check failed"}
+JSON;
+ // Expected Parameters.
+ $ocsquery = [
+ 'path' => $file,
+ 'shareType' => \repository_nextcloud\ocs_client::SHARE_TYPE_PUBLIC,
+ 'publicUpload' => false,
+ 'permissions' => \repository_nextcloud\ocs_client::SHARE_PERMISSION_READ
+ ];
+
+ // With test whether mock is called with right parameters.
+ $mock->expects($this->once())->method('call')->with('create_share', $ocsquery)->will($this->returnValue($expectedresponse));
+ $this->set_private_property($mock, 'ocsclient');
+
+ // Suppress (expected) XML parse error... Nextcloud sometimes returns JSON on extremely bad errors.
+ libxml_use_internal_errors(true);
+
+ // Method get_link correctly raises an exception.
+ $this->expectException(\repository_nextcloud\request_exception::class);
+ $this->repo->get_link($file);
+ }
+
+ /**
+ * Test get_file reference, merely returns the input if no optional_param is set.
+ */
+ public function test_get_file_reference_withoutoptionalparam() {
+ $this->assertEquals('/somefile', $this->repo->get_file_reference('/somefile'));
+ }
+
+ /**
+ * Test get_file reference in case the optional param is set. Therefore has to simulate the get_link method.
+ */
+ public function test_get_file_reference_withoptionalparam() {
+ $_GET['usefilereference'] = true;
+ $filename = '/somefile';
+ // Calls for get link(). Therefore, mocks for get_link are build.
+ $mock = $this->getMockBuilder(\repository_nextcloud\ocs_client::class)->disableOriginalConstructor()->disableOriginalClone(
+ )->getMock();
+ $expectedresponse = <<<XML
+<?xml version="1.0"?>
+<ocs>
+ <meta>
+ <status>ok</status>
+ <statuscode>100</statuscode>
+ <message/>
+ </meta>
+ <data>
+ <id>2</id>
+ <share_type>3</share_type>
+ <uid_owner>admin</uid_owner>
+ <displayname_owner>admin</displayname_owner>
+ <permissions>1</permissions>
+ <stime>1502883721</stime>
+ <parent/>
+ <expiration/>
+ <token>QXbqrJj8DcMaXen</token>
+ <uid_file_owner>admin</uid_file_owner>
+ <displayname_file_owner>admin</displayname_file_owner>
+ <path>/somefile</path>
+ <item_type>file</item_type>
+ <mimetype>application/pdf</mimetype>
+ <storage_id>home::admin</storage_id>
+ <storage>1</storage>
+ <item_source>6</item_source>
+ <file_source>6</file_source>
+ <file_parent>4</file_parent>
+ <file_target>/somefile</file_target>
+ <share_with/>
+ <share_with_displayname/>
+ <name/>
+ <url>https://www.default.test/somefile</url>
+ <mail_send>0</mail_send>
+ </data>
+</ocs>
+XML;
+ // Expected Parameters.
+ $ocsquery = ['path' => $filename,
+ 'shareType' => \repository_nextcloud\ocs_client::SHARE_TYPE_PUBLIC,
+ 'publicUpload' => false,
+ 'permissions' => \repository_nextcloud\ocs_client::SHARE_PERMISSION_READ,
+ ];
+
+ // With test whether mock is called with right parameters.
+ $mock->expects($this->once())->method('call')->with('create_share', $ocsquery)->will($this->returnValue($expectedresponse));
+ $this->set_private_property($mock, 'ocsclient');
+
+ $expected = new \stdClass();
+ $expected->type = 'FILE_REFERENCE';
+ $expected->link = 'https://www.default.test' . $filename . '/download';
+ // Method redirects to get_link() and return the suitable value.
+ $this->assertEquals(json_encode($expected), $this->repo->get_file_reference($filename));
+ }
+
+ /**
+ * Test logout.
+ */
+ public function test_logout() {
+ $mock = $this->createMock(\core\oauth2\client::class);
+
+ $mock->expects($this->exactly(2))->method('log_out');
+ $this->set_private_property($mock, 'client');
+ $this->repo->options['ajax'] = false;
+ $this->expectOutputString('<a target="_blank" rel="noopener noreferrer">Log in to your account</a>' .
+ '<a target="_blank" rel="noopener noreferrer">Log in to your account</a>');
+
+ $this->assertEquals($this->repo->print_login(), $this->repo->logout());
+
+ $mock->expects($this->exactly(2))->method('get_login_url')->will($this->returnValue(new moodle_url('url')));
+
+ $this->repo->options['ajax'] = true;
+ $this->assertEquals($this->repo->print_login(), $this->repo->logout());
+
+ }
+ /**
+ * Test for the get_file method from the repository_nextcloud class.
+ */
+ public function test_get_file() {
+ // WebDAV socket is not open.
+ $mock = $this->createMock(\webdav_client::class);
+ $mock->expects($this->once())->method('open')->will($this->returnValue(false));
+ $private = $this->set_private_property($mock, 'dav');
+
+ $this->assertFalse($this->repo->get_file('path'));
+
+ // WebDAV socket is open and the request successful.
+ $mock = $this->createMock(\webdav_client::class);
+ $mock->expects($this->once())->method('open')->will($this->returnValue(true));
+ $mock->expects($this->once())->method('get_file')->will($this->returnValue(true));
+ $private->setValue($this->repo, $mock);
+
+ $result = $this->repo->get_file('path', 'file');
+
+ $this->assertNotNull($result['path']);
+ }
+
+ /**
+ * Test callback.
+ */
+ public function test_callback() {
+ $mock = $this->createMock(\core\oauth2\client::class);
+ // Should call check_login exactly once.
+ $mock->expects($this->once())->method('log_out');
+ $mock->expects($this->once())->method('is_logged_in');
+
+ $this->set_private_property($mock, 'client');
+
+ $this->repo->callback();
+ }
+ /**
+ * Test check_login.
+ */
+ public function test_check_login() {
+ $mock = $this->createMock(\core\oauth2\client::class);
+ $mock->expects($this->once())->method('is_logged_in')->will($this->returnValue(true));
+ $this->set_private_property($mock, 'client');
+
+ $this->assertTrue($this->repo->check_login());
+ }
+ /**
+ * Test print_login.
+ */
+ public function test_print_login() {
+ $mock = $this->createMock(\core\oauth2\client::class);
+ $mock->expects($this->exactly(2))->method('get_login_url')->will($this->returnValue(new moodle_url('url')));
+ $this->set_private_property($mock, 'client');
+
+ // Test with ajax activated.
+ $this->repo->options['ajax'] = true;
+
+ $url = new moodle_url('url');
+ $ret = array();
+ $btn = new \stdClass();
+ $btn->type = 'popup';
+ $btn->url = $url->out(false);
+ $ret['login'] = array($btn);
+
+ $this->assertEquals($ret, $this->repo->print_login());
+
+ // Test without ajax.
+ $this->repo->options['ajax'] = false;
+
+ $output = html_writer::link($url, get_string('login', 'repository'),
+ array('target' => '_blank', 'rel' => 'noopener noreferrer'));
+ $this->expectOutputString($output);
+ $this->repo->print_login();
+ }
+
+ /**
+ * Test the initiate_webdavclient function.
+ */
+ public function test_initiate_webdavclient() {
+ global $CFG;
+
+ $idwebdav = $this->get_endpoint_id('webdav_endpoint');
+ if (!empty($idwebdav)) {
+ foreach ($idwebdav as $id) {
+ \core\oauth2\api::delete_endpoint($id);
+ }
+ }
+
+ $generator = $this->getDataGenerator()->get_plugin_generator('repository_nextcloud');
+ $generator->test_create_single_endpoint($this->issuer->get('id'), "webdav_endpoint",
+ "https://www.default.test:8080/webdav/index.php");
+
+ $fakeaccesstoken = new stdClass();
+ $fakeaccesstoken->token = "fake access token";
+ $oauthmock = $this->createMock(\core\oauth2\client::class);
+ $oauthmock->expects($this->once())->method('get_accesstoken')->will($this->returnValue($fakeaccesstoken));
+ $this->set_private_property($oauthmock, 'client');
+
+ $dav = phpunit_util::call_internal_method($this->repo, "initiate_webdavclient", [], 'repository_nextcloud');
+
+ // Verify that port is set correctly (private property).
+ $refclient = new ReflectionClass($dav);
+
+ $property = $refclient->getProperty('_port');
+ $property->setAccessible(true);
+
+ $port = $property->getValue($dav);
+
+ $this->assertEquals('8080', $port);
+ }
+
+ /**
+ * Test supported_returntypes.
+ * FILE_INTERNAL when no system account is connected.
+ * FILE_INTERNAL | FILE_CONTROLLED_LINK | FILE_EXTERNAL | FILE_REFERENCE when a system account is connected.
+ */
+ public function test_supported_returntypes() {
+ global $DB;
+ $this->assertEquals(FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE, $this->repo->supported_returntypes());
+ $dataobject = new stdClass();
+ $dataobject->timecreated = time();
+ $dataobject->timemodified = time();
+ $dataobject->usermodified = 2;
+ $dataobject->issuerid = $this->issuer->get('id');
+ $dataobject->refreshtoken = 'sometokenthatwillnotbeused';
+ $dataobject->grantedscopes = 'openid profile email';
+ $dataobject->email = 'some.email@some.de';
+ $dataobject->username = 'someusername';
+
+ $DB->insert_record('oauth2_system_account', $dataobject);
+ // When a system account is registered the file_type FILE_CONTROLLED_LINK is supported.
+ $this->assertEquals(FILE_INTERNAL | FILE_EXTERNAL | FILE_CONTROLLED_LINK | FILE_REFERENCE,
+ $this->repo->supported_returntypes());
+ }
+
+ /**
+ * The reference_file_selected() methode is called every time a FILE_CONTROLLED_LINK is chosen for upload.
+ * Since the function is very long the private function are tested separately, and merely the abortion of the
+ * function are tested.
+ *
+ */
+ public function test_reference_file_selected_error() {
+ $this->repo->disabled = true;
+ $this->expectException(\repository_exception::class);
+ $this->repo->reference_file_selected('', context_system::instance(), '', '', '');
+
+ $this->repo->disabled = false;
+ $this->expectException(\repository_exception::class);
+ $this->expectExceptionMessage('Cannot connect as system user');
+ $this->repo->reference_file_selected('', context_system::instance(), '', '', '');
+
+ $mock = $this->createMock(\core\oauth2\client::class);
+ $mock->expects($this->once())->method('get_system_oauth_client')->with($this->issuer)->willReturn(true);
+
+ $this->expectException(\repository_exception::class);
+ $this->expectExceptionMessage('Cannot connect as current user');
+ $this->repo->reference_file_selected('', context_system::instance(), '', '', '');
+
+ $this->repo->expects($this->once())->method('get_user_oauth_client')->willReturn(true);
+ $this->expectException(\repository_exception::class);
+ $this->expectExceptionMessage('cannotdownload');
+ $this->repo->reference_file_selected('', context_system::instance(), '', '', '');
+
+ $this->repo->expects($this->once())->method('get_user_oauth_client')->willReturn(true);
+ $this->expectException(\repository_exception::class);
+ $this->expectExceptionMessage('cannotdownload');
+ $this->repo->reference_file_selected('', context_system::instance(), '', '', '');
+
+ $this->repo->expects($this->once())->method('get_user_oauth_client')->willReturn(true);
+ $this->repo->expects($this->once())->method('copy_file_to_path')->willReturn(array('statuscode' =>
+ array('success' => 400)));
+ $this->expectException(\repository_exception::class);
+ $this->expectExceptionMessage('Could not copy file');
+ $this->repo->reference_file_selected('', context_system::instance(), '', '', '');
+
+ $this->repo->expects($this->once())->method('get_user_oauth_client')->willReturn(true);
+ $this->repo->expects($this->once())->method('copy_file_to_path')->willReturn(array('statuscode' =>
+ array('success' => 201)));
+ $this->repo->expects($this->once())->method('delete_share_dataowner_sysaccount')->willReturn(
+ array('statuscode' => array('success' => 400)));
+ $this->expectException(\repository_exception::class);
+ $this->expectExceptionMessage('Share is still present');
+ $this->repo->reference_file_selected('', context_system::instance(), '', '', '');
+
+ $this->repo->expects($this->once())->method('get_user_oauth_client')->willReturn(true);
+ $this->repo->expects($this->once())->method('copy_file_to_path')->willReturn(array('statuscode' =>
+ array('success' => 201)));
+ $this->repo->expects($this->once())->method('delete_share_dataowner_sysaccount')->willReturn(
+ array('statuscode' => array('success' => 100)));
+ $filereturn = array();
+ $filereturn->link = 'some/fullpath' . 'some/target/path';
+ $filereturn->name = 'mysource';
+ $filereturn->usesystem = true;
+ $filereturn = json_encode($filereturn);
+ $return = $this->repo->reference_file_selected('mysource', context_system::instance(), '', '', '');
+ $this->assertEquals($filereturn, $return);
+ }
+
+ /**
+ * Test the send_file function for access controlled links.
+ */
+ public function test_send_file_errors() {
+ $fs = get_file_storage();
+ $storedfile = $fs->create_file_from_reference([
+ 'contextid' => context_system::instance()->id,
+ 'component' => 'core',
+ 'filearea' => 'unittest',
+ 'itemid' => 0,
+ 'filepath' => '/',
+ 'filename' => 'testfile.txt',
+ ], $this->repo->id, json_encode([
+ 'type' => 'FILE_CONTROLLED_LINK',
+ 'link' => 'https://test.local/fakelink/',
+ 'usesystem' => true,
+ ]));
+ $this->set_private_property('', 'client');
+ $this->expectException(repository_nextcloud\request_exception::class);
+ $this->expectExceptionMessage(get_string('contactadminwith', 'repository_nextcloud',
+ 'The OAuth client could not be connected.'));
+
+ $this->repo->send_file($storedfile, '', '', '');
+
+ // Testing whether the mock up appears is topic to behat.
+ $mock = $this->createMock(\core\oauth2\client::class);
+ $mock->expects($this->once())->method('is_logged_in')->willReturn(true);
+ $this->repo->send_file($storedfile, '', '', '');
+
+ // Checks that setting for foldername are used.
+ $mock->expects($this->once())->method('is_dir')->with('Moodlefiles')->willReturn(false);
+ // In case of false as return value mkcol is called to create the folder.
+ $parsedwebdavurl = parse_url($this->issuer->get_endpoint_url('webdav'));
+ $webdavprefix = $parsedwebdavurl['path'];
+ $mock->expects($this->once())->method('mkcol')->with(
+ $webdavprefix . 'Moodlefiles')->willReturn(400);
+ $this->expectException(\repository_nextcloud\request_exception::class);
+ $this->expectExceptionMessage(get_string('requestnotexecuted', 'repository_nextcloud'));
+ $this->repo->send_file($storedfile, '', '', '');
+
+ $expectedresponse = <<<XML
+<?xml version="1.0"?>
+<ocs>
+ <meta>
+ <status>ok</status>
+ <statuscode>100</statuscode>
+ <message/>
+ </meta>
+ <data>
+ <element>
+ <id>6</id>
+ <share_type>0</share_type>
+ <uid_owner>tech</uid_owner>
+ <displayname_owner>tech</displayname_owner>
+ <permissions>19</permissions>
+ <stime>1511877999</stime>
+ <parent/>
+ <expiration/>
+ <token/>
+ <uid_file_owner>tech</uid_file_owner>
+ <displayname_file_owner>tech</displayname_file_owner>
+ <path>/System/Category Miscellaneous/Course Example Course/File morefiles/mod_resource/content/0/merge.txt</path>
+ <item_type>file</item_type>
+ <mimetype>text/plain</mimetype>
+ <storage_id>home::tech</storage_id>
+ <storage>4</storage>
+ <item_source>824</item_source>
+ <file_source>824</file_source>
+ <file_parent>823</file_parent>
+ <file_target>/merge (3).txt</file_target>
+ <share_with>user2</share_with>
+ <share_with_displayname>user1</share_with_displayname>
+ <mail_send>0</mail_send>
+ </element>
+ <element>
+ <id>5</id>
+ <share_type>0</share_type>
+ <uid_owner>tech</uid_owner>
+ <displayname_owner>tech</displayname_owner>
+ <permissions>19</permissions>
+ <stime>1511877999</stime>
+ <parent/>
+ <expiration/>
+ <token/>
+ <uid_file_owner>tech</uid_file_owner>
+ <displayname_file_owner>tech</displayname_file_owner>
+ <path>/System/Category Miscellaneous/Course Example Course/File morefiles/mod_resource/content/0/merge.txt</path>
+ <item_type>file</item_type>
+ <mimetype>text/plain</mimetype>
+ <storage_id>home::tech</storage_id>
+ <storage>4</storage>
+ <item_source>824</item_source>
+ <file_source>824</file_source>
+ <file_parent>823</file_parent>
+ <file_target>/merged (3).txt</file_target>
+ <share_with>user1</share_with>
+ <share_with_displayname>user1</share_with_displayname>
+ <mail_send>0</mail_send>
+ </element>
+ </data>
+</ocs>
+XML;
+
+ // Checks that setting for foldername are used.
+ $mock->expects($this->once())->method('is_dir')->with('Moodlefiles')->willReturn(true);
+ // In case of true as return value mkcol is not called to create the folder.
+ $shareid = 5;
+
+ $mockocsclient = $this->getMockBuilder(
+ \repository_nextcloud\ocs_client::class)->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $mockocsclient->expects($this->exactly(2))->method('call')->with('get_information_of_share',
+ array('share_id' => $shareid))->will($this->returnValue($expectedresponse));
+ $this->set_private_property($mock, 'ocsclient');
+ $this->repo->expects($this->once())->method('move_file_to_folder')->with('/merged (3).txt', 'Moodlefiles',
+ $mock)->willReturn(array('success' => 201));
+
+ $this->repo->send_file('', '', '', '');
+
+ // Create test for statuscode 403.
+
+ // Checks that setting for foldername are used.
+ $mock->expects($this->once())->method('is_dir')->with('Moodlefiles')->willReturn(true);
+ // In case of true as return value mkcol is not called to create the folder.
+ $shareid = 5;
+ $mockocsclient = $this->getMockBuilder(\repository_nextcloud\ocs_client::class
+ )->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $mockocsclient->expects($this->exactly(1))->method('call')->with('get_shares',
+ array('path' => '/merged (3).txt', 'reshares' => true))->will($this->returnValue($expectedresponse));
+ $mockocsclient->expects($this->exactly(1))->method('call')->with('get_information_of_share',
+ array('share_id' => $shareid))->will($this->returnValue($expectedresponse));
+ $this->set_private_property($mock, 'ocsclient');
+ $this->repo->expects($this->once())->method('move_file_to_folder')->with('/merged (3).txt', 'Moodlefiles',
+ $mock)->willReturn(array('success' => 201));
+ $this->repo->send_file('', '', '', '');
+ }
+
+ /**
+ * Helper method, which inserts a given mock value into the repository_nextcloud object.
+ *
+ * @param mixed $value mock value that will be inserted.
+ * @param string $propertyname name of the private property.
+ * @return ReflectionProperty the resulting reflection property.
+ */
+ protected function set_private_property($value, $propertyname) {
+ $refclient = new ReflectionClass($this->repo);
+ $private = $refclient->getProperty($propertyname);
+ $private->setAccessible(true);
+ $private->setValue($this->repo, $value);
+
+ return $private;
+ }
+ /**
+ * Helper method to set required return parameters for get_listing.
+ *
+ * @return array array, which contains the parameters.
+ */
+ protected function get_initialised_return_array() {
+ $ret = array();
+ $ret['dynload'] = true;
+ $ret['nosearch'] = true;
+ $ret['nologin'] = false;
+ $ret['path'] = [
+ [
+ 'name' => $this->repo->get_meta()->name,
+ 'path' => '',
+ ]
+ ];
+ $ret['manage'] = '';
+ $ret['defaultreturntype'] = FILE_INTERNAL;
+ $ret['list'] = array();
+
+ return $ret;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains tests for the repository_nextcloud class.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Jan Dageförde (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class repository_nextcloud_ocs_testcase
+ * @group repository_nextcloud
+ * @copyright 2017 Jan Dageförde (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class repository_nextcloud_ocs_testcase extends advanced_testcase {
+
+ /**
+ * @var \core\oauth2\issuer
+ */
+ private $issuer;
+
+ /**
+ * SetUp to create issuer and endpoints for OCS testing.
+ */
+ protected function setUp() {
+ $this->resetAfterTest(true);
+
+ // Admin is neccessary to create issuer object.
+ $this->setAdminUser();
+
+ $generator = $this->getDataGenerator()->get_plugin_generator('repository_nextcloud');
+ $this->issuer = $generator->test_create_issuer();
+ $generator->test_create_endpoints($this->issuer->get('id'));
+ }
+
+ /**
+ * Test whether required REST API functions are declared.
+ */
+ public function test_api_functions() {
+ $mock = $this->createMock(\core\oauth2\client::class);
+ $mock->expects($this->once())->method('get_issuer')->willReturn($this->issuer);
+
+ $client = new \repository_nextcloud\ocs_client($mock);
+ $functions = $client->get_api_functions();
+
+ // Assert that relevant (and used) functions are actually present.
+ $this->assertArrayHasKey('create_share', $functions);
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Nextcloud repository version details.
+ *
+ * @package repository_nextcloud
+ * @copyright 2017 Project seminar (Learnweb, University of Münster)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2018091300; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires = 2018090700; // Requires this Moodle version.
+$plugin->component = 'repository_nextcloud'; // Full name of the plugin (used for diagnostics).