MDL-58943 repository: Add repository_nextcloud

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
This commit is contained in:
Nina Herrmann
2018-10-31 10:42:57 +08:00
committed by Damyon Wiese
parent 8b019fb709
commit b4e24a82db
16 changed files with 3659 additions and 0 deletions
@@ -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
];
}
}
+47
View File
@@ -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);
}
}
@@ -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);
}
}
+179
View File
@@ -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);
}
}
}
@@ -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';
}
}
+47
View File
@@ -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);
}
}
+35
View File
@@ -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
)
)
);
+61
View File
@@ -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.';
+882
View File
@@ -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;
}
}
+1
View File
@@ -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>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -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);
}
}
}
@@ -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;
}
}
+84
View File
@@ -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;
}
}
+948
View File
@@ -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 = '[email protected]';
$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;
}
}
+67
View File
@@ -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);
}
}
+29
View File
@@ -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).