diff --git a/admin/webservice/forms.php b/admin/webservice/forms.php index b1c09aee44f..3aef57c2608 100644 --- a/admin/webservice/forms.php +++ b/admin/webservice/forms.php @@ -73,12 +73,17 @@ class external_service_form extends moodleform { $mform->addHelpButton('restrictedusers', 'restrictedusers', 'webservice'); $mform->setType('restrictedusers', PARAM_BOOL); - //can users download files + // Can users download files? $mform->addElement('advcheckbox', 'downloadfiles', get_string('downloadfiles', 'webservice')); $mform->setAdvanced('downloadfiles'); $mform->addHelpButton('downloadfiles', 'downloadfiles', 'webservice'); $mform->setType('downloadfiles', PARAM_BOOL); + // Can users upload files? + $mform->addElement('advcheckbox', 'uploadfiles', get_string('uploadfiles', 'webservice')); + $mform->setAdvanced('uploadfiles'); + $mform->addHelpButton('uploadfiles', 'uploadfiles', 'webservice'); + /// needed to select automatically the 'No required capability" option $currentcapabilityexist = false; if (empty($service->requiredcapability)) { @@ -297,4 +302,4 @@ class web_service_token_form extends moodleform { return $errors; } -} \ No newline at end of file +} diff --git a/files/externallib.php b/files/externallib.php index f2b0ab6f92d..e086c6ea5e3 100644 --- a/files/externallib.php +++ b/files/externallib.php @@ -272,11 +272,24 @@ class core_files_external extends external_api { $filepath = '/'; } + // Only allow uploads to draft or private areas (private is deprecated but still supported) + if (!($fileinfo['component'] == 'user' and in_array($fileinfo['filearea'], array('private', 'draft')))) { + throw new coding_exception('File can be uploaded to user private or draft areas only'); + } else { + $component = 'user'; + $filearea = $fileinfo['filearea']; + } + + $itemid = 0; if (isset($fileinfo['itemid'])) { + $itemid = $fileinfo['itemid']; + } + if ($filearea == 'draft' && $itemid <= 0) { + // Generate a draft area for the files. + $itemid = file_get_unused_draft_itemid(); + } else if ($filearea == 'private') { // TODO MDL-31116 in user private area, itemid is always 0. $itemid = 0; - } else { - throw new coding_exception('itemid cannot be empty'); } // We need to preserve backword compatibility. Context id is no more a required. @@ -287,13 +300,8 @@ class core_files_external extends external_api { // Get and validate context. $context = self::get_context_from_params($fileinfo); self::validate_context($context); - - if (!($fileinfo['component'] == 'user' and $fileinfo['filearea'] == 'private')) { - throw new coding_exception('File can be uploaded to user private area only'); - } else { - // TODO MDL-31116 hard-coded to use user_private area. - $component = 'user'; - $filearea = 'private'; + if (($fileinfo['component'] == 'user' and $fileinfo['filearea'] == 'private')) { + debugging('Uploading directly to user private files area is deprecated. Upload to a draft area and then move the files with core_user::add_user_private_files'); } $browser = get_file_browser(); diff --git a/lang/en/webservice.php b/lang/en/webservice.php index 74eece27e6c..83cdb584e6f 100644 --- a/lang/en/webservice.php +++ b/lang/en/webservice.php @@ -183,6 +183,8 @@ $string['tokencreator'] = 'Creator'; $string['unknownoptionkey'] = 'Unknown option key ({$a})'; $string['unnamedstringparam'] = 'A string parameter is unnamed.'; $string['updateusersettings'] = 'Update'; +$string['uploadfiles'] = 'Can upload files'; +$string['uploadfiles_help'] = 'If enabled, any user can upload files with their security keys to their own private files area or a draft file area. Any user file quotas apply.'; $string['userasclients'] = 'Users as clients with token'; $string['userasclientsdescription'] = 'The following steps help you to set up the Moodle web service for users as clients. These steps also help to set up the recommended token (security keys) authentication method. In this use case, the user will generate his token from the security keys page via My profile settings.'; $string['usermissingcaps'] = 'Missing capabilities: {$a}'; diff --git a/lib/db/install.xml b/lib/db/install.xml index 27e3b733bab..60e3b363256 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -2565,6 +2565,7 @@ + diff --git a/lib/db/services.php b/lib/db/services.php index 6b377d7bb07..311c0dd7caa 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -883,6 +883,7 @@ $services = array( 'enabled' => 0, 'restrictedusers' => 0, 'shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE, - 'downloadfiles' => 1 + 'downloadfiles' => 1, + 'uploadfiles' => 1 ), ); diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index ee19a965ec0..cdc076ba0a3 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2313,5 +2313,19 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2013072600.01); } + if ($oldversion < 2013081200.00) { + // Define field uploadfiles to be added to external_services. + $table = new xmldb_table('external_services'); + $field = new xmldb_field('uploadfiles', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'downloadfiles'); + + // Conditionally launch add field uploadfiles. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2013081200.00); + } + return true; } diff --git a/lib/upgradelib.php b/lib/upgradelib.php index 1ce09beb3bd..0ee41400416 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -1045,6 +1045,7 @@ function external_update_descriptions($component) { $service['requiredcapability'] = empty($service['requiredcapability']) ? null : $service['requiredcapability']; $service['restrictedusers'] = !isset($service['restrictedusers']) ? 1 : $service['restrictedusers']; $service['downloadfiles'] = !isset($service['downloadfiles']) ? 0 : $service['downloadfiles']; + $service['uploadfiles'] = !isset($service['uploadfiles']) ? 0 : $service['uploadfiles']; $service['shortname'] = !isset($service['shortname']) ? null : $service['shortname']; $update = false; @@ -1060,6 +1061,10 @@ function external_update_descriptions($component) { $dbservice->downloadfiles = $service['downloadfiles']; $update = true; } + if ($dbservice->uploadfiles != $service['uploadfiles']) { + $dbservice->uploadfiles = $service['uploadfiles']; + $update = true; + } //if shortname is not a PARAM_ALPHANUMEXT, fail (tested here for service update and creation) if (isset($service['shortname']) and (clean_param($service['shortname'], PARAM_ALPHANUMEXT) != $service['shortname'])) { @@ -1114,6 +1119,7 @@ function external_update_descriptions($component) { $dbservice->requiredcapability = empty($service['requiredcapability']) ? null : $service['requiredcapability']; $dbservice->restrictedusers = !isset($service['restrictedusers']) ? 1 : $service['restrictedusers']; $dbservice->downloadfiles = !isset($service['downloadfiles']) ? 0 : $service['downloadfiles']; + $dbservice->uploadfiles = !isset($service['uploadfiles']) ? 0 : $service['uploadfiles']; $dbservice->shortname = !isset($service['shortname']) ? null : $service['shortname']; $dbservice->component = $component; $dbservice->timecreated = time(); diff --git a/user/externallib.php b/user/externallib.php index bbaa87ae9f1..787ddb5ada0 100644 --- a/user/externallib.php +++ b/user/externallib.php @@ -926,6 +926,68 @@ class core_user_external extends external_api { return new external_single_structure($userfields); } + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.6 + */ + public static function add_user_private_files_parameters() { + return new external_function_parameters( + array( + 'draftid' => new external_value(PARAM_INT, 'draft area id') + ) + ); + } + + /** + * Copy files from a draft area to users private files area. + * + * @param int $draftid Id of a draft area containing files. + * @return array An array of warnings + * @since Moodle 2.6 + */ + public static function add_user_private_files($draftid) { + global $CFG, $USER, $DB; + + require_once($CFG->dirroot . "/user/lib.php"); + $params = self::validate_parameters(self::add_user_private_files_parameters(), array('draftid'=>$draftid)); + + if (isguestuser()) { + throw new invalid_parameter_exception('Guest users cannot upload files'); + } + + $context = context_user::instance($USER->id); + require_capability('moodle/user:manageownfiles', $context); + + $maxbytes = $CFG->userquota; + $maxareabytes = $CFG->userquota; + if (has_capability('moodle/user:ignoreuserquota', $context)) { + $maxbytes = USER_CAN_IGNORE_FILE_SIZE_LIMITS; + $maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED; + } + + $options = array('subdirs' => 1, + 'maxbytes' => $maxbytes, + 'maxfiles' => -1, + 'accepted_types' => '*', + 'areamaxbytes' => $maxareabytes); + + file_save_draft_area_files($draftid, $context->id, 'user', 'private', 0, $options); + + return null; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.2 + */ + public static function add_user_private_files_returns() { + return null; + } + } /** diff --git a/user/tests/externallib_test.php b/user/tests/externallib_test.php index f58271e62fd..011cc71650d 100644 --- a/user/tests/externallib_test.php +++ b/user/tests/externallib_test.php @@ -30,6 +30,7 @@ global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/user/externallib.php'); +require_once($CFG->dirroot . '/files/externallib.php'); class core_user_externallib_testcase extends externallib_advanced_testcase { @@ -667,4 +668,48 @@ class core_user_externallib_testcase extends externallib_advanced_testcase { $this->setExpectedException('required_capability_exception'); core_user_external::update_users(array($user1)); } + + /** + * Test add_user_private_files + */ + public function test_add_user_private_files() { + global $USER, $CFG, $DB; + + $this->resetAfterTest(true); + + $context = context_system::instance(); + $roleid = $this->assignUserCapability('moodle/user:manageownfiles', $context->id); + + $context = context_user::instance($USER->id); + $contextid = $context->id; + $component = "user"; + $filearea = "draft"; + $itemid = 0; + $filepath = "/"; + $filename = "Simple.txt"; + $filecontent = base64_encode("Let us create a nice simple file"); + $contextlevel = null; + $instanceid = null; + $browser = get_file_browser(); + + // Call the files api to create a file. + $draftfile = core_files_external::upload($contextid, $component, $filearea, $itemid, $filepath, + $filename, $filecontent, $contextlevel, $instanceid); + + $draftid = $draftfile['itemid']; + // Make sure the file was created. + $file = $browser->get_file_info($context, $component, $filearea, $draftid, $filepath, $filename); + $this->assertNotEmpty($file); + + // Make sure the file does not exist in the user private files. + $file = $browser->get_file_info($context, $component, 'private', 0, $filepath, $filename); + $this->assertEmpty($file); + + // Call the external function. + core_user_external::add_user_private_files($draftid); + + // Make sure the file was added to the user private files. + $file = $browser->get_file_info($context, $component, 'private', 0, $filepath, $filename); + $this->assertNotEmpty($file); + } } diff --git a/version.php b/version.php index 99768c68260..431f5abf222 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2013080801.00; // YYYYMMDD = weekly release date of this DEV branch +$version = 2013081200.00; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes diff --git a/webservice/externallib.php b/webservice/externallib.php index ecc78e054d3..b91243b1e39 100644 --- a/webservice/externallib.php +++ b/webservice/externallib.php @@ -108,6 +108,7 @@ class core_webservice_external extends external_api { $service = $DB->get_record_sql($servicesql, array($token, $USER->id)); $siteinfo['downloadfiles'] = $service->downloadfiles; + $siteinfo['uploadfiles'] = $service->uploadfiles; if (!empty($service)) { // Return the release and version number for web service users only. @@ -195,6 +196,8 @@ class core_webservice_external extends external_api { ), 'downloadfiles' => new external_value(PARAM_INT, '1 if users are allowed to download files, 0 if not', VALUE_OPTIONAL), + 'uploadfiles' => new external_value(PARAM_INT, '1 if users are allowed to upload files, 0 if not', + VALUE_OPTIONAL), 'release' => new external_value(PARAM_TEXT, 'Moodle release number', VALUE_OPTIONAL), 'version' => new external_value(PARAM_TEXT, 'Moodle version number', VALUE_OPTIONAL), 'mobilecssurl' => new external_value(PARAM_URL, 'Mobile custom CSS theme', VALUE_OPTIONAL) diff --git a/webservice/tests/externallib_test.php b/webservice/tests/externallib_test.php index 7b8588d1742..92399af0c32 100644 --- a/webservice/tests/externallib_test.php +++ b/webservice/tests/externallib_test.php @@ -62,6 +62,7 @@ class core_webservice_externallib_testcase extends externallib_advanced_testcase $webservice->component = 'moodle'; $webservice->timecreated = time(); $webservice->downloadfiles = true; + $webservice->uploadfiles = true; $externalserviceid = $DB->insert_record('external_services', $webservice); // Add a function to the service @@ -97,6 +98,8 @@ class core_webservice_externallib_testcase extends externallib_advanced_testcase $function = array_pop($siteinfo['functions']); $this->assertEquals($function['name'], 'core_course_get_contents'); $this->assertEquals($function['version'], $siteinfo['version']); + $this->assertEquals(1, $siteinfo['downloadfiles']); + $this->assertEquals(1, $siteinfo['uploadfiles']); } } diff --git a/webservice/upgrade.txt b/webservice/upgrade.txt new file mode 100644 index 00000000000..4f797431d70 --- /dev/null +++ b/webservice/upgrade.txt @@ -0,0 +1,13 @@ +This files describes API changes in /webservice/* +information provided here is intended especially for developers. + +This information is intended for authors of webservices, not people writing webservice clients. + +=== 2.6 === + +* webservice/upload.php +Accepts 2 new post parameters to allow uploading of files to a users draft area. + - filearea should be either 'private' (default) or 'draft' + - itemid unused if the filearea is 'private', for 'draft' it can be the id of a previously + created draft area - or 0 which will generate a new draft area for the files. + diff --git a/webservice/upload.php b/webservice/upload.php index c7b563aab29..93310cdf357 100644 --- a/webservice/upload.php +++ b/webservice/upload.php @@ -23,6 +23,11 @@ * filepath => the private file aera path (where files will be stored) * [_FILES] => for example you can send the files with , * or with curl magic: 'file_1' => '@/path/to/file', or ... + * filearea => 'private' or 'draft' (default = 'private'). These are the only 2 areas we are allowing + * direct uploads via webservices. The private file area is deprecated - please don't use it. + * itemid => For draft areas this is the draftid - this can be used to add a list of files + * to a draft area in separate requests. If it is 0, a new draftid will be generated. + * For private files, this is ignored. * * @package core_webservice * @copyright 2011 Dongsheng Cai @@ -42,6 +47,11 @@ define('NO_MOODLE_COOKIES', true); require_once(dirname(dirname(__FILE__)) . '/config.php'); require_once($CFG->dirroot . '/webservice/lib.php'); $filepath = optional_param('filepath', '/', PARAM_PATH); +// The default file area is 'private' for user private files. This +// area is actually deprecated and only supported for backwards compatibility with +// the mobile app. +$filearea = optional_param('filearea', 'private', PARAM_ALPHA); +$itemid = optional_param('itemid', 0, PARAM_INT); echo $OUTPUT->header(); @@ -49,6 +59,10 @@ echo $OUTPUT->header(); $token = required_param('token', PARAM_ALPHANUM); $webservicelib = new webservice(); $authenticationinfo = $webservicelib->authenticate_user($token); +$fileuploaddisabled = empty($authenticationinfo['service']->uploadfiles); +if ($fileuploaddisabled) { + throw new webservice_access_exception('Web service file upload must be enabled in external service settings'); +} // check the user can manage his own files (can upload) $context = context_user::instance($USER->id); @@ -105,13 +119,34 @@ foreach ($_FILES as $fieldname=>$uploaded_file) { $fs = get_file_storage(); -$usedspace = 0; -$privatefiles = $fs->get_area_files($context->id, 'user', 'private', false, 'id', false); -foreach ($privatefiles as $file) { - $usedspace += $file->get_filesize(); +if ($filearea == 'draft' && $itemid <= 0) { + $itemid = file_get_unused_draft_itemid(); } -if ($totalsize > ($CFG->userquota - $usedspace)) { +// Get any existing file size limits. +$maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED; +$maxupload = get_user_max_upload_file_size($context, $CFG->maxbytes); +if ($filearea == 'private') { + // Private files area is limited by $CFG->userquota. + if (!has_capability('moodle/user:ignoreuserquota', $context)) { + $maxareabytes = $CFG->userquota; + } + + // Count the size of all existing files in this area. + if ($maxareabytes > 0) { + $usedspace = 0; + $existingfiles = $fs->get_area_files($context->id, 'user', $filearea, false, 'id', false); + foreach ($existingfiles as $file) { + $usedspace += $file->get_filesize(); + } + if ($totalsize > ($maxareabytes - $usedspace)) { + throw new file_exception('userquotalimit'); + } + } +} + +// Check the size of this upload. +if ($maxupload !== USER_CAN_IGNORE_FILE_SIZE_LIMITS && $totalsize > $maxupload) { throw new file_exception('userquotalimit'); } @@ -126,10 +161,10 @@ foreach ($files as $file) { $file_record->component = 'user'; $file_record->contextid = $context->id; $file_record->userid = $USER->id; - $file_record->filearea = 'private'; + $file_record->filearea = $filearea; $file_record->filename = $file->filename; $file_record->filepath = $filepath; - $file_record->itemid = 0; + $file_record->itemid = $itemid; $file_record->license = $CFG->sitedefaultlicense; $file_record->author = fullname($authenticationinfo['user']); $file_record->source = '';