Skip to content

Commit

Permalink
MDL-67975 nextcloud: Add support for Link to file
Browse files Browse the repository at this point in the history
This change set would bring the following new additions
to the nextcloud repo:
1. Create a new radio button in filepicker: "Link to file"
2. When user clicks this radio button a warning message
   would be created, saying this file would become public.
   Meaning a public link is created in the nextcloud server.
3. Created a sync_reference method to sync the files downloaded
   from nextcloud server. The sync/refresh time given is 1 day/24 hours.
4. Made sure that when the file is downloaded, we use the file
   from moodledata file pool.

Signed-off-by: Sujith Haridasan <sujith@moodle.com>
  • Loading branch information
sharidas committed Jun 9, 2021
1 parent 193a0a6 commit 23f7bdc
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 8 deletions.
25 changes: 25 additions & 0 deletions repository/filepicker.js
Expand Up @@ -1224,6 +1224,7 @@ M.core_filepicker.init = function(Y, options) {
var client_id = this.options.client_id;
var selectnode = this.selectnode;
var getfile = selectnode.one('.fp-select-confirm');
var filePickerHelper = this;
// bind labels with corresponding inputs
selectnode.all('.fp-saveas,.fp-linktype-2,.fp-linktype-1,.fp-linktype-4,fp-linktype-8,.fp-setauthor,.fp-setlicense').each(function (node) {
node.all('label').set('for', node.one('input,select').generateID());
Expand All @@ -1239,6 +1240,28 @@ M.core_filepicker.init = function(Y, options) {
node.addClassIf('uneditable', !allowinputs);
node.all('input,select').set('disabled', allowinputs?'':'disabled');
});

// If the link to the file is selected, only then.
// Remember: this is not to be done for all repos.
// Only for those repos where the filereferencewarning is set.
// The value 4 represents FILE_REFERENCE here.
if (e.currentTarget.get('value') === '4') {
var filereferencewarning = filePickerHelper.active_repo.filereferencewarning;
if (filereferencewarning) {
var fileReferenceNode = e.currentTarget.ancestor('.fp-linktype-4');
var fileReferenceWarningNode = Y.Node.create('<div/>').
addClass('alert alert-warning px-3 py-1 my-1 small').
setAttrs({role: 'alert'}).
setContent(filereferencewarning);
fileReferenceNode.append(fileReferenceWarningNode);
}
} else {
var fileReferenceInput = selectnode.one('.fp-linktype-4 input');
var fileReferenceWarningNode = fileReferenceInput.ancestor('.fp-linktype-4').one('.alert-warning');
if (fileReferenceWarningNode) {
fileReferenceWarningNode.remove();
}
}
}
};
selectnode.all('.fp-linktype-2,.fp-linktype-1,.fp-linktype-4,.fp-linktype-8').each(function (node) {
Expand Down Expand Up @@ -1574,6 +1597,8 @@ M.core_filepicker.init = function(Y, options) {
this.active_repo.message = (data.message || '');
this.active_repo.help = data.help?data.help:null;
this.active_repo.manage = data.manage?data.manage:null;
// Warning message related to the file reference option, if applicable to the given repository.
this.active_repo.filereferencewarning = data.filereferencewarning ? data.filereferencewarning : null;
this.print_header();
},
print_login: function(data) {
Expand Down
3 changes: 3 additions & 0 deletions repository/nextcloud/lang/en/repository_nextcloud.php
Expand Up @@ -62,3 +62,6 @@
$string['noclientconnection'] = 'The OAuth clients could not be connected.';
$string['pathnotcreated'] = 'Folder path {$a} could not be created in the system account.';
$string['endpointnotdefined'] = 'Endpoint {$a} not defined.';

// Warnings.
$string['externalpubliclinkwarning'] = '<b>Warning:</b> This file will become public.';
89 changes: 86 additions & 3 deletions repository/nextcloud/lib.php
Expand Up @@ -92,6 +92,12 @@ class repository_nextcloud extends repository {
*/
private $controlledlinkfoldername;

/**
* Curl instance that can be used to fetch file from nextcloud instance.
* @var curl
*/
private $curl;

/**
* repository_nextcloud constructor.
*
Expand Down Expand Up @@ -143,6 +149,7 @@ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = a
}

$this->ocsclient = new ocs_client($this->get_user_oauth_client());
$this->curl = new curl();
}

/**
Expand Down Expand Up @@ -291,6 +298,7 @@ public function get_listing($path='', $page = '') {
*
*/
public function get_link($url) {
// Create a read only public link, remember no update possible in this file/folder.
$ocsparams = [
'path' => $url,
'shareType' => ocs_client::SHARE_TYPE_PUBLIC,
Expand Down Expand Up @@ -319,9 +327,16 @@ public function get_link($url) {
* This method does not do any translation of the file source.
*
* @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
* @return string file reference, ready to be stored or json encoded string for public link reference
*/
public function get_file_reference($source) {
$usefilereference = optional_param('usefilereference', false, PARAM_BOOL);
if ($usefilereference) {
return json_encode([
'type' => 'FILE_REFERENCE',
'link' => $this->get_link($source),
]);
}
// The simple relative path to the file is enough.
return $source;
}
Expand Down Expand Up @@ -420,6 +435,12 @@ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownloa
$repositoryname = $this->get_name();
$reference = json_decode($storedfile->get_reference());

// If the file is a reference which means its a public link in nextcloud.
if ($reference->type === 'FILE_REFERENCE') {
// This file points to the public link just fetch the latest one from nextcloud repo.
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',
Expand Down Expand Up @@ -751,10 +772,10 @@ public function supported_returntypes() {
} else if ($setting === 'external') {
return FILE_CONTROLLED_LINK;
} else {
return FILE_CONTROLLED_LINK | FILE_INTERNAL;
return FILE_CONTROLLED_LINK | FILE_INTERNAL | FILE_REFERENCE;
}
} else {
return FILE_INTERNAL;
return FILE_INTERNAL | FILE_REFERENCE;
}
}

Expand Down Expand Up @@ -866,6 +887,7 @@ private function get_listing_prepare_response($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.
'filereferencewarning' => get_string('externalpubliclinkwarning', 'repository_nextcloud'),
];

// If relative path is a non-top-level path, calculate all its parents' paths.
Expand Down Expand Up @@ -909,4 +931,65 @@ public function get_reference_details($reference, $filestatus = 0) {

return $path;
}

/**
* Synchronize the external file if there is an update happened to it.
*
* If the file has been updated in the nextcloud instance, this method
* would take care of the file we copy into the moodle file pool.
*
* The call to this method reaches from stored_file::sync_external_file()
*
* @param stored_file $file
* @return bool true if synced successfully else false if not ready to sync or reference link not set
*/
public function sync_reference(stored_file $file):bool {
global $CFG;

if ($file->get_referencelastsync() + DAYSECS > time()) {
// Synchronize once per day.
return false;
}

$reference = json_decode($file->get_reference());

if (!isset($reference->link)) {
return false;
}

$url = $reference->link;
if (file_extension_in_typegroup($file->get_filepath() . $file->get_filename(), 'web_image')) {
$saveas = $this->prepare_file(uniqid());
try {
$result = $this->curl->download_one($url, [], [
'filepath' => $saveas,
'timeout' => $CFG->repositorysyncimagetimeout,
'followlocation' => true,
]);

$info = $this->curl->get_info();

if ($result === true && isset($info['http_code']) && $info['http_code'] === 200) {
$file->set_synchronised_content_from_file($saveas);
return true;
}
} catch (Exception $e) {
// If the download fails lets download with get().
$this->curl->get($url, null, ['timeout' => $CFG->repositorysyncimagetimeout, 'followlocation' => true, 'nobody' => true]);
$info = $this->curl->get_info();

if (isset($info['http_code']) && $info['http_code'] === 200 &&
array_key_exists('download_content_length', $info) &&
$info['download_content_length'] >= 0) {
$filesize = (int)$info['download_content_length'];
$file->set_synchronized(null, $filesize);
return true;
}

$file->set_missingsource();
return true;
}
}
return false;
}
}
156 changes: 151 additions & 5 deletions repository/nextcloud/tests/lib_test.php
Expand Up @@ -623,12 +623,12 @@ public function test_initiate_webdavclient() {

/**
* Test supported_returntypes.
* FILE_INTERNAL when no system account is connected.
* FILE_INTERNAL | FILE_CONTROLLED_LINK when a system account is connected.
* FILE_INTERNAL | FILE_REFERENCE when no system account is connected.
* FILE_INTERNAL | FILE_CONTROLLED_LINK | FILE_REFERENCE when a system account is connected.
*/
public function test_supported_returntypes() {
global $DB;
$this->assertEquals(FILE_INTERNAL, $this->repo->supported_returntypes());
$this->assertEquals(FILE_INTERNAL | FILE_REFERENCE, $this->repo->supported_returntypes());
$dataobject = new stdClass();
$dataobject->timecreated = time();
$dataobject->timemodified = time();
Expand All @@ -641,12 +641,12 @@ public function test_supported_returntypes() {

$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_CONTROLLED_LINK,
$this->assertEquals(FILE_INTERNAL | 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.
* The reference_file_selected() method 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.
*
Expand Down Expand Up @@ -844,6 +844,150 @@ public function test_send_file_errors() {
$this->repo->send_file('', '', '', '');
}

/**
* This function provides the data for test_sync_reference
*
* @return array[]
*/
public function sync_reference_provider():array {
return [
'referecncelastsync done recently' => [
[
'storedfile_record' => [
'contextid' => context_system::instance()->id,
'component' => 'core',
'filearea' => 'unittest',
'itemid' => 0,
'filepath' => '/',
'filename' => 'testfile.txt',
],
'storedfile_reference' => json_encode(
[
'type' => 'FILE_REFERENCE',
'link' => 'https://test.local/fakelink/',
'usesystem' => true,
'referencelastsync' => DAYSECS + time()
]
),
],
'mockfunctions' => ['get_referencelastsync'],
'expectedresult' => false
],
'file without link' => [
[
'storedfile_record' => [
'contextid' => context_system::instance()->id,
'component' => 'core',
'filearea' => 'unittest',
'itemid' => 0,
'filepath' => '/',
'filename' => 'testfile.txt',
],
'storedfile_reference' => json_encode(
[
'type' => 'FILE_REFERENCE',
'usesystem' => true,
]
),
],
'mockfunctions' => [],
'expectedresult' => false
],
'file extenstion to exclude' => [
[
'storedfile_record' => [
'contextid' => context_system::instance()->id,
'component' => 'core',
'filearea' => 'unittest',
'itemid' => 0,
'filepath' => '/',
'filename' => 'testfile.txt',
],
'storedfile_reference' => json_encode(
[
'link' => 'https://test.local/fakelink/',
'type' => 'FILE_REFERENCE',
'usesystem' => true,
]
),
],
'mockfunctions' => [],
'expectedresult' => false
],
'file extenstion for image' => [
[
'storedfile_record' => [
'contextid' => context_system::instance()->id,
'component' => 'core',
'filearea' => 'unittest',
'itemid' => 0,
'filepath' => '/',
'filename' => 'testfile.png',
],
'storedfile_reference' => json_encode(
[
'link' => 'https://test.local/fakelink/',
'type' => 'FILE_REFERENCE',
'usesystem' => true,
]
),
'mock_curl' => true,
],
'mockfunctions' => [''],
'expectedresult' => true
],
];
}

/**
* Testing sync_reference
*
* @dataProvider sync_reference_provider
* @param array $storedfileargs
* @param array $storedfilemethodsmock
* @param bool $expectedresult
* @return void
*/
public function test_sync_reference(array $storedfileargs, $storedfilemethodsmock, bool $expectedresult):void {
$this->resetAfterTest(true);

if (isset($storedfilemethodsmock[0])) {
$storedfile = $this->createMock(stored_file::class);

if ($storedfilemethodsmock[0] === 'get_referencelastsync') {
if (!$expectedresult) {
$storedfile->method('get_referencelastsync')->willReturn(DAYSECS + time());
}
} else {
$storedfile->method('get_referencelastsync')->willReturn(null);
}

$storedfile->method('get_reference')->willReturn($storedfileargs['storedfile_reference']);
$storedfile->method('get_filepath')->willReturn($storedfileargs['storedfile_record']['filepath']);
$storedfile->method('get_filename')->willReturn($storedfileargs['storedfile_record']['filename']);

if ((isset($storedfileargs['mock_curl']) && $storedfileargs)) {
// Lets mock curl, else it would not serve the purpose here.
$curl = $this->createMock(curl::class);
$curl->method('download_one')->willReturn(true);
$curl->method('get_info')->willReturn(['http_code' => 200]);

$reflectionproperty = new \ReflectionProperty($this->repo, 'curl');
$reflectionproperty->setAccessible(true);
$reflectionproperty->setValue($this->repo, $curl);
}
} else {
$fs = get_file_storage();
$storedfile = $fs->create_file_from_reference(
$storedfileargs['storedfile_record'],
$this->repo->id,
$storedfileargs['storedfile_reference']);
}

$actualresult = $this->repo->sync_reference($storedfile);
$this->assertEquals($expectedresult, $actualresult);
}

/**
* Helper method, which inserts a given mock value into the repository_nextcloud object.
*
Expand Down Expand Up @@ -879,6 +1023,8 @@ protected function get_initialised_return_array() {
$ret['defaultreturntype'] = FILE_INTERNAL;
$ret['list'] = array();

$ret['filereferencewarning'] = get_string('externalpubliclinkwarning', 'repository_nextcloud');

return $ret;
}
}

0 comments on commit 23f7bdc

Please sign in to comment.