MDL-58943 repository: Add repository_nextcloud
authorNina Herrmann <nina.herrmann@uni-muenster.de>
Wed, 15 Aug 2018 15:16:08 +0000 (17:16 +0200)
committerDamyon Wiese <damyon@moodle.com>
Wed, 31 Oct 2018 02:42:57 +0000 (10:42 +0800)
Based on repository_owncloud from
https://github.com/learnweb/moodle-repository_owncloud at 5b5fdbb.
Differences to the original:

* Renamed to repository_nextcloud
* Updated version.php for core
* Removed compatibility polyfills (webdav, privacy)
* Removed boilerplate files (e.g. README, CI config)
* Implement null_provider for privacy API as personal data is neither
  stored nor transmitted to the external system

16 files changed:
repository/nextcloud/classes/access_controlled_link_manager.php [new file with mode: 0644]
repository/nextcloud/classes/configuration_exception.php [new file with mode: 0755]
repository/nextcloud/classes/issuer_management.php [new file with mode: 0644]
repository/nextcloud/classes/ocs_client.php [new file with mode: 0644]
repository/nextcloud/classes/privacy/provider.php [new file with mode: 0644]
repository/nextcloud/classes/request_exception.php [new file with mode: 0755]
repository/nextcloud/db/access.php [new file with mode: 0755]
repository/nextcloud/lang/en/repository_nextcloud.php [new file with mode: 0755]
repository/nextcloud/lib.php [new file with mode: 0755]
repository/nextcloud/pix/icon.svg [new file with mode: 0644]
repository/nextcloud/tests/access_controlled_link_manager_test.php [new file with mode: 0644]
repository/nextcloud/tests/fixtures/testable_access_controlled_link_manager.php [new file with mode: 0644]
repository/nextcloud/tests/generator/lib.php [new file with mode: 0755]
repository/nextcloud/tests/lib_test.php [new file with mode: 0644]
repository/nextcloud/tests/ocs_test.php [new file with mode: 0644]
repository/nextcloud/version.php [new file with mode: 0755]

diff --git a/repository/nextcloud/classes/access_controlled_link_manager.php b/repository/nextcloud/classes/access_controlled_link_manager.php
new file mode 100644 (file)
index 0000000..0cd7d13
--- /dev/null
@@ -0,0 +1,456 @@
+<?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
+            ];
+    }
+}
diff --git a/repository/nextcloud/classes/configuration_exception.php b/repository/nextcloud/classes/configuration_exception.php
new file mode 100755 (executable)
index 0000000..d8cf769
--- /dev/null
@@ -0,0 +1,47 @@
+<?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
diff --git a/repository/nextcloud/classes/issuer_management.php b/repository/nextcloud/classes/issuer_management.php
new file mode 100644 (file)
index 0000000..d697d2a
--- /dev/null
@@ -0,0 +1,87 @@
+<?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);
+    }
+}
diff --git a/repository/nextcloud/classes/ocs_client.php b/repository/nextcloud/classes/ocs_client.php
new file mode 100644 (file)
index 0000000..9048fbf
--- /dev/null
@@ -0,0 +1,179 @@
+<?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
diff --git a/repository/nextcloud/classes/privacy/provider.php b/repository/nextcloud/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..afbcad1
--- /dev/null
@@ -0,0 +1,46 @@
+<?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';
+    }
+}
diff --git a/repository/nextcloud/classes/request_exception.php b/repository/nextcloud/classes/request_exception.php
new file mode 100755 (executable)
index 0000000..6f626a7
--- /dev/null
@@ -0,0 +1,47 @@
+<?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
diff --git a/repository/nextcloud/db/access.php b/repository/nextcloud/db/access.php
new file mode 100755 (executable)
index 0000000..15ea059
--- /dev/null
@@ -0,0 +1,35 @@
+<?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
+        )
+    )
+);
diff --git a/repository/nextcloud/lang/en/repository_nextcloud.php b/repository/nextcloud/lang/en/repository_nextcloud.php
new file mode 100755 (executable)
index 0000000..c56b5e8
--- /dev/null
@@ -0,0 +1,61 @@
+<?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.';
diff --git a/repository/nextcloud/lib.php b/repository/nextcloud/lib.php
new file mode 100755 (executable)
index 0000000..f8d6624
--- /dev/null
@@ -0,0 +1,882 @@
+<?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
diff --git a/repository/nextcloud/pix/icon.svg b/repository/nextcloud/pix/icon.svg
new file mode 100644 (file)
index 0000000..a7e9847
--- /dev/null
@@ -0,0 +1 @@
+<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
diff --git a/repository/nextcloud/tests/access_controlled_link_manager_test.php b/repository/nextcloud/tests/access_controlled_link_manager_test.php
new file mode 100644 (file)
index 0000000..178adef
--- /dev/null
@@ -0,0 +1,633 @@
+<?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
diff --git a/repository/nextcloud/tests/fixtures/testable_access_controlled_link_manager.php b/repository/nextcloud/tests/fixtures/testable_access_controlled_link_manager.php
new file mode 100644 (file)
index 0000000..561bfbb
--- /dev/null
@@ -0,0 +1,57 @@
+<?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;
+    }
+}
diff --git a/repository/nextcloud/tests/generator/lib.php b/repository/nextcloud/tests/generator/lib.php
new file mode 100755 (executable)
index 0000000..d905ce8
--- /dev/null
@@ -0,0 +1,84 @@
+<?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;
+    }
+}
diff --git a/repository/nextcloud/tests/lib_test.php b/repository/nextcloud/tests/lib_test.php
new file mode 100644 (file)
index 0000000..48212e6
--- /dev/null
@@ -0,0 +1,948 @@
+<?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;
+    }
+}
diff --git a/repository/nextcloud/tests/ocs_test.php b/repository/nextcloud/tests/ocs_test.php
new file mode 100644 (file)
index 0000000..a96691b
--- /dev/null
@@ -0,0 +1,67 @@
+<?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);
+    }
+}
diff --git a/repository/nextcloud/version.php b/repository/nextcloud/version.php
new file mode 100755 (executable)
index 0000000..24a2bf1
--- /dev/null
@@ -0,0 +1,29 @@
+<?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).