diff --git a/repository/filepicker.js b/repository/filepicker.js index 5c5f0fa2fb471..e989e4304d53b 100644 --- a/repository/filepicker.js +++ b/repository/filepicker.js @@ -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()); @@ -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('
'). + 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) { @@ -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) { diff --git a/repository/nextcloud/lang/en/repository_nextcloud.php b/repository/nextcloud/lang/en/repository_nextcloud.php index cca3992afca14..3375095c448e3 100644 --- a/repository/nextcloud/lang/en/repository_nextcloud.php +++ b/repository/nextcloud/lang/en/repository_nextcloud.php @@ -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'] = 'Warning: This file will become public.'; diff --git a/repository/nextcloud/lib.php b/repository/nextcloud/lib.php index 62f38c0a1e440..5e4dce0418d09 100644 --- a/repository/nextcloud/lib.php +++ b/repository/nextcloud/lib.php @@ -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. * @@ -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(); } /** @@ -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, @@ -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; } @@ -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', @@ -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; } } @@ -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. @@ -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; + } } diff --git a/repository/nextcloud/tests/lib_test.php b/repository/nextcloud/tests/lib_test.php index cf6fe64f5a16f..0554fecaf55ba 100644 --- a/repository/nextcloud/tests/lib_test.php +++ b/repository/nextcloud/tests/lib_test.php @@ -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(); @@ -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. * @@ -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. * @@ -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; } }