Differences Between: [Versions 310 and 402] [Versions 39 and 402]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Manages the creation and usage of access controlled links. 19 * 20 * @package repository_nextcloud 21 * @copyright 2017 Nina Herrmann (Learnweb, University of Münster) 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace repository_nextcloud; 25 26 use context; 27 use \core\oauth2\api; 28 use \core\notification; 29 use repository_exception; 30 31 defined('MOODLE_INTERNAL') || die(); 32 require_once($CFG->libdir . '/webdavlib.php'); 33 34 /** 35 * Manages the creation and usage of access controlled links. 36 * 37 * @package repository_nextcloud 38 * @copyright 2017 Nina Herrmann (Learnweb, University of Münster) 39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 40 */ 41 class access_controlled_link_manager{ 42 /** 43 * OCS client that uses the Open Collaboration Services REST API. 44 * @var ocs_client 45 */ 46 protected $ocsclient; 47 /** 48 * ocsclient of the systemaccount. 49 * @var ocs_client 50 */ 51 protected $systemocsclient; 52 /** 53 * Client to manage oauth2 features from the systemaccount. 54 * @var \core\oauth2\client 55 */ 56 protected $systemoauthclient; 57 /** 58 * Client to manage webdav request from the systemaccount.. 59 * @var \webdav_client 60 */ 61 protected $systemwebdavclient; 62 /** 63 * Issuer from the oauthclient. 64 * @var \core\oauth2\issuer 65 */ 66 protected $issuer; 67 /** 68 * Name of the related repository. 69 * @var string 70 */ 71 protected $repositoryname; 72 73 /** 74 * Access_controlled_link_manager constructor. 75 * @param ocs_client $ocsclient 76 * @param \core\oauth2\client $systemoauthclient 77 * @param ocs_client $systemocsclient 78 * @param \core\oauth2\issuer $issuer 79 * @param string $repositoryname 80 * @throws configuration_exception 81 */ 82 public function __construct($ocsclient, $systemoauthclient, $systemocsclient, $issuer, $repositoryname) { 83 $this->ocsclient = $ocsclient; 84 $this->systemoauthclient = $systemoauthclient; 85 $this->systemocsclient = $systemocsclient; 86 87 $this->repositoryname = $repositoryname; 88 $this->issuer = $issuer; 89 $this->systemwebdavclient = $this->create_system_dav(); 90 } 91 92 /** 93 * Deletes the share of the systemaccount and a user. In case the share could not be deleted a notification is 94 * displayed. 95 * @param int $shareid Remote ID of the share to be deleted. 96 */ 97 public function delete_share_dataowner_sysaccount($shareid) { 98 $shareid = (int) $shareid; 99 $deleteshareparams = [ 100 'share_id' => $shareid 101 ]; 102 $deleteshareresponse = $this->ocsclient->call('delete_share', $deleteshareparams); 103 $xml = simplexml_load_string($deleteshareresponse); 104 105 if (empty($xml->meta->statuscode) || $xml->meta->statuscode != 100 ) { 106 notification::warning('You just shared a file with a access controlled link. 107 However, the share between you and the systemaccount could not be deleted and is still present in your instance.'); 108 } 109 } 110 111 /** 112 * Creates a share between a user and the system account. If $username is set the sharing direction is system account -> user, 113 * otherwise user -> system account. 114 * @param string $path Remote path of the file that will be shared 115 * @param string $username optional when set the file is shared with the corresponding user otherwise with 116 * the systemaccount. 117 * @param bool $maywrite if false, only(!) read access is granted. 118 * @return array statuscode, shareid, and filetarget 119 * @throws request_exception 120 */ 121 public function create_share_user_sysaccount($path, $username = null, $maywrite = false) { 122 $result = array(); 123 124 if ($username != null) { 125 $shareusername = $username; 126 } else { 127 $systemaccount = \core\oauth2\api::get_system_account($this->issuer); 128 $shareusername = $systemaccount->get('username'); 129 } 130 $permissions = ocs_client::SHARE_PERMISSION_READ; 131 if ($maywrite) { 132 // Add more privileges (write, reshare) if allowed for the given user. 133 $permissions |= ocs_client::SHARE_PERMISSION_ALL; 134 } 135 $createshareparams = [ 136 'path' => $path, 137 'shareType' => ocs_client::SHARE_TYPE_USER, 138 'publicUpload' => false, 139 'shareWith' => $shareusername, 140 'permissions' => $permissions, 141 ]; 142 143 // File is now shared with the system account. 144 if ($username === null) { 145 $createshareresponse = $this->ocsclient->call('create_share', $createshareparams); 146 } else { 147 $createshareresponse = $this->systemocsclient->call('create_share', $createshareparams); 148 } 149 $xml = simplexml_load_string($createshareresponse); 150 151 $statuscode = (int)$xml->meta->statuscode; 152 if ($statuscode != 100 && $statuscode != 403) { 153 $details = get_string('filenotaccessed', 'repository_nextcloud'); 154 throw new request_exception(get_string('request_exception', 155 'repository_nextcloud', array('instance' => $this->repositoryname, 'errormessage' => $details))); 156 } 157 $result['shareid'] = (int)$xml->data->id; 158 $result['statuscode'] = $statuscode; 159 $result['filetarget'] = (string)$xml->data[0]->file_target; 160 161 return $result; 162 } 163 164 /** Copy or moves a file to a new path. 165 * @param string $srcpath source path 166 * @param string $dstpath 167 * @param string $operation move or copy 168 * @param \webdav_client $webdavclient needed when moving files. 169 * @return String Http-status of the request 170 * @throws configuration_exception 171 * @throws \coding_exception 172 * @throws \moodle_exception 173 * @throws \repository_nextcloud\request_exception 174 */ 175 public function transfer_file_to_path($srcpath, $dstpath, $operation, $webdavclient = null) { 176 $this->systemwebdavclient->open(); 177 $webdavendpoint = issuer_management::parse_endpoint_url('webdav', $this->issuer); 178 179 $srcpath = ltrim($srcpath, '/'); 180 $sourcepath = $webdavendpoint['path'] . $srcpath; 181 $dstpath = ltrim($dstpath, '/'); 182 $destinationpath = $webdavendpoint['path'] . $dstpath . '/' . $srcpath; 183 184 if ($operation === 'copy') { 185 $result = $this->systemwebdavclient->copy_file($sourcepath, $destinationpath, true); 186 } else if ($operation === 'move') { 187 $result = $webdavclient->move($sourcepath, $destinationpath, false); 188 if ($result == 412) { 189 // A file with that name already exists at that target. Find a unique location! 190 $increment = 0; // Will be appended to/inserted into the filename. 191 // Define the pattern that is used to insert the increment to the filename. 192 if (substr_count($srcpath, '.') === 0) { 193 // No file extension; append increment to the (sprintf-escaped) name. 194 $namepattern = str_replace('%', '%%', $destinationpath) . ' (%s)'; 195 } else { 196 // Append the increment to the second-to-last component, which is presumably the one before the extension. 197 // Again, the original path is sprintf-escaped. 198 $components = explode('.', str_replace('%', '%%', $destinationpath)); 199 $components[count($components) - 2] .= ' (%s)'; 200 $namepattern = implode('.', $components); 201 } 202 } 203 while ($result == 412) { 204 $increment++; 205 $destinationpath = sprintf($namepattern, $increment); 206 $result = $webdavclient->move($sourcepath, $destinationpath, false); 207 } 208 } 209 $this->systemwebdavclient->close(); 210 if (!($result == 201 || $result == 204 || $result == 412)) { 211 $details = get_string('contactadminwith', 'repository_nextcloud', 212 'A webdav request to ' . $operation . ' a file failed.'); 213 throw new request_exception(array('instance' => $this->repositoryname, 'errormessage' => $details)); 214 } 215 return $result; 216 } 217 218 /** 219 * Creates a unique folder path for the access controlled link. 220 * @param context $context 221 * @param string $component 222 * @param string $filearea 223 * @param string $itemid 224 * @return string $result full generated path. 225 * @throws request_exception If the folder path cannot be created. 226 */ 227 public function create_folder_path_access_controlled_links($context, $component, $filearea, $itemid) { 228 global $CFG, $SITE; 229 // The fullpath to store the file is generated from the context. 230 $contextlist = array_reverse($context->get_parent_contexts(true)); 231 $fullpath = ''; 232 $allfolders = []; 233 foreach ($contextlist as $ctx) { 234 // Prepare human readable context folders names, making sure they are still unique within the site. 235 $prevlang = force_current_language($CFG->lang); 236 $foldername = $ctx->get_context_name(); 237 force_current_language($prevlang); 238 239 if ($ctx->contextlevel === CONTEXT_SYSTEM) { 240 // Append the site short name to the root folder. 241 $foldername .= ' ('.$SITE->shortname.')'; 242 // Append the relevant object id. 243 } else if ($ctx->instanceid) { 244 $foldername .= ' (id '.$ctx->instanceid.')'; 245 } else { 246 // This does not really happen but just in case. 247 $foldername .= ' (ctx '.$ctx->id.')'; 248 } 249 250 $foldername = clean_param($foldername, PARAM_FILE); 251 $allfolders[] = $foldername; 252 } 253 254 $allfolders[] = clean_param($component, PARAM_FILE); 255 $allfolders[] = clean_param($filearea, PARAM_FILE); 256 $allfolders[] = clean_param($itemid, PARAM_FILE); 257 258 // Extracts the end of the webdavendpoint. 259 $parsedwebdavurl = issuer_management::parse_endpoint_url('webdav', $this->issuer); 260 $webdavprefix = $parsedwebdavurl['path']; 261 $this->systemwebdavclient->open(); 262 // Checks whether folder exist and creates non-existent folders. 263 foreach ($allfolders as $foldername) { 264 $fullpath .= '/' . $foldername; 265 $isdir = $this->systemwebdavclient->is_dir($webdavprefix . $fullpath); 266 // Folder already exist, continue. 267 if ($isdir === true) { 268 continue; 269 } 270 $response = $this->systemwebdavclient->mkcol($webdavprefix . $fullpath); 271 272 if ($response != 201) { 273 $this->systemwebdavclient->close(); 274 $details = get_string('contactadminwith', 'repository_nextcloud', 275 get_string('pathnotcreated', 'repository_nextcloud', $fullpath)); 276 throw new request_exception(array('instance' => $this->repositoryname, 277 'errormessage' => $details)); 278 } 279 } 280 $this->systemwebdavclient->close(); 281 return $fullpath; 282 } 283 284 /** Creates a new webdav_client for the system account. 285 * @return \webdav_client 286 * @throws configuration_exception 287 */ 288 public function create_system_dav() { 289 $webdavendpoint = issuer_management::parse_endpoint_url('webdav', $this->issuer); 290 291 // Selects the necessary information (port, type, server) from the path to build the webdavclient. 292 $server = $webdavendpoint['host']; 293 if ($webdavendpoint['scheme'] === 'https') { 294 $webdavtype = 'ssl://'; 295 $webdavport = 443; 296 } else if ($webdavendpoint['scheme'] === 'http') { 297 $webdavtype = ''; 298 $webdavport = 80; 299 } 300 301 // Override default port, if a specific one is set. 302 if (isset($webdavendpoint['port'])) { 303 $webdavport = $webdavendpoint['port']; 304 } 305 306 // Authentication method is `bearer` for OAuth 2. Pass oauth client from which WebDAV obtains the token when needed. 307 $dav = new \webdav_client($server, '', '', 'bearer', $webdavtype, 308 $this->systemoauthclient->get_accesstoken()->token, $webdavendpoint['path']); 309 310 $dav->port = $webdavport; 311 $dav->debug = false; 312 return $dav; 313 } 314 315 /** Creates a folder to store access controlled links. 316 * @param string $controlledlinkfoldername 317 * @param \webdav_client $webdavclient 318 * @throws \coding_exception 319 * @throws configuration_exception 320 * @throws request_exception 321 */ 322 public function create_storage_folder($controlledlinkfoldername, $webdavclient) { 323 $parsedwebdavurl = issuer_management::parse_endpoint_url('webdav', $this->issuer); 324 $webdavprefix = $parsedwebdavurl['path']; 325 // Checks whether folder exist and creates non-existent folders. 326 $webdavclient->open(); 327 $isdir = $webdavclient->is_dir($webdavprefix . $controlledlinkfoldername); 328 // Folder already exist, continue. 329 if (!$isdir) { 330 $responsecreateshare = $webdavclient->mkcol($webdavprefix . $controlledlinkfoldername); 331 332 if ($responsecreateshare != 201) { 333 $webdavclient->close(); 334 throw new request_exception(array('instance' => $this->repositoryname, 335 'errormessage' => get_string('contactadminwith', 'repository_nextcloud', 336 'The folder to store files in the user account could not be created.'))); 337 } 338 } 339 $webdavclient->close(); 340 } 341 342 /** Gets all shares from a path (the path is file specific) and extracts the share of a specific user. In case 343 * multiple shares exist the first one is taken. Multiple shares can only appear when shares are created outside 344 * of this plugin, therefore this case is not handled. 345 * @param string $path 346 * @param string $username 347 * @return \SimpleXMLElement 348 * @throws \moodle_exception 349 */ 350 public function get_shares_from_path($path, $username) { 351 $ocsparams = [ 352 'path' => $path, 353 'reshares' => true 354 ]; 355 356 $getsharesresponse = $this->systemocsclient->call('get_shares', $ocsparams); 357 $xml = simplexml_load_string($getsharesresponse); 358 $validelement = array(); 359 foreach ($fileid = $xml->data->element as $element) { 360 if ($element->share_with == $username) { 361 $validelement = $element; 362 break; 363 } 364 } 365 if (empty($validelement)) { 366 throw new request_exception(array('instance' => $this->repositoryname, 367 'errormessage' => get_string('filenotaccessed', 'repository_nextcloud'))); 368 369 } 370 return $validelement->id; 371 } 372 373 /** This method can only be used if the response is from a newly created share. In this case there is more information 374 * in the response. For a reference refer to 375 * https://docs.nextcloud.com/server/13/developer_manual/core/ocs-share-api.html#get-information-about-a-known-share. 376 * @param int $shareid 377 * @param string $username 378 * @return mixed the id of the share 379 * @throws \coding_exception 380 * @throws \repository_nextcloud\request_exception 381 */ 382 public function get_share_information_from_shareid($shareid, $username) { 383 $ocsparams = [ 384 'share_id' => (int) $shareid 385 ]; 386 387 $shareinformation = $this->ocsclient->call('get_information_of_share', $ocsparams); 388 $xml = simplexml_load_string($shareinformation); 389 foreach ($fileid = $xml->data->element as $element) { 390 if ($element->share_with == $username) { 391 $validelement = $element; 392 break; 393 } 394 } 395 if (empty($validelement)) { 396 throw new request_exception(array('instance' => $this->repositoryname, 397 'errormessage' => get_string('filenotaccessed', 'repository_nextcloud'))); 398 399 } 400 return (string) $validelement->file_target; 401 } 402 403 /** 404 * Find a file that has previously been shared with the system account. 405 * @param string $path Path to file in user context. 406 * @return array shareid: ID of share, filetarget: path to file in sys account. 407 * @throws request_exception If the share cannot be resolved. 408 */ 409 public function find_share_in_sysaccount($path) { 410 $systemaccount = \core\oauth2\api::get_system_account($this->issuer); 411 $systemaccountuser = $systemaccount->get('username'); 412 413 // Find out share ID from user files. 414 $ocsparams = [ 415 'path' => $path, 416 'reshares' => true 417 ]; 418 419 $getsharesresponse = $this->ocsclient->call('get_shares', $ocsparams); 420 $xml = simplexml_load_string($getsharesresponse); 421 $validelement = array(); 422 foreach ($fileid = $xml->data->element as $element) { 423 if ($element->share_with == $systemaccountuser) { 424 $validelement = $element; 425 break; 426 } 427 } 428 if (empty($validelement)) { 429 throw new request_exception(array('instance' => $this->repositoryname, 430 'errormessage' => get_string('filenotaccessed', 'repository_nextcloud'))); 431 } 432 $shareid = (int) $validelement->id; 433 434 // Use share id to find file name in system account's context. 435 $ocsparams = [ 436 'share_id' => $shareid 437 ]; 438 439 $shareinformation = $this->systemocsclient->call('get_information_of_share', $ocsparams); 440 $xml = simplexml_load_string($shareinformation); 441 foreach ($fileid = $xml->data->element as $element) { 442 if ($element->share_with == $systemaccountuser) { 443 $validfile = $element; 444 break; 445 } 446 } 447 if (empty($validfile)) { 448 throw new request_exception(array('instance' => $this->repositoryname, 449 'errormessage' => get_string('filenotaccessed', 'repository_nextcloud'))); 450 451 } 452 return [ 453 'shareid' => $shareid, 454 'filetarget' => (string) $validfile->file_target 455 ]; 456 } 457 458 /** 459 * Download a file from the system account for the purpose of offline usage. 460 * @param string $srcpath Name of a file owned by the system account 461 * @param string $targetpath Temporary filename in Moodle 462 * @throws repository_exception The download was unsuccessful, maybe the file does not exist. 463 */ 464 public function download_for_offline_usage(string $srcpath, string $targetpath): void { 465 $this->systemwebdavclient->open(); 466 $webdavendpoint = issuer_management::parse_endpoint_url('webdav', $this->issuer); 467 $srcpath = ltrim($srcpath, '/'); 468 $sourcepath = $webdavendpoint['path'] . $srcpath; 469 470 // Write file into temp location. 471 if (!$this->systemwebdavclient->get_file($sourcepath, $targetpath)) { 472 $this->systemwebdavclient->close(); 473 throw new repository_exception('cannotdownload', 'repository'); 474 } 475 $this->systemwebdavclient->close(); 476 } 477 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body