From 60237253a23644c38cbb3f8ea66c8e054295b6f6 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 17 Feb 2017 16:37:53 +0800 Subject: [PATCH 01/84] MDL-58090 oauth2: Store a list of oauth2 services Build an admin page where OAuth 2 services can be installed and configured. Part of MDL-58220 --- admin/oauth2callback.php | 11 + admin/tool/oauth2/classes/form/issuer.php | 107 ++++++ admin/tool/oauth2/classes/output/renderer.php | 167 +++++++++ admin/tool/oauth2/issuers.php | 142 +++++++ admin/tool/oauth2/lang/en/tool_oauth2.php | 63 ++++ admin/tool/oauth2/pix/auth.svg | 3 + admin/tool/oauth2/pix/no.svg | 3 + admin/tool/oauth2/pix/yes.svg | 3 + admin/tool/oauth2/settings.php | 29 ++ .../oauth2/tests/behat/manage_tasks.feature | 53 +++ admin/tool/oauth2/tests/form_test.php | 275 ++++++++++++++ admin/tool/oauth2/version.php | 30 ++ auth/classes/output/login.php | 11 +- auth/oauth2/auth.php | 40 ++ auth/oauth2/classes/auth.php | 350 +++++++++++++++++ auth/oauth2/lang/en/auth_oauth2.php | 29 ++ auth/oauth2/login.php | 45 +++ auth/oauth2/version.php | 29 ++ lib/classes/oauth2/api.php | 352 ++++++++++++++++++ lib/classes/oauth2/client.php | 117 ++++++ lib/classes/oauth2/client_microsoft.php | 54 +++ lib/classes/oauth2/client_oauth2.php | 43 +++ lib/classes/oauth2/client_openid_connect.php | 98 +++++ lib/classes/oauth2/endpoint.php | 58 +++ lib/classes/oauth2/issuer.php | 131 +++++++ lib/classes/oauth2/system_account.php | 60 +++ lib/db/install.php | 2 + lib/db/install.xml | 52 ++- lib/db/upgrade.php | 93 +++++ lib/filelib.php | 5 +- lib/oauthlib.php | 64 +++- lib/templates/login.mustache | 6 +- theme/boost/templates/core/login.mustache | 7 +- version.php | 2 +- 34 files changed, 2512 insertions(+), 22 deletions(-) create mode 100644 admin/tool/oauth2/classes/form/issuer.php create mode 100644 admin/tool/oauth2/classes/output/renderer.php create mode 100644 admin/tool/oauth2/issuers.php create mode 100644 admin/tool/oauth2/lang/en/tool_oauth2.php create mode 100644 admin/tool/oauth2/pix/auth.svg create mode 100644 admin/tool/oauth2/pix/no.svg create mode 100644 admin/tool/oauth2/pix/yes.svg create mode 100644 admin/tool/oauth2/settings.php create mode 100644 admin/tool/oauth2/tests/behat/manage_tasks.feature create mode 100644 admin/tool/oauth2/tests/form_test.php create mode 100644 admin/tool/oauth2/version.php create mode 100644 auth/oauth2/auth.php create mode 100644 auth/oauth2/classes/auth.php create mode 100644 auth/oauth2/lang/en/auth_oauth2.php create mode 100644 auth/oauth2/login.php create mode 100644 auth/oauth2/version.php create mode 100644 lib/classes/oauth2/api.php create mode 100644 lib/classes/oauth2/client.php create mode 100644 lib/classes/oauth2/client_microsoft.php create mode 100644 lib/classes/oauth2/client_oauth2.php create mode 100644 lib/classes/oauth2/client_openid_connect.php create mode 100644 lib/classes/oauth2/endpoint.php create mode 100644 lib/classes/oauth2/issuer.php create mode 100644 lib/classes/oauth2/system_account.php mode change 100644 => 100755 lib/db/install.xml diff --git a/admin/oauth2callback.php b/admin/oauth2callback.php index 08bb01d78e3..709e28ce99b 100644 --- a/admin/oauth2callback.php +++ b/admin/oauth2callback.php @@ -30,6 +30,17 @@ require_once(__DIR__ . '/../config.php'); +$error = optional_param('error', '', PARAM_RAW); +if ($error) { + $message = optional_param('error_description', '', PARAM_RAW); + if ($message) { + print_error($message); + } else { + print_error($error); + } + die(); +} + // The authorization code generated by the authorization server. $code = required_param('code', PARAM_RAW); // The state parameter we've given (used in moodle as a redirect url). diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php new file mode 100644 index 00000000000..ce03af63e4a --- /dev/null +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -0,0 +1,107 @@ +. + +/** + * This file contains the form add/update oauth2 issuer. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_oauth2\form; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; +use core\form\persistent; + +/** + * Issuer form. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issuer extends persistent { + + protected static $persistentclass = 'core\\oauth2\\issuer'; + + protected static $fieldstoremove = array('submitbutton', 'action'); + + /** + * Define the form - called by parent constructor + */ + public function definition() { + global $PAGE; + + $mform = $this->_form; + $provider = $this->get_persistent(); + + $mform->addElement('header', 'generalhdr', get_string('general')); + + // Name. + $mform->addElement('text', 'name', get_string('issuername', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('name', 'issuername', 'tool_oauth2'); + + // Client ID. + $mform->addElement('text', 'clientid', get_string('issuerclientid', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('clientid', null, 'required', null, 'client'); + $mform->addRule('clientid', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('clientid', 'issuerclientid', 'tool_oauth2'); + + // Client Secret. + $mform->addElement('text', 'clientsecret', get_string('issuerclientsecret', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('clientsecret', null, 'required', null, 'client'); + $mform->addRule('clientsecret', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('clientsecret', 'issuerclientsecret', 'tool_oauth2'); + + // Base Url. + $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addRule('baseurl', null, 'required', null, 'client'); + $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); + $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2'); + + // Offline access type + $options = $provider->get_behaviour_list(); + $mform->addElement('select', 'behaviour', get_string('issuerbehaviour', 'tool_oauth2'), $options); + $mform->addHelpButton('behaviour', 'issuerbehaviour', 'tool_oauth2'); + + // Image. + $mform->addElement('text', 'image', get_string('issuerimage', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addRule('image', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); + $mform->addHelpButton('image', 'issuername', 'tool_oauth2'); + + // Show on login page. + $mform->addElement('checkbox', 'showonloginpage', get_string('issuershowonloginpage', 'tool_oauth2')); + $mform->addHelpButton('showonloginpage', 'issuershowonloginpage', 'tool_oauth2'); + + + $mform->addElement('hidden', 'sortorder'); + $mform->setType('sortorder', PARAM_INT); + + $mform->addElement('hidden', 'action', 'edit'); + $mform->setType('action', PARAM_RAW); + + $mform->addElement('hidden', 'id', $provider->get('id')); + $mform->setType('id', PARAM_INT); + + $this->add_action_buttons(true, get_string('savechanges', 'tool_oauth2')); + } + +} + diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php new file mode 100644 index 00000000000..f26ca777f91 --- /dev/null +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -0,0 +1,167 @@ +. + +/** + * Output rendering for the plugin. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_oauth2\output; + +use plugin_renderer_base; +use html_table; +use html_table_cell; +use html_table_row; +use html_writer; +use core\oauth2\issuer; +use core\oauth2\api; +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Implements the plugin renderer + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + /** + * This function will render one beautiful table with all the issuers. + * + * @param \core\oauth2\issuer[] $issuers - list of all issuers. + * @return string HTML to output. + */ + public function issuers_table($issuers) { + global $CFG, $OUTPUT; + + $table = new html_table(); + $table->head = [ + get_string('name'), + get_string('configuredstatus', 'tool_oauth2'), + get_string('loginissuer', 'tool_oauth2'), + get_string('discoverystatus', 'tool_oauth2'), + get_string('systemauthstatus', 'tool_oauth2'), + get_string('edit'), + ]; + $table->attributes['class'] = 'admintable generaltable'; + $data = []; + + $index = 0; + + foreach ($issuers as $issuer) { + // We need to handle the first and last ones specially. + $first = false; + if ($index == 0) { + $first = true; + } + $last = false; + if ($index == count($issuers) - 1) { + $last = true; + } + + // Name. + $name = $issuer->get('name'); + $image = $issuer->get('image'); + if ($image) { + $name = ' ' . $name; + } + $namecell = new html_table_cell($name); + $namecell->header = true; + + // Configured. + if (!empty($issuer->get('clientid')) && !empty($issuer->get('clientsecret'))) { + $configured = $OUTPUT->pix_icon('yes', get_string('configured', 'tool_oauth2'), 'tool_oauth2'); + } else { + $configured = $OUTPUT->pix_icon('no', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2'); + } + $configuredstatuscell = new html_table_cell($configured); + + // Login issuer. + if (!empty($issuer->get('showonloginpage'))) { + $loginissuer = $OUTPUT->pix_icon('yes', get_string('loginissuer', 'tool_oauth2'), 'tool_oauth2'); + } else { + $loginissuer = $OUTPUT->pix_icon('no', get_string('notloginissuer', 'tool_oauth2'), 'tool_oauth2'); + } + $loginissuerstatuscell = new html_table_cell($loginissuer); + + // Discovered. + if (!empty($issuer->get('scopessupported'))) { + $discovered = $OUTPUT->pix_icon('yes', get_string('discovered', 'tool_oauth2'), 'tool_oauth2'); + } else { + $discovered = $OUTPUT->pix_icon('no', get_string('notdiscovered', 'tool_oauth2'), 'tool_oauth2'); + } + $discoverystatuscell = new html_table_cell($discovered); + + // Connected. + if ($issuer->is_system_account_connected()) { + $systemauth = $OUTPUT->pix_icon('yes', get_string('systemaccountconnected', 'tool_oauth2'), 'tool_oauth2'); + } else { + $systemauth = $OUTPUT->pix_icon('no', get_string('systemaccountnotconnected', 'tool_oauth2'), 'tool_oauth2'); + } + + if ($issuer->is_system_account_setup_supported()) { + $params = ['id' => $issuer->get('id'), 'action' => 'auth']; + $authurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + $icon = $OUTPUT->pix_icon('auth', get_string('connectsystemaccount', 'tool_oauth2'), 'tool_oauth2'); + $authlink = html_writer::link($authurl, $icon); + $systemauth .= ' ' . $authlink; + } + + $systemauthstatuscell = new html_table_cell($systemauth); + + // Action links. + $links = ''; + $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['id' => $issuer->get('id'), 'action' => 'edit']); + $editlink = html_writer::link($editurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))); + + $links .= ' ' . $editlink; + $deleteurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['id' => $issuer->get('id'), 'action' => 'delete']); + $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $links .= ' ' . $deletelink; + if (!$last) { + $params = ['id' => $issuer->get('id'), 'action' => 'movedown', 'sesskey' => sesskey()]; + $movedownurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + $movedownlink = html_writer::link($movedownurl, $OUTPUT->pix_icon('t/down', get_string('movedown'))); + $links .= ' ' . $movedownlink; + } + if (!$first) { + $params = ['id' => $issuer->get('id'), 'action' => 'moveup', 'sesskey' => sesskey()]; + $moveupurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + $moveuplink = html_writer::link($moveupurl, $OUTPUT->pix_icon('t/up', get_string('moveup'))); + $links .= ' ' . $moveuplink; + } + + $editcell = new html_table_cell($links); + + $row = new html_table_row([ + $namecell, + $configuredstatuscell, + $loginissuerstatuscell, + $discoverystatuscell, + $systemauthstatuscell, + $editcell, + ]); + + $data[] = $row; + $index++; + } + $table->data = $data; + return html_writer::table($table); + } +} diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php new file mode 100644 index 00000000000..d80f91d1e2a --- /dev/null +++ b/admin/tool/oauth2/issuers.php @@ -0,0 +1,142 @@ +. + +/** + * OAuth 2 Configuration page. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/tablelib.php'); + +$PAGE->set_url('/admin/tool/oauth2/issuers.php'); +$PAGE->set_context(context_system::instance()); +$PAGE->set_pagelayout('admin'); +$strheading = get_string('pluginname', 'tool_oauth2'); +$PAGE->set_title($strheading); +$PAGE->set_heading($strheading); + +require_login(); + +require_capability('moodle/site:config', context_system::instance()); + +$renderer = $PAGE->get_renderer('tool_oauth2'); + +$action = optional_param('action', '', PARAM_ALPHAEXT); +$idpid = optional_param('id', '', PARAM_RAW); +$issuer = null; +$mform = null; + +if ($idpid) { + $issuer = \core\oauth2\api::get_issuer($idpid); + if (!$issuer) { + print_error('invaliddata'); + } +} + +if ($action == 'edit') { + if ($issuer) { + $PAGE->navbar->add(get_string('editissuer', 'tool_oauth2', $issuer->get('name'))); + } else { + $PAGE->navbar->add(get_string('createnewissuer', 'tool_oauth2')); + } + + $mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer]); +} + +if ($mform && $mform->is_cancelled()) { + redirect(new moodle_url('/admin/tool/oauth2/issuers.php')); +} else if ($action == 'edit') { + + if ($data = $mform->get_data()) { + + try { + if (!empty($data->id)) { + core\oauth2\api::update_issuer($data); + } else { + core\oauth2\api::create_issuer($data); + } + redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); + } catch (Exception $e) { + redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR); + } + } else { + echo $OUTPUT->header(); + if ($issuer) { + echo $OUTPUT->heading(get_string('editissuer', 'tool_oauth2', $issuer->get('name'))); + } else { + echo $OUTPUT->heading(get_string('createnewissuer', 'tool_oauth2')); + } + $mform->display(); + echo $OUTPUT->footer(); + } + +} else if ($action == 'delete') { + + if (!optional_param('confirm', false, PARAM_BOOL)) { + $continueparams = ['action' => 'delete', 'id' => $idpid, 'sesskey' => sesskey(), 'confirm' => true]; + $continueurl = new moodle_url('/admin/tool/oauth2/issuers.php', $continueparams); + $cancelurl = new moodle_url('/admin/tool/oauth2/issuers.php'); + echo $OUTPUT->header(); + echo $OUTPUT->confirm(get_string('deleteconfirm', 'tool_oauth2', $issuer->get('name')), $continueurl, $cancelurl); + echo $OUTPUT->footer(); + } else { + require_sesskey(); + core\oauth2\api::delete_issuer($idpid); + redirect($PAGE->url, get_string('issuerdeleted', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); + } + +} else if ($action == 'auth') { + + if (!optional_param('confirm', false, PARAM_BOOL)) { + $continueparams = ['action' => 'auth', 'id' => $idpid, 'sesskey' => sesskey(), 'confirm' => true]; + $continueurl = new moodle_url('/admin/tool/oauth2/issuers.php', $continueparams); + $cancelurl = new moodle_url('/admin/tool/oauth2/issuers.php'); + echo $OUTPUT->header(); + echo $OUTPUT->confirm(get_string('authconfirm', 'tool_oauth2', $issuer->get('name')), $continueurl, $cancelurl); + echo $OUTPUT->footer(); + } else { + require_sesskey(); + $params = ['sesskey' => sesskey(), 'id' => $idpid, 'action' => 'auth', 'confirm' => true, 'response' => true]; + if (core\oauth2\api::connect_system_account($issuer, new moodle_url('/admin/tool/oauth2/issuers.php', $params))) { + redirect($PAGE->url, get_string('authconnected', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); + } else { + redirect($PAGE->url, get_string('authnotconnected', 'tool_oauth2'), null, \core\output\notification::NOTIFY_ERROR); + } + } +} else if ($action == 'moveup') { + require_sesskey(); + core\oauth2\api::move_up_issuer($idpid); + redirect($PAGE->url); + +} else if ($action == 'movedown') { + require_sesskey(); + core\oauth2\api::move_down_issuer($idpid); + redirect($PAGE->url); + +} else { + echo $OUTPUT->header(); + $issuers = core\oauth2\api::get_all_issuers(); + echo $renderer->issuers_table($issuers); + + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']); + echo $renderer->single_button($addurl, get_string('createnewissuer', 'tool_oauth2')); + echo $OUTPUT->footer(); +} diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php new file mode 100644 index 00000000000..73bce13bed4 --- /dev/null +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -0,0 +1,63 @@ +. + +/** + * Strings for component 'tool_oauth2', language 'en' + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Open ID Connect configuration'; +$string['editissuer'] = 'Edit identity issuer: {$a}'; +$string['issuername'] = 'Name'; +$string['issuername_help'] = 'Name of the identity issuer. May be displayed on login page.'; +$string['issuerimage'] = 'Logo URL'; +$string['issuerimage_help'] = 'An image url used to show a logo for this issuer. May be displayed on login page.'; +$string['issuerclientid'] = 'Client Id'; +$string['issuerclientid_help'] = 'The OAuth client ID for this issuer.'; +$string['issuerclientsecret'] = 'Client Secret'; +$string['issuerclientsecret_help'] = 'The OAuth client secret for this issuer.'; +$string['issuerbaseurl'] = 'Service base url'; +$string['issuerbaseurl_help'] = 'Base url used to access the service.'; +$string['issuershowonloginpage'] = 'Show on login page.'; +$string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to login with accounts from this issuer.'; +$string['issuerbehaviour'] = 'Behaviour'; +$string['issuerbehaviour_help'] = 'Choose from one of the supported behaviours. + +* OAuth 2.0 - OAuth 2.0 API with no authentication +* OpenID Connect - Standards based OAuth 2.0 API with Authentication +* Microsoft OAuth 2.0 - Non-standard OAuth 2.0 combined with Microsoft Graph API'; +$string['savechanges'] = 'Save changes'; +$string['configuredstatus'] = 'Configured'; +$string['discoverystatus'] = 'Discovery'; +$string['systemauthstatus'] = 'System account connected'; +$string['configured'] = 'Configured'; +$string['notconfigured'] = 'Not configured'; +$string['discovered'] = 'Service discovery successful'; +$string['notdiscovered'] = 'Service discovery not successful'; +$string['loginissuer'] = 'Allow login'; +$string['notloginissuer'] = 'Do not allow login'; +$string['systemaccountconnected'] = 'System account connected'; +$string['systemaccountnotconnected'] = 'System account not connected'; +$string['createnewissuer'] = 'Create new identity issuer'; +$string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.'; +$string['issuerdeleted'] = 'Identity issuer deleted'; +$string['connectsystemaccount'] = 'Connect to a system account'; +$string['authconfirm'] = 'This action will grant permanent API access to Moodle for the authenticated account. This is intended to be used as a system account for managing files owned by Moodle.'; +$string['authconnected'] = 'The system account is now connected for offline access'; +$string['authnotconnected'] = 'The system account was not connected for offline access'; diff --git a/admin/tool/oauth2/pix/auth.svg b/admin/tool/oauth2/pix/auth.svg new file mode 100644 index 00000000000..409824ad998 --- /dev/null +++ b/admin/tool/oauth2/pix/auth.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/admin/tool/oauth2/pix/no.svg b/admin/tool/oauth2/pix/no.svg new file mode 100644 index 00000000000..0185d868b95 --- /dev/null +++ b/admin/tool/oauth2/pix/no.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/admin/tool/oauth2/pix/yes.svg b/admin/tool/oauth2/pix/yes.svg new file mode 100644 index 00000000000..714d4c73c79 --- /dev/null +++ b/admin/tool/oauth2/pix/yes.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/admin/tool/oauth2/settings.php b/admin/tool/oauth2/settings.php new file mode 100644 index 00000000000..950a02137d1 --- /dev/null +++ b/admin/tool/oauth2/settings.php @@ -0,0 +1,29 @@ +. + +/** + * Oauth2 system configuration. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($hassiteconfig) { + $ADMIN->add('server', new admin_externalpage('oauth2', new lang_string('pluginname','tool_oauth2'), "$CFG->wwwroot/$CFG->admin/tool/oauth2/issuers.php")); +} diff --git a/admin/tool/oauth2/tests/behat/manage_tasks.feature b/admin/tool/oauth2/tests/behat/manage_tasks.feature new file mode 100644 index 00000000000..73fc27dd61b --- /dev/null +++ b/admin/tool/oauth2/tests/behat/manage_tasks.feature @@ -0,0 +1,53 @@ +@tool @tool_task @javascript +Feature: Manage scheduled tasks + In order to configure scheduled tasks + As an admin + I need to be able to disable, enable, edit and reset to default scheduled tasks + + Background: + Given I log in as "admin" + And I navigate to "Scheduled tasks" node in "Site administration > Server" + + Scenario: Disable scheduled task + When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" + Then I should see "Edit task schedule: Log table cleanup" + And I set the following fields to these values: + | disabled | 1 | + And I press "Save changes" + Then I should see "Changes saved" + And I should see "Task disabled" in the "Log table cleanup" "table_row" + + Scenario: Enable scheduled task + When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" + Then I should see "Edit task schedule: Log table cleanup" + And I set the following fields to these values: + | disabled | 0 | + And I press "Save changes" + Then I should see "Changes saved" + And I should not see "Task disabled" in the "Log table cleanup" "table_row" + + Scenario: Edit scheduled task + When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" + Then I should see "Edit task schedule: Log table cleanup" + And I set the following fields to these values: + | minute | */5 | + | hour | 1 | + | day | 2 | + | month | 3 | + | dayofweek | 4 | + And I press "Save changes" + Then I should see "Changes saved" + And the following should exist in the "admintable" table: + | Component | Minute | Hour | Day | Day of week | Month | + | Standard log | */5 | 1 | 2 | 4 | 3 | + + Scenario: Reset scheduled task to default + When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" + Then I should see "Edit task schedule: Log table cleanup" + And I set the following fields to these values: + | resettodefaults | 1 | + And I press "Save changes" + Then I should see "Changes saved" + And the following should not exist in the "admintable" table: + | Name | Component | Minute | Hour | Day | Day of week | Month | + | Log table cleanup | Standard log | */5 | 1 | 2 | 4 | 3 | \ No newline at end of file diff --git a/admin/tool/oauth2/tests/form_test.php b/admin/tool/oauth2/tests/form_test.php new file mode 100644 index 00000000000..a9b8aa12bf9 --- /dev/null +++ b/admin/tool/oauth2/tests/form_test.php @@ -0,0 +1,275 @@ +. + +/** + * File containing tests for the mform class. + * + * @package tool_task + * @copyright 2014 onwards Ankit Agarwal + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Mform test class. + * + * @package tool_task + * @copyright 2014 onwards Ankit Agarwal + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late + */ +class tool_task_form_testcase extends advanced_testcase { + + /** + * Test validations for minute field. + */ + public function test_validate_fields_minute() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '65'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/65'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '2,20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '20,30,45'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '65,20,30'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '25,75'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '2-20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '20-30'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '65-20'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '25-75'); + $this->assertFalse($valid); + } + + /** + * Test validations for minute hour. + */ + public function test_validate_fields_hour() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '65'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/65'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '2,20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '20,30,45'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '65,20,30'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '25,75'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '2-20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '20-30'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '65-20'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '25-75'); + $this->assertFalse($valid); + } + + /** + * Test validations for day field. + */ + public function test_validate_fields_day() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '65'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/65'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '2,20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '20,30,25'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '65,20,30'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '25,35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '2-20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '20-30'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '65-20'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '25-35'); + $this->assertFalse($valid); + } + + /** + * Test validations for month field. + */ + public function test_validate_fields_month() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '10'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/12'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '2,11'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '2,10,12'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '65,2,13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '25,35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '2-12'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '3-6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '65-2'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '25-26'); + $this->assertFalse($valid); + } + + /** + * Test validations for dayofweek field. + */ + public function test_validate_fields_dayofweek() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '0'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '7'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '20'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '2,6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '2,6,3'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '65,2,13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '25,35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '2-6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '65-2'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '3-7'); + $this->assertFalse($valid); + } +} + diff --git a/admin/tool/oauth2/version.php b/admin/tool/oauth2/version.php new file mode 100644 index 00000000000..8d18262c360 --- /dev/null +++ b/admin/tool/oauth2/version.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version info + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2016112900; // Requires this Moodle version +$plugin->component = 'tool_oauth2'; // Full name of the plugin (used for diagnostics) + diff --git a/auth/classes/output/login.php b/auth/classes/output/login.php index a58dfe64433..bebfed86915 100644 --- a/auth/classes/output/login.php +++ b/auth/classes/output/login.php @@ -121,10 +121,17 @@ class login implements renderable, templatable { } public function export_for_template(renderer_base $output) { - global $CFG; + global $CFG, $OUTPUT; $identityproviders = array_map(function($idp) use ($output) { - $idp['icon'] = $idp['icon']->export_for_template($output); + global $OUTPUT; + + if (!empty($idp['icon'])) { + $idp['iconurl'] = $OUTPUT->pix_url($idp['icon']->key, $idp['icon']->component); + } else if ($idp['iconurl'] instanceof moodle_url) { + $idp['iconurl'] = $idp['iconurl']->out(false); + } + unset($idp['icon']); if ($idp['url'] instanceof moodle_url) { $idp['url'] = $idp['url']->out(false); } diff --git a/auth/oauth2/auth.php b/auth/oauth2/auth.php new file mode 100644 index 00000000000..0d5ecd7f79a --- /dev/null +++ b/auth/oauth2/auth.php @@ -0,0 +1,40 @@ +. + +/** + * Open ID authentication. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/authlib.php'); + +/** + * Plugin for oauth2 authentication. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ +class auth_plugin_oauth2 extends \auth_oauth2\auth { + +} + + diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php new file mode 100644 index 00000000000..4bf2cc9fb68 --- /dev/null +++ b/auth/oauth2/classes/auth.php @@ -0,0 +1,350 @@ +. + +/** + * Anobody can login with any password. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +namespace auth_oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use pix_icon; +use moodle_url; +use core_text; +use stdClass; +use core\oauth2\issuer; +use core\oauth2\client; + +require_once($CFG->libdir.'/authlib.php'); + +/** + * Plugin for oauth2 authentication. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ +class auth extends \auth_plugin_base { + + /** + * @var stdClass $userinfo The set of user info returned from the oauth handshake + */ + private static $userinfo; + + /** + * @var stdClass $userpicture The url to a picture. + */ + private static $userpicture; + + /** + * Constructor. + */ + public function __construct() { + $this->authtype = 'oauth2'; + $this->config = get_config('auth_oauth2'); + } + + /** + * Returns true if the username and password work or don't exist and false + * if the user exists and the password is wrong. + * + * @param string $username The username + * @param string $password The password + * @return bool Authentication success or failure. + */ + public function user_login($username, $password) { + $cached = $this->get_static_user_info(); + $verifyusername = $cached['username']; + if ($verifyusername == $username) { + return true; + } + } + + /** + * We don't want to allow users setting an internal password. + * + * @return bool + */ + public function prevent_local_passwords() { + return true; + } + + /** + * Returns true if this authentication plugin is 'internal'. + * + * @return bool + */ + public function is_internal() { + return false; + } + + /** + * Indicates if moodle should automatically update internal user + * records with data from external sources using the information + * from auth_plugin_base::get_userinfo(). + * + * @return bool true means automatically copy data from ext to user table + */ + public function is_synchronised_with_external() { + return true; + } + + /** + * Returns true if this authentication plugin can change the user's + * password. + * + * @return bool + */ + public function can_change_password() { + return false; + } + + /** + * Returns the URL for changing the user's pw, or empty if the default can + * be used. + * + * @return moodle_url + */ + public function change_password_url() { + return null; + } + + /** + * Returns true if plugin allows resetting of internal password. + * + * @return bool + */ + public function can_reset_password() { + return false; + } + + /** + * Returns true if plugin can be manually set. + * + * @return bool + */ + public function can_be_manually_set() { + return true; + } + + /** + * Prints a form for configuring this authentication plugin. + * + * This function is called from admin/auth.php, and outputs a full page with + * a form for configuring this plugin. + * + * @param stdClass $config + * @param string $err + * @param array userfields + */ + public function config_form($config, $err, $userfields) { + echo get_string('plugindescription', 'auth_oauth2'); + + // Force all fields updated on login and locked. + + foreach ($userfields as $field) { + set_config('field_updatelocal_' . $field, 'onlogin', 'auth_oauth2'); + set_config('field_lock_' . $field, 'unlockedifempty', 'auth_oauth2'); + } + return; + } + + /** + * Return the userinfo from the oauth handshake. Will only be valid + * for the logged in user. + */ + public function get_userinfo($username) { + $cached = $this->get_static_user_info(); + if (!empty($cached) && $cached['username'] == $username) { + return $cached; + } + return false; + } + + private function is_ready_for_login_page($issuer) { + return !empty($issuer->get('clientid')) && + !empty($issuer->get('clientsecret')) && + $issuer->is_authentication_supported() && + !empty($issuer->get('showonloginpage')); + } + + /** + * Return a list of identity providers to display on the login page. + */ + public function loginpage_idp_list($wantsurl) { + $providers = \core\oauth2\api::get_all_issuers(); + $result = []; + if (empty($wantsurl)) { + $wantsurl = '/'; + } + foreach ($providers as $idp) { + if ($this->is_ready_for_login_page($idp)) { + $params = ['id' => $idp->get('id'), 'wantsurl' => $wantsurl, 'sesskey' => sesskey()]; + $url = new moodle_url('/auth/oauth2/login.php', $params); + $icon = $idp->get('image'); + $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get('name')]; + } + } + return $result; + } + + /** + * Statically cache the user info from the oauth handshake + * @param stdClass $userinfo + */ + private function set_static_user_info($userinfo) { + self::$userinfo = $userinfo; + } + + /** + * Get the static cached user info + * @return stdClass + */ + private function get_static_user_info() { + return self::$userinfo; + } + + /** + * Statically cache the user picture from the oauth handshake + * @param string $userpicture + */ + private function set_static_user_picture($userpicture) { + self::$userpicture = $userpicture; + } + + /** + * Get the static cached user picture + * @return string + */ + private function get_static_user_picture() { + return self::$userpicture; + } + + /** + * If this user has no picture - but we got one from oauth - set it. + * @return boolean True if the image was updated. + */ + private function update_picture($user) { + global $CFG, $DB, $USER; + + require_once($CFG->libdir . '/filelib.php'); + require_once($CFG->libdir . '/gdlib.php'); + + $fs = get_file_storage(); + $userid = $user->id; + if (!empty($user->picture)) { + return false; + } + $picture = $this->get_static_user_picture(); + if (empty($picture)) { + return false; + } + + $context = \context_user::instance($userid, MUST_EXIST); + $fs->delete_area_files($context->id, 'user', 'newicon'); + + $filerecord = array( + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'newicon', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => 'image' + ); + + try { + $fs->create_file_from_string($filerecord, $picture); + } catch (\file_exception $e) { + return get_string($e->errorcode, $e->module, $e->a); + } + + $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false); + + // There should only be one. + $iconfile = reset($iconfile); + + // Something went wrong while creating temp file - remove the uploaded file. + if (!$iconfile = $iconfile->copy_content_to_temp()) { + $fs->delete_area_files($context->id, 'user', 'newicon'); + return false; + } + + // Copy file to temporary location and the send it for processing icon. + $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile); + // Delete temporary file. + @unlink($iconfile); + // Remove uploaded file. + $fs->delete_area_files($context->id, 'user', 'newicon'); + // Set the user's picture. + $updateuser = new stdClass(); + $updateuser->id = $userid; + $updateuser->picture = $newpicture; + $USER->picture = $newpicture; + user_update_user($updateuser); + return true; + } + + /** + * Complete the login process after oauth handshake is complete. + * @param \core\oauth2\client $client + * @param string $redirecturl + * @return none Either redirects or throws an exception + */ + public function complete_login(client $client, $redirecturl) { + global $CFG, $SESSION; + + $userinfo = $client->get_userinfo(); + + if (!$userinfo) { + $errormsg = get_string('notloggedin', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); + } + + $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); + + if (!empty($userinfo['picture'])) { + $this->set_static_user_picture($userinfo['picture']); + unset($userinfo['picture']); + } + + if (!empty($userinfo['lang'])) { + $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); + if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { + unset($userinfo['lang']); + } + } + $this->set_static_user_info($userinfo); + + $user = authenticate_user_login($userinfo['username'], ''); + + if ($user) { + complete_user_login($user); + $this->update_picture($user); + redirect($redirecturl); + } + $errormsg = get_string('notloggedin', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); + } +} + + diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php new file mode 100644 index 00000000000..d221f328364 --- /dev/null +++ b/auth/oauth2/lang/en/auth_oauth2.php @@ -0,0 +1,29 @@ +. + +/** + * Strings for component 'auth_oauth2', language 'en'. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['auth_oauth2description'] = 'OAuth 2 standards based authentication'; +$string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; +$string['pluginname'] = 'OAuth 2'; +$string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; +$string['notloggedin'] = 'The login attempt failed.'; diff --git a/auth/oauth2/login.php b/auth/oauth2/login.php new file mode 100644 index 00000000000..835a39b9ba7 --- /dev/null +++ b/auth/oauth2/login.php @@ -0,0 +1,45 @@ +. + +/** + * Open ID authentication. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +require_once('../../config.php'); + +$issuerid = required_param('id', PARAM_INT); +$wantsurl = new moodle_url(optional_param('wantsurl', '/', PARAM_URL)); + +require_sesskey(); + +$issuer = new \core\oauth2\issuer($issuerid); + +$returnparams = ['wantsurl' => $wantsurl, 'sesskey' => sesskey(), 'id' => $issuerid]; +$returnurl = new moodle_url('/auth/oauth2/login.php', $returnparams); + +$client = \core\oauth2\api::get_user_oauth_client($issuer, $returnurl); + +if ($client) { + $auth = new \auth_oauth2\auth(); + $auth->complete_login($client, $wantsurl); +} else { + throw new moodle_exception('Could not get an OAuth client.'); +} + diff --git a/auth/oauth2/version.php b/auth/oauth2/version.php new file mode 100644 index 00000000000..11f8fe144dc --- /dev/null +++ b/auth/oauth2/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version information + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2016112900; // Requires this Moodle version +$plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics) diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php new file mode 100644 index 00000000000..00230d1620e --- /dev/null +++ b/lib/classes/oauth2/api.php @@ -0,0 +1,352 @@ +. + +/** + * Class for loading/storing oauth2 endpoints from the DB. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +require_once($CFG->libdir . '/filelib.php'); + +use context_system; +use curl; +use stdClass; +use moodle_exception; +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Static list of api methods for system oauth2 configuration. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + + /** + * Called from install.php and upgrade.php - install the default list of issuers + * @return int The number of issuers installed. + */ + public static function install_default_issuers() { + // Setup default list of identity issuers. + $record = (object) [ + 'name' => 'Google', + 'image' => 'https://accounts.google.com/favicon.ico', + 'behaviour' => issuer::BEHAVIOUR_OPENID_CONNECT, + 'baseurl' => 'http://accounts.google.com/', + 'clientid' => '', + 'clientsecret' => '', + 'showonloginpage' => true + ]; + + $issuer = new issuer(0, $record); + $issuer->create(); + + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => 'discovery_endpoint', + 'url' => 'https://accounts.google.com/.well-known/openid-configuration' + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + + $record = (object) [ + 'name' => 'Microsoft', + 'image' => 'https://www.microsoft.com/favicon.ico', + 'behaviour' => issuer::BEHAVIOUR_MICROSOFT, + 'baseurl' => 'http://login.microsoftonline.com/common/oauth2/v2.0/', + 'clientid' => '', + 'clientsecret' => '', + 'showonloginpage' => true + ]; + + $issuer = new issuer(0, $record); + $issuer->create(); + + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => 'authorization_endpoint', + 'url' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize' + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => 'token_endpoint', + 'url' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token' + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => 'end_session_endpoint', + 'url' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token' + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + return issuer::count_records(); + } + + public static function get_all_issuers() { + return issuer::get_records([], 'sortorder'); + } + + public static function get_issuer($id) { + return new issuer($id); + } + + public static function get_system_account(issuer $issuer) { + return system_account::get_record(['issuerid' => $issuer->get('id')]); + } + + public static function get_system_oauth_client(issuer $issuer) { + } + + public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '') { + $client = \core\oauth2\client::create($issuer, $currenturl, $additionalscopes); + + if (!$client->is_logged_in()) { + redirect($client->get_login_url()); + } + return $client; + } + + public static function get_endpoints(issuer $issuer) { + require_capability('moodle/site:config', context_system::instance()); + + return endpoint::get_records(['issuerid' => $issuer->get('id')]); + } + + protected static function guess_image($issuer) { + if (empty($issuer->get('image'))) { + $baseurl = parse_url($issuer->get('discoveryurl')); + $imageurl = $baseurl['scheme'] . '://' . $baseurl['host'] . '/favicon.ico'; + $issuer->set('image', $imageurl); + $issuer->update(); + } + } + + /** + * If the behaviour supports discovery for this issuer, try and determine the list of valid endpoints. + * + * @param issuer $issuer + * @return int The number of discovered services. + */ + protected static function discover_endpoints($issuer) { + $curl = new curl(); + + if ($issuer->get('behaviour') != issuer::BEHAVIOUR_OPENID_CONNECT) { + return 0; + } + + $url = $issuer->get_endpoint_url('discovery'); + if (!$url) { + $url = $issuer->get('url') . '/.well-known/openid-configuration'; + } + + if (!$json = $curl->get($issuer->get_endpoint_url('discovery'))) { + $msg = 'Could not discover end points for identity issuer' . $issuer->get('name'); + throw new moodle_exception($msg); + } + + if ($msg = $curl->error) { + throw new moodle_exception('Could not discover service endpoints: ' . $msg); + } + + $info = json_decode($json); + if (empty($info)) { + $msg = 'Could not discover end points for identity issuer' . $issuer->get('name'); + throw new moodle_exception($msg); + } + + foreach (endpoint::get_records(['issuerid' => $issuer->get('id')]) as $endpoint) { + if ($endpoint->get('name') != 'discovery_endpoint') { + $endpoint->delete(); + } + } + + foreach ($info as $key => $value) { + if (substr_compare($key, '_endpoint', - strlen('_endpoint')) === 0) { + $record = new stdClass(); + $record->issuerid = $issuer->get('id'); + $record->name = $key; + $record->url = $value; + + $endpoint = new endpoint(0, $record); + $endpoint->create(); + } + + if ($key == 'scopes_supported') { + $issuer->set('scopessupported', implode(' ', $value)); + $issuer->update(); + } + } + + return endpoint::count_records(['issuerid' => $issuer->get('id')]); + } + + public static function update_issuer($data) { + require_capability('moodle/site:config', context_system::instance()); + $issuer = new issuer(0, $data); + + // Will throw exceptions on validation failures. + $issuer->update(); + + // Perform service discovery. + self::discover_endpoints($issuer); + self::guess_image($issuer); + } + + public static function create_issuer($data) { + require_capability('moodle/site:config', context_system::instance()); + $issuer = new issuer(0, $data); + + // Will throw exceptions on validation failures. + $issuer->create(); + + // Perform service discovery. + self::discover_endpoints($issuer); + self::guess_image($issuer); + } + + /** + * Reorder this identity issuer. + * + * Requires moodle/site:config capability at the system context. + * + * @param int $id The id of the identity issuer to move. + * @return boolean + */ + public static function move_up_issuer($id) { + require_capability('moodle/site:config', context_system::instance()); + $current = new issuer($id); + + $sortorder = $current->get('sortorder'); + if ($sortorder == 0) { + return false; + } + + $sortorder = $sortorder - 1; + $current->set('sortorder', $sortorder); + + $filters = array('sortorder' => $sortorder); + $children = issuer::get_records($filters, 'id'); + foreach ($children as $needtoswap) { + $needtoswap->set('sortorder', $sortorder + 1); + $needtoswap->update(); + } + + // OK - all set. + $result = $current->update(); + + return $result; + } + + public static function move_down_issuer($id) { + require_capability('moodle/site:config', context_system::instance()); + $current = new issuer($id); + + $max = issuer::count_records(); + if ($max > 0) { + $max--; + } + + $sortorder = $current->get('sortorder'); + if ($sortorder >= $max) { + return false; + } + $sortorder = $sortorder + 1; + $current->set('sortorder', $sortorder); + + $filters = array('sortorder' => $sortorder); + $children = issuer::get_records($filters); + foreach ($children as $needtoswap) { + $needtoswap->set('sortorder', $sortorder - 1); + $needtoswap->update(); + } + + // OK - all set. + $result = $current->update(); + + return $result; + } + + public static function delete_issuer($id) { + require_capability('moodle/site:config', context_system::instance()); + $issuer = new issuer($id); + + $systemaccount = self::get_system_account($issuer); + if ($systemaccount) { + $systemaccount->delete(); + } + $endpoints = self::get_endpoints($issuer); + if ($endpoints) { + foreach ($endpoints as $endpoint) { + $endpoint->delete(); + } + } + + // Will throw exceptions on validation failures. + $issuer->delete(); + } + + public static function connect_system_account($issuer, $returnurl) { + require_capability('moodle/site:config', context_system::instance()); + + // We need to authenticate with an oauth 2 client AS a system user and get a refresh token for offline access. + $scopesrequired = 'openid email profile'; + + // Allow callbacks to inject non-standard scopes to the auth request. + + $client = client::create($issuer, $returnurl, $scopesrequired, true); + + if (!optional_param('response', false, PARAM_BOOL)) { + $client->log_out(); + } + + if (!$client->is_logged_in()) { + redirect($client->get_login_url()); + } + + $refreshtoken = $client->get_refresh_token(); + if (!$refreshtoken) { + return false; + } + + $systemaccount = self::get_system_account($issuer); + if ($systemaccount) { + $systemaccount->delete(); + } + $record = new stdClass(); + $record->issuerid = $issuer->get('id'); + $record->refreshtoken = $refreshtoken; + $record->grantedscopes = $scopesrequired; + + $systemaccount = new system_account(0, $record); + + $systemaccount->create(); + + $client->log_out(); + return true; + } +} diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php new file mode 100644 index 00000000000..7410e64fad5 --- /dev/null +++ b/lib/classes/oauth2/client.php @@ -0,0 +1,117 @@ +. + +/** + * Configurable oauth2 client class. + * + * @package core\oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/oauthlib.php'); +require_once($CFG->libdir . '/filelib.php'); + +use moodle_url; +use curl; + +/** + * Configurable oauth2 client class where the urls come from DB. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class client extends \oauth2_client { + + /** @var \core\oauth2\issuer $issuer */ + private $issuer; + + /** @var bool $system */ + protected $system = false; + + /** + * Constructor. + * + * @param issuer $issuer + * @param moodle_url $returnurl + */ + public function __construct(issuer $issuer, moodle_url $returnurl, $scopesrequired, $system) { + $this->issuer = $issuer; + $this->system = $system; + $scopes = $this->get_login_scopes(); + $additionalscopes = explode(' ', $scopesrequired); + + foreach ($additionalscopes as $scope) { + if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { + $scopes .= ' ' . $scope; + } + } + parent::__construct($issuer->get('clientid'), $issuer->get('clientsecret'), $returnurl, $scopes); + } + + public static function create(issuer $issuer, moodle_url $returnurl, $scopesrequired, $system = false) { + if ($issuer->get('behaviour') == issuer::BEHAVIOUR_OPENID_CONNECT) { + return new client_openid_connect($issuer, $returnurl, $scopesrequired, $system); + } else if ($issuer->get('behaviour') == issuer::BEHAVIOUR_OAUTH2) { + return new client_oauth2($issuer, $returnurl, $scopesrequired, $system); + } else if ($issuer->get('behaviour') == issuer::BEHAVIOUR_MICROSOFT) { + return new client_microsoft($issuer, $returnurl, $scopesrequired, $system); + } + } + + /** + * Returns the auth url for OAuth 2.0 request + * @return string the auth url + */ + protected function auth_url() { + return $this->issuer->get_endpoint_url('authorization'); + } + + public function get_issuer() { + return $this->issuer; + } + + public function get_additional_login_parameters() { + if ($this->issuer->get('behaviour') == issuer::BEHAVIOUR_OPENID_CONNECT) { + return ['access_type' => 'offline', 'prompt' => 'consent']; + } + return []; + } + + protected function get_login_scopes() { + return 'openid profile email'; + } + + /** + * Returns the token url for OAuth 2.0 request + * @return string the auth url + */ + protected function token_url() { + return $this->issuer->get_endpoint_url('token'); + } + + protected function get_tokenname() { + $name = static::class; + if ($this->system) { + $name .= '-system'; + } + return $name; + } + +} diff --git a/lib/classes/oauth2/client_microsoft.php b/lib/classes/oauth2/client_microsoft.php new file mode 100644 index 00000000000..3d0bcd617e7 --- /dev/null +++ b/lib/classes/oauth2/client_microsoft.php @@ -0,0 +1,54 @@ +. + +/** + * Configurable oauth2 client class. + * + * @package core\oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; + +/** + * We have to call directly to the graph APIs because the Microsoft Open ID Connect API is + * lame. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class client_microsoft extends client { + + public function get_additional_login_parameters() { + return ['prompt' => 'consent']; + } + + public function get_login_scopes() { + return 'openid profile email user.read'; + } + + public function get_userinfo() { + $me = $client->get('https://graph.microsoft.com/v1.0/me/'); + var_dump($me); + + + } + +} diff --git a/lib/classes/oauth2/client_oauth2.php b/lib/classes/oauth2/client_oauth2.php new file mode 100644 index 00000000000..96fb804abd6 --- /dev/null +++ b/lib/classes/oauth2/client_oauth2.php @@ -0,0 +1,43 @@ +. + +/** + * Configurable oauth2 client class. + * + * @package core\oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; + +/** + * Configurable oauth2 client class where the urls come from DB. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class client_oauth2 extends client { + + public function get_additional_login_parameters() { + return ['access_type' => 'offline', 'prompt' => 'consent']; + } + + +} diff --git a/lib/classes/oauth2/client_openid_connect.php b/lib/classes/oauth2/client_openid_connect.php new file mode 100644 index 00000000000..4ff9837f0cb --- /dev/null +++ b/lib/classes/oauth2/client_openid_connect.php @@ -0,0 +1,98 @@ +. + +/** + * Configurable oauth2 client class. + * + * @package core\oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; +use stdClass; +use Exception; + +/** + * Configurable oauth2 client class where the urls come from DB. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class client_openid_connect extends client { + + /** + * Returns a mapping of openid properties to moodle properties. + * + * @return array + */ + private function get_mapping() { + return [ + 'given_name' => 'firstname', + 'middle_name' => 'middlename', + 'family_name' => 'lastname', + 'email' => 'email', + 'username' => 'username', + 'website' => 'url', + 'nickname' => 'alternatename', + 'picture' => 'picture', + 'address' => 'address', + 'phone' => 'phone', + 'locale' => 'lang' + ]; + } + + public function get_additional_login_parameters() { + if ($this->system) { + return ['access_type' => 'offline', 'prompt' => 'consent']; + } + return []; + } + + public function get_userinfo() { + $url = $this->get_issuer()->get_endpoint_url('userinfo'); + $response = $this->get($url); + if (!$response) { + return false; + } + $userinfo = new stdClass(); + try { + $userinfo = json_decode($response); + } catch (Exception $e) { + return false; + } + if (!empty($userinfo->preferred_username)) { + $userinfo->username = $userinfo->preferred_username; + } else { + $userinfo->username = $userinfo->sub; + } + + $map = $this->get_mapping(); + + $user = new stdClass(); + foreach ($map as $openidproperty => $moodleproperty) { + if (!empty($userinfo->$openidproperty)) { + $user->$moodleproperty = $userinfo->$openidproperty; + } + } + + return (array)$user; + } + +} diff --git a/lib/classes/oauth2/endpoint.php b/lib/classes/oauth2/endpoint.php new file mode 100644 index 00000000000..fe3a0122e90 --- /dev/null +++ b/lib/classes/oauth2/endpoint.php @@ -0,0 +1,58 @@ +. + +/** + * Class for loading/storing oauth2 endpoints from the DB. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing oauth2 endpoints from the DB + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class endpoint extends persistent { + + const TABLE = 'oauth2_endpoint'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'issuerid' => array( + 'type' => PARAM_INT + ), + 'name' => array( + 'type' => PARAM_RAW, + ), + 'url' => array( + 'type' => PARAM_URL, + ) + ); + } +} diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php new file mode 100644 index 00000000000..71eec0994bb --- /dev/null +++ b/lib/classes/oauth2/issuer.php @@ -0,0 +1,131 @@ +. + +/** + * Class for loading/storing issuers from the DB. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing issuer from the DB + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issuer extends persistent { + + const TABLE = 'oauth2_issuer'; + + const BEHAVIOUR_OPENID_CONNECT = 'Open ID Connect'; + const BEHAVIOUR_MICROSOFT = 'Microsoft OAuth 2.0'; + const BEHAVIOUR_OAUTH2 = 'OAuth 2.0'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'name' => array( + 'type' => PARAM_TEXT + ), + 'image' => array( + 'type' => PARAM_URL, + 'null' => NULL_ALLOWED, + 'default' => null + ), + 'clientid' => array( + 'type' => PARAM_RAW + ), + 'clientsecret' => array( + 'type' => PARAM_RAW + ), + 'behaviour' => array( + 'type' => PARAM_NOTAGS, + 'choices' => array(self::BEHAVIOUR_OPENID_CONNECT, self::BEHAVIOUR_MICROSOFT, self::BEHAVIOUR_OAUTH2), + 'default' => self::BEHAVIOUR_OPENID_CONNECT + ), + 'baseurl' => array( + 'type' => PARAM_URL + ), + 'showonloginpage' => array( + 'type' => PARAM_BOOL, + 'default' => false + ), + 'scopessupported' => array( + 'type' => PARAM_RAW, + 'null' => NULL_ALLOWED, + 'default' => null + ), + 'sortorder' => array( + 'type' => PARAM_INT, + 'default' => 0, + ) + ); + } + + public function get_endpoint_url($type) { + $endpoint = endpoint::get_record([ + 'issuerid' => $this->get('id'), + 'name' => $type . '_endpoint' + ]); + + if ($endpoint) { + return $endpoint->get('url'); + } + return false; + } + public function is_authentication_supported() { + $supportedloginbehaviours = [ + self::BEHAVIOUR_OPENID_CONNECT, + self::BEHAVIOUR_MICROSOFT, + ]; + return in_array($this->get('behaviour'), $supportedloginbehaviours); + } + + public function is_system_account_setup_supported() { + $supportedsystemaccountbehaviours = [ + self::BEHAVIOUR_OPENID_CONNECT, + self::BEHAVIOUR_MICROSOFT, + ]; + return in_array($this->get('behaviour'), $supportedsystemaccountbehaviours); + } + + public function get_behaviour_list() { + return [ + self::BEHAVIOUR_OPENID_CONNECT => self::BEHAVIOUR_OPENID_CONNECT, + self::BEHAVIOUR_OAUTH2 => self::BEHAVIOUR_OAUTH2, + self::BEHAVIOUR_MICROSOFT => self::BEHAVIOUR_MICROSOFT + ]; + } + + public function is_system_account_connected() { + $sys = system_account::get_record(['issuerid' => $this->get('id')]); + if (!empty($sys) and !empty($sys->get('refreshtoken'))) { + return true; + } + return false; + } +} diff --git a/lib/classes/oauth2/system_account.php b/lib/classes/oauth2/system_account.php new file mode 100644 index 00000000000..ee682a7978f --- /dev/null +++ b/lib/classes/oauth2/system_account.php @@ -0,0 +1,60 @@ +. + +/** + * When using OAuth sometimes it makes sense to authenticate as a system user, and not the current user. + * In this case we use a refresh token to get an access token and the system admin must manually authorize the + * system account. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing oauth2 refresh tokens from the DB. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class system_account extends persistent { + + const TABLE = 'oauth2_system_account'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'issuerid' => array( + 'type' => PARAM_INT + ), + 'refreshtoken' => array( + 'type' => PARAM_RAW, + ), + 'grantedscopes' => array( + 'type' => PARAM_RAW, + ) + ); + } +} diff --git a/lib/db/install.php b/lib/db/install.php index f291828a1fa..8a45a6aa4ff 100644 --- a/lib/db/install.php +++ b/lib/db/install.php @@ -321,4 +321,6 @@ function xmldb_main_install() { require_once($CFG->libdir . '/db/upgradelib.php'); make_default_scale(); make_competence_scale(); + + \core\oauth2\api::install_default_providers(); } diff --git a/lib/db/install.xml b/lib/db/install.xml old mode 100644 new mode 100755 index 2e52477544f..0233dedcf37 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3463,5 +3463,55 @@ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 69ef988fa08..189c3338734 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2611,5 +2611,98 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2017031400.00); } + if ($oldversion < 2017032400.21) { + + // Define table oauth2_issuer to be created. + $table = new xmldb_table('oauth2_issuer'); + + // Adding fields to table oauth2_issuer. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('image', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('baseurl', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('clientid', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('clientsecret', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('behaviour', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, 'none'); + $table->add_field('scopessupported', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('showonloginpage', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table oauth2_issuer. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Conditionally launch create table for oauth2_issuer. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2017032400.21); + } + + if ($oldversion < 2017032400.22) { + + // Define table oauth2_endpoint to be created. + $table = new xmldb_table('oauth2_endpoint'); + + // Adding fields to table oauth2_endpoint. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('url', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('issuerid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table oauth2_endpoint. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('issuer_id_key', XMLDB_KEY_FOREIGN, array('issuerid'), 'oauth2_issuer', array('id')); + + // Conditionally launch create table for oauth2_endpoint. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2017032400.22); + } + + if ($oldversion < 2017032400.23) { + + // Define table oauth2_system_account to be created. + $table = new xmldb_table('oauth2_system_account'); + + // Adding fields to table oauth2_system_account. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('issuerid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('refreshtoken', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('grantedscopes', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + + // Adding keys to table oauth2_system_account. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('issueridkey', XMLDB_KEY_FOREIGN_UNIQUE, array('issuerid'), 'oauth2_issuer', array('id')); + + // Conditionally launch create table for oauth2_system_account. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2017032400.23); + } + + if ($oldversion < 2017033100.01) { + + \core\oauth2\api::install_default_issuers(); + // Main savepoint reached. + upgrade_main_savepoint(true, 2017033100.01); + } + return true; } diff --git a/lib/filelib.php b/lib/filelib.php index 9cca2b9ad70..bdc90bd114b 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -2766,9 +2766,9 @@ class curl { // All settings of this class should be init here. $this->resetopt(); - if (!empty($settings['debug'])) { + //if (!empty($settings['debug'])) { $this->debug = true; - } + //} if (!empty($settings['cookie'])) { if($settings['cookie'] === true) { $this->cookie = $CFG->dataroot.'/curl_cookie.txt'; @@ -2949,6 +2949,7 @@ class curl { * Set HTTP Request Header * * @param array $header + * @param bool $replace If true, will remove any existing headers before appending the new one. */ public function setHeader($header) { if (is_array($header)) { diff --git a/lib/oauthlib.php b/lib/oauthlib.php index 7d83e82e68b..9efb6d100f6 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -393,6 +393,8 @@ abstract class oauth2_client extends curl { private $scope = ''; /** var stdClass access token object */ private $accesstoken = null; + /** var stdClass refresh token string */ + private $refreshtoken = ''; /** * Returns the auth url for OAuth 2.0 request @@ -463,6 +465,10 @@ abstract class oauth2_client extends curl { return new moodle_url('/admin/oauth2callback.php'); } + public function get_additional_login_parameters() { + return []; + } + /** * Returns the login link for this oauth request * @@ -471,15 +477,32 @@ abstract class oauth2_client extends curl { public function get_login_url() { $callbackurl = self::callback_url(); - $url = new moodle_url($this->auth_url(), - array('client_id' => $this->clientid, - 'response_type' => 'code', - 'redirect_uri' => $callbackurl->out(false), - 'state' => $this->returnurl->out_as_local_url(false), - 'scope' => $this->scope, - )); + $params = array_merge( + [ + 'client_id' => $this->clientid, + 'response_type' => 'code', + 'redirect_uri' => $callbackurl->out(false), + 'state' => $this->returnurl->out_as_local_url(false), + 'scope' => $this->scope, + ], + $this->get_additional_login_parameters() + ); - return $url; + return new moodle_url($this->auth_url(), $params); + } + + /** + * Given an array of name value pairs - build a valid HTTP POST application/x-www-form-urlencoded string. + * + * @param array $params Name / value pairs. + * @return string POST data. + */ + public function build_post_data($params) { + $result = []; + foreach ($params as $name => $value) { + $result[] = str_replace('&', '%26', $name) . '=' . str_replace('&', '%26', $value); + } + return implode('&', $result); } /** @@ -490,10 +513,10 @@ abstract class oauth2_client extends curl { */ public function upgrade_token($code) { $callbackurl = self::callback_url(); - $params = array('client_id' => $this->clientid, + $params = array('code' => $code, + 'client_id' => $this->clientid, 'client_secret' => $this->clientsecret, 'grant_type' => 'authorization_code', - 'code' => $code, 'redirect_uri' => $callbackurl->out(false), ); @@ -501,7 +524,7 @@ abstract class oauth2_client extends curl { if ($this->use_http_get()) { $response = $this->get($this->token_url(), $params); } else { - $response = $this->post($this->token_url(), $params); + $response = $this->post($this->token_url(), $this->build_post_data($params)); } if (!$this->info['http_code'] === 200) { @@ -514,6 +537,10 @@ abstract class oauth2_client extends curl { return false; } + if (isset($r->refresh_token)) { + $this->refreshtoken = $r->refresh_token; + } + // Store the token an expiry time. $accesstoken = new stdClass; $accesstoken->token = $r->access_token; @@ -552,7 +579,11 @@ abstract class oauth2_client extends curl { } } - return parent::request($murl->out(false), $options); + $response = parent::request($murl->out(false), $options); + + $this->resetHeader(); + + return $response; } /** @@ -603,6 +634,15 @@ abstract class oauth2_client extends curl { } } + /** + * Get a refresh token!!! + * + * @return string + */ + public function get_refresh_token() { + return $this->refreshtoken; + } + /** * Retrieve a token stored. * diff --git a/lib/templates/login.mustache b/lib/templates/login.mustache index e30c537624f..a0545c38573 100644 --- a/lib/templates/login.mustache +++ b/lib/templates/login.mustache @@ -148,9 +148,9 @@ {{#identityproviders}} diff --git a/theme/boost/templates/core/login.mustache b/theme/boost/templates/core/login.mustache index 3496224ef75..c253f728136 100644 --- a/theme/boost/templates/core/login.mustache +++ b/theme/boost/templates/core/login.mustache @@ -178,7 +178,12 @@ diff --git a/version.php b/version.php index f8fd9032855..8a5f4a2be68 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017033100.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017033100.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From 8445556b4ca7adc75239a8fa81fae4db3665e451 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 24 Feb 2017 16:40:37 +0800 Subject: [PATCH 02/84] MDL-58090 oauth2: Move code from subclasses to config Allow the behaviour of each oauth service to be customised by config instead of subclasses. Part of MDL-58220 --- admin/tool/oauth2/classes/form/endpoint.php | 81 +++++++++ admin/tool/oauth2/classes/form/issuer.php | 6 +- .../classes/form/user_field_mapping.php | 80 +++++++++ admin/tool/oauth2/classes/output/renderer.php | 142 +++++++++++++++- admin/tool/oauth2/endpoints.php | 128 +++++++++++++++ admin/tool/oauth2/issuers.php | 26 +-- admin/tool/oauth2/lang/en/tool_oauth2.php | 23 ++- admin/tool/oauth2/userfieldmappings.php | 126 ++++++++++++++ auth/oauth2/classes/auth.php | 5 + lib/classes/oauth2/api.php | 155 +++++++++++++++--- lib/classes/oauth2/client.php | 74 ++++++++- lib/classes/oauth2/client_microsoft.php | 17 +- lib/classes/oauth2/client_openid_connect.php | 52 ------ lib/classes/oauth2/endpoint.php | 8 + lib/classes/oauth2/user_field_mapping.php | 78 +++++++++ lib/db/install.xml | 19 ++- lib/db/upgrade.php | 31 +++- lib/filelib.php | 4 +- lib/oauthlib.php | 19 +++ theme/boost/templates/core/login.mustache | 2 +- version.php | 2 +- 21 files changed, 961 insertions(+), 117 deletions(-) create mode 100644 admin/tool/oauth2/classes/form/endpoint.php create mode 100644 admin/tool/oauth2/classes/form/user_field_mapping.php create mode 100644 admin/tool/oauth2/endpoints.php create mode 100644 admin/tool/oauth2/userfieldmappings.php create mode 100644 lib/classes/oauth2/user_field_mapping.php diff --git a/admin/tool/oauth2/classes/form/endpoint.php b/admin/tool/oauth2/classes/form/endpoint.php new file mode 100644 index 00000000000..b4bd4be3cda --- /dev/null +++ b/admin/tool/oauth2/classes/form/endpoint.php @@ -0,0 +1,81 @@ +. + +/** + * This file contains the form add/update oauth2 endpoint. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_oauth2\form; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; +use core\form\persistent; + +/** + * Issuer form. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class endpoint extends persistent { + + protected static $persistentclass = 'core\\oauth2\\endpoint'; + + protected static $fieldstoremove = array('submitbutton', 'action'); + + /** + * Define the form - called by parent constructor + */ + public function definition() { + global $PAGE; + + $mform = $this->_form; + $endpoint = $this->get_persistent(); + + $mform->addElement('header', 'generalhdr', get_string('general')); + + // Name. + $mform->addElement('text', 'name', get_string('endpointname', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('name', 'endpointname', 'tool_oauth2'); + + // Url. + $mform->addElement('text', 'url', get_string('endpointurl', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addRule('url', null, 'required', null, 'client'); + $mform->addRule('url', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); + $mform->addHelpButton('url', 'endpointurl', 'tool_oauth2'); + + $mform->addElement('hidden', 'action', 'edit'); + $mform->setType('action', PARAM_RAW); + + $mform->addElement('hidden', 'issuerid', $endpoint->get('issuerid')); + $mform->setType('issuerid', PARAM_INT); + $mform->setConstant('issuerid', $this->_customdata['issuerid']); + + $mform->addElement('hidden', 'id', $endpoint->get('id')); + $mform->setType('id', PARAM_INT); + + $this->add_action_buttons(true, get_string('savechanges', 'tool_oauth2')); + } + +} + diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index ce03af63e4a..d16755c32bf 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -48,7 +48,7 @@ class issuer extends persistent { global $PAGE; $mform = $this->_form; - $provider = $this->get_persistent(); + $endpoint = $this->get_persistent(); $mform->addElement('header', 'generalhdr', get_string('general')); @@ -77,7 +77,7 @@ class issuer extends persistent { $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2'); // Offline access type - $options = $provider->get_behaviour_list(); + $options = $endpoint->get_behaviour_list(); $mform->addElement('select', 'behaviour', get_string('issuerbehaviour', 'tool_oauth2'), $options); $mform->addHelpButton('behaviour', 'issuerbehaviour', 'tool_oauth2'); @@ -97,7 +97,7 @@ class issuer extends persistent { $mform->addElement('hidden', 'action', 'edit'); $mform->setType('action', PARAM_RAW); - $mform->addElement('hidden', 'id', $provider->get('id')); + $mform->addElement('hidden', 'id', $endpoint->get('id')); $mform->setType('id', PARAM_INT); $this->add_action_buttons(true, get_string('savechanges', 'tool_oauth2')); diff --git a/admin/tool/oauth2/classes/form/user_field_mapping.php b/admin/tool/oauth2/classes/form/user_field_mapping.php new file mode 100644 index 00000000000..e8fa760356e --- /dev/null +++ b/admin/tool/oauth2/classes/form/user_field_mapping.php @@ -0,0 +1,80 @@ +. + +/** + * This file contains the form add/update oauth2 user_field_mapping. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_oauth2\form; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; +use core\form\persistent; + +/** + * Issuer form. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_field_mapping extends persistent { + + protected static $persistentclass = 'core\\oauth2\\user_field_mapping'; + + protected static $fieldstoremove = array('submitbutton', 'action'); + + /** + * Define the form - called by parent constructor + */ + public function definition() { + global $PAGE; + + $mform = $this->_form; + $userfieldmapping = $this->get_persistent(); + + $mform->addElement('header', 'generalhdr', get_string('general')); + + // External. + $mform->addElement('text', 'externalfield', get_string('userfieldexternalfield', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('externalfield', null, 'required', null, 'client'); + $mform->addRule('externalfield', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('externalfield', 'userfieldexternalfield', 'tool_oauth2'); + + // Internal. + $choices = $userfieldmapping->get_internalfield_list(); + $mform->addElement('select', 'internalfield', get_string('userfieldinternalfield', 'tool_oauth2'), $choices); + $mform->addHelpButton('internalfield', 'userfieldinternalfield', 'tool_oauth2'); + + $mform->addElement('hidden', 'action', 'edit'); + $mform->setType('action', PARAM_RAW); + + $mform->addElement('hidden', 'issuerid', $userfieldmapping->get('issuerid')); + $mform->setConstant('issuerid', $this->_customdata['issuerid']); + $mform->setType('issuerid', PARAM_INT); + + $mform->addElement('hidden', 'id', $userfieldmapping->get('id')); + $mform->setType('id', PARAM_INT); + + $this->add_action_buttons(true, get_string('savechanges', 'tool_oauth2')); + } + +} + diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index f26ca777f91..0d4514d2bcc 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -79,7 +79,7 @@ class renderer extends plugin_renderer_base { $name = $issuer->get('name'); $image = $issuer->get('image'); if ($image) { - $name = ' ' . $name; + $name = ' ' . s($name); } $namecell = new html_table_cell($name); $namecell->header = true; @@ -104,7 +104,11 @@ class renderer extends plugin_renderer_base { if (!empty($issuer->get('scopessupported'))) { $discovered = $OUTPUT->pix_icon('yes', get_string('discovered', 'tool_oauth2'), 'tool_oauth2'); } else { - $discovered = $OUTPUT->pix_icon('no', get_string('notdiscovered', 'tool_oauth2'), 'tool_oauth2'); + if ($issuer->get('behaviour') == $issuer::BEHAVIOUR_OPENID_CONNECT) { + $discovered = $OUTPUT->pix_icon('no', get_string('notdiscovered', 'tool_oauth2'), 'tool_oauth2'); + } else { + $discovered = '-'; + } } $discoverystatuscell = new html_table_cell($discovered); @@ -125,22 +129,37 @@ class renderer extends plugin_renderer_base { $systemauthstatuscell = new html_table_cell($systemauth); - // Action links. $links = ''; + // Action links. $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['id' => $issuer->get('id'), 'action' => 'edit']); $editlink = html_writer::link($editurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))); - $links .= ' ' . $editlink; + + // Endpoints. + $editendpointsurl = new moodle_url('/admin/tool/oauth2/endpoints.php', ['issuerid' => $issuer->get('id')]); + $str = get_string('editendpoints', 'tool_oauth2'); + $editendpointlink = html_writer::link($editendpointsurl, $OUTPUT->pix_icon('t/viewdetails', $str)); + $links .= ' ' . $editendpointlink; + + // User field mapping. + $edituserfieldmappingsurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', ['issuerid' => $issuer->get('id')]); + $str = get_string('edituserfieldmappings', 'tool_oauth2'); + $edituserfieldmappinglink = html_writer::link($edituserfieldmappingsurl, $OUTPUT->pix_icon('t/user', $str)); + $links .= ' ' . $edituserfieldmappinglink; + + // Delete. $deleteurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['id' => $issuer->get('id'), 'action' => 'delete']); $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); $links .= ' ' . $deletelink; if (!$last) { + // Move down. $params = ['id' => $issuer->get('id'), 'action' => 'movedown', 'sesskey' => sesskey()]; $movedownurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); $movedownlink = html_writer::link($movedownurl, $OUTPUT->pix_icon('t/down', get_string('movedown'))); $links .= ' ' . $movedownlink; } if (!$first) { + // Move up. $params = ['id' => $issuer->get('id'), 'action' => 'moveup', 'sesskey' => sesskey()]; $moveupurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); $moveuplink = html_writer::link($moveupurl, $OUTPUT->pix_icon('t/up', get_string('moveup'))); @@ -164,4 +183,119 @@ class renderer extends plugin_renderer_base { $table->data = $data; return html_writer::table($table); } + + /** + * This function will render one beautiful table with all the endpoints. + * + * @param \core\oauth2\endpoint[] $endpoints - list of all endpoints. + * @return string HTML to output. + */ + public function endpoints_table($endpoints, $issuerid) { + global $CFG, $OUTPUT; + + $table = new html_table(); + $table->head = [ + get_string('name'), + get_string('url'), + get_string('edit'), + ]; + $table->attributes['class'] = 'admintable generaltable'; + $data = []; + + $index = 0; + + foreach ($endpoints as $endpoint) { + // Name. + $name = $endpoint->get('name'); + $namecell = new html_table_cell(s($name)); + $namecell->header = true; + + // Url + $url = $endpoint->get('url'); + $urlcell = new html_table_cell(s($url)); + + $links = ''; + // Action links. + $editparams = ['issuerid' => $issuerid, 'endpointid' => $endpoint->get('id'), 'action' => 'edit']; + $editurl = new moodle_url('/admin/tool/oauth2/endpoints.php', $editparams); + $editlink = html_writer::link($editurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))); + $links .= ' ' . $editlink; + + // Delete. + $deleteparams = ['issuerid' => $issuerid, 'endpointid' => $endpoint->get('id'), 'action' => 'delete']; + $deleteurl = new moodle_url('/admin/tool/oauth2/endpoints.php', $deleteparams); + $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $links .= ' ' . $deletelink; + + $editcell = new html_table_cell($links); + + $row = new html_table_row([ + $namecell, + $urlcell, + $editcell, + ]); + + $data[] = $row; + $index++; + } + $table->data = $data; + return html_writer::table($table); + } + + /** + * This function will render one beautiful table with all the user_field_mappings. + * + * @param \core\oauth2\user_field_mapping[] $userfieldmappings - list of all user_field_mappings. + * @return string HTML to output. + */ + public function user_field_mappings_table($userfieldmappings, $issuerid) { + global $CFG, $OUTPUT; + + $table = new html_table(); + $table->head = [ + get_string('userfieldexternalfield', 'tool_oauth2'), + get_string('userfieldinternalfield', 'tool_oauth2'), + get_string('edit'), + ]; + $table->attributes['class'] = 'admintable generaltable'; + $data = []; + + $index = 0; + + foreach ($userfieldmappings as $userfieldmapping) { + // External field + $externalfield = $userfieldmapping->get('externalfield'); + $externalfieldcell = new html_table_cell(s($externalfield)); + + // Internal field + $internalfield = $userfieldmapping->get('internalfield'); + $internalfieldcell = new html_table_cell(s($internalfield)); + + $links = ''; + // Action links. + $editparams = ['issuerid' => $issuerid, 'userfieldmappingid' => $userfieldmapping->get('id'), 'action' => 'edit']; + $editurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', $editparams); + $editlink = html_writer::link($editurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))); + $links .= ' ' . $editlink; + + // Delete. + $deleteparams = ['issuerid' => $issuerid, 'userfieldmappingid' => $userfieldmapping->get('id'), 'action' => 'delete']; + $deleteurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', $deleteparams); + $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $links .= ' ' . $deletelink; + + $editcell = new html_table_cell($links); + + $row = new html_table_row([ + $externalfieldcell, + $internalfieldcell, + $editcell, + ]); + + $data[] = $row; + $index++; + } + $table->data = $data; + return html_writer::table($table); + } } diff --git a/admin/tool/oauth2/endpoints.php b/admin/tool/oauth2/endpoints.php new file mode 100644 index 00000000000..cf3a3fe3b0b --- /dev/null +++ b/admin/tool/oauth2/endpoints.php @@ -0,0 +1,128 @@ +. + +/** + * OAuth 2 Endpoing Configuration page. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/tablelib.php'); + +$PAGE->set_url('/admin/tool/oauth2/endpoints.php', ['issuerid' => required_param('issuerid', PARAM_INT)]); +$PAGE->set_context(context_system::instance()); +$PAGE->set_pagelayout('admin'); +$strheading = get_string('pluginname', 'tool_oauth2'); +$PAGE->set_title($strheading); +$PAGE->set_heading($strheading); + +require_login(); + +require_capability('moodle/site:config', context_system::instance()); + +$renderer = $PAGE->get_renderer('tool_oauth2'); + +$action = optional_param('action', '', PARAM_ALPHAEXT); +$issuerid = required_param('issuerid', PARAM_INT); +$endpointid = optional_param('endpointid', '', PARAM_INT); +$endpoint = null; +$mform = null; + +$issuer = \core\oauth2\api::get_issuer($issuerid); +if (!$issuer) { + print_error('invaliddata'); +} +$PAGE->navbar->override_active_url(new moodle_url('/admin/tool/oauth2/issuers.php'), true); + +if (!empty($endpointid)) { + $endpoint = \core\oauth2\api::get_endpoint($endpointid); +} + +if ($action == 'edit') { + if ($endpoint) { + $strparams = [ 'issuer' => s($issuer->get('name')), 'endpoint' => s($endpoint->get('name')) ]; + $PAGE->navbar->add(get_string('editendpoint', 'tool_oauth2', $strparams)); + } else { + $PAGE->navbar->add(get_string('createnewendpoint', 'tool_oauth2', s($issuer->get('name')))); + } + + $mform = new \tool_oauth2\form\endpoint(null, ['persistent' => $endpoint, 'issuerid' => $issuerid]); +} + +if ($mform && $mform->is_cancelled()) { + redirect(new moodle_url('/admin/tool/oauth2/endpoints.php', ['issuerid' => $issuerid])); +} else if ($action == 'edit') { + + if ($data = $mform->get_data()) { + + try { + if (!empty($data->id)) { + core\oauth2\api::update_endpoint($data); + } else { + core\oauth2\api::create_endpoint($data); + } + redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); + } catch (Exception $e) { + redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR); + } + } else { + echo $OUTPUT->header(); + if ($endpoint) { + $strparams = [ 'issuer' => s($issuer->get('name')), 'endpoint' => s($endpoint->get('name')) ]; + echo $OUTPUT->heading(get_string('editendpoint', 'tool_oauth2', $strparams)); + } else { + echo $OUTPUT->heading(get_string('createnewendpoint', 'tool_oauth2', s($issuer->get('name')))); + } + $mform->display(); + echo $OUTPUT->footer(); + } + +} else if ($action == 'delete') { + + if (!optional_param('confirm', false, PARAM_BOOL)) { + $continueparams = [ + 'action' => 'delete', + 'issuerid' => $issuerid, + 'endpointid' => $endpointid, + 'sesskey' => sesskey(), + 'confirm' => true + ]; + $continueurl = new moodle_url('/admin/tool/oauth2/endpoints.php', $continueparams); + $cancelurl = new moodle_url('/admin/tool/oauth2/endpoints.php'); + echo $OUTPUT->header(); + $strparams = [ 'issuer' => s($issuer->get('name')), 'endpoint' => s($endpoint->get('name')) ]; + echo $OUTPUT->confirm(get_string('deleteendpointconfirm', 'tool_oauth2', $strparams), $continueurl, $cancelurl); + echo $OUTPUT->footer(); + } else { + require_sesskey(); + core\oauth2\api::delete_endpoint($endpointid); + redirect($PAGE->url, get_string('endpointdeleted', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); + } + +} else { + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('endpointsforissuer', 'tool_oauth2', s($issuer->get('name')))); + $endpoints = core\oauth2\api::get_endpoints($issuer); + echo $renderer->endpoints_table($endpoints, $issuerid); + + $addurl = new moodle_url('/admin/tool/oauth2/endpoints.php', ['action' => 'edit', 'issuerid' => $issuerid]); + echo $renderer->single_button($addurl, get_string('createnewendpoint', 'tool_oauth2', s($issuer->get('name')))); + echo $OUTPUT->footer(); +} diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php index d80f91d1e2a..af6c762bcf0 100644 --- a/admin/tool/oauth2/issuers.php +++ b/admin/tool/oauth2/issuers.php @@ -40,12 +40,12 @@ require_capability('moodle/site:config', context_system::instance()); $renderer = $PAGE->get_renderer('tool_oauth2'); $action = optional_param('action', '', PARAM_ALPHAEXT); -$idpid = optional_param('id', '', PARAM_RAW); +$issuerid = optional_param('id', '', PARAM_RAW); $issuer = null; $mform = null; -if ($idpid) { - $issuer = \core\oauth2\api::get_issuer($idpid); +if ($issuerid) { + $issuer = \core\oauth2\api::get_issuer($issuerid); if (!$issuer) { print_error('invaliddata'); } @@ -53,7 +53,7 @@ if ($idpid) { if ($action == 'edit') { if ($issuer) { - $PAGE->navbar->add(get_string('editissuer', 'tool_oauth2', $issuer->get('name'))); + $PAGE->navbar->add(get_string('editissuer', 'tool_oauth2', s($issuer->get('name')))); } else { $PAGE->navbar->add(get_string('createnewissuer', 'tool_oauth2')); } @@ -80,7 +80,7 @@ if ($mform && $mform->is_cancelled()) { } else { echo $OUTPUT->header(); if ($issuer) { - echo $OUTPUT->heading(get_string('editissuer', 'tool_oauth2', $issuer->get('name'))); + echo $OUTPUT->heading(get_string('editissuer', 'tool_oauth2', s($issuer->get('name')))); } else { echo $OUTPUT->heading(get_string('createnewissuer', 'tool_oauth2')); } @@ -91,30 +91,30 @@ if ($mform && $mform->is_cancelled()) { } else if ($action == 'delete') { if (!optional_param('confirm', false, PARAM_BOOL)) { - $continueparams = ['action' => 'delete', 'id' => $idpid, 'sesskey' => sesskey(), 'confirm' => true]; + $continueparams = ['action' => 'delete', 'id' => $issuerid, 'sesskey' => sesskey(), 'confirm' => true]; $continueurl = new moodle_url('/admin/tool/oauth2/issuers.php', $continueparams); $cancelurl = new moodle_url('/admin/tool/oauth2/issuers.php'); echo $OUTPUT->header(); - echo $OUTPUT->confirm(get_string('deleteconfirm', 'tool_oauth2', $issuer->get('name')), $continueurl, $cancelurl); + echo $OUTPUT->confirm(get_string('deleteconfirm', 'tool_oauth2', s($issuer->get('name'))), $continueurl, $cancelurl); echo $OUTPUT->footer(); } else { require_sesskey(); - core\oauth2\api::delete_issuer($idpid); + core\oauth2\api::delete_issuer($issuerid); redirect($PAGE->url, get_string('issuerdeleted', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); } } else if ($action == 'auth') { if (!optional_param('confirm', false, PARAM_BOOL)) { - $continueparams = ['action' => 'auth', 'id' => $idpid, 'sesskey' => sesskey(), 'confirm' => true]; + $continueparams = ['action' => 'auth', 'id' => $issuerid, 'sesskey' => sesskey(), 'confirm' => true]; $continueurl = new moodle_url('/admin/tool/oauth2/issuers.php', $continueparams); $cancelurl = new moodle_url('/admin/tool/oauth2/issuers.php'); echo $OUTPUT->header(); - echo $OUTPUT->confirm(get_string('authconfirm', 'tool_oauth2', $issuer->get('name')), $continueurl, $cancelurl); + echo $OUTPUT->confirm(get_string('authconfirm', 'tool_oauth2', s($issuer->get('name'))), $continueurl, $cancelurl); echo $OUTPUT->footer(); } else { require_sesskey(); - $params = ['sesskey' => sesskey(), 'id' => $idpid, 'action' => 'auth', 'confirm' => true, 'response' => true]; + $params = ['sesskey' => sesskey(), 'id' => $issuerid, 'action' => 'auth', 'confirm' => true, 'response' => true]; if (core\oauth2\api::connect_system_account($issuer, new moodle_url('/admin/tool/oauth2/issuers.php', $params))) { redirect($PAGE->url, get_string('authconnected', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); } else { @@ -123,12 +123,12 @@ if ($mform && $mform->is_cancelled()) { } } else if ($action == 'moveup') { require_sesskey(); - core\oauth2\api::move_up_issuer($idpid); + core\oauth2\api::move_up_issuer($issuerid); redirect($PAGE->url); } else if ($action == 'movedown') { require_sesskey(); - core\oauth2\api::move_down_issuer($idpid); + core\oauth2\api::move_down_issuer($issuerid); redirect($PAGE->url); } else { diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 73bce13bed4..46dc3d8e47b 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -22,8 +22,17 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['pluginname'] = 'Open ID Connect configuration'; +$string['pluginname'] = 'OAuth 2 Services'; $string['editissuer'] = 'Edit identity issuer: {$a}'; +$string['editendpoint'] = 'Edit endpoint: {$a->endpoint} for issuer {$a->issuer}'; +$string['endpointsforissuer'] = 'Endpoints for issuer: {$a}'; +$string['edituserfieldmapping'] = 'Edit user field mapping for issuer {$a}'; +$string['userfieldmappingsforissuer'] = 'User field mappings for issuer: {$a}'; +$string['issuers'] = 'Issuers'; +$string['endpointname'] = 'Name'; +$string['endpointname_help'] = 'Key used to search for this endpoint. Must end with "_endpoint".'; +$string['endpointurl'] = 'Url'; +$string['endpointurl_help'] = 'URL for this endpoint. Must use https:// protocol.'; $string['issuername'] = 'Name'; $string['issuername_help'] = 'Name of the identity issuer. May be displayed on login page.'; $string['issuerimage'] = 'Logo URL'; @@ -47,6 +56,8 @@ $string['configuredstatus'] = 'Configured'; $string['discoverystatus'] = 'Discovery'; $string['systemauthstatus'] = 'System account connected'; $string['configured'] = 'Configured'; +$string['editendpoints'] = 'Configure endpoints'; +$string['edituserfieldmappings'] = 'Configure user field mappings'; $string['notconfigured'] = 'Not configured'; $string['discovered'] = 'Service discovery successful'; $string['notdiscovered'] = 'Service discovery not successful'; @@ -55,9 +66,19 @@ $string['notloginissuer'] = 'Do not allow login'; $string['systemaccountconnected'] = 'System account connected'; $string['systemaccountnotconnected'] = 'System account not connected'; $string['createnewissuer'] = 'Create new identity issuer'; +$string['createnewendpoint'] = 'Create new endpoint for issuer "{$a}"'; +$string['createnewuserfieldmapping'] = 'Create new user field mapping for issuer "{$a}"'; $string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.'; +$string['deleteendpointconfirm'] = 'Are you sure you want to delete the endpoint "{$a->endpoint}" for issuer "{$a->issuer}"? Any plugins relying on this endpoint will stop working.'; +$string['deleteuserfieldmappingconfirm'] = 'Are you sure you want to delete the user field mapping for issuer "{$a}"?'; $string['issuerdeleted'] = 'Identity issuer deleted'; +$string['endpointdeleted'] = 'Endpoint deleted'; +$string['userfieldmappingdeleted'] = 'User field mapping deleted'; $string['connectsystemaccount'] = 'Connect to a system account'; $string['authconfirm'] = 'This action will grant permanent API access to Moodle for the authenticated account. This is intended to be used as a system account for managing files owned by Moodle.'; $string['authconnected'] = 'The system account is now connected for offline access'; $string['authnotconnected'] = 'The system account was not connected for offline access'; +$string['userfieldexternalfield'] = 'External field name'; +$string['userfieldexternalfield_help'] = 'Name of the field provided by the external OAuth system.'; +$string['userfieldinternalfield'] = 'Internal field name'; +$string['userfieldinternalfield_help'] = 'Name of the Moodle user field that should be mapped from the external field.'; diff --git a/admin/tool/oauth2/userfieldmappings.php b/admin/tool/oauth2/userfieldmappings.php new file mode 100644 index 00000000000..1bf277d9069 --- /dev/null +++ b/admin/tool/oauth2/userfieldmappings.php @@ -0,0 +1,126 @@ +. + +/** + * OAuth 2 Endpoing Configuration page. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/tablelib.php'); + +$PAGE->set_url('/admin/tool/oauth2/userfieldmappings.php', ['issuerid' => required_param('issuerid', PARAM_INT)]); +$PAGE->set_context(context_system::instance()); +$PAGE->set_pagelayout('admin'); +$strheading = get_string('pluginname', 'tool_oauth2'); +$PAGE->set_title($strheading); +$PAGE->set_heading($strheading); + +require_login(); + +require_capability('moodle/site:config', context_system::instance()); + +$renderer = $PAGE->get_renderer('tool_oauth2'); + +$action = optional_param('action', '', PARAM_ALPHAEXT); +$issuerid = required_param('issuerid', PARAM_INT); +$userfieldmappingid = optional_param('userfieldmappingid', '', PARAM_INT); +$userfieldmapping = null; +$mform = null; + +$issuer = \core\oauth2\api::get_issuer($issuerid); +if (!$issuer) { + print_error('invaliddata'); +} +$PAGE->navbar->override_active_url(new moodle_url('/admin/tool/oauth2/issuers.php'), true); + +if (!empty($userfieldmappingid)) { + $userfieldmapping = \core\oauth2\api::get_user_field_mapping($userfieldmappingid); +} + +if ($action == 'edit') { + if ($userfieldmapping) { + $PAGE->navbar->add(get_string('edituserfieldmapping', 'tool_oauth2', s($issuer->get('name')))); + } else { + $PAGE->navbar->add(get_string('createnewuserfieldmapping', 'tool_oauth2', s($issuer->get('name')))); + } + + $mform = new \tool_oauth2\form\user_field_mapping(null, ['persistent' => $userfieldmapping, 'issuerid' => $issuerid]); +} + +if ($mform && $mform->is_cancelled()) { + redirect(new moodle_url('/admin/tool/oauth2/userfieldmappings.php', ['issuerid' => $issuerid])); +} else if ($action == 'edit') { + + if ($data = $mform->get_data()) { + + try { + if (!empty($data->id)) { + core\oauth2\api::update_user_field_mapping($data); + } else { + core\oauth2\api::create_user_field_mapping($data); + } + redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); + } catch (Exception $e) { + redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR); + } + } else { + echo $OUTPUT->header(); + if ($issuer) { + echo $OUTPUT->heading(get_string('edituserfieldmapping', 'tool_oauth2', s($issuer->get('name')))); + } else { + echo $OUTPUT->heading(get_string('createnewuserfieldmapping', 'tool_oauth2', s($issuer->get('name')))); + } + $mform->display(); + echo $OUTPUT->footer(); + } + +} else if ($action == 'delete') { + + if (!optional_param('confirm', false, PARAM_BOOL)) { + $continueparams = [ + 'action' => 'delete', + 'issuerid' => $issuerid, + 'userfieldmappingid' => $userfieldmappingid, + 'sesskey' => sesskey(), + 'confirm' => true + ]; + $continueurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', $continueparams); + $cancelurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php'); + echo $OUTPUT->header(); + $str = get_string('deleteuserfieldmappingconfirm', 'tool_oauth2', s($issuer->get('name'))); + echo $OUTPUT->confirm($str, $continueurl, $cancelurl); + echo $OUTPUT->footer(); + } else { + require_sesskey(); + core\oauth2\api::delete_user_field_mapping($userfieldmappingid); + redirect($PAGE->url, get_string('userfieldmappingdeleted', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); + } + +} else { + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('userfieldmappingsforissuer', 'tool_oauth2', s($issuer->get('name')))); + $userfieldmappings = core\oauth2\api::get_user_field_mappings($issuer); + echo $renderer->user_field_mappings_table($userfieldmappings, $issuerid); + + $addurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', ['action' => 'edit', 'issuerid' => $issuerid]); + echo $renderer->single_button($addurl, get_string('createnewuserfieldmapping', 'tool_oauth2', s($issuer->get('name')))); + echo $OUTPUT->footer(); +} diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 4bf2cc9fb68..545a162f2dd 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -318,6 +318,11 @@ class auth extends \auth_plugin_base { $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } + if (empty($userinfo['username'])) { + $errormsg = get_string('notloggedin', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); + } $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 00230d1620e..5fc61507344 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -68,6 +68,7 @@ class api { $endpoint = new endpoint(0, $record); $endpoint->create(); + // Microsoft is a custom setup. $record = (object) [ 'name' => 'Microsoft', 'image' => 'https://www.microsoft.com/favicon.ico', @@ -81,29 +82,43 @@ class api { $issuer = new issuer(0, $record); $issuer->create(); - $record = (object) [ - 'issuerid' => $issuer->get('id'), - 'name' => 'authorization_endpoint', - 'url' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize' + $endpoints = [ + 'authorization_endpoint' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + 'token_endpoint' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + 'userinfo_endpoint' => 'https://graph.microsoft.com/v1.0/me/', + 'userpicture_endpoint' => 'https://graph.microsoft.com/v1.0/me/photo/$value', ]; - $endpoint = new endpoint(0, $record); - $endpoint->create(); - $record = (object) [ - 'issuerid' => $issuer->get('id'), - 'name' => 'token_endpoint', - 'url' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token' - ]; - $endpoint = new endpoint(0, $record); - $endpoint->create(); + foreach ($endpoints as $name => $url) { + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => $name, + 'url' => $url + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + } - $record = (object) [ - 'issuerid' => $issuer->get('id'), - 'name' => 'end_session_endpoint', - 'url' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token' + // Create the field mappings. + $mapping = [ + 'givenName' => 'firstname', + 'surname' => 'lastname', + 'mail' => 'email', + 'userPrincipalName' => 'username', + 'displayName' => 'alternatename', + 'officeLocation' => 'address', + 'mobilePhone' => 'phone', + 'preferredLanguage' => 'lang' ]; - $endpoint = new endpoint(0, $record); - $endpoint->create(); + foreach ($mapping as $external => $internal) { + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'externalfield' => $external, + 'internalfield' => $internal + ]; + $userfieldmapping = new user_field_mapping(0, $record); + $userfieldmapping->create(); + } return issuer::count_records(); } @@ -115,6 +130,14 @@ class api { return new issuer($id); } + public static function get_endpoint($id) { + return new endpoint($id); + } + + public static function get_user_field_mapping($id) { + return new user_field_mapping($id); + } + public static function get_system_account(issuer $issuer) { return system_account::get_record(['issuerid' => $issuer->get('id')]); } @@ -132,11 +155,13 @@ class api { } public static function get_endpoints(issuer $issuer) { - require_capability('moodle/site:config', context_system::instance()); - return endpoint::get_records(['issuerid' => $issuer->get('id')]); } + public static function get_user_field_mappings(issuer $issuer) { + return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]); + } + protected static function guess_image($issuer) { if (empty($issuer->get('image'))) { $baseurl = parse_url($issuer->get('discoveryurl')); @@ -202,6 +227,32 @@ class api { } } + // We got to here - must be a decent OpenID connect service. Add the default user field mapping list. + + // Create the field mappings. + $mapping = [ + 'given_name' => 'firstname', + 'middle_name' => 'middlename', + 'family_name' => 'lastname', + 'email' => 'email', + 'sub' => 'username', + 'website' => 'url', + 'nickname' => 'alternatename', + 'picture' => 'picture', + 'address' => 'address', + 'phone' => 'phone', + 'locale' => 'lang' + ]; + foreach ($mapping as $external => $internal) { + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'externalfield' => $external, + 'internalfield' => $internal + ]; + $userfieldmapping = new user_field_mapping(0, $record); + $userfieldmapping->create(); + } + return endpoint::count_records(['issuerid' => $issuer->get('id')]); } @@ -215,6 +266,7 @@ class api { // Perform service discovery. self::discover_endpoints($issuer); self::guess_image($issuer); + return $issuer; } public static function create_issuer($data) { @@ -227,6 +279,45 @@ class api { // Perform service discovery. self::discover_endpoints($issuer); self::guess_image($issuer); + return $issuer; + } + + public static function update_endpoint($data) { + require_capability('moodle/site:config', context_system::instance()); + $endpoint = new endpoint(0, $data); + + // Will throw exceptions on validation failures. + $endpoint->update(); + + return $endpoint; + } + + public static function create_endpoint($data) { + require_capability('moodle/site:config', context_system::instance()); + $endpoint = new endpoint(0, $data); + + // Will throw exceptions on validation failures. + $endpoint->create(); + return $endpoint; + } + + public static function update_user_field_mapping($data) { + require_capability('moodle/site:config', context_system::instance()); + $userfieldmapping = new user_field_mapping(0, $data); + + // Will throw exceptions on validation failures. + $userfieldmapping->update(); + + return $userfieldmapping; + } + + public static function create_user_field_mapping($data) { + require_capability('moodle/site:config', context_system::instance()); + $userfieldmapping = new user_field_mapping(0, $data); + + // Will throw exceptions on validation failures. + $userfieldmapping->create(); + return $userfieldmapping; } /** @@ -307,7 +398,23 @@ class api { } // Will throw exceptions on validation failures. - $issuer->delete(); + return $issuer->delete(); + } + + public static function delete_endpoint($id) { + require_capability('moodle/site:config', context_system::instance()); + $endpoint = new endpoint($id); + + // Will throw exceptions on validation failures. + return $endpoint->delete(); + } + + public static function delete_user_field_mapping($id) { + require_capability('moodle/site:config', context_system::instance()); + $userfieldmapping = new user_field_mapping($id); + + // Will throw exceptions on validation failures. + return $userfieldmapping->delete(); } public static function connect_system_account($issuer, $returnurl) { @@ -324,6 +431,10 @@ class api { $client->log_out(); } + if (optional_param('error', '', PARAM_RAW)) { + return false; + } + if (!$client->is_logged_in()) { redirect($client->get_login_url()); } diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index 7410e64fad5..9b3b579107a 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -30,6 +30,7 @@ require_once($CFG->libdir . '/filelib.php'); use moodle_url; use curl; +use stdClass; /** * Configurable oauth2 client class where the urls come from DB. @@ -83,35 +84,98 @@ abstract class client extends \oauth2_client { return $this->issuer->get_endpoint_url('authorization'); } - public function get_issuer() { + /** + * Get the oauth2 issuer for this client. + * + * @return \core\oauth2\issuer Issuer + */ + public function get_issuer() { return $this->issuer; } + /** + * Override to append additional params to a authentication request. + * + * @return array (name value pairs). + */ public function get_additional_login_parameters() { - if ($this->issuer->get('behaviour') == issuer::BEHAVIOUR_OPENID_CONNECT) { - return ['access_type' => 'offline', 'prompt' => 'consent']; - } return []; } + /** + * Override to change the scopes requested with an authentiction request. + * + * @return string + */ protected function get_login_scopes() { return 'openid profile email'; } /** * Returns the token url for OAuth 2.0 request + * + * We are overriding the parent function so we get this from the configured endpoint. + * * @return string the auth url */ protected function token_url() { return $this->issuer->get_endpoint_url('token'); } + /** + * We want a unique key for each issuer / and a different key for system vs user oauth. + * + * @return string The unique key for the session value. + */ protected function get_tokenname() { - $name = static::class; + $name = 'oauth2-state-' . $this->issuer->get('id'); if ($this->system) { $name .= '-system'; } return $name; } + protected function get_userinfo_mapping() { + $fields = user_field_mapping::get_records(['issuerid' => $this->issuer->get('id')]); + + $map = []; + foreach ($fields as $field) { + $map[$field->get('externalfield')] = $field->get('internalfield'); + } + return $map; + } + + public function get_userinfo() { + $url = $this->get_issuer()->get_endpoint_url('userinfo'); + $response = $this->get($url); + if (!$response) { + return false; + } + $userinfo = new stdClass(); + try { + $userinfo = json_decode($response); + } catch (Exception $e) { + return false; + } + + $map = $this->get_userinfo_mapping(); + + $user = new stdClass(); + foreach ($map as $openidproperty => $moodleproperty) { + if (!empty($userinfo->$openidproperty)) { + $user->$moodleproperty = $userinfo->$openidproperty; + } + } + + if (!empty($user->picture)) { + $user->picture = download_file_content($user->picture, null, null, false, 10, 10, true, null, false); + } else { + $pictureurl = $this->issuer->get_endpoint_url('userpicture'); + if (!empty($pictureurl)) { + $user->picture = $this->get($pictureurl); + } + } + + return (array)$user; + } } diff --git a/lib/classes/oauth2/client_microsoft.php b/lib/classes/oauth2/client_microsoft.php index 3d0bcd617e7..7059c4d9e6a 100644 --- a/lib/classes/oauth2/client_microsoft.php +++ b/lib/classes/oauth2/client_microsoft.php @@ -26,6 +26,7 @@ namespace core\oauth2; defined('MOODLE_INTERNAL') || die(); use moodle_url; +use stdClass; /** * We have to call directly to the graph APIs because the Microsoft Open ID Connect API is @@ -36,19 +37,13 @@ use moodle_url; */ class client_microsoft extends client { - public function get_additional_login_parameters() { - return ['prompt' => 'consent']; - } - public function get_login_scopes() { - return 'openid profile email user.read'; + if ($this->system) { + return 'openid profile email user.read offline_access'; + } else { + return 'openid profile email user.read'; + } } - public function get_userinfo() { - $me = $client->get('https://graph.microsoft.com/v1.0/me/'); - var_dump($me); - - - } } diff --git a/lib/classes/oauth2/client_openid_connect.php b/lib/classes/oauth2/client_openid_connect.php index 4ff9837f0cb..805102c0f8d 100644 --- a/lib/classes/oauth2/client_openid_connect.php +++ b/lib/classes/oauth2/client_openid_connect.php @@ -37,62 +37,10 @@ use Exception; */ class client_openid_connect extends client { - /** - * Returns a mapping of openid properties to moodle properties. - * - * @return array - */ - private function get_mapping() { - return [ - 'given_name' => 'firstname', - 'middle_name' => 'middlename', - 'family_name' => 'lastname', - 'email' => 'email', - 'username' => 'username', - 'website' => 'url', - 'nickname' => 'alternatename', - 'picture' => 'picture', - 'address' => 'address', - 'phone' => 'phone', - 'locale' => 'lang' - ]; - } - public function get_additional_login_parameters() { if ($this->system) { return ['access_type' => 'offline', 'prompt' => 'consent']; } return []; } - - public function get_userinfo() { - $url = $this->get_issuer()->get_endpoint_url('userinfo'); - $response = $this->get($url); - if (!$response) { - return false; - } - $userinfo = new stdClass(); - try { - $userinfo = json_decode($response); - } catch (Exception $e) { - return false; - } - if (!empty($userinfo->preferred_username)) { - $userinfo->username = $userinfo->preferred_username; - } else { - $userinfo->username = $userinfo->sub; - } - - $map = $this->get_mapping(); - - $user = new stdClass(); - foreach ($map as $openidproperty => $moodleproperty) { - if (!empty($userinfo->$openidproperty)) { - $user->$moodleproperty = $userinfo->$openidproperty; - } - } - - return (array)$user; - } - } diff --git a/lib/classes/oauth2/endpoint.php b/lib/classes/oauth2/endpoint.php index fe3a0122e90..5916455a172 100644 --- a/lib/classes/oauth2/endpoint.php +++ b/lib/classes/oauth2/endpoint.php @@ -26,6 +26,7 @@ namespace core\oauth2; defined('MOODLE_INTERNAL') || die(); use core\persistent; +use lang_string; /** * Class for loading/storing oauth2 endpoints from the DB @@ -55,4 +56,11 @@ class endpoint extends persistent { ) ); } + + protected function validate_url($value) { + if (strpos($value, 'https://') !== 0) { + return new lang_string('sslonlyaccess', 'error'); + } + return true; + } } diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php new file mode 100644 index 00000000000..1ccf26b0e9e --- /dev/null +++ b/lib/classes/oauth2/user_field_mapping.php @@ -0,0 +1,78 @@ +. + +/** + * Class for loading/storing oauth2 endpoints from the DB. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing oauth2 user field mappings from the DB + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_field_mapping extends persistent { + + const TABLE = 'oauth2_user_field_mapping'; + + private static $userfields = [ + 'firstname', + 'middlename', + 'lastname', + 'email', + 'username', + 'idnumber', + 'url', + 'alternatename', + 'picture', + 'address', + 'phone', + 'lang' + ]; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'issuerid' => array( + 'type' => PARAM_INT + ), + 'externalfield' => array( + 'type' => PARAM_ALPHANUMEXT, + ), + 'internalfield' => array( + 'type' => PARAM_ALPHANUMEXT, + 'choices' => self::$userfields, + ) + ); + } + + public function get_internalfield_list() { + return array_combine(self::$userfields, self::$userfields); + } +} diff --git a/lib/db/install.xml b/lib/db/install.xml index 0233dedcf37..ba926fd74af 100755 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3513,5 +3513,22 @@ + + + + + + + + + + + + + + + + +
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 189c3338734..5666423c38c 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2699,10 +2699,39 @@ function xmldb_main_upgrade($oldversion) { if ($oldversion < 2017033100.01) { - \core\oauth2\api::install_default_issuers(); + // Define table oauth2_user_field_mapping to be created. + $table = new xmldb_table('oauth2_user_field_mapping'); + + // Adding fields to table oauth2_user_field_mapping. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('issuerid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('externalfield', XMLDB_TYPE_CHAR, '64', null, XMLDB_NOTNULL, null, null); + $table->add_field('internalfield', XMLDB_TYPE_CHAR, '64', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table oauth2_user_field_mapping. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('issuerkey', XMLDB_KEY_FOREIGN, array('issuerid'), 'oauth2_issuer', array('id')); + $table->add_key('uniqexternal', XMLDB_KEY_UNIQUE, array('issuerid', 'externalfield')); + $table->add_key('uniqinternal', XMLDB_KEY_UNIQUE, array('issuerid', 'internalfield')); + + // Conditionally launch create table for oauth2_user_field_mapping. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + // Main savepoint reached. upgrade_main_savepoint(true, 2017033100.01); } + if ($oldversion < 2017033100.02) { + + \core\oauth2\api::install_default_issuers(); + // Main savepoint reached. + upgrade_main_savepoint(true, 2017033100.02); + } + return true; } diff --git a/lib/filelib.php b/lib/filelib.php index bdc90bd114b..e326406e13b 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -2766,9 +2766,9 @@ class curl { // All settings of this class should be init here. $this->resetopt(); - //if (!empty($settings['debug'])) { + if (!empty($settings['debug'])) { $this->debug = true; - //} + } if (!empty($settings['cookie'])) { if($settings['cookie'] === true) { $this->cookie = $CFG->dataroot.'/curl_cookie.txt'; diff --git a/lib/oauthlib.php b/lib/oauthlib.php index 9efb6d100f6..7260defeb96 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -441,6 +441,15 @@ abstract class oauth2_client extends curl { // We have a token so we are logged in. if (isset($this->accesstoken->token)) { + // Check that the access token has all the requested scopes. + $scopecheck = ' ' . $this->accesstoken->scope . ' '; + + $requiredscopes = explode(' ', $this->scope); + foreach ($requiredscopes as $requiredscope) { + if (strpos($scopecheck, ' ' . $requiredscope . ' ') === false) { + return false; + } + } return true; } @@ -533,6 +542,10 @@ abstract class oauth2_client extends curl { $r = json_decode($response); + if (!empty($r->error)) { + throw new moodle_exception($r->error . ' ' . $r->error_description); + } + if (!isset($r->access_token)) { return false; } @@ -548,6 +561,12 @@ abstract class oauth2_client extends curl { // Expires 10 seconds before actual expiry. $accesstoken->expires = (time() + ($r->expires_in - 10)); } + if (isset($r->scope)) { + $accesstoken->scope = $r->scope; + } else { + $accesstoken->scope = $this->scope; + } + // Also add the scopes. $this->store_token($accesstoken); return true; diff --git a/theme/boost/templates/core/login.mustache b/theme/boost/templates/core/login.mustache index c253f728136..90fb24c96c5 100644 --- a/theme/boost/templates/core/login.mustache +++ b/theme/boost/templates/core/login.mustache @@ -178,7 +178,7 @@
{{#identityproviders}}
- + {{#iconurl}} {{/iconurl}} diff --git a/version.php b/version.php index 8a5f4a2be68..14d302ac2a2 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017033100.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017033100.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From 485a22fc98c4519e6a71636d794e0633da8894e5 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Mon, 27 Feb 2017 10:48:44 +0800 Subject: [PATCH 03/84] MDL-58090 oauth2: Get rid of behaviour field OAuth services working fully from config - no more subclasses for each OAuth issuer. Part of MDL-58220 --- admin/tool/oauth2/classes/form/issuer.php | 25 +++++++- admin/tool/oauth2/lang/en/tool_oauth2.php | 8 +++ lib/classes/oauth2/api.php | 9 ++- lib/classes/oauth2/client.php | 62 ++++++++++++++------ lib/classes/oauth2/client_microsoft.php | 49 ---------------- lib/classes/oauth2/client_oauth2.php | 43 -------------- lib/classes/oauth2/client_openid_connect.php | 46 --------------- lib/classes/oauth2/issuer.php | 17 ++++++ lib/db/install.xml | 4 ++ lib/db/upgrade.php | 4 ++ 10 files changed, 106 insertions(+), 161 deletions(-) delete mode 100644 lib/classes/oauth2/client_microsoft.php delete mode 100644 lib/classes/oauth2/client_oauth2.php delete mode 100644 lib/classes/oauth2/client_openid_connect.php diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index d16755c32bf..8456b8ecb82 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -76,11 +76,33 @@ class issuer extends persistent { $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2'); - // Offline access type + // Offline access type. $options = $endpoint->get_behaviour_list(); $mform->addElement('select', 'behaviour', get_string('issuerbehaviour', 'tool_oauth2'), $options); $mform->addHelpButton('behaviour', 'issuerbehaviour', 'tool_oauth2'); + // Login scopes. + $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('loginscopes', null, 'required', null, 'client'); + $mform->addRule('loginscopes', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('loginscopes', 'issuerloginscopes', 'tool_oauth2'); + + // Login scopes offline. + $mform->addElement('text', 'loginscopesoffline', get_string('issuerloginscopesoffline', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('loginscopesoffline', null, 'required', null, 'client'); + $mform->addRule('loginscopesoffline', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('loginscopesoffline', 'issuerloginscopesoffline', 'tool_oauth2'); + + // Login params. + $mform->addElement('text', 'loginparams', get_string('issuerloginparams', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('loginparams', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('loginparams', 'issuerloginparams', 'tool_oauth2'); + + // Login params offline. + $mform->addElement('text', 'loginparamsoffline', get_string('issuerloginparamsoffline', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('loginparamsoffline', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('loginparamsoffline', 'issuerloginparamsoffline', 'tool_oauth2'); + // Image. $mform->addElement('text', 'image', get_string('issuerimage', 'tool_oauth2'), 'maxlength="1024"'); $mform->addRule('image', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); @@ -89,7 +111,6 @@ class issuer extends persistent { // Show on login page. $mform->addElement('checkbox', 'showonloginpage', get_string('issuershowonloginpage', 'tool_oauth2')); $mform->addHelpButton('showonloginpage', 'issuershowonloginpage', 'tool_oauth2'); - $mform->addElement('hidden', 'sortorder'); $mform->setType('sortorder', PARAM_INT); diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 46dc3d8e47b..301457bd862 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -43,6 +43,14 @@ $string['issuerclientsecret'] = 'Client Secret'; $string['issuerclientsecret_help'] = 'The OAuth client secret for this issuer.'; $string['issuerbaseurl'] = 'Service base url'; $string['issuerbaseurl_help'] = 'Base url used to access the service.'; +$string['issuerloginscopes'] = 'Scopes included in a login request.'; +$string['issuerloginscopes_help'] = 'Some systems require additional scopes for a login request in order to read the users basic profile. The standard scopes for an OpenID Connect compliant system are "openid profile email".'; +$string['issuerloginscopesoffline'] = 'Scopes included in a login request for offline access.'; +$string['issuerloginscopesoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Microsoft requires an additional scope "offline_access"'; +$string['issuerloginparams'] = 'Additional parameters included in a login request.'; +$string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the users basic profile.'; +$string['issuerloginparamsoffline'] = 'Additional parameters included in a login request for offline access.'; +$string['issuerloginparamsoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Google requires the additional params: "access_type=offline&prompt=consent" these parameters should be in url query parameter format.'; $string['issuershowonloginpage'] = 'Show on login page.'; $string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to login with accounts from this issuer.'; $string['issuerbehaviour'] = 'Behaviour'; diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 5fc61507344..d05a1491fa5 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -54,6 +54,7 @@ class api { 'baseurl' => 'http://accounts.google.com/', 'clientid' => '', 'clientsecret' => '', + 'loginparamsoffline' => 'access_type=offline&prompt=consent', 'showonloginpage' => true ]; @@ -75,6 +76,8 @@ class api { 'behaviour' => issuer::BEHAVIOUR_MICROSOFT, 'baseurl' => 'http://login.microsoftonline.com/common/oauth2/v2.0/', 'clientid' => '', + 'loginscopes' => 'openid profile email user.read', + 'loginscopesoffline' => 'openid profile email user.read offline_access', 'clientsecret' => '', 'showonloginpage' => true ]; @@ -146,7 +149,7 @@ class api { } public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '') { - $client = \core\oauth2\client::create($issuer, $currenturl, $additionalscopes); + $client = new \core\oauth2\client($issuer, $currenturl, $additionalscopes); if (!$client->is_logged_in()) { redirect($client->get_login_url()); @@ -186,7 +189,7 @@ class api { $url = $issuer->get_endpoint_url('discovery'); if (!$url) { - $url = $issuer->get('url') . '/.well-known/openid-configuration'; + $url = $issuer->get('baseurl') . '/.well-known/openid-configuration'; } if (!$json = $curl->get($issuer->get_endpoint_url('discovery'))) { @@ -425,7 +428,7 @@ class api { // Allow callbacks to inject non-standard scopes to the auth request. - $client = client::create($issuer, $returnurl, $scopesrequired, true); + $client = new client($issuer, $returnurl, $scopesrequired, true); if (!optional_param('response', false, PARAM_BOOL)) { $client->log_out(); diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index 9b3b579107a..263db910eac 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -38,7 +38,7 @@ use stdClass; * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -abstract class client extends \oauth2_client { +class client extends \oauth2_client { /** @var \core\oauth2\issuer $issuer */ private $issuer; @@ -52,30 +52,22 @@ abstract class client extends \oauth2_client { * @param issuer $issuer * @param moodle_url $returnurl */ - public function __construct(issuer $issuer, moodle_url $returnurl, $scopesrequired, $system) { + public function __construct(issuer $issuer, moodle_url $returnurl, $scopesrequired, $system = false) { $this->issuer = $issuer; $this->system = $system; $scopes = $this->get_login_scopes(); $additionalscopes = explode(' ', $scopesrequired); foreach ($additionalscopes as $scope) { - if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { - $scopes .= ' ' . $scope; + if (!empty($scope)) { + if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { + $scopes .= ' ' . $scope; + } } } parent::__construct($issuer->get('clientid'), $issuer->get('clientsecret'), $returnurl, $scopes); } - public static function create(issuer $issuer, moodle_url $returnurl, $scopesrequired, $system = false) { - if ($issuer->get('behaviour') == issuer::BEHAVIOUR_OPENID_CONNECT) { - return new client_openid_connect($issuer, $returnurl, $scopesrequired, $system); - } else if ($issuer->get('behaviour') == issuer::BEHAVIOUR_OAUTH2) { - return new client_oauth2($issuer, $returnurl, $scopesrequired, $system); - } else if ($issuer->get('behaviour') == issuer::BEHAVIOUR_MICROSOFT) { - return new client_microsoft($issuer, $returnurl, $scopesrequired, $system); - } - } - /** * Returns the auth url for OAuth 2.0 request * @return string the auth url @@ -99,7 +91,22 @@ abstract class client extends \oauth2_client { * @return array (name value pairs). */ public function get_additional_login_parameters() { - return []; + $params = ''; + if ($this->system) { + if (!empty($this->issuer->get('loginparamsoffline'))) { + $params = $this->issuer->get('loginparamsoffline'); + } + } else { + if (!empty($this->issuer->get('loginparams'))) { + $params = $this->issuer->get('loginparams'); + } + } + if (empty($params)) { + return []; + } + $result = []; + parse_str($params, $result); + return $result; } /** @@ -108,7 +115,11 @@ abstract class client extends \oauth2_client { * @return string */ protected function get_login_scopes() { - return 'openid profile email'; + if ($this->system) { + return $this->issuer->get('loginscopesoffline'); + } else { + return $this->issuer->get('loginscopes'); + } } /** @@ -162,8 +173,23 @@ abstract class client extends \oauth2_client { $user = new stdClass(); foreach ($map as $openidproperty => $moodleproperty) { - if (!empty($userinfo->$openidproperty)) { - $user->$moodleproperty = $userinfo->$openidproperty; + // We support nested objects via a-b-c syntax. + $getfunc = function($obj, $prop) use (&$getfunc) { + $proplist = explode('-', $prop, 2); + if (empty($proplist[0]) || empty($obj->{$proplist[0]})) { + return false; + } + $obj = $obj->{$proplist[0]}; + + if (count($proplist) > 1) { + return $getfunc($obj, $proplist[1]); + } + return $obj; + }; + + $resolved = $getfunc($userinfo, $openidproperty); + if (!empty($resolved)) { + $user->$moodleproperty = $resolved; } } diff --git a/lib/classes/oauth2/client_microsoft.php b/lib/classes/oauth2/client_microsoft.php deleted file mode 100644 index 7059c4d9e6a..00000000000 --- a/lib/classes/oauth2/client_microsoft.php +++ /dev/null @@ -1,49 +0,0 @@ -. - -/** - * Configurable oauth2 client class. - * - * @package core\oauth2 - * @copyright 2017 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -namespace core\oauth2; - -defined('MOODLE_INTERNAL') || die(); - -use moodle_url; -use stdClass; - -/** - * We have to call directly to the graph APIs because the Microsoft Open ID Connect API is - * lame. - * - * @copyright 2017 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class client_microsoft extends client { - - public function get_login_scopes() { - if ($this->system) { - return 'openid profile email user.read offline_access'; - } else { - return 'openid profile email user.read'; - } - } - - -} diff --git a/lib/classes/oauth2/client_oauth2.php b/lib/classes/oauth2/client_oauth2.php deleted file mode 100644 index 96fb804abd6..00000000000 --- a/lib/classes/oauth2/client_oauth2.php +++ /dev/null @@ -1,43 +0,0 @@ -. - -/** - * Configurable oauth2 client class. - * - * @package core\oauth2 - * @copyright 2017 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -namespace core\oauth2; - -defined('MOODLE_INTERNAL') || die(); - -use moodle_url; - -/** - * Configurable oauth2 client class where the urls come from DB. - * - * @copyright 2017 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class client_oauth2 extends client { - - public function get_additional_login_parameters() { - return ['access_type' => 'offline', 'prompt' => 'consent']; - } - - -} diff --git a/lib/classes/oauth2/client_openid_connect.php b/lib/classes/oauth2/client_openid_connect.php deleted file mode 100644 index 805102c0f8d..00000000000 --- a/lib/classes/oauth2/client_openid_connect.php +++ /dev/null @@ -1,46 +0,0 @@ -. - -/** - * Configurable oauth2 client class. - * - * @package core\oauth2 - * @copyright 2017 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -namespace core\oauth2; - -defined('MOODLE_INTERNAL') || die(); - -use moodle_url; -use stdClass; -use Exception; - -/** - * Configurable oauth2 client class where the urls come from DB. - * - * @copyright 2017 Damyon Wiese - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class client_openid_connect extends client { - - public function get_additional_login_parameters() { - if ($this->system) { - return ['access_type' => 'offline', 'prompt' => 'consent']; - } - return []; - } -} diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 71eec0994bb..1f71b3a1a97 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -79,6 +79,22 @@ class issuer extends persistent { 'null' => NULL_ALLOWED, 'default' => null ), + 'loginscopes' => array( + 'type' => PARAM_RAW, + 'default' => 'openid profile email' + ), + 'loginscopesoffline' => array( + 'type' => PARAM_RAW, + 'default' => 'openid profile email' + ), + 'loginparams' => array( + 'type' => PARAM_RAW, + 'default' => '' + ), + 'loginparamsoffline' => array( + 'type' => PARAM_RAW, + 'default' => '' + ), 'sortorder' => array( 'type' => PARAM_INT, 'default' => 0, @@ -97,6 +113,7 @@ class issuer extends persistent { } return false; } + public function is_authentication_supported() { $supportedloginbehaviours = [ self::BEHAVIOUR_OPENID_CONNECT, diff --git a/lib/db/install.xml b/lib/db/install.xml index ba926fd74af..e361e514ad2 100755 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -3489,6 +3489,10 @@ + + + + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 5666423c38c..31d6126a45d 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2626,6 +2626,10 @@ function xmldb_main_upgrade($oldversion) { $table->add_field('baseurl', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('clientid', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('clientsecret', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('loginscopes', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('loginscopesoffline', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('loginparams', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('loginparamsoffline', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('behaviour', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, 'none'); $table->add_field('scopessupported', XMLDB_TYPE_TEXT, null, null, null, null, null); $table->add_field('showonloginpage', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); From ddf65b8c0563d6393c78e94280d9ae70be3fde69 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Mon, 27 Feb 2017 12:24:48 +0800 Subject: [PATCH 04/84] MDL-58090 oauth2: Facebook Add defaults for facebook authentication and drop behaviour field completely. Part of MDL-58220 --- admin/tool/oauth2/classes/form/issuer.php | 5 -- admin/tool/oauth2/classes/output/renderer.php | 2 +- lib/classes/oauth2/api.php | 87 +++++++++++++++---- lib/classes/oauth2/issuer.php | 38 ++------ lib/db/install.xml | 1 - lib/db/upgrade.php | 1 - 6 files changed, 80 insertions(+), 54 deletions(-) diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index 8456b8ecb82..b9f643845ac 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -76,11 +76,6 @@ class issuer extends persistent { $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2'); - // Offline access type. - $options = $endpoint->get_behaviour_list(); - $mform->addElement('select', 'behaviour', get_string('issuerbehaviour', 'tool_oauth2'), $options); - $mform->addHelpButton('behaviour', 'issuerbehaviour', 'tool_oauth2'); - // Login scopes. $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2'), 'maxlength="255"'); $mform->addRule('loginscopes', null, 'required', null, 'client'); diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index 0d4514d2bcc..d9fa4513cc5 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -104,7 +104,7 @@ class renderer extends plugin_renderer_base { if (!empty($issuer->get('scopessupported'))) { $discovered = $OUTPUT->pix_icon('yes', get_string('discovered', 'tool_oauth2'), 'tool_oauth2'); } else { - if ($issuer->get('behaviour') == $issuer::BEHAVIOUR_OPENID_CONNECT) { + if (!empty($issuer->get_endpoint_url('discovery'))) { $discovered = $OUTPUT->pix_icon('no', get_string('notdiscovered', 'tool_oauth2'), 'tool_oauth2'); } else { $discovered = '-'; diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index d05a1491fa5..9d62a2de3f8 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -41,19 +41,11 @@ defined('MOODLE_INTERNAL') || die(); */ class api { - /** - * Called from install.php and upgrade.php - install the default list of issuers - * @return int The number of issuers installed. - */ - public static function install_default_issuers() { - // Setup default list of identity issuers. + private static function create_google() { $record = (object) [ 'name' => 'Google', 'image' => 'https://accounts.google.com/favicon.ico', - 'behaviour' => issuer::BEHAVIOUR_OPENID_CONNECT, 'baseurl' => 'http://accounts.google.com/', - 'clientid' => '', - 'clientsecret' => '', 'loginparamsoffline' => 'access_type=offline&prompt=consent', 'showonloginpage' => true ]; @@ -68,17 +60,65 @@ class api { ]; $endpoint = new endpoint(0, $record); $endpoint->create(); + } + private static function create_facebook() { + // Facebook is a custom setup. + $record = (object) [ + 'name' => 'Facebook', + 'image' => 'https://facebookbrand.com/wp-content/themes/fb-branding/prj-fb-branding/assets/images/fb-art.png', + 'loginscopes' => 'public_profile email', + 'loginscopesoffline' => 'public_profile email', + 'showonloginpage' => true + ]; + + $issuer = new issuer(0, $record); + $issuer->create(); + + $endpoints = [ + 'authorization_endpoint' => 'https://www.facebook.com/v2.8/dialog/oauth', + 'token_endpoint' => 'https://graph.facebook.com/v2.8/oauth/access_token', + 'userinfo_endpoint' => 'https://graph.facebook.com/v2.8/me?fields=id,first_name,last_name,link,picture,name,email' + ]; + + foreach ($endpoints as $name => $url) { + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => $name, + 'url' => $url + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + } + + // Create the field mappings. + $mapping = [ + 'name' => 'alternatename', + 'last_name' => 'lastname', + 'email' => 'email', + 'id' => 'username', + 'first_name' => 'firstname', + 'picture-data-url' => 'picture', + 'link' => 'url', + ]; + foreach ($mapping as $external => $internal) { + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'externalfield' => $external, + 'internalfield' => $internal + ]; + $userfieldmapping = new user_field_mapping(0, $record); + $userfieldmapping->create(); + } + } + + private static function create_microsoft() { // Microsoft is a custom setup. $record = (object) [ 'name' => 'Microsoft', 'image' => 'https://www.microsoft.com/favicon.ico', - 'behaviour' => issuer::BEHAVIOUR_MICROSOFT, - 'baseurl' => 'http://login.microsoftonline.com/common/oauth2/v2.0/', - 'clientid' => '', 'loginscopes' => 'openid profile email user.read', 'loginscopesoffline' => 'openid profile email user.read offline_access', - 'clientsecret' => '', 'showonloginpage' => true ]; @@ -122,6 +162,18 @@ class api { $userfieldmapping = new user_field_mapping(0, $record); $userfieldmapping->create(); } + } + + /** + * Called from install.php and upgrade.php - install the default list of issuers + * @return int The number of issuers installed. + */ + public static function install_default_issuers() { + // Setup default list of identity issuers. + self::create_google(); + self::create_microsoft(); + self::create_facebook(); + return issuer::count_records(); } @@ -175,7 +227,7 @@ class api { } /** - * If the behaviour supports discovery for this issuer, try and determine the list of valid endpoints. + * If the discovery endpoint exists for this issuer, try and determine the list of valid endpoints. * * @param issuer $issuer * @return int The number of discovered services. @@ -183,7 +235,7 @@ class api { protected static function discover_endpoints($issuer) { $curl = new curl(); - if ($issuer->get('behaviour') != issuer::BEHAVIOUR_OPENID_CONNECT) { + if (empty($issuer->get('baseurl'))) { return 0; } @@ -192,7 +244,7 @@ class api { $url = $issuer->get('baseurl') . '/.well-known/openid-configuration'; } - if (!$json = $curl->get($issuer->get_endpoint_url('discovery'))) { + if (!$json = $curl->get($url)) { $msg = 'Could not discover end points for identity issuer' . $issuer->get('name'); throw new moodle_exception($msg); } @@ -231,6 +283,9 @@ class api { } // We got to here - must be a decent OpenID connect service. Add the default user field mapping list. + foreach (user_field_mapping::get_records(['issuerid' => $issuer->get('id')]) as $userfieldmapping) { + $userfieldmapping->delete(); + } // Create the field mappings. $mapping = [ diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 1f71b3a1a97..3d32a21e9a3 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -37,10 +37,6 @@ class issuer extends persistent { const TABLE = 'oauth2_issuer'; - const BEHAVIOUR_OPENID_CONNECT = 'Open ID Connect'; - const BEHAVIOUR_MICROSOFT = 'Microsoft OAuth 2.0'; - const BEHAVIOUR_OAUTH2 = 'OAuth 2.0'; - /** * Return the definition of the properties of this model. * @@ -57,18 +53,16 @@ class issuer extends persistent { 'default' => null ), 'clientid' => array( - 'type' => PARAM_RAW + 'type' => PARAM_RAW, + 'default' => '' ), 'clientsecret' => array( - 'type' => PARAM_RAW - ), - 'behaviour' => array( - 'type' => PARAM_NOTAGS, - 'choices' => array(self::BEHAVIOUR_OPENID_CONNECT, self::BEHAVIOUR_MICROSOFT, self::BEHAVIOUR_OAUTH2), - 'default' => self::BEHAVIOUR_OPENID_CONNECT + 'type' => PARAM_RAW, + 'default' => '' ), 'baseurl' => array( - 'type' => PARAM_URL + 'type' => PARAM_URL, + 'null' => NULL_ALLOWED, ), 'showonloginpage' => array( 'type' => PARAM_BOOL, @@ -115,27 +109,11 @@ class issuer extends persistent { } public function is_authentication_supported() { - $supportedloginbehaviours = [ - self::BEHAVIOUR_OPENID_CONNECT, - self::BEHAVIOUR_MICROSOFT, - ]; - return in_array($this->get('behaviour'), $supportedloginbehaviours); + return (!empty($this->get_endpoint_url('userinfo'))); } public function is_system_account_setup_supported() { - $supportedsystemaccountbehaviours = [ - self::BEHAVIOUR_OPENID_CONNECT, - self::BEHAVIOUR_MICROSOFT, - ]; - return in_array($this->get('behaviour'), $supportedsystemaccountbehaviours); - } - - public function get_behaviour_list() { - return [ - self::BEHAVIOUR_OPENID_CONNECT => self::BEHAVIOUR_OPENID_CONNECT, - self::BEHAVIOUR_OAUTH2 => self::BEHAVIOUR_OAUTH2, - self::BEHAVIOUR_MICROSOFT => self::BEHAVIOUR_MICROSOFT - ]; + return true; } public function is_system_account_connected() { diff --git a/lib/db/install.xml b/lib/db/install.xml index e361e514ad2..41d8b5a2763 100755 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -3493,7 +3493,6 @@ - diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 31d6126a45d..5868b30ff58 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2630,7 +2630,6 @@ function xmldb_main_upgrade($oldversion) { $table->add_field('loginscopesoffline', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('loginparams', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('loginparamsoffline', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); - $table->add_field('behaviour', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, 'none'); $table->add_field('scopessupported', XMLDB_TYPE_TEXT, null, null, null, null, null); $table->add_field('showonloginpage', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); From 237fd80cd2173a8fe1d41b310b67114c0c45581c Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Mon, 27 Feb 2017 16:19:04 +0800 Subject: [PATCH 05/84] MDL-58090 oauth2: API to get a system client Request an oauth client with an access token retrieved with the stored refresh token from the OAuth issuer. Part of MDL-58220 --- lib/classes/oauth2/api.php | 43 +++++++++++++++++++++++++ lib/classes/oauth2/client.php | 59 +++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 9d62a2de3f8..bd1a9c51523 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -197,7 +197,50 @@ class api { return system_account::get_record(['issuerid' => $issuer->get('id')]); } + public static function get_system_scopes_for_issuer($issuer) { + $scopes = $issuer->get('loginscopesoffline'); + + $pluginsfunction = get_plugins_with_function('oauth2_system_scopes', 'lib.php'); + foreach ($pluginsfunction as $plugintype => $plugins) { + foreach ($plugins as $pluginfunction) { + // Get additional scopes from the plugin. + $pluginscopes = $pluginfunction($issuer); + if (empty($pluginscopes)) { + continue; + } + + // Merge the additional scopes with the existing ones. + $additionalscopes = explode(' ', $pluginscopes); + + foreach ($additionalscopes as $scope) { + if (!empty($scope)) { + if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { + $scopes .= ' ' . $scope; + } + } + } + } + } + + return $scopes; + } + public static function get_system_oauth_client(issuer $issuer) { + $systemaccount = self::get_system_account($issuer); + if (empty($systemaccount)) { + return false; + } + // Get all the scopes! + $scopes = self::get_system_scopes_for_issuer($issuer); + + $client = new \core\oauth2\client($issuer, null, $scopes, true); + + if (!$client->is_logged_in()) { + if (!$client->update_refresh_token($systemaccount)) { + return false; + } + } + return $client; } public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '') { diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index 263db910eac..3947b0c3dac 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -156,6 +156,65 @@ class client extends \oauth2_client { return $map; } + /** + * Upgrade a refresh token from oauth 2.0 to an access token + * + * @return boolean true if token is upgraded succesfully + */ + public function upgrade_refresh_token(system_account $systemaccount) { + $refreshtoken = $systemaccount->get('refreshtoken'); + + $params = array('refresh_token' => $refreshtoken, + 'client_id' => $this->clientid, + 'client_secret' => $this->clientsecret, + 'grant_type' => 'refresh_token' + ); + + // Requests can either use http GET or POST. + if ($this->use_http_get()) { + $response = $this->get($this->token_url(), $params); + } else { + $response = $this->post($this->token_url(), $this->build_post_data($params)); + } + + if (!$this->info['http_code'] === 200) { + throw new moodle_exception('Could not upgrade oauth token'); + } + + $r = json_decode($response); + + if (!empty($r->error)) { + throw new moodle_exception($r->error . ' ' . $r->error_description); + } + + if (!isset($r->access_token)) { + return false; + } + + if (isset($r->refresh_token)) { + $systemaccount->set('refreshtoken', $r->refresh_token); + $systemaccount->update(); + $this->refreshtoken = $r->refresh_token; + } + + // Store the token an expiry time. + $accesstoken = new stdClass; + $accesstoken->token = $r->access_token; + if (isset($r->expires_in)) { + // Expires 10 seconds before actual expiry. + $accesstoken->expires = (time() + ($r->expires_in - 10)); + } + if (isset($r->scope)) { + $accesstoken->scope = $r->scope; + } else { + $accesstoken->scope = $this->scope; + } + // Also add the scopes. + $this->store_token($accesstoken); + + return true; + } + public function get_userinfo() { $url = $this->get_issuer()->get_endpoint_url('userinfo'); $response = $this->get($url); From dc4b56852ac7ebdf50e5707406a4a0c31ed61d62 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Feb 2017 10:12:47 +0800 Subject: [PATCH 06/84] MDL-58090 oauth2: Do not install services by default Instead provide buttons to quickly create pre-configured versions of any of the known OAuth 2 services. Part of MDL-58220 --- admin/tool/oauth2/classes/form/issuer.php | 11 +++++---- admin/tool/oauth2/issuers.php | 26 ++++++++++++++++++++++ admin/tool/oauth2/lang/en/tool_oauth2.php | 8 ++++++- lib/classes/oauth2/api.php | 27 ++++++++++++++--------- lib/classes/oauth2/issuer.php | 2 +- lib/db/upgrade.php | 7 ------ 6 files changed, 55 insertions(+), 26 deletions(-) diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index b9f643845ac..728f21bfd21 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -70,12 +70,6 @@ class issuer extends persistent { $mform->addRule('clientsecret', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('clientsecret', 'issuerclientsecret', 'tool_oauth2'); - // Base Url. - $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'), 'maxlength="1024"'); - $mform->addRule('baseurl', null, 'required', null, 'client'); - $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); - $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2'); - // Login scopes. $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2'), 'maxlength="255"'); $mform->addRule('loginscopes', null, 'required', null, 'client'); @@ -98,6 +92,11 @@ class issuer extends persistent { $mform->addRule('loginparamsoffline', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('loginparamsoffline', 'issuerloginparamsoffline', 'tool_oauth2'); + // Base Url. + $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); + $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2'); + // Image. $mform->addElement('text', 'image', get_string('issuerimage', 'tool_oauth2'), 'maxlength="1024"'); $mform->addRule('image', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php index af6c762bcf0..631cca2856b 100644 --- a/admin/tool/oauth2/issuers.php +++ b/admin/tool/oauth2/issuers.php @@ -87,6 +87,30 @@ if ($mform && $mform->is_cancelled()) { $mform->display(); echo $OUTPUT->footer(); } +} else if ($action == 'edittemplate') { + + $type = optional_param('type', '', PARAM_ALPHA); + if (empty($type)) { + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('createfromtemplate', 'tool_oauth2')); + echo '

' . get_string('createfromtemplatedesc', 'tool_oauth2') . '

'; + $params = ['action' => 'edittemplate', 'type' => 'google', 'sesskey' => sesskey()]; + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + echo $renderer->single_button($addurl, get_string('createnewgoogleissuer', 'tool_oauth2')); + $params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey()]; + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + echo $renderer->single_button($addurl, get_string('createnewmicrosoftissuer', 'tool_oauth2')); + $params = ['action' => 'edittemplate', 'type' => 'facebook', 'sesskey' => sesskey()]; + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + echo $renderer->single_button($addurl, get_string('createnewfacebookissuer', 'tool_oauth2')); + echo $OUTPUT->footer(); + } else { + require_sesskey(); + $issuer = core\oauth2\api::create_standard_issuer($type); + $params = ['action' => 'edit', 'id' => $issuer->get('id')]; + $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + redirect($editurl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); + } } else if ($action == 'delete') { @@ -138,5 +162,7 @@ if ($mform && $mform->is_cancelled()) { $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']); echo $renderer->single_button($addurl, get_string('createnewissuer', 'tool_oauth2')); + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edittemplate']); + echo $renderer->single_button($addurl, get_string('createnewstandardissuer', 'tool_oauth2')); echo $OUTPUT->footer(); } diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 301457bd862..5f47f8f3183 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -73,7 +73,11 @@ $string['loginissuer'] = 'Allow login'; $string['notloginissuer'] = 'Do not allow login'; $string['systemaccountconnected'] = 'System account connected'; $string['systemaccountnotconnected'] = 'System account not connected'; -$string['createnewissuer'] = 'Create new identity issuer'; +$string['createnewissuer'] = 'Create new custom service'; +$string['createnewgoogleissuer'] = 'Create new Google service'; +$string['createnewmicrosoftissuer'] = 'Create new Microsoft service'; +$string['createnewfacebookissuer'] = 'Create new Facebook service'; +$string['createnewstandardissuer'] = 'Create service from a template'; $string['createnewendpoint'] = 'Create new endpoint for issuer "{$a}"'; $string['createnewuserfieldmapping'] = 'Create new user field mapping for issuer "{$a}"'; $string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.'; @@ -90,3 +94,5 @@ $string['userfieldexternalfield'] = 'External field name'; $string['userfieldexternalfield_help'] = 'Name of the field provided by the external OAuth system.'; $string['userfieldinternalfield'] = 'Internal field name'; $string['userfieldinternalfield_help'] = 'Name of the Moodle user field that should be mapped from the external field.'; +$string['createfromtemplate'] = 'Create an OAuth 2 service from a template'; +$string['createfromtemplatedesc'] = 'Choose one of the OAuth 2 service template below to create an OAuth service with a valid configuration for one of the known service types. This will create the OAuth 2 service, with all the correct end points and parameters required for authentication, but you will still need to enter the client ID and secret for the new service before it can be used.'; diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index bd1a9c51523..0302d8bded0 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -60,6 +60,7 @@ class api { ]; $endpoint = new endpoint(0, $record); $endpoint->create(); + return $issuer; } private static function create_facebook() { @@ -67,6 +68,7 @@ class api { $record = (object) [ 'name' => 'Facebook', 'image' => 'https://facebookbrand.com/wp-content/themes/fb-branding/prj-fb-branding/assets/images/fb-art.png', + 'baseurl' => '', 'loginscopes' => 'public_profile email', 'loginscopesoffline' => 'public_profile email', 'showonloginpage' => true @@ -110,6 +112,7 @@ class api { $userfieldmapping = new user_field_mapping(0, $record); $userfieldmapping->create(); } + return $issuer; } private static function create_microsoft() { @@ -117,6 +120,7 @@ class api { $record = (object) [ 'name' => 'Microsoft', 'image' => 'https://www.microsoft.com/favicon.ico', + 'baseurl' => '', 'loginscopes' => 'openid profile email user.read', 'loginscopesoffline' => 'openid profile email user.read offline_access', 'showonloginpage' => true @@ -162,19 +166,20 @@ class api { $userfieldmapping = new user_field_mapping(0, $record); $userfieldmapping->create(); } + return $issuer; } - /** - * Called from install.php and upgrade.php - install the default list of issuers - * @return int The number of issuers installed. - */ - public static function install_default_issuers() { - // Setup default list of identity issuers. - self::create_google(); - self::create_microsoft(); - self::create_facebook(); - - return issuer::count_records(); + public static function create_standard_issuer($type) { + require_capability('moodle/site:config', context_system::instance()); + if ($type == 'google') { + return self::create_google(); + } else if ($type == 'microsoft') { + return self::create_microsoft(); + } else if ($type == 'facebook') { + return self::create_facebook(); + } else { + throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); + } } public static function get_all_issuers() { diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 3d32a21e9a3..040d3e66ae8 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -62,7 +62,7 @@ class issuer extends persistent { ), 'baseurl' => array( 'type' => PARAM_URL, - 'null' => NULL_ALLOWED, + 'default' => '' ), 'showonloginpage' => array( 'type' => PARAM_BOOL, diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 5868b30ff58..37f596d1aa4 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2729,12 +2729,5 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2017033100.01); } - if ($oldversion < 2017033100.02) { - - \core\oauth2\api::install_default_issuers(); - // Main savepoint reached. - upgrade_main_savepoint(true, 2017033100.02); - } - return true; } From 2b09b2daa31ae02af5e13737a55129759bbaf06e Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Feb 2017 10:35:51 +0800 Subject: [PATCH 07/84] MDL-58090 oauth2: Load and resave the DB files Edit the DB files with XMLDB to detect/fix errors. Part of MDL-58220 --- lib/db/install.php | 2 -- lib/db/install.xml | 10 +++++----- lib/db/upgrade.php | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/db/install.php b/lib/db/install.php index 8a45a6aa4ff..f291828a1fa 100644 --- a/lib/db/install.php +++ b/lib/db/install.php @@ -321,6 +321,4 @@ function xmldb_main_install() { require_once($CFG->libdir . '/db/upgradelib.php'); make_default_scale(); make_competence_scale(); - - \core\oauth2\api::install_default_providers(); } diff --git a/lib/db/install.xml b/lib/db/install.xml index 41d8b5a2763..29518ad8246 100755 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3489,10 +3489,10 @@ - - - - + + + + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 37f596d1aa4..06988419548 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2613,7 +2613,7 @@ function xmldb_main_upgrade($oldversion) { if ($oldversion < 2017032400.21) { - // Define table oauth2_issuer to be created. + // Define table oauth2_issuer to be created. $table = new xmldb_table('oauth2_issuer'); // Adding fields to table oauth2_issuer. @@ -2702,7 +2702,7 @@ function xmldb_main_upgrade($oldversion) { if ($oldversion < 2017033100.01) { - // Define table oauth2_user_field_mapping to be created. + // Define table oauth2_user_field_mapping to be created. $table = new xmldb_table('oauth2_user_field_mapping'); // Adding fields to table oauth2_user_field_mapping. From f9f243f93e023340efbc0e9d018ccbf340e471a8 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Feb 2017 12:12:14 +0800 Subject: [PATCH 08/84] MDL-58090 oauth2: Complete phpdocs Part of MDL-58220 --- admin/tool/oauth2/userfieldmappings.php | 2 +- auth/oauth2/auth.php | 3 +- auth/oauth2/classes/auth.php | 10 +- auth/oauth2/lang/en/auth_oauth2.php | 4 +- auth/oauth2/login.php | 2 +- lib/classes/oauth2/api.php | 158 ++++++++++++++++++++++ lib/classes/oauth2/client.php | 11 ++ lib/classes/oauth2/endpoint.php | 7 + lib/classes/oauth2/issuer.php | 17 +++ lib/classes/oauth2/user_field_mapping.php | 5 + lib/classes/plugin_manager.php | 4 +- 11 files changed, 215 insertions(+), 8 deletions(-) diff --git a/admin/tool/oauth2/userfieldmappings.php b/admin/tool/oauth2/userfieldmappings.php index 1bf277d9069..d0961ceb9d8 100644 --- a/admin/tool/oauth2/userfieldmappings.php +++ b/admin/tool/oauth2/userfieldmappings.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * OAuth 2 Endpoing Configuration page. + * OAuth 2 Endpoint Configuration page. * * @package tool_oauth2 * @copyright 2017 Damyon Wiese diff --git a/auth/oauth2/auth.php b/auth/oauth2/auth.php index 0d5ecd7f79a..601dd4b2c4d 100644 --- a/auth/oauth2/auth.php +++ b/auth/oauth2/auth.php @@ -27,7 +27,8 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** - * Plugin for oauth2 authentication. + * Plugin for oauth2 authentication. This is a way to use namespaces even though + * moodle expects a non-namespaced file here. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 545a162f2dd..f188e439a8c 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -179,7 +179,12 @@ class auth extends \auth_plugin_base { return false; } - private function is_ready_for_login_page($issuer) { + /** + * Do some checks on the identity provider before showing it on the login page. + * @param core\oauth2\issuer + * @return boolean + */ + private function is_ready_for_login_page(\core\oauth2\issuer $issuer) { return !empty($issuer->get('clientid')) && !empty($issuer->get('clientsecret')) && $issuer->is_authentication_supported() && @@ -188,6 +193,9 @@ class auth extends \auth_plugin_base { /** * Return a list of identity providers to display on the login page. + * + * @param string|moodle_url $wantsurl The requested URL. + * @return array (containing url, iconurl and name). */ public function loginpage_idp_list($wantsurl) { $providers = \core\oauth2\api::get_all_issuers(); diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php index d221f328364..ce34fa5aae2 100644 --- a/auth/oauth2/lang/en/auth_oauth2.php +++ b/auth/oauth2/lang/en/auth_oauth2.php @@ -24,6 +24,6 @@ $string['auth_oauth2description'] = 'OAuth 2 standards based authentication'; $string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; -$string['pluginname'] = 'OAuth 2'; -$string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; $string['notloggedin'] = 'The login attempt failed.'; +$string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; +$string['pluginname'] = 'OAuth 2'; diff --git a/auth/oauth2/login.php b/auth/oauth2/login.php index 835a39b9ba7..658c5ad134f 100644 --- a/auth/oauth2/login.php +++ b/auth/oauth2/login.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Open ID authentication. + * Open ID authentication. This file is a simple login entry point for OAuth identity providers. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 0302d8bded0..3b4915e1de6 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -41,6 +41,10 @@ defined('MOODLE_INTERNAL') || die(); */ class api { + /** + * Create a google ready OAuth 2 service. + * @return core\oauth2\issuer + */ private static function create_google() { $record = (object) [ 'name' => 'Google', @@ -63,6 +67,10 @@ class api { return $issuer; } + /** + * Create a facebook ready OAuth 2 service. + * @return core\oauth2\issuer + */ private static function create_facebook() { // Facebook is a custom setup. $record = (object) [ @@ -115,6 +123,10 @@ class api { return $issuer; } + /** + * Create a microsoft ready OAuth 2 service. + * @return core\oauth2\issuer + */ private static function create_microsoft() { // Microsoft is a custom setup. $record = (object) [ @@ -182,26 +194,62 @@ class api { } } + /** + * List all the issuers, ordered by the sortorder field + * @return core\oauth2\issuer[] + */ public static function get_all_issuers() { return issuer::get_records([], 'sortorder'); } + /** + * Get a single issuer by id. + * + * @param int $id + * @return core\oauth2\issuer + */ public static function get_issuer($id) { return new issuer($id); } + /** + * Get a single endpoint by id. + * + * @param int $id + * @return core\oauth2\endpoint + */ public static function get_endpoint($id) { return new endpoint($id); } + /** + * Get a single user field mapping by id. + * + * @param int $id + * @return core\oauth2\user_field_mapping + */ public static function get_user_field_mapping($id) { return new user_field_mapping($id); } + /** + * Get the system account for an installed OAuth service. + * Never ever ever expose this to a webservice because it contains the refresh token which grants API access. + * + * @param int $id + * @return core\oauth2\user_field_mapping + */ public static function get_system_account(issuer $issuer) { return system_account::get_record(['issuerid' => $issuer->get('id')]); } + /** + * Get the full list of system scopes required by an oauth issuer. + * This includes the list required for login as well as any scopes injected by the oauth2_system_scopes callback in plugins. + * + * @param core\oauth2\issuer $issuer + * @return string + */ public static function get_system_scopes_for_issuer($issuer) { $scopes = $issuer->get('loginscopesoffline'); @@ -230,6 +278,13 @@ class api { return $scopes; } + /** + * Get an authenticated oauth2 client using the system account. + * This call uses the refresh token to get an access token. + * + * @param core\oauth2\issuer $issuer + * @return core\oauth2\client + */ public static function get_system_oauth_client(issuer $issuer) { $systemaccount = self::get_system_account($issuer); if (empty($systemaccount)) { @@ -248,6 +303,15 @@ class api { return $client; } + /** + * Get an authenticated oauth2 client using the current user account. + * This call does the redirect dance back to the current page after authentication. + * + * @param core\oauth2\issuer $issuer The desired OAuth issuer + * @param moodle_url $url The url to the current page. + * @param string $additionalscopes The additional scopes required for authorization. + * @return core\oauth2\client + */ public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '') { $client = new \core\oauth2\client($issuer, $currenturl, $additionalscopes); @@ -257,14 +321,31 @@ class api { return $client; } + /** + * Get the list of defined endpoints for this OAuth issuer + * + * @param core\oauth2\issuer $issuer The desired OAuth issuer + * @return core\oauth2\endpoint[] + */ public static function get_endpoints(issuer $issuer) { return endpoint::get_records(['issuerid' => $issuer->get('id')]); } + /** + * Get the list of defined mapping from OAuth user fields to moodle user fields. + * + * @param core\oauth2\issuer $issuer The desired OAuth issuer + * @return core\oauth2\user_field_mapping[] + */ public static function get_user_field_mappings(issuer $issuer) { return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]); } + /** + * Guess an image from the discovery URL. + * + * @param core\oauth2\issuer $issuer The desired OAuth issuer + */ protected static function guess_image($issuer) { if (empty($issuer->get('image'))) { $baseurl = parse_url($issuer->get('discoveryurl')); @@ -362,6 +443,12 @@ class api { return endpoint::count_records(['issuerid' => $issuer->get('id')]); } + /** + * Take the data from the mform and update the issuer. + * + * @param stdClass $data + * @return core\oauth2\issuer + */ public static function update_issuer($data) { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer(0, $data); @@ -375,6 +462,12 @@ class api { return $issuer; } + /** + * Take the data from the mform and create the issuer. + * + * @param stdClass $data + * @return core\oauth2\issuer + */ public static function create_issuer($data) { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer(0, $data); @@ -388,6 +481,12 @@ class api { return $issuer; } + /** + * Take the data from the mform and update the endpoint. + * + * @param stdClass $data + * @return core\oauth2\endpoint + */ public static function update_endpoint($data) { require_capability('moodle/site:config', context_system::instance()); $endpoint = new endpoint(0, $data); @@ -398,6 +497,12 @@ class api { return $endpoint; } + /** + * Take the data from the mform and create the endpoint. + * + * @param stdClass $data + * @return core\oauth2\endpoint + */ public static function create_endpoint($data) { require_capability('moodle/site:config', context_system::instance()); $endpoint = new endpoint(0, $data); @@ -407,6 +512,12 @@ class api { return $endpoint; } + /** + * Take the data from the mform and update the user field mapping. + * + * @param stdClass $data + * @return core\oauth2\user_field_mapping + */ public static function update_user_field_mapping($data) { require_capability('moodle/site:config', context_system::instance()); $userfieldmapping = new user_field_mapping(0, $data); @@ -417,6 +528,12 @@ class api { return $userfieldmapping; } + /** + * Take the data from the mform and create the user field mapping. + * + * @param stdClass $data + * @return core\oauth2\user_field_mapping + */ public static function create_user_field_mapping($data) { require_capability('moodle/site:config', context_system::instance()); $userfieldmapping = new user_field_mapping(0, $data); @@ -459,6 +576,14 @@ class api { return $result; } + /** + * Reorder this identity issuer. + * + * Requires moodle/site:config capability at the system context. + * + * @param int $id The id of the identity issuer to move. + * @return boolean + */ public static function move_down_issuer($id) { require_capability('moodle/site:config', context_system::instance()); $current = new issuer($id); @@ -488,6 +613,14 @@ class api { return $result; } + /** + * Delete an identity issuer. + * + * Requires moodle/site:config capability at the system context. + * + * @param int $id The id of the identity issuer to delete. + * @return boolean + */ public static function delete_issuer($id) { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer($id); @@ -507,6 +640,14 @@ class api { return $issuer->delete(); } + /** + * Delete an endpoint. + * + * Requires moodle/site:config capability at the system context. + * + * @param int $id The id of the endpoint to delete. + * @return boolean + */ public static function delete_endpoint($id) { require_capability('moodle/site:config', context_system::instance()); $endpoint = new endpoint($id); @@ -515,6 +656,14 @@ class api { return $endpoint->delete(); } + /** + * Delete a user_field_mapping. + * + * Requires moodle/site:config capability at the system context. + * + * @param int $id The id of the user_field_mapping to delete. + * @return boolean + */ public static function delete_user_field_mapping($id) { require_capability('moodle/site:config', context_system::instance()); $userfieldmapping = new user_field_mapping($id); @@ -523,6 +672,15 @@ class api { return $userfieldmapping->delete(); } + /** + * Perform the OAuth dance and get a refresh token. + * + * Requires moodle/site:config capability at the system context. + * + * @param core\oauth2\issuer $issuer + * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication). + * @return boolean + */ public static function connect_system_account($issuer, $returnurl) { require_capability('moodle/site:config', context_system::instance()); diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index 3947b0c3dac..4595205c1bd 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -146,6 +146,11 @@ class client extends \oauth2_client { return $name; } + /** + * Get a list of the mapping user fields in an associative array. + * + * @return array + */ protected function get_userinfo_mapping() { $fields = user_field_mapping::get_records(['issuerid' => $this->issuer->get('id')]); @@ -215,6 +220,12 @@ class client extends \oauth2_client { return true; } + /** + * Fetch the user info from the user info endpoint and map all + * the fields back into moodle fields. + * + * @return array (Moodle user fields for the logged in user). + */ public function get_userinfo() { $url = $this->get_issuer()->get_endpoint_url('userinfo'); $response = $this->get($url); diff --git a/lib/classes/oauth2/endpoint.php b/lib/classes/oauth2/endpoint.php index 5916455a172..3a62e13e6d5 100644 --- a/lib/classes/oauth2/endpoint.php +++ b/lib/classes/oauth2/endpoint.php @@ -57,6 +57,13 @@ class endpoint extends persistent { ); } + /** + * Custom validator for end point URLs. + * Because we send Bearer tokens we must ensure SSL. + * + * @param $value The value to check. + * @return boolean + */ protected function validate_url($value) { if (strpos($value, 'https://') !== 0) { return new lang_string('sslonlyaccess', 'error'); diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 040d3e66ae8..5755c5062cd 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -96,6 +96,11 @@ class issuer extends persistent { ); } + /** + * Helper the get a named service endpoint. + * @param string $type + * @return string|false + */ public function get_endpoint_url($type) { $endpoint = endpoint::get_record([ 'issuerid' => $this->get('id'), @@ -108,14 +113,26 @@ class issuer extends persistent { return false; } + /** + * Does this OAuth service support user authentication? + * @return boolean + */ public function is_authentication_supported() { return (!empty($this->get_endpoint_url('userinfo'))); } + /** + * Does this OAuth service support system authentication? + * @return boolean + */ public function is_system_account_setup_supported() { return true; } + /** + * Do we have a refresh token for a system account? + * @return boolean + */ public function is_system_account_connected() { $sys = system_account::get_record(['issuerid' => $this->get('id')]); if (!empty($sys) and !empty($sys->get('refreshtoken'))) { diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php index 1ccf26b0e9e..0cf0178a5e2 100644 --- a/lib/classes/oauth2/user_field_mapping.php +++ b/lib/classes/oauth2/user_field_mapping.php @@ -72,6 +72,11 @@ class user_field_mapping extends persistent { ); } + /** + * Return the list of internal fields + * in a format they can be used for choices in a select menu + * @return array + */ public function get_internalfield_list() { return array_combine(self::$userfields, self::$userfields); } diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 5f54b5621ce..88193ff83b4 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1701,7 +1701,7 @@ class core_plugin_manager { 'auth' => array( 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'lti', 'manual', 'mnet', - 'nntp', 'nologin', 'none', 'pam', 'pop3', 'shibboleth', 'webservice' + 'nntp', 'nologin', 'none', 'oauth2', 'pam', 'pop3', 'shibboleth', 'webservice' ), 'availability' => array( @@ -1902,7 +1902,7 @@ class core_plugin_manager { 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang', 'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon', 'langimport', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound', 'mobile', 'multilangupgrade', 'monitor', - 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary', + 'oauth2', 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary', 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb' ), From 931c0234684f876be0ed1e6c617882589d6c3788 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Feb 2017 17:14:36 +0800 Subject: [PATCH 09/84] MDL-58090 oauth2: Add unit tests Part of MDL-58220 --- admin/tool/oauth2/issuers.php | 2 + lib/classes/oauth2/api.php | 6 +- lib/classes/oauth2/client.php | 13 +++- lib/classes/oauth2/issuer.php | 11 +++ lib/filelib.php | 17 +++++ lib/oauthlib.php | 9 ++- lib/tests/oauth2_test.php | 133 ++++++++++++++++++++++++++++++++++ 7 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 lib/tests/oauth2_test.php diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php index 631cca2856b..ac8872729ba 100644 --- a/admin/tool/oauth2/issuers.php +++ b/admin/tool/oauth2/issuers.php @@ -158,6 +158,7 @@ if ($mform && $mform->is_cancelled()) { } else { echo $OUTPUT->header(); $issuers = core\oauth2\api::get_all_issuers(); + var_dump(\core\oauth2\api::get_system_oauth_client($issuers[0], $PAGE->url)); echo $renderer->issuers_table($issuers); $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']); @@ -165,4 +166,5 @@ if ($mform && $mform->is_cancelled()) { $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edittemplate']); echo $renderer->single_button($addurl, get_string('createnewstandardissuer', 'tool_oauth2')); echo $OUTPUT->footer(); + } diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 3b4915e1de6..a5594e2e901 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -296,7 +296,7 @@ class api { $client = new \core\oauth2\client($issuer, null, $scopes, true); if (!$client->is_logged_in()) { - if (!$client->update_refresh_token($systemaccount)) { + if (!$client->upgrade_refresh_token($systemaccount)) { return false; } } @@ -685,11 +685,11 @@ class api { require_capability('moodle/site:config', context_system::instance()); // We need to authenticate with an oauth 2 client AS a system user and get a refresh token for offline access. - $scopesrequired = 'openid email profile'; + $scopes = self::get_system_scopes_for_issuer($issuer); // Allow callbacks to inject non-standard scopes to the auth request. - $client = new client($issuer, $returnurl, $scopesrequired, true); + $client = new client($issuer, $returnurl, $scopes, true); if (!optional_param('response', false, PARAM_BOOL)) { $client->log_out(); diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index 4595205c1bd..eb48669f438 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -50,9 +50,11 @@ class client extends \oauth2_client { * Constructor. * * @param issuer $issuer - * @param moodle_url $returnurl + * @param moodle_url|null $returnurl + * @param string $scopesrequired + * @param boolean $system */ - public function __construct(issuer $issuer, moodle_url $returnurl, $scopesrequired, $system = false) { + public function __construct(issuer $issuer, $returnurl, $scopesrequired, $system = false) { $this->issuer = $issuer; $this->system = $system; $scopes = $this->get_login_scopes(); @@ -65,6 +67,9 @@ class client extends \oauth2_client { } } } + if (empty($returnurl)) { + $returnurl = new moodle_url('/'); + } parent::__construct($issuer->get('clientid'), $issuer->get('clientsecret'), $returnurl, $scopes); } @@ -170,8 +175,8 @@ class client extends \oauth2_client { $refreshtoken = $systemaccount->get('refreshtoken'); $params = array('refresh_token' => $refreshtoken, - 'client_id' => $this->clientid, - 'client_secret' => $this->clientsecret, + 'client_id' => $this->issuer->get('clientid'), + 'client_secret' => $this->issuer->get('clientsecret'), 'grant_type' => 'refresh_token' ); diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 5755c5062cd..23708387731 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -96,6 +96,17 @@ class issuer extends persistent { ); } + /** + * Hook to execute before validate. + * + * @return void + */ + protected function before_validate() { + if (($this->get('id') && $this->get('sortorder') === null) || !$this->get('id')) { + $this->set('sortorder', $this->count_records()); + } + } + /** * Helper the get a named service endpoint. * @param string $type diff --git a/lib/filelib.php b/lib/filelib.php index e326406e13b..e3904767963 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -2741,6 +2741,8 @@ class curl { private $securityhelper; /** @var bool ignoresecurity a flag which can be supplied to the constructor, allowing security to be bypassed. */ private $ignoresecurity; + /** @var array $mockresponses For unit testing only - return the head of this list instead of making the next request. */ + private static $mockresponses = []; /** * Curl constructor. @@ -3265,6 +3267,14 @@ class curl { $this->responsefinished = false; } + public static function mock_response($response) { + if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) { + array_push(self::$mockresponses, $response); + } else { + throw new coding_excpetion('mock_response function is only available for unit tests.'); + } + } + /** * Single HTTP Request * @@ -3276,6 +3286,13 @@ class curl { // Reset here so that the data is valid when result returned from cache, or if we return due to a blacklist hit. $this->reset_request_state_vars(); + if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) { + if ($mockresponse = array_pop(self::$mockresponses)) { + $this->info = [ 'http_code' => 200 ]; + return $mockresponse; + } + } + // If curl security is enabled, check the URL against the blacklist before calling curl_exec. // Note: This will only check the base url. In the case of redirects, the blacklist is also after the curl_exec. if (!$this->ignoresecurity && $this->securityhelper->url_is_blocked($url)) { diff --git a/lib/oauthlib.php b/lib/oauthlib.php index 7260defeb96..19771cc2e54 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -390,11 +390,13 @@ abstract class oauth2_client extends curl { /** var moodle_url URL to return to after authenticating */ private $returnurl = null; /** var string scope of the authentication request */ - private $scope = ''; + protected $scope = ''; /** var stdClass access token object */ private $accesstoken = null; /** var stdClass refresh token string */ private $refreshtoken = ''; + /** var string mocknextresponse string */ + private $mocknextresponse = ''; /** * Returns the auth url for OAuth 2.0 request @@ -474,6 +476,11 @@ abstract class oauth2_client extends curl { return new moodle_url('/admin/oauth2callback.php'); } + /** + * An additional array of url params to pass with a login request. + * + * @return array of name value pairs. + */ public function get_additional_login_parameters() { return []; } diff --git a/lib/tests/oauth2_test.php b/lib/tests/oauth2_test.php new file mode 100644 index 00000000000..fc7097acd8b --- /dev/null +++ b/lib/tests/oauth2_test.php @@ -0,0 +1,133 @@ +. + +/** + * Tests for oauth2 apis (\core\oauth2\*). + * + * @package core + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Tests for myprofilelib apis. + * + * @package core + * @copyright 2015 onwards Ankit agarwal + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +class core_oauth2_testcase extends advanced_testcase { + + /** + * Tests the core_myprofile_navigation() function as an admin viewing a user's course profile. + */ + public function test_create_and_delete_standard_issuers() { + $this->resetAfterTest(); + $this->setAdminUser(); + \core\oauth2\api::create_standard_issuer('google'); + \core\oauth2\api::create_standard_issuer('facebook'); + \core\oauth2\api::create_standard_issuer('microsoft'); + + $issuers = \core\oauth2\api::get_all_issuers(); + + $this->assertEquals($issuers[0]->get('name'), 'Google'); + $this->assertEquals($issuers[1]->get('name'), 'Facebook'); + $this->assertEquals($issuers[2]->get('name'), 'Microsoft'); + + \core\oauth2\api::move_down_issuer($issuers[0]->get('id')); + + $issuers = \core\oauth2\api::get_all_issuers(); + + $this->assertEquals($issuers[0]->get('name'), 'Facebook'); + $this->assertEquals($issuers[1]->get('name'), 'Google'); + $this->assertEquals($issuers[2]->get('name'), 'Microsoft'); + + \core\oauth2\api::delete_issuer($issuers[1]->get('id')); + + $issuers = \core\oauth2\api::get_all_issuers(); + + $this->assertEquals($issuers[0]->get('name'), 'Facebook'); + $this->assertEquals($issuers[1]->get('name'), 'Microsoft'); + } + + /** + * Tests we can list and delete each of the persistents related to an issuer. + */ + public function test_getters() { + $this->resetAfterTest(); + $this->setAdminUser(); + $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); + + $same = \core\oauth2\api::get_issuer($issuer->get('id')); + $this->assertEquals($issuer, $same); + + $endpoints = \core\oauth2\api::get_endpoints($issuer); + $same = \core\oauth2\api::get_endpoint($endpoints[0]->get('id')); + $this->assertEquals($endpoints[0]->get('id'), $same->get('id')); + $this->assertEquals($endpoints[0]->get('name'), $same->get('name')); + + $todelete = $endpoints[0]; + \core\oauth2\api::delete_endpoint($todelete->get('id')); + $endpoints = \core\oauth2\api::get_endpoints($issuer); + $this->assertNotEquals($endpoints[0]->get('id'), $todelete->get('id')); + + $userfields = \core\oauth2\api::get_user_field_mappings($issuer); + $same = \core\oauth2\api::get_user_field_mapping($userfields[0]->get('id')); + $this->assertEquals($userfields[0]->get('id'), $same->get('id')); + + $todelete = $userfields[0]; + \core\oauth2\api::delete_user_field_mapping($todelete->get('id')); + $userfields = \core\oauth2\api::get_user_field_mappings($issuer); + $this->assertNotEquals($userfields[0]->get('id'), $todelete->get('id')); + } + + /** + * Tests we can get a logged in oauth client for a system account. + */ + public function test_get_system_oauth_client() { + global $SESSION; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); + + $requiredscopes = \core\oauth2\api::get_system_scopes_for_issuer($issuer); + // Fake a system account. + $data = (object) [ + 'issuerid' => $issuer->get('id'), + 'refreshtoken' => 'abc', + 'grantedscopes' => $requiredscopes + ]; + $sys = new \core\oauth2\system_account(0, $data); + $sys->create(); + + // Fake a response with an access token. + $response = json_encode( + (object) [ + 'access_token' => 'fdas...', + 'token_type' => 'Bearer', + 'expires_in' => '3600', + 'id_token' => 'llfsd..', + ] + ); + curl::mock_response($response); + $client = \core\oauth2\api::get_system_oauth_client($issuer); + $this->assertTrue($client->is_logged_in()); + } +} From 722a6d0881e2b55266f98efdd6d682420f819248 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Feb 2017 17:22:44 +0800 Subject: [PATCH 10/84] MDL-58090 oauth2: Remove wrong tests from branch Part of MDL-58220 --- .../oauth2/tests/behat/manage_tasks.feature | 53 ---- admin/tool/oauth2/tests/form_test.php | 275 ------------------ 2 files changed, 328 deletions(-) delete mode 100644 admin/tool/oauth2/tests/behat/manage_tasks.feature delete mode 100644 admin/tool/oauth2/tests/form_test.php diff --git a/admin/tool/oauth2/tests/behat/manage_tasks.feature b/admin/tool/oauth2/tests/behat/manage_tasks.feature deleted file mode 100644 index 73fc27dd61b..00000000000 --- a/admin/tool/oauth2/tests/behat/manage_tasks.feature +++ /dev/null @@ -1,53 +0,0 @@ -@tool @tool_task @javascript -Feature: Manage scheduled tasks - In order to configure scheduled tasks - As an admin - I need to be able to disable, enable, edit and reset to default scheduled tasks - - Background: - Given I log in as "admin" - And I navigate to "Scheduled tasks" node in "Site administration > Server" - - Scenario: Disable scheduled task - When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" - Then I should see "Edit task schedule: Log table cleanup" - And I set the following fields to these values: - | disabled | 1 | - And I press "Save changes" - Then I should see "Changes saved" - And I should see "Task disabled" in the "Log table cleanup" "table_row" - - Scenario: Enable scheduled task - When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" - Then I should see "Edit task schedule: Log table cleanup" - And I set the following fields to these values: - | disabled | 0 | - And I press "Save changes" - Then I should see "Changes saved" - And I should not see "Task disabled" in the "Log table cleanup" "table_row" - - Scenario: Edit scheduled task - When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" - Then I should see "Edit task schedule: Log table cleanup" - And I set the following fields to these values: - | minute | */5 | - | hour | 1 | - | day | 2 | - | month | 3 | - | dayofweek | 4 | - And I press "Save changes" - Then I should see "Changes saved" - And the following should exist in the "admintable" table: - | Component | Minute | Hour | Day | Day of week | Month | - | Standard log | */5 | 1 | 2 | 4 | 3 | - - Scenario: Reset scheduled task to default - When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" - Then I should see "Edit task schedule: Log table cleanup" - And I set the following fields to these values: - | resettodefaults | 1 | - And I press "Save changes" - Then I should see "Changes saved" - And the following should not exist in the "admintable" table: - | Name | Component | Minute | Hour | Day | Day of week | Month | - | Log table cleanup | Standard log | */5 | 1 | 2 | 4 | 3 | \ No newline at end of file diff --git a/admin/tool/oauth2/tests/form_test.php b/admin/tool/oauth2/tests/form_test.php deleted file mode 100644 index a9b8aa12bf9..00000000000 --- a/admin/tool/oauth2/tests/form_test.php +++ /dev/null @@ -1,275 +0,0 @@ -. - -/** - * File containing tests for the mform class. - * - * @package tool_task - * @copyright 2014 onwards Ankit Agarwal - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; - -/** - * Mform test class. - * - * @package tool_task - * @copyright 2014 onwards Ankit Agarwal - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late - */ -class tool_task_form_testcase extends advanced_testcase { - - /** - * Test validations for minute field. - */ - public function test_validate_fields_minute() { - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '65'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/65'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '1,2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '2,20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '20,30,45'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '65,20,30'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '25,75'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '1-2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '2-20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '20-30'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '65-20'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '25-75'); - $this->assertFalse($valid); - } - - /** - * Test validations for minute hour. - */ - public function test_validate_fields_hour() { - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '65'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/65'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '1,2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '2,20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '20,30,45'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '65,20,30'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '25,75'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '1-2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '2-20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '20-30'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '65-20'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '25-75'); - $this->assertFalse($valid); - } - - /** - * Test validations for day field. - */ - public function test_validate_fields_day() { - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '65'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '35'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/65'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/35'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '1,2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '2,20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '20,30,25'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '65,20,30'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '25,35'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '1-2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '2-20'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '20-30'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '65-20'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '25-35'); - $this->assertFalse($valid); - } - - /** - * Test validations for month field. - */ - public function test_validate_fields_month() { - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '10'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '13'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '35'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/12'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/13'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/35'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '1,2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '2,11'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '2,10,12'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '65,2,13'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '25,35'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '1-2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '2-12'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '3-6'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '65-2'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '25-26'); - $this->assertFalse($valid); - } - - /** - * Test validations for dayofweek field. - */ - public function test_validate_fields_dayofweek() { - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '0'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '6'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '7'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '20'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/1'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/6'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/13'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/35'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '1,2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '2,6'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '2,6,3'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '65,2,13'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '25,35'); - $this->assertFalse($valid); - - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '1-2'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '2-6'); - $this->assertTrue($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '65-2'); - $this->assertFalse($valid); - $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '3-7'); - $this->assertFalse($valid); - } -} - From 870a4a824ecaac80c3bc3f1eea133f0e32edef8c Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 1 Mar 2017 11:01:54 +0800 Subject: [PATCH 11/84] MDL-58090 oauth2: Improve help Provide a link to the help for setting up an OAuth 2 service. Part of MDL-58220 --- admin/tool/oauth2/issuers.php | 3 ++- admin/tool/oauth2/lang/en/tool_oauth2.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php index ac8872729ba..b8ada448e0a 100644 --- a/admin/tool/oauth2/issuers.php +++ b/admin/tool/oauth2/issuers.php @@ -157,8 +157,9 @@ if ($mform && $mform->is_cancelled()) { } else { echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('pluginname', 'tool_oauth2')); + echo $OUTPUT->doc_link('OAuth2_Services', get_string('serviceshelp', 'tool_oauth2')); $issuers = core\oauth2\api::get_all_issuers(); - var_dump(\core\oauth2\api::get_system_oauth_client($issuers[0], $PAGE->url)); echo $renderer->issuers_table($issuers); $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']); diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 5f47f8f3183..2ee10daa14c 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -96,3 +96,4 @@ $string['userfieldinternalfield'] = 'Internal field name'; $string['userfieldinternalfield_help'] = 'Name of the Moodle user field that should be mapped from the external field.'; $string['createfromtemplate'] = 'Create an OAuth 2 service from a template'; $string['createfromtemplatedesc'] = 'Choose one of the OAuth 2 service template below to create an OAuth service with a valid configuration for one of the known service types. This will create the OAuth 2 service, with all the correct end points and parameters required for authentication, but you will still need to enter the client ID and secret for the new service before it can be used.'; +$string['serviceshelp'] = 'Service provider setup instructions: (Google, Facebook, Microsoft).'; From 299112498b5e59d504b506268ca3369caf606384 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 2 Mar 2017 09:15:50 +0800 Subject: [PATCH 12/84] MDL-58090 oauth2: Coding style Part of MDL-58220 --- admin/tool/oauth2/classes/form/endpoint.php | 2 ++ admin/tool/oauth2/classes/form/issuer.php | 2 ++ .../classes/form/user_field_mapping.php | 2 ++ admin/tool/oauth2/classes/output/renderer.php | 11 ++++++---- admin/tool/oauth2/settings.php | 3 ++- admin/tool/oauth2/version.php | 6 +++--- auth/oauth2/auth.php | 3 +-- auth/oauth2/classes/auth.php | 6 ++++-- lib/classes/oauth2/api.php | 20 ++++++++++++------- lib/classes/oauth2/client.php | 1 + lib/classes/oauth2/endpoint.php | 5 +++-- lib/classes/oauth2/issuer.php | 1 + lib/classes/oauth2/system_account.php | 1 + lib/classes/oauth2/user_field_mapping.php | 2 ++ lib/filelib.php | 5 +++++ 15 files changed, 49 insertions(+), 21 deletions(-) diff --git a/admin/tool/oauth2/classes/form/endpoint.php b/admin/tool/oauth2/classes/form/endpoint.php index b4bd4be3cda..35aa835fcc8 100644 --- a/admin/tool/oauth2/classes/form/endpoint.php +++ b/admin/tool/oauth2/classes/form/endpoint.php @@ -37,8 +37,10 @@ use core\form\persistent; */ class endpoint extends persistent { + /** @var string $persistentclass */ protected static $persistentclass = 'core\\oauth2\\endpoint'; + /** @var array $fieldstoremove */ protected static $fieldstoremove = array('submitbutton', 'action'); /** diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index 728f21bfd21..80a44820af0 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -37,8 +37,10 @@ use core\form\persistent; */ class issuer extends persistent { + /** @var string $persistentclass */ protected static $persistentclass = 'core\\oauth2\\issuer'; + /** @var array $fieldstoremove */ protected static $fieldstoremove = array('submitbutton', 'action'); /** diff --git a/admin/tool/oauth2/classes/form/user_field_mapping.php b/admin/tool/oauth2/classes/form/user_field_mapping.php index e8fa760356e..fff419d9ab5 100644 --- a/admin/tool/oauth2/classes/form/user_field_mapping.php +++ b/admin/tool/oauth2/classes/form/user_field_mapping.php @@ -37,8 +37,10 @@ use core\form\persistent; */ class user_field_mapping extends persistent { + /** @var string $persistentclass */ protected static $persistentclass = 'core\\oauth2\\user_field_mapping'; + /** @var array $fieldstoremove */ protected static $fieldstoremove = array('submitbutton', 'action'); /** diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index d9fa4513cc5..a9568d8de68 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -142,7 +142,8 @@ class renderer extends plugin_renderer_base { $links .= ' ' . $editendpointlink; // User field mapping. - $edituserfieldmappingsurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', ['issuerid' => $issuer->get('id')]); + $params = ['issuerid' => $issuer->get('id')]; + $edituserfieldmappingsurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', $params); $str = get_string('edituserfieldmappings', 'tool_oauth2'); $edituserfieldmappinglink = html_writer::link($edituserfieldmappingsurl, $OUTPUT->pix_icon('t/user', $str)); $links .= ' ' . $edituserfieldmappinglink; @@ -188,6 +189,7 @@ class renderer extends plugin_renderer_base { * This function will render one beautiful table with all the endpoints. * * @param \core\oauth2\endpoint[] $endpoints - list of all endpoints. + * @param int $issuerid * @return string HTML to output. */ public function endpoints_table($endpoints, $issuerid) { @@ -210,7 +212,7 @@ class renderer extends plugin_renderer_base { $namecell = new html_table_cell(s($name)); $namecell->header = true; - // Url + // Url. $url = $endpoint->get('url'); $urlcell = new html_table_cell(s($url)); @@ -246,6 +248,7 @@ class renderer extends plugin_renderer_base { * This function will render one beautiful table with all the user_field_mappings. * * @param \core\oauth2\user_field_mapping[] $userfieldmappings - list of all user_field_mappings. + * @param int $issuerid * @return string HTML to output. */ public function user_field_mappings_table($userfieldmappings, $issuerid) { @@ -263,11 +266,11 @@ class renderer extends plugin_renderer_base { $index = 0; foreach ($userfieldmappings as $userfieldmapping) { - // External field + // External field. $externalfield = $userfieldmapping->get('externalfield'); $externalfieldcell = new html_table_cell(s($externalfield)); - // Internal field + // Internal field. $internalfield = $userfieldmapping->get('internalfield'); $internalfieldcell = new html_table_cell(s($internalfield)); diff --git a/admin/tool/oauth2/settings.php b/admin/tool/oauth2/settings.php index 950a02137d1..46d64c3a7a3 100644 --- a/admin/tool/oauth2/settings.php +++ b/admin/tool/oauth2/settings.php @@ -25,5 +25,6 @@ defined('MOODLE_INTERNAL') || die; if ($hassiteconfig) { - $ADMIN->add('server', new admin_externalpage('oauth2', new lang_string('pluginname','tool_oauth2'), "$CFG->wwwroot/$CFG->admin/tool/oauth2/issuers.php")); + $ADMIN->add('server', new admin_externalpage('oauth2', new lang_string('pluginname','tool_oauth2'), + "$CFG->wwwroot/$CFG->admin/tool/oauth2/issuers.php")); } diff --git a/admin/tool/oauth2/version.php b/admin/tool/oauth2/version.php index 8d18262c360..22ed6ee6d81 100644 --- a/admin/tool/oauth2/version.php +++ b/admin/tool/oauth2/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2016112900; // Requires this Moodle version -$plugin->component = 'tool_oauth2'; // Full name of the plugin (used for diagnostics) +$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2016112900; // Requires this Moodle version. +$plugin->component = 'tool_oauth2'; // Full name of the plugin (used for diagnostics). diff --git a/auth/oauth2/auth.php b/auth/oauth2/auth.php index 601dd4b2c4d..fbb98fd2414 100644 --- a/auth/oauth2/auth.php +++ b/auth/oauth2/auth.php @@ -27,8 +27,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** - * Plugin for oauth2 authentication. This is a way to use namespaces even though - * moodle expects a non-namespaced file here. + * Plugin for oauth2 authentication. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index f188e439a8c..22906567a9d 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -153,7 +153,7 @@ class auth extends \auth_plugin_base { * * @param stdClass $config * @param string $err - * @param array userfields + * @param array $userfields */ public function config_form($config, $err, $userfields) { echo get_string('plugindescription', 'auth_oauth2'); @@ -170,6 +170,7 @@ class auth extends \auth_plugin_base { /** * Return the userinfo from the oauth handshake. Will only be valid * for the logged in user. + * @param $string username */ public function get_userinfo($username) { $cached = $this->get_static_user_info(); @@ -181,7 +182,7 @@ class auth extends \auth_plugin_base { /** * Do some checks on the identity provider before showing it on the login page. - * @param core\oauth2\issuer + * @param core\oauth2\issuer $issuer * @return boolean */ private function is_ready_for_login_page(\core\oauth2\issuer $issuer) { @@ -248,6 +249,7 @@ class auth extends \auth_plugin_base { /** * If this user has no picture - but we got one from oauth - set it. + * @param stdClass $user * @return boolean True if the image was updated. */ private function update_picture($user) { diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index a5594e2e901..9aa49b5cb28 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -17,12 +17,14 @@ /** * Class for loading/storing oauth2 endpoints from the DB. * - * @package core_oauth2 + * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core\oauth2; +defined('MOODLE_INTERNAL') || die(); + require_once($CFG->libdir . '/filelib.php'); use context_system; @@ -31,7 +33,6 @@ use stdClass; use moodle_exception; use moodle_url; -defined('MOODLE_INTERNAL') || die(); /** * Static list of api methods for system oauth2 configuration. @@ -181,6 +182,11 @@ class api { return $issuer; } + /** + * Create one of the standard issuers. + * @param string $type One of google, facebook, microsoft + * @return \core\oauth2\issuer + */ public static function create_standard_issuer($type) { require_capability('moodle/site:config', context_system::instance()); if ($type == 'google') { @@ -236,8 +242,8 @@ class api { * Get the system account for an installed OAuth service. * Never ever ever expose this to a webservice because it contains the refresh token which grants API access. * - * @param int $id - * @return core\oauth2\user_field_mapping + * @param \core\oauth2\issuer $id + * @return \core\oauth2\client */ public static function get_system_account(issuer $issuer) { return system_account::get_record(['issuerid' => $issuer->get('id')]); @@ -247,7 +253,7 @@ class api { * Get the full list of system scopes required by an oauth issuer. * This includes the list required for login as well as any scopes injected by the oauth2_system_scopes callback in plugins. * - * @param core\oauth2\issuer $issuer + * @param \core\oauth2\issuer $issuer * @return string */ public static function get_system_scopes_for_issuer($issuer) { @@ -308,7 +314,7 @@ class api { * This call does the redirect dance back to the current page after authentication. * * @param core\oauth2\issuer $issuer The desired OAuth issuer - * @param moodle_url $url The url to the current page. + * @param moodle_url $currenturl The url to the current page. * @param string $additionalscopes The additional scopes required for authorization. * @return core\oauth2\client */ @@ -715,7 +721,7 @@ class api { $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->refreshtoken = $refreshtoken; - $record->grantedscopes = $scopesrequired; + $record->grantedscopes = $scopes; $systemaccount = new system_account(0, $record); diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index eb48669f438..e216e92c316 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -169,6 +169,7 @@ class client extends \oauth2_client { /** * Upgrade a refresh token from oauth 2.0 to an access token * + * @param \core\oauth2\system_account $systemaccount * @return boolean true if token is upgraded succesfully */ public function upgrade_refresh_token(system_account $systemaccount) { diff --git a/lib/classes/oauth2/endpoint.php b/lib/classes/oauth2/endpoint.php index 3a62e13e6d5..9832bc0ef9a 100644 --- a/lib/classes/oauth2/endpoint.php +++ b/lib/classes/oauth2/endpoint.php @@ -36,6 +36,7 @@ use lang_string; */ class endpoint extends persistent { + /** @const TABLE */ const TABLE = 'oauth2_endpoint'; /** @@ -61,8 +62,8 @@ class endpoint extends persistent { * Custom validator for end point URLs. * Because we send Bearer tokens we must ensure SSL. * - * @param $value The value to check. - * @return boolean + * @param string $value The value to check. + * @return lang_string|boolean */ protected function validate_url($value) { if (strpos($value, 'https://') !== 0) { diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 23708387731..1ff6cef8384 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -35,6 +35,7 @@ use core\persistent; */ class issuer extends persistent { + /** @const TABLE */ const TABLE = 'oauth2_issuer'; /** diff --git a/lib/classes/oauth2/system_account.php b/lib/classes/oauth2/system_account.php index ee682a7978f..2f770fc0315 100644 --- a/lib/classes/oauth2/system_account.php +++ b/lib/classes/oauth2/system_account.php @@ -37,6 +37,7 @@ use core\persistent; */ class system_account extends persistent { + /** @const TABLE */ const TABLE = 'oauth2_system_account'; /** diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php index 0cf0178a5e2..a67a089247e 100644 --- a/lib/classes/oauth2/user_field_mapping.php +++ b/lib/classes/oauth2/user_field_mapping.php @@ -35,8 +35,10 @@ use core\persistent; */ class user_field_mapping extends persistent { + /** @const TABLE */ const TABLE = 'oauth2_user_field_mapping'; + /** @var array $userfields - List of standard Moodle userfields. */ private static $userfields = [ 'firstname', 'middlename', diff --git a/lib/filelib.php b/lib/filelib.php index e3904767963..57782bf6782 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -3267,6 +3267,11 @@ class curl { $this->responsefinished = false; } + /** + * For use only in unit tests - we can pre-set the next curl response. + * This is useful for unit testing APIs that call external systems. + * @param string $response + */ public static function mock_response($response) { if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) { array_push(self::$mockresponses, $response); From 0e59638bdb5bc8e389ec66bfa86fb4d0300f9477 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 2 Mar 2017 16:45:43 +0800 Subject: [PATCH 13/84] MDL-58126 googledocs: Re-implement the google docs repo Use latest REST API (v3) - but avoid the google client libraries (too big, too much to update, not integrated with auth). Part of MDL-58220 --- admin/tool/oauth2/settings.php | 2 +- auth/oauth2/auth.php | 2 +- auth/oauth2/login.php | 4 + lib/classes/oauth2/api.php | 3 - lib/classes/oauth2/rest.php | 105 +++++++ lib/classes/oauth2/rest_exception.php | 40 +++ repository/googledocs/classes/rest.php | 60 ++++ .../lang/en/repository_googledocs.php | 14 +- repository/googledocs/lib.php | 280 ++++++++---------- repository/googledocs/tests/generator/lib.php | 29 +- 10 files changed, 372 insertions(+), 167 deletions(-) create mode 100644 lib/classes/oauth2/rest.php create mode 100644 lib/classes/oauth2/rest_exception.php create mode 100644 repository/googledocs/classes/rest.php diff --git a/admin/tool/oauth2/settings.php b/admin/tool/oauth2/settings.php index 46d64c3a7a3..947f79cdf88 100644 --- a/admin/tool/oauth2/settings.php +++ b/admin/tool/oauth2/settings.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die; if ($hassiteconfig) { - $ADMIN->add('server', new admin_externalpage('oauth2', new lang_string('pluginname','tool_oauth2'), + $ADMIN->add('server', new admin_externalpage('oauth2', new lang_string('pluginname', 'tool_oauth2'), "$CFG->wwwroot/$CFG->admin/tool/oauth2/issuers.php")); } diff --git a/auth/oauth2/auth.php b/auth/oauth2/auth.php index fbb98fd2414..0d5ecd7f79a 100644 --- a/auth/oauth2/auth.php +++ b/auth/oauth2/auth.php @@ -27,7 +27,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** - * Plugin for oauth2 authentication. + * Plugin for oauth2 authentication. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese diff --git a/auth/oauth2/login.php b/auth/oauth2/login.php index 658c5ad134f..eba8b62b3d2 100644 --- a/auth/oauth2/login.php +++ b/auth/oauth2/login.php @@ -37,6 +37,10 @@ $returnurl = new moodle_url('/auth/oauth2/login.php', $returnparams); $client = \core\oauth2\api::get_user_oauth_client($issuer, $returnurl); if ($client) { + if (!$client->is_logged_in()) { + redirect($client->get_login_url()); + } + $auth = new \auth_oauth2\auth(); $auth->complete_login($client, $wantsurl); } else { diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 9aa49b5cb28..51fd8fca2c4 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -321,9 +321,6 @@ class api { public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '') { $client = new \core\oauth2\client($issuer, $currenturl, $additionalscopes); - if (!$client->is_logged_in()) { - redirect($client->get_login_url()); - } return $client; } diff --git a/lib/classes/oauth2/rest.php b/lib/classes/oauth2/rest.php new file mode 100644 index 00000000000..03afc9fb4f4 --- /dev/null +++ b/lib/classes/oauth2/rest.php @@ -0,0 +1,105 @@ +. + +/** + * Rest API base class mapping rest api methods to endpoints with http methods, args and post body. + * + * @package core + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +use curl; +use coding_exception; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/filelib.php'); + +/** + * Rest API base class mapping rest api methods to endpoints with http methods, args and post body. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class rest { + + /** @var curl $curl */ + private $curl; + + /** + * Constructor. + * + * @param curl $curl + */ + public function __construct(curl $curl) { + $this->curl = $curl; + } + + /** + * Abstract function to define the functions of the rest API. + * + * @return array Example: + * [ 'listFiles' => [ 'method' => 'get', 'args' => [ 'folder' => PARAM_STRING ], 'response' => 'json' ] ] + */ + public abstract function get_api_functions(); + + /** + * Call a function from the Api with a set of arguments and optional data. + * + * @param string $functionname + * @param array $functionargs + */ + public function call($functionname, $functionargs) { + $functions = $this->get_api_functions(); + $supportedmethods = [ 'get', 'put', 'post', 'patch', 'head', 'delete' ]; + if (empty($functions[$functionname])) { + throw new coding_exception('unsupported api functionname: ' . $functionname); + } + + $method = $functions[$functionname]['method']; + $endpoint = $functions[$functionname]['endpoint']; + $responsetype = $functions[$functionname]['response']; + if (!in_array($method, $supportedmethods)) { + throw new coding_exception('unsupported api method: ' . $method); + } + + $args = $functions[$functionname]['args']; + $callargs = []; + foreach ($args as $argname => $argtype) { + if (isset($functionargs[$argname])) { + $callargs[$argname] = clean_param($functionargs[$argname], $argtype); + } + } + + $response = $this->curl->$method($endpoint, $callargs); + + if ($this->curl->errno == 0) { + if ($responsetype == 'json') { + $json = json_decode($response); + + if (!empty($json->error)) { + throw new rest_exception($json->error->message, $json->error->code); + } + return $json; + } + return $response; + } else { + throw new rest_exception($this->curl->error, $this->curl->errno); + } + } +} diff --git a/lib/classes/oauth2/rest_exception.php b/lib/classes/oauth2/rest_exception.php new file mode 100644 index 00000000000..41994745a77 --- /dev/null +++ b/lib/classes/oauth2/rest_exception.php @@ -0,0 +1,40 @@ +. + +/** + * Rest Exception class containing error code and message. + * + * @package core + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +use Exception; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/filelib.php'); + +/** + * Rest Exception class containing error code and message. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class rest_exception extends Exception { + +} diff --git a/repository/googledocs/classes/rest.php b/repository/googledocs/classes/rest.php new file mode 100644 index 00000000000..f66aa8bd884 --- /dev/null +++ b/repository/googledocs/classes/rest.php @@ -0,0 +1,60 @@ +. + +/** + * Google Drive Rest API. + * + * @package repository_googledocs + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace repository_googledocs; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Google Drive Rest API. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class rest extends \core\oauth2\rest { + + /** + * Define the functions of the rest API. + * + * @return array Example: + * [ 'listFiles' => [ 'method' => 'get', 'endpoint' => 'http://...', 'args' => [ 'folder' => PARAM_STRING ] ] ] + */ + public function get_api_functions() { + return [ + 'list' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files', + 'method' => 'get', + 'args' => [ + 'corpus' => PARAM_RAW, + 'orderBy' => PARAM_RAW, + 'fields' => PARAM_RAW, + 'pageSize' => PARAM_INT, + 'pageToken' => PARAM_RAW, + 'q' => PARAM_RAW, + 'spaces' => PARAM_RAW + ], + 'response' => 'json' + ] + ]; + } +} diff --git a/repository/googledocs/lang/en/repository_googledocs.php b/repository/googledocs/lang/en/repository_googledocs.php index ca4d255ca5f..5654925eb09 100644 --- a/repository/googledocs/lang/en/repository_googledocs.php +++ b/repository/googledocs/lang/en/repository_googledocs.php @@ -22,15 +22,21 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['clientid'] = 'Client ID'; $string['configplugin'] = 'Configure Google Drive plugin'; $string['docsformat'] = 'Default document import format'; $string['drawingformat'] = 'Default drawing import format'; $string['googledocs:view'] = 'View Google Drive repository'; $string['importformat'] = 'Configure the default import formats from google'; -$string['oauthinfo'] = '

To use this plugin, you must register your site with Google, as described in the documentation Google OAuth 2.0 setup.

As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':

{$a->callbackurl}

Once registered, you will be provided with a client ID and secret which can be used to configure all Google Drive and Picasa plugins.

Please also note that you will have to enable the service \'Drive API\'.

'; $string['pluginname'] = 'Google Drive'; $string['presentationformat'] = 'Default presentation import format'; -$string['secret'] = 'Secret'; -$string['servicenotenabled'] = 'Access not configured. Make sure the service \'Drive API\' is enabled.'; $string['spreadsheetformat'] = 'Default spreadsheet import format'; +$string['issuer'] = 'OAuth 2 service'; +$string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the Google Drive API. If the services does not exist yet, you might need to create it.'; +$string['servicenotenabled'] = 'Access not configured. Make sure the service \'Drive API\' is enabled.'; +$string['oauth2serviceslink'] = 'OAuth 2 Services Configuration'; +$string['searchfor'] = 'Search for {$a}'; + +// Deprecated since Moodle 3.3. +$string['oauthinfo'] = '

To use this plugin, you must register your site with Google, as described in the documentation Google OAuth 2.0 setup.

As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':

{$a->callbackurl}

Once registered, you will be provided with a client ID and secret which can be used to configure all Google Drive and Picasa plugins.

Please also note that you will have to enable the service \'Drive API\'.

'; +$string['secret'] = 'Secret'; +$string['clientid'] = 'Client ID'; diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 82fdfe45e4f..852ee5d1bae 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -39,28 +39,21 @@ require_once($CFG->libdir . '/google/lib.php'); class repository_googledocs extends repository { /** - * Google Client. - * @var Google_Client + * OAuth 2 client + * @var \core\oauth2\client */ private $client = null; /** - * Google Drive Service. - * @var Google_Drive_Service + * OAuth 2 Issuer + * @var \core\oauth2\issuer */ - private $service = null; + private $issuer = null; /** - * Session key to store the accesstoken. - * @var string + * Additional scopes required for drive. */ - const SESSIONKEY = 'googledrive_accesstoken'; - - /** - * URI to the callback file for OAuth. - * @var string - */ - const CALLBACKURL = '/admin/oauth2callback.php'; + const SCOPES = 'https://www.googleapis.com/auth/drive'; /** * Constructor. @@ -74,52 +67,26 @@ class repository_googledocs extends repository { public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) { parent::__construct($repositoryid, $context, $options, $readonly = 0); - $callbackurl = new moodle_url(self::CALLBACKURL); - - $this->client = get_google_client(); - $this->client->setClientId(get_config('googledocs', 'clientid')); - $this->client->setClientSecret(get_config('googledocs', 'secret')); - $this->client->setScopes(array(Google_Service_Drive::DRIVE_READONLY)); - $this->client->setRedirectUri($callbackurl->out(false)); - $this->service = new Google_Service_Drive($this->client); - - $this->check_login(); + $this->issuer = \core\oauth2\api::get_issuer(get_config('googledocs', 'issuerid')); } /** - * Returns the access token if any. + * Get a cached user authenticated oauth client. * - * @return string|null access token. + * @return \core\oauth2\client */ - protected function get_access_token() { - global $SESSION; - if (isset($SESSION->{self::SESSIONKEY})) { - return $SESSION->{self::SESSIONKEY}; + protected function get_user_oauth_client() { + if ($this->client) { + return $this->client; } - return null; - } + $returnurl = new moodle_url('/repository/repository_callback.php'); + $returnurl->param('callback', 'yes'); + $returnurl->param('repo_id', $this->id); + $returnurl->param('sesskey', sesskey()); - /** - * Store the access token in the session. - * - * @param string $token token to store. - * @return void - */ - protected function store_access_token($token) { - global $SESSION; - $SESSION->{self::SESSIONKEY} = $token; - } + $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES); - /** - * Callback method during authentication. - * - * @return void - */ - public function callback() { - if ($code = optional_param('oauth2code', null, PARAM_RAW)) { - $this->client->authenticate($code); - $this->store_access_token($this->client->getAccessToken()); - } + return $this->client; } /** @@ -128,11 +95,8 @@ class repository_googledocs extends repository { * @return bool true when logged in. */ public function check_login() { - if ($token = $this->get_access_token()) { - $this->client->setAccessToken($token); - return true; - } - return false; + $client = $this->get_user_oauth_client(); + return $client->is_logged_in(); } /** @@ -141,13 +105,9 @@ class repository_googledocs extends repository { * @return void|array for ajax. */ public function print_login() { - $returnurl = new moodle_url('/repository/repository_callback.php'); - $returnurl->param('callback', 'yes'); - $returnurl->param('repo_id', $this->id); - $returnurl->param('sesskey', sesskey()); + $client = $this->get_user_oauth_client(); + $url = $client->get_login_url(); - $url = new moodle_url($this->client->createAuthUrl()); - $url->param('state', $returnurl->out_as_local_url(false)); if ($this->options['ajax']) { $popup = new stdClass(); $popup->type = 'popup'; @@ -159,11 +119,11 @@ class repository_googledocs extends repository { } /** - * Build the breadcrumb from a path. - * - * @param string $path to create a breadcrumb from. - * @return array containing name and path of each crumb. - */ + * Build the breadcrumb from a path. + * + * @param string $path to create a breadcrumb from. + * @return array containing name and path of each crumb. + */ protected function build_breadcrumb($path) { $bread = explode('/', $path); $crumbtrail = ''; @@ -181,15 +141,15 @@ class repository_googledocs extends repository { } /** - * Generates a safe path to a node. - * - * Typically, a node will be id|Name of the node. - * - * @param string $id of the node. - * @param string $name of the node, will be URL encoded. - * @param string $root to append the node on, must be a result of this function. - * @return string path to the node. - */ + * Generates a safe path to a node. + * + * Typically, a node will be id|Name of the node. + * + * @param string $id of the node. + * @param string $name of the node, will be URL encoded. + * @param string $root to append the node on, must be a result of this function. + * @return string path to the node. + */ protected function build_node_path($id, $name = '', $root = '') { $path = $id; if (!empty($name)) { @@ -202,12 +162,12 @@ class repository_googledocs extends repository { } /** - * Returns information about a node in a path. - * - * @see self::build_node_path() - * @param string $node to extrat information from. - * @return array about the node. - */ + * Returns information about a node in a path. + * + * @see self::build_node_path() + * @param string $node to extrat information from. + * @return array about the node. + */ protected function explode_node_path($node) { if (strpos($node, '|') !== false) { list($id, $name) = explode('|', $node, 2); @@ -265,16 +225,17 @@ class repository_googledocs extends repository { /** * Search throughout the Google Drive. * - * @param string $search_text text to search for. + * @param string $searchtext text to search for. * @param int $page search page. * @return array of results. */ - public function search($search_text, $page = 0) { + public function search($searchtext, $page = 0) { $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs')); - $path = $this->build_node_path('search', $search_text, $path); + $str = get_string('searchfor', 'repository_googledocs', $searchtext); + $path = $this->build_node_path('search', $str, $path); // Query the Drive. - $q = "fullText contains '" . str_replace("'", "\'", $search_text) . "'"; + $q = "fullText contains '" . str_replace("'", "\'", $searchtext) . "'"; $q .= ' AND trashed = false'; $results = $this->query($q, $path); @@ -304,14 +265,17 @@ class repository_googledocs extends repository { $files = array(); $folders = array(); - $fields = "items(id,title,mimeType,downloadUrl,fileExtension,exportLinks,modifiedDate,fileSize,thumbnailLink)"; - $params = array('q' => $q, 'fields' => $fields); $config = get_config('googledocs'); + $fields = "files(id,name,mimeType,webContentLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)"; + $params = array('q' => $q, 'fields' => $fields, 'spaces' => 'drive'); try { // Retrieving files and folders. - $response = $this->service->files->listFiles($params); - } catch (Google_Service_Exception $e) { + $client = $this->get_user_oauth_client(); + $service = new repository_googledocs\rest($client); + + $response = $service->call('list', $params); + } catch (Exception $e) { if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) { // This is raised when the service Drive API has not been enabled on Google APIs control panel. throw new repository_exception('servicenotenabled', 'repository_googledocs'); @@ -320,14 +284,15 @@ class repository_googledocs extends repository { } } - $items = isset($response['items']) ? $response['items'] : array(); - foreach ($items as $item) { - if ($item['mimeType'] == 'application/vnd.google-apps.folder') { + $base = 'https://www.googleapis.com/drive/v3'; + $gfiles = isset($response->files) ? $response->files : array(); + foreach ($gfiles as $gfile) { + if ($gfile->mimeType == 'application/vnd.google-apps.folder') { // This is a folder. - $folders[$item['title'] . $item['id']] = array( - 'title' => $item['title'], - 'path' => $this->build_node_path($item['id'], $item['title'], $path), - 'date' => strtotime($item['modifiedDate']), + $folders[$gfile->name . $gfile->id] = array( + 'title' => $gfile->name, + 'path' => $this->build_node_path($gfile->id, $gfile->name, $path), + 'date' => strtotime($gfile->modifiedTime), 'thumbnail' => $OUTPUT->image_url(file_folder_icon(64))->out(false), 'thumbnail_height' => 64, 'thumbnail_width' => 64, @@ -335,16 +300,18 @@ class repository_googledocs extends repository { ); } else { // This is a file. - if (isset($item['fileExtension'])) { - // The file has an extension, therefore there is a download link. - $title = $item['title']; - $source = $item['downloadUrl']; + if (isset($gfile->fileExtension)) { + // The file has an extension, therefore we can download it. + $title = $gfile->name; + $params = ['alt' => 'media']; + $sourceurl = new moodle_url($base . '/files/' . $gfile->id, $params); + $source = $sourceurl->out(false); } else { // The file is probably a Google Doc file, we get the corresponding export link. // This should be improved by allowing the user to select the type of export they'd like. - $type = str_replace('application/vnd.google-apps.', '', $item['mimeType']); + $type = str_replace('application/vnd.google-apps.', '', $gfile->mimeType); $title = ''; - $exportType = ''; + $exporttype = ''; $types = get_mimetypes_array(); switch ($type){ @@ -355,54 +322,52 @@ class repository_googledocs extends repository { // Moodle user 'text/rtf' as the MIME type for RTF files. // Google uses 'application/rtf' for the same type of file. // See https://developers.google.com/drive/v3/web/manage-downloads. - $exportType = 'application/rtf'; + $exporttype = 'application/rtf'; } else { - $exportType = $types[$ext]['type']; + $exporttype = $types[$ext]['type']; } break; case 'presentation': $ext = $config->presentationformat; $title = $item['title'] . '.'. $ext; - $exportType = $types[$ext]['type']; + $exporttype = $types[$ext]['type']; break; case 'spreadsheet': $ext = $config->spreadsheetformat; $title = $item['title'] . '.'. $ext; - $exportType = $types[$ext]['type']; + $exporttype = $types[$ext]['type']; break; case 'drawing': $ext = $config->drawingformat; $title = $item['title'] . '.'. $ext; - $exportType = $types[$ext]['type']; + $exporttype = $types[$ext]['type']; break; } // Skips invalid/unknown types. - if (empty($title) || !isset($item['exportLinks'][$exportType])) { + if (empty($title)) { continue; } - $source = $item['exportLinks'][$exportType]; + $params = ['mimeType' => $exporttype]; + $sourceurl = new moodle_url($base . '/files/' . $gfile->id . '/export', $params); + $source = $sourceurl->out(false); } - // Adds the file to the file list. Using the itemId along with the title as key + // Adds the file to the file list. Using the itemId along with the name as key // of the array because Google Drive allows files with identical names. - $files[$title . $item['id']] = array( + $thumb = ''; + if (isset($gfile->thumbnailLink)) { + $thumb = $gfile->thumbnailLink; + } else if (isset($gfile->iconLink)) { + $thumb = $gfile->iconLink; + } + $files[$title . $gfile->id] = array( 'title' => $title, 'source' => $source, - 'date' => strtotime($item['modifiedDate']), - 'size' => isset($item['fileSize']) ? $item['fileSize'] : null, - 'thumbnail' => $OUTPUT->image_url(file_extension_icon($title, 64))->out(false), + 'date' => strtotime($gfile->modifiedTime), + 'size' => isset($gfile->size) ? $gfile->size : null, + 'thumbnail' => $thumb, 'thumbnail_height' => 64, 'thumbnail_width' => 64, - // Do not use real thumbnails as they wouldn't work if the user disabled 3rd party - // plugins in his browser, or if they're not logged in their Google account. ); - - // Sometimes the real thumbnails can't be displayed, for example if 3rd party cookies are disabled - // or if the user is not logged in Google anymore. But this restriction does not seem to be applied - // to a small subset of files. - $extension = strtolower(pathinfo($title, PATHINFO_EXTENSION)); - if (isset($item['thumbnailLink']) && in_array($extension, array('jpg', 'png', 'txt', 'pdf'))) { - $files[$title . $item['id']]['realthumbnail'] = $item['thumbnailLink']; - } } } @@ -419,7 +384,8 @@ class repository_googledocs extends repository { * @return string */ public function logout() { - $this->store_access_token(null); + $client = $this->get_user_oauth_client(); + $client->log_out(); return parent::logout(); } @@ -433,18 +399,18 @@ class repository_googledocs extends repository { public function get_file($reference, $filename = '') { global $CFG; - $auth = $this->client->getAuth(); - $request = $auth->authenticatedRequest(new Google_Http_Request($reference)); - if ($request->getResponseHttpCode() == 200) { - $path = $this->prepare_file($filename); - $content = $request->getResponseBody(); - if (file_put_contents($path, $content) !== false) { - @chmod($path, $CFG->filepermissions); - return array( - 'path' => $path, - 'url' => $reference - ); - } + $client = $this->get_user_oauth_client(); + + $path = $this->prepare_file($filename); + $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5]; + $result = $client->download_one($reference, null, $options); + + if ($result) { + @chmod($path, $CFG->filepermissions); + return array( + 'path' => $path, + 'url' => $reference + ); } throw new repository_exception('cannotdownload', 'repository'); } @@ -490,11 +456,20 @@ class repository_googledocs extends repository { * @return array */ public static function get_type_option_names() { - return array('clientid', 'secret', 'pluginname', + return array('issuerid', 'pluginname', 'documentformat', 'drawingformat', 'presentationformat', 'spreadsheetformat'); } + /** + * Store the access token. + */ + public function callback() { + $client = $this->get_user_oauth_client(); + // This will upgrade to an access token if we have an authorization code. + $client->is_logged_in(); + } + /** * Edit/Create Admin Settings Moodle form. * @@ -502,25 +477,24 @@ class repository_googledocs extends repository { * @param string $classname repository class name. */ public static function type_config_form($mform, $classname = 'repository') { - $callbackurl = new moodle_url(self::CALLBACKURL); + $url = (string)new moodle_url('/admin/tool/oauth2/issuers.php'); - $a = new stdClass; - $a->docsurl = get_docs_url('Google_OAuth_2.0_setup'); - $a->callbackurl = $callbackurl->out(false); - - $mform->addElement('static', null, '', get_string('oauthinfo', 'repository_googledocs', $a)); + $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_googledocs', $url)); parent::type_config_form($mform); - $mform->addElement('text', 'clientid', get_string('clientid', 'repository_googledocs')); - $mform->setType('clientid', PARAM_RAW_TRIMMED); - $mform->addElement('text', 'secret', get_string('secret', 'repository_googledocs')); - $mform->setType('secret', PARAM_RAW_TRIMMED); + $options = []; + $issuers = \core\oauth2\api::get_all_issuers(); + + foreach ($issuers as $issuer) { + $options[$issuer->get('id')] = s($issuer->get('name')); + } + $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_googledocs'), $options); + $mform->addHelpButton('issuerid', 'issuer', 'repository_googledocs'); + $mform->addRule('issuerid', $strrequired, 'required', null, 'client'); $strrequired = get_string('required'); - $mform->addRule('clientid', $strrequired, 'required', null, 'client'); - $mform->addRule('secret', $strrequired, 'required', null, 'client'); - $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs', $a)); + $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs')); // Documents. $docsformat = array(); diff --git a/repository/googledocs/tests/generator/lib.php b/repository/googledocs/tests/generator/lib.php index 168c406a795..0fd3e7d3da8 100644 --- a/repository/googledocs/tests/generator/lib.php +++ b/repository/googledocs/tests/generator/lib.php @@ -23,6 +23,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use \core\oauth2\issuer; +use \core\oauth2\endpoint; + /** * Google Docs repository data generator class * @@ -41,11 +44,27 @@ class repository_googledocs_generator extends testing_repository_generator { */ protected function prepare_type_record(array $record) { $record = parent::prepare_type_record($record); - if (!isset($record['clientid'])) { - $record['clientid'] = 'clientid'; - } - if (!isset($record['secret'])) { - $record['secret'] = 'secret'; + $issuerrecord = (object) [ + 'name' => 'Google', + 'image' => 'https://accounts.google.com/favicon.ico', + 'baseurl' => 'http://accounts.google.com/', + 'loginparamsoffline' => 'access_type=offline&prompt=consent', + 'showonloginpage' => true + ]; + + $issuer = new issuer(0, $issuerrecord); + $issuer->create(); + + $endpointrecord = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => 'discovery_endpoint', + 'url' => 'https://accounts.google.com/.well-known/openid-configuration' + ]; + $endpoint = new endpoint(0, $endpointrecord); + $endpoint->create(); + + if (!isset($record['issuerid'])) { + $record['issuerid'] = $issuer->get('id'); } if (!isset($record['documentformat'])) { $record['documentformat'] = 'pdf'; From d247a63dfa49661c00499be26b352009ef3d4ef0 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Sun, 5 Mar 2017 14:28:33 +0800 Subject: [PATCH 14/84] MDL-58128 oauth2: Add a scheduled task for refresh We need to make sure our refresh tokens do not expire. We run a scheduled task to update the refresh token once per hour. Part of MDL-58220 --- lang/en/admin.php | 3 + .../oauth2/refresh_system_tokens_task.php | 88 +++++++++++++++++++ lib/db/tasks.php | 9 ++ 3 files changed, 100 insertions(+) create mode 100644 lib/classes/oauth2/refresh_system_tokens_task.php diff --git a/lang/en/admin.php b/lang/en/admin.php index 3b5f902b6ec..9559b7003d1 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -795,6 +795,8 @@ $string['notifyloginthreshold'] = 'Threshold for email notifications'; $string['notloggedinroleid'] = 'Role for visitors'; $string['numberofmissingstrings'] = 'Number of missing strings: {$a}'; $string['numberofstrings'] = 'Total number of strings: {$a->strings}
Missing: {$a->missing} ({$a->missingpercent} %)'; +$string['oauthrefreshtokenexpired'] = 'The refresh token for one of the OAuth services {$a->issuer} on your site {$a->siteurl} has expired. This will limit the functionality of any plugins that use this service. To fix this issue, visit the OAuth 2 Services configuration page and click on the "Connect system account" icon in the table row for this service. Be sure to login using the same service account for the OAuth system each time.'; +$string['oauthrefreshtokenexpiredshort'] = 'OAuth refresh token expired for {$a->issuer} on your site {$a->siteurl}.'; $string['onlynoreply'] = 'Only when from a no-reply address'; $string['opcacherecommended'] = 'PHP opcode caching improves performance and lowers memory requirements, OPcache extension is recommended and fully supported.'; $string['opensslrecommended'] = 'Installing the optional OpenSSL library is highly recommended -- it enables Moodle Networking functionality.'; @@ -1094,6 +1096,7 @@ $string['taskpasswordresetcleanup'] = 'Cleanup password reset attempts'; $string['taskplagiarismcron'] = 'Background processing for legacy cron in plagiarism plugins'; $string['taskportfoliocron'] = 'Background processing for portfolio plugins'; $string['taskquestioncron'] = 'Background processing for question engine'; +$string['taskrefreshoauthtokens'] = 'Refresh OAuth tokens for service accounts'; $string['taskregistrationcron'] = 'Site registration'; $string['tasksendfailedloginnotifications'] = 'Send failed login notifications'; $string['tasksendnewuserpasswords'] = 'Send new user passwords'; diff --git a/lib/classes/oauth2/refresh_system_tokens_task.php b/lib/classes/oauth2/refresh_system_tokens_task.php new file mode 100644 index 00000000000..c6e3db8fc08 --- /dev/null +++ b/lib/classes/oauth2/refresh_system_tokens_task.php @@ -0,0 +1,88 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +use \core\task\scheduled_task; + +/** + * Simple task to delete old messaging records. + */ +class refresh_system_tokens_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskrefreshsystemtokens', 'admin'); + } + + /** + * Notify admins when an OAuth refresh token expires. Should not happen if cron is running regularly. + * @param \core\oauth2\issuer $issuer + */ + protected function notify_admins(\core\oauth2\issuer $issuer) { + $admins = get_admins(); + + if (empty($admins)) { + return; + } + foreach ($admins as $admin) { + $strparams = ['siteurl' => $CFG->wwwroot, 'issuer' => $issuer->get('name')]; + $long = get_string('oauthrefreshtokenexpired', 'core_admin', $strparams); + $short = get_string('oauthrefreshtokenexpiredshort', 'core_admin', $strparams); + $message = new \core\message\message(); + $message->courseid = SITEID; + $message->component = 'moodle'; + $message->name = 'oauthrefreshtokenexpired'; + $message->userfrom = core\user::get_noreply_user(); + $message->userto = $admin; + $message->subject = $short; + $message->fullmessage = $long; + $message->fullmessageformat = FORMAT_PLAIN; + $message->fullmessagehtml = $long; + $message->smallmessage = $short; + $message->notification = 1; + message_send($message); + } + } + + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + $issuers = \core\oauth2\api::get_all_issuers(); + foreach ($issuers as $issuer) { + if ($issuer->is_system_account_connected()) { + if (!\core\oauth2\api::get_system_oauth_client($issuer)) { + $this->notify_admins($issuer); + } + } + } + } + +} diff --git a/lib/db/tasks.php b/lib/db/tasks.php index c820348d6c2..4366f69fb45 100644 --- a/lib/db/tasks.php +++ b/lib/db/tasks.php @@ -347,4 +347,13 @@ $tasks = array( 'dayofweek' => '*', 'month' => '*' ), + array( + 'classname' => 'core\oauth2\refresh_system_tokens_task', + 'blocking' => 0, + 'minute' => 'R', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), ); From 6c9cd495a2279b379173040dac1189f583797456 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Sun, 5 Mar 2017 16:13:53 +0800 Subject: [PATCH 15/84] MDL-58128 googledocs: Upgrade repo config Part of MDL-58220 --- repository/googledocs/db/upgrade.php | 17 +++++++++++++++++ repository/googledocs/version.php | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/repository/googledocs/db/upgrade.php b/repository/googledocs/db/upgrade.php index da8d731a947..2a5698ef3c1 100644 --- a/repository/googledocs/db/upgrade.php +++ b/repository/googledocs/db/upgrade.php @@ -48,6 +48,23 @@ function xmldb_repository_googledocs_upgrade($oldversion) { // Plugin savepoint reached. upgrade_plugin_savepoint(true, 2017011100, 'repository', 'googledocs'); } + if ($oldversion < 2017030500) { + $clientid = get_config('clientid', 'googledocs'); + $secret = get_config('secret', 'googledocs'); + + // Update from repo config to use an OAuth service. + if (!empty($clientid) && !empty($secret)) { + $issuer = \core\oauth2\api::create_standard_issuer('google'); + + $issuer->set('clientid', $clientid); + $issuer->set('secret', $secret); + + $issuer->update(); + + set_config('issuerid', $issuer->get('id'), 'googledocs'); + } + upgrade_plugin_savepoint(true, 2017030500, 'repository', 'googledocs'); + } return true; } diff --git a/repository/googledocs/version.php b/repository/googledocs/version.php index 04eaa0e8fab..ce8f197427a 100644 --- a/repository/googledocs/version.php +++ b/repository/googledocs/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2017011100; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2017030500; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2016112900; // Requires this Moodle version. $plugin->component = 'repository_googledocs'; // Full name of the plugin (used for diagnostics). From 989e14fea094672b401f5dcf3d3071d70573ae32 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 3 Mar 2017 16:08:28 +0800 Subject: [PATCH 16/84] MDL-58128 googledocs: Support reference files. Add options so the admin can control the types of files this repository can support (and the default). Part of MDL-58220 --- lib/classes/oauth2/rest.php | 25 +- lib/filelib.php | 28 ++ repository/filepicker.js | 8 + repository/googledocs/classes/rest.php | 71 ++- repository/googledocs/db/upgrade.php | 3 + .../lang/en/repository_googledocs.php | 6 + repository/googledocs/lib.php | 443 +++++++++++++++++- repository/googledocs/version.php | 2 +- repository/lib.php | 25 + repository/repository_ajax.php | 5 + 10 files changed, 591 insertions(+), 25 deletions(-) diff --git a/lib/classes/oauth2/rest.php b/lib/classes/oauth2/rest.php index 03afc9fb4f4..708ad35ac5d 100644 --- a/lib/classes/oauth2/rest.php +++ b/lib/classes/oauth2/rest.php @@ -64,7 +64,7 @@ abstract class rest { * @param string $functionname * @param array $functionargs */ - public function call($functionname, $functionargs) { + public function call($functionname, $functionargs, $rawpost = false) { $functions = $this->get_api_functions(); $supportedmethods = [ 'get', 'put', 'post', 'patch', 'head', 'delete' ]; if (empty($functions[$functionname])) { @@ -86,7 +86,30 @@ abstract class rest { } } + // Allow params in the URL path like /me/{parent}/children. + foreach ($callargs as $argname => $value) { + $newendpoint = str_replace('{' . $argname . '}', $value, $endpoint); + if ($newendpoint != $endpoint) { + $endpoint = $newendpoint; + unset($callargs[$argname]); + } + } + + if ($rawpost !== false) { + $queryparams = $this->curl->build_post_data($callargs); + if (!empty($queryparams)) { + $endpoint .= '?' . $queryparams; + } + $callargs = $rawpost; + } + + error_log('CALL REST'); + error_log($endpoint); + error_log(json_encode($callargs)); + error_log($method); + $this->curl->setHeader('Content-type: application/json'); $response = $this->curl->$method($endpoint, $callargs); + error_log($response); if ($this->curl->errno == 0) { if ($responsetype == 'json') { diff --git a/lib/filelib.php b/lib/filelib.php index 57782bf6782..8d1d3cf818b 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -3458,6 +3458,34 @@ class curl { return $this->request($url, $options); } + /** + * HTTP PATCH method + * + * @param string $url + * @param array|string $params + * @param array $options + * @return bool + */ + public function patch($url, $params = '', $options = array()) { + $options['CURLOPT_CUSTOMREQUEST'] = 'PATCH'; + if (is_array($params)) { + $this->_tmp_file_post_params = array(); + foreach ($params as $key => $value) { + if ($value instanceof stored_file) { + $value->add_to_curl_request($this, $key); + } else { + $this->_tmp_file_post_params[$key] = $value; + } + } + $options['CURLOPT_POSTFIELDS'] = $this->_tmp_file_post_params; + unset($this->_tmp_file_post_params); + } else { + // $params is the raw post data + $options['CURLOPT_POSTFIELDS'] = $params; + } + return $this->request($url, $options); + } + /** * HTTP POST method * diff --git a/repository/filepicker.js b/repository/filepicker.js index 386a6abcd17..3a2ff4edd89 100644 --- a/repository/filepicker.js +++ b/repository/filepicker.js @@ -29,6 +29,7 @@ * Active repository options * ===== * this.active_repo.id + * this.active_repo.defaultreturntype * this.active_repo.nosearch * this.active_repo.norefresh * this.active_repo.nologin @@ -1094,6 +1095,12 @@ M.core_filepicker.init = function(Y, options) { firstfilelink = (firstfilelink==null && allowed) ? filelinktypes[i] : firstfilelink; filelinkcount += allowed ? 1 : 0; } + var defaultreturntype = this.options.repositories[this.active_repo.id].defaultreturntype; + if (defaultreturntype) { + if (filelink[defaultreturntype]) { + firstfilelink = defaultreturntype; + } + } // make radio buttons enabled if this file-link-type is available and only if there are more than one file-link-type option // check the first available file-link-type option for (var linktype in filelink) { @@ -1415,6 +1422,7 @@ M.core_filepicker.init = function(Y, options) { this.objecttag = data.object?data.object:null; this.active_repo = {}; this.active_repo.issearchresult = data.issearchresult ? true : false; + this.active_repo.defaultreturntype = data.defaultreturntype?data.defaultreturntype:null; this.active_repo.dynload = data.dynload?data.dynload:false; this.active_repo.pages = Number(data.pages?data.pages:null); this.active_repo.page = Number(data.page?data.page:null); diff --git a/repository/googledocs/classes/rest.php b/repository/googledocs/classes/rest.php index f66aa8bd884..38028ba3434 100644 --- a/repository/googledocs/classes/rest.php +++ b/repository/googledocs/classes/rest.php @@ -54,7 +54,76 @@ class rest extends \core\oauth2\rest { 'spaces' => PARAM_RAW ], 'response' => 'json' - ] + ], + 'get' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', + 'method' => 'get', + 'args' => [ + 'fields' => PARAM_RAW, + 'fileid' => PARAM_RAW + ], + 'response' => 'json' + ], + 'copy' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/copy', + 'method' => 'post', + 'args' => [ + 'fields' => PARAM_RAW, + 'fileid' => PARAM_RAW + ], + 'response' => 'json' + ], + 'create' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files', + 'method' => 'post', + 'args' => [ + 'fields' => PARAM_RAW + ], + 'response' => 'json' + ], + 'update' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', + 'method' => 'patch', + 'args' => [ + 'fileid' => PARAM_RAW, + 'fields' => PARAM_RAW, + 'addParents' => PARAM_RAW, + 'removeParents' => PARAM_RAW + ], + 'response' => 'json' + ], + 'create_permission' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/permissions', + 'method' => 'post', + 'args' => [ + 'fileid' => PARAM_RAW, + 'emailMessage' => PARAM_RAW, + 'sendNotificationEmail' => PARAM_RAW, + 'transferOwnership' => PARAM_RAW, + ], + 'response' => 'json' + ], + 'update_permission' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/permissions/{permissionid}', + 'method' => 'patch', + 'args' => [ + 'fileid' => PARAM_RAW, + 'permissionid' => PARAM_RAW, + 'emailMessage' => PARAM_RAW, + 'sendNotificationEmail' => PARAM_RAW, + 'transferOwnership' => PARAM_RAW, + ], + 'response' => 'json' + ], + 'list_permissions' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/permissions', + 'method' => 'get', + 'args' => [ + 'fileid' => PARAM_RAW, + 'fields' => PARAM_RAW + ], + 'response' => 'json' + ], ]; } } diff --git a/repository/googledocs/db/upgrade.php b/repository/googledocs/db/upgrade.php index 2a5698ef3c1..9d4643230a1 100644 --- a/repository/googledocs/db/upgrade.php +++ b/repository/googledocs/db/upgrade.php @@ -63,6 +63,9 @@ function xmldb_repository_googledocs_upgrade($oldversion) { set_config('issuerid', $issuer->get('id'), 'googledocs'); } + if ($oldversion < 2017030600) { + set_config('supportedfiles', 'both', 'googledocs'); + } upgrade_plugin_savepoint(true, 2017030500, 'repository', 'googledocs'); } diff --git a/repository/googledocs/lang/en/repository_googledocs.php b/repository/googledocs/lang/en/repository_googledocs.php index 5654925eb09..e64dabcb73e 100644 --- a/repository/googledocs/lang/en/repository_googledocs.php +++ b/repository/googledocs/lang/en/repository_googledocs.php @@ -35,6 +35,12 @@ $string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk $string['servicenotenabled'] = 'Access not configured. Make sure the service \'Drive API\' is enabled.'; $string['oauth2serviceslink'] = 'OAuth 2 Services Configuration'; $string['searchfor'] = 'Search for {$a}'; +$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.'; // Deprecated since Moodle 3.3. $string['oauthinfo'] = '

To use this plugin, you must register your site with Google, as described in the documentation Google OAuth 2.0 setup.

As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':

{$a->callbackurl}

Once registered, you will be provided with a client ID and secret which can be used to configure all Google Drive and Picasa plugins.

Please also note that you will have to enable the service \'Drive API\'.

'; diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 852ee5d1bae..bfffc736f92 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -185,7 +185,6 @@ class repository_googledocs extends repository { ); } - /** * List the files and folders. * @@ -217,6 +216,7 @@ class repository_googledocs extends repository { $ret = array(); $ret['dynload'] = true; + $ret['defaultreturntype'] = $this->default_returntype(); $ret['path'] = $this->build_breadcrumb($path); $ret['list'] = $results; return $ret; @@ -266,7 +266,7 @@ class repository_googledocs extends repository { $files = array(); $folders = array(); $config = get_config('googledocs'); - $fields = "files(id,name,mimeType,webContentLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)"; + $fields = "files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)"; $params = array('q' => $q, 'fields' => $fields, 'spaces' => 'drive'); try { @@ -284,7 +284,6 @@ class repository_googledocs extends repository { } } - $base = 'https://www.googleapis.com/drive/v3'; $gfiles = isset($response->files) ? $response->files : array(); foreach ($gfiles as $gfile) { if ($gfile->mimeType == 'application/vnd.google-apps.folder') { @@ -300,12 +299,11 @@ class repository_googledocs extends repository { ); } else { // This is a file. + $link = isset($gfile->webContentLink) ? $gfile->webContentLink : ''; if (isset($gfile->fileExtension)) { // The file has an extension, therefore we can download it. + $source = json_encode(['id' => $gfile->id, 'exportformat' => 'download', 'link' => $link]); $title = $gfile->name; - $params = ['alt' => 'media']; - $sourceurl = new moodle_url($base . '/files/' . $gfile->id, $params); - $source = $sourceurl->out(false); } else { // The file is probably a Google Doc file, we get the corresponding export link. // This should be improved by allowing the user to select the type of export they'd like. @@ -317,7 +315,7 @@ class repository_googledocs extends repository { switch ($type){ case 'document': $ext = $config->documentformat; - $title = $item['title'] . '.'. $ext; + $title = $gfile->name . '.'. $ext; if ($ext === 'rtf') { // Moodle user 'text/rtf' as the MIME type for RTF files. // Google uses 'application/rtf' for the same type of file. @@ -329,17 +327,17 @@ class repository_googledocs extends repository { break; case 'presentation': $ext = $config->presentationformat; - $title = $item['title'] . '.'. $ext; + $title = $gfile->name . '.'. $ext; $exporttype = $types[$ext]['type']; break; case 'spreadsheet': $ext = $config->spreadsheetformat; - $title = $item['title'] . '.'. $ext; + $title = $gfile->name . '.'. $ext; $exporttype = $types[$ext]['type']; break; case 'drawing': $ext = $config->drawingformat; - $title = $item['title'] . '.'. $ext; + $title = $gfile->name . '.'. $ext; $exporttype = $types[$ext]['type']; break; } @@ -347,9 +345,7 @@ class repository_googledocs extends repository { if (empty($title)) { continue; } - $params = ['mimeType' => $exporttype]; - $sourceurl = new moodle_url($base . '/files/' . $gfile->id . '/export', $params); - $source = $sourceurl->out(false); + $source = json_encode(['id' => $gfile->id, 'exportformat' => $exporttype, 'link' => $link]); } // Adds the file to the file list. Using the itemId along with the name as key // of the array because Google Drive allows files with identical names. @@ -400,10 +396,24 @@ class repository_googledocs extends repository { global $CFG; $client = $this->get_user_oauth_client(); + $base = 'https://www.googleapis.com/drive/v3'; + $source = json_decode($reference); + + if ($source->exportformat == 'download') { + $params = ['alt' => 'media']; + $sourceurl = new moodle_url($base . '/files/' . $source->id, $params); + $source = $sourceurl->out(false); + } else { + $params = ['mimeType' => $source->exportformat]; + $sourceurl = new moodle_url($base . '/files/' . $source->id . '/export', $params); + $source = $sourceurl->out(false); + } + + // We use download_one and not the rest API because it has special timeouts etc. $path = $this->prepare_file($filename); $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5]; - $result = $client->download_one($reference, null, $options); + $result = $client->download_one($source, null, $options); if ($result) { @chmod($path, $CFG->filepermissions); @@ -425,7 +435,8 @@ class repository_googledocs extends repository { * @return string file reference. */ public function get_file_reference($source) { - return clean_param($source, PARAM_URL); + // We could do some magic upgrade code here. + return $source; } /** @@ -446,7 +457,34 @@ class repository_googledocs extends repository { * @return int */ public function supported_returntypes() { - return FILE_INTERNAL; + // We can only support references if the system account is connected. + if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) { + $setting = get_config('googledocs', 'supportedreturntypes'); + if ($setting == 'internal') { + return FILE_INTERNAL; + } else if ($setting == 'external') { + return FILE_REFERENCE; + } else { + return FILE_REFERENCE | FILE_INTERNAL; + } + } else { + return FILE_INTERNAL; + } + } + + /** + * Which return type should be selected by default. + * + * @return int + */ + public function default_returntype() { + $setting = get_config('googledocs', 'defaultreturntype'); + $supported = get_config('googledocs', 'supportedreturntypes'); + if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') { + return FILE_INTERNAL; + } else { + return FILE_REFERENCE; + } } /** @@ -458,7 +496,8 @@ class repository_googledocs extends repository { public static function get_type_option_names() { return array('issuerid', 'pluginname', 'documentformat', 'drawingformat', - 'presentationformat', 'spreadsheetformat'); + 'presentationformat', 'spreadsheetformat', + 'defaultreturntype', 'supportedreturntypes'); } /** @@ -466,10 +505,348 @@ class repository_googledocs extends repository { */ public function callback() { $client = $this->get_user_oauth_client(); - // This will upgrade to an access token if we have an authorization code. + // 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(); } + /** + * Repository method to serve the referenced file + * + * @see send_stored_file + * + * @param stored_file $storedfile the file that contains the reference + * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime) + * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only + * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin + * @param array $options additional options affecting the file serving + */ + public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) { + // TODO. + $source = json_decode($storedfile->get_reference()); + + if ($source->link) { + header('Location: ' . $source->link); + } else { + $details = 'File is missing source link'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + } + + /** + * List the permissions on a file. + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The id of the file. + * + * @return array + */ + protected function list_file_permissions(\repository_googledocs\rest $client, $fileid) { + $fields = "permissions(id,type,emailAddress,role,allowFileDiscovery,displayName)"; + return $client->call('list_permissions', ['fileid' => $fileid]); + } + + /** + * See if a folder exists within a folder + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $foldername The folder we are looking for. + * @param string $parentid The parent folder we are looking in. + * + * @return string|boolean The file id if it exists or false. + */ + protected function folder_exists_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) { + $q = '\'' . addslashes($parentid) . '\' in parents and trashed = false and name = \'' . addslashes($foldername). '\''; + $fields = 'files(id, name)'; + $params = [ 'q' => $q, 'fields' => $fields]; + $response = $client->call('list', $params); + $missing = true; + foreach ($response->files as $child) { + if ($child->name == $foldername) { + return $child->id; + } + } + return false; + } + + /** + * Create a folder within a folder + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $foldername The folder we are creating. + * @param string $parentid The parent folder we are creating in. + * + * @return string The file id of the new folder. + */ + protected function create_folder_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) { + $fields = 'id'; + $params = ['fields' => $fields]; + $folder = ['mimeType' => 'application/vnd.google-apps.folder', 'name' => $foldername, 'parents' => [$parentid]]; + $created = $client->call('create', $params, json_encode($folder)); + if (empty($created->id)) { + $details = 'Cannot create folder:' . $foldername; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return $created->id; + } + + /** + * Get capabilities for a file. + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are checking. + * + * @return stdClass The file info with capabilities. + */ + protected function get_file_capabilities(\repository_googledocs\rest $client, $fileid) { + $fields = "id,capabilities,writersCanShare"; + $params = [ + 'fileid' => $fileid, + 'fields' => $fields + ]; + return $client->call('get', $params); + } + + /** + * Update file owner. + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are updating. + * + * @return boolean Did it work? + */ + protected function update_file_owner(\repository_googledocs\rest $client, $fileid, $owneremail) { + $updateowner = [ + 'emailAddress' => $owneremail, + 'role' => 'owner', + 'type' => 'user' + ]; + $params = ['fileid' => $fileid, 'transferOwnership' => 'true']; + try { + $response = $client->call('create_permission', $params, json_encode($updateowner)); + } catch (\core\oauth2\rest_exception $re) { + return false; + } + return !empty($response->id); + } + + /** + * Copy a file and return the new file details. A side effect of the copy + * is that the owner will be the account authenticated with this oauth client. + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are copying. + * + * @return stdClass file details. + */ + protected function copy_file(\repository_googledocs\rest $client, $fileid) { + $fields = "id,name,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink"; + $params = [ + 'fileid' => $fileid, + 'fields' => $fields + ]; + $fileinfo = $client->call('copy', $params, ' '); + if (empty($fileinfo->id)) { + $details = 'Cannot copy file:' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return $fileinfo; + } + + /** + * Add a writer to the permissions on the file. + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are updating. + * @param string $email The email of the writer account to add. + * @return boolean + */ + protected function add_writer_to_file($client, $fileid, $email) { + $updateeditor = [ + 'emailAddress' => $email, + 'role' => 'writer', + 'type' => 'user' + ]; + $params = ['fileid' => $fileid]; + $response = $client->call('create_permission', $params, json_encode($updateeditor)); + if (empty($response->id)) { + $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return true; + } + + /** + * Move from root to folder + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are updating. + * @param string $folderid The id of the folder we are moving to + * @return boolean + */ + protected function move_file_from_root_to_folder($client, $fileid, $folderid) { + // Set the parent. + $params = [ + 'fileid' => $fileid, 'addParents' => $folderid, 'removeParents' => 'root' + ]; + $response = $client->call('update', $params, ' '); + if (empty($response->id)) { + $details = 'Cannot move the file to a folder: ' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return true; + } + + /** + * Prevent writers from sharing. + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are updating. + * @return boolean + */ + protected function prevent_writers_from_sharing_file($client, $fileid) { + // We don't want anyone but Moodle to change the sharing settings. + $params = [ + 'fileid' => $fileid + ]; + $update = [ + 'writersCanShare' => false + ]; + $response = $client->call('update', $params, json_encode($update)); + if (empty($response->id)) { + $details = 'Cannot prevent writers from sharing document: ' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return true; + } + + /** + * Allow anyone with the link to read the file. + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are updating. + * @return boolean + */ + protected function set_file_sharing_anyone_with_link_can_read($client, $fileid) { + $updateread = [ + 'type' => 'anyone', + 'role' => 'reader', + 'allowFileDiscovery' => 'false' + ]; + $params = ['fileid' => $fileid]; + $response = $client->call('create_permission', $params, json_encode($updateread)); + if (empty($response->id) || $response->id != 'anyoneWithLink') { + $details = 'Cannot update link sharing for the document: ' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return true; + } + + /** + * Called when a file is selected as a "link". + * Invoked at MOODLE/repository/repository_ajax.php + * + * @param string $reference this reference is generated by + * repository::get_file_reference() + * @param context $context the target context for this new file. + * @return string $modifiedreference (final one before saving to DB) + */ + public function reference_file_selected($reference, $context) { + // What we need to do here is transfer ownership to the system user (or copy) + // then set the permissions so anyone with the share link can view, + // finally update the reference to contain the share link if it was not + // already there (and point to new file id if we copied). + $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); + + if ($systemauth === false) { + $details = 'Cannot connect as system user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $systemuserinfo = $systemauth->get_userinfo(); + $systemuseremail = $systemuserinfo['email']; + + $source = json_decode($reference); + + $userauth = $this->get_user_oauth_client(); + if ($userauth === false) { + $details = 'Cannot connect as current user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $userinfo = $userauth->get_userinfo(); + $useremail = $userinfo['email']; + + $userservice = new repository_googledocs\rest($userauth); + $systemservice = new repository_googledocs\rest($systemauth); + + // Get the list of existing permissions so we can see if the owner is already the system account, + // and whether we need to update the link sharing options. + $permissions = $this->list_file_permissions($userservice, $source->id); + + $readshareupdaterequired = true; + $ownerupdaterequired = true; + foreach ($permissions->permissions as $permission) { + if ($permission->type == 'user' && + $permission->role == 'owner' && + isset($permission->emailAddress) && + $permission->emailAddress == $systemuseremail) { + $ownerupdaterequired = false; + } + if ($permission->id == 'anyoneWithLink' && + $permission->type == 'anyone' && + $permission->role == 'reader' && + $permission->allowFileDiscovery == false) { + $readshareupdaterequired = false; + } + } + + // Now move it to a sensible folder. + $contextlist = array_reverse($context->get_parent_contexts(true)); + + $parentid = 'root'; + foreach ($contextlist as $context) { + // Make sure a folder exists here. + $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid); + if ($folderid !== false) { + $parentid = $folderid; + } else { + // Create it. + $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid); + } + } + + // See if we have edit capability before the copy. + $fileinfo = $this->get_file_capabilities($userservice, $source->id); + $canedit = !empty($fileinfo->capabilities->canEdit); + $writerscanshare = !empty($fileinfo->writersCanShare); + + // The owner was not the system user so we have to update the file. + if ($ownerupdaterequired) { + + $worked = $this->update_file_owner($userservice, $source->id, $systemuseremail); + if (!$worked) { + // Updating the owner only works for "google files" like documents etc. For binary + // files we will get here. + $source = $this->copy_file($systemservice, $source->id); + + $readshareupdaterequired = true; + $writerscanshare = true; + + if ($canedit) { + $this->add_writer_to_file($systemservice, $source->id, $useremail); + } + } + + $this->move_file_from_root_to_folder($systemservice, $source->id, $parentid); + } + + if ($writerscanshare) { + // We don't want anyone but Moodle to change the sharing settings. + $this->prevent_writers_from_sharing_file($systemservice, $source->id); + } + + if ($readshareupdaterequired) { + $this->set_file_sharing_anyone_with_link_can_read($systemservice, $source->id); + } + } + /** * Edit/Create Admin Settings Moodle form. * @@ -477,7 +854,8 @@ class repository_googledocs extends repository { * @param string $classname repository class name. */ public static function type_config_form($mform, $classname = 'repository') { - $url = (string)new moodle_url('/admin/tool/oauth2/issuers.php'); + $url = new moodle_url('/admin/tool/oauth2/issuers.php'); + $url = $url->out(); $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_googledocs', $url)); @@ -488,11 +866,26 @@ class repository_googledocs extends repository { foreach ($issuers as $issuer) { $options[$issuer->get('id')] = s($issuer->get('name')); } + + $strrequired = get_string('required'); + $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_googledocs'), $options); $mform->addHelpButton('issuerid', 'issuer', 'repository_googledocs'); $mform->addRule('issuerid', $strrequired, 'required', null, 'client'); - $strrequired = get_string('required'); + $mform->addElement('static', null, '', get_string('fileoptions', 'repository_googledocs')); + $choices = [ + 'internal' => get_string('internal', 'repository_googledocs'), + 'external' => get_string('external', 'repository_googledocs'), + 'both' => get_string('both', 'repository_googledocs') + ]; + $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_googledocs'), $choices); + + $choices = [ + FILE_INTERNAL => get_string('internal', 'repository_googledocs'), + FILE_REFERENCE => get_string('external', 'repository_googledocs'), + ]; + $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_googledocs'), $choices); $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs')); @@ -529,7 +922,8 @@ class repository_googledocs extends repository { $presentationformat['txt'] = 'txt'; core_collator::ksort($presentationformat, core_collator::SORT_NATURAL); - $mform->addElement('select', 'presentationformat', get_string('presentationformat', 'repository_googledocs'), $presentationformat); + $str = get_string('presentationformat', 'repository_googledocs'); + $mform->addElement('select', 'presentationformat', $str, $presentationformat); $mform->setDefault('presentationformat', $presentationformat['pptx']); $mform->setType('presentationformat', PARAM_ALPHANUM); @@ -541,9 +935,14 @@ class repository_googledocs extends repository { $spreadsheetformat['xlsx'] = 'xlsx'; core_collator::ksort($spreadsheetformat, core_collator::SORT_NATURAL); - $mform->addElement('select', 'spreadsheetformat', get_string('spreadsheetformat', 'repository_googledocs'), $spreadsheetformat); + $str = get_string('spreadsheetformat', 'repository_googledocs'); + $mform->addElement('select', 'spreadsheetformat', $str, $spreadsheetformat); $mform->setDefault('spreadsheetformat', $spreadsheetformat['xlsx']); $mform->setType('spreadsheetformat', PARAM_ALPHANUM); } } + // Icon from: http://www.iconspedia.com/icon/google-2706.html. +function repository_googledocs_oauth2_system_scopes() { + return 'https://www.googleapis.com/auth/drive'; +} diff --git a/repository/googledocs/version.php b/repository/googledocs/version.php index ce8f197427a..4aa78df7655 100644 --- a/repository/googledocs/version.php +++ b/repository/googledocs/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2017030500; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2017030600; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2016112900; // Requires this Moodle version. $plugin->component = 'repository_googledocs'; // Full name of the plugin (used for diagnostics). diff --git a/repository/lib.php b/repository/lib.php index c11f4cdc61f..6ed4db745bd 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -1281,6 +1281,19 @@ abstract class repository implements cacheable_object { public function cache_file_by_reference($reference, $storedfile) { } + /** + * reference_file_selected + * Invoked at MOODLE/repository/repository_ajax.php + * + * @param string $reference this reference is generated by + * repository::get_file_reference() + * @param context $context the target context for this new file. + * @return string updated reference (final one before it's saved to db). + */ + public function reference_file_selected($reference, $context) { + return $reference; + } + /** * Return the source information * @@ -1890,6 +1903,17 @@ abstract class repository implements cacheable_object { return (FILE_INTERNAL | FILE_EXTERNAL); } + /** + * Tells how the file can be picked from this repository + * + * Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE + * + * @return int + */ + public function default_returntype() { + return FILE_INTERNAL; + } + /** * Provide repository instance information for Ajax * @@ -1904,6 +1928,7 @@ abstract class repository implements cacheable_object { $meta->icon = $OUTPUT->image_url('icon', 'repository_'.$meta->type)->out(false); $meta->supported_types = file_get_typegroup('extension', $this->supported_filetypes()); $meta->return_types = $this->supported_returntypes(); + $meta->defaultreturntype = $this->default_returntype(); $meta->sortorder = $this->options['sortorder']; return $meta; } diff --git a/repository/repository_ajax.php b/repository/repository_ajax.php index 631d4d1a0d0..ba703765f1b 100644 --- a/repository/repository_ajax.php +++ b/repository/repository_ajax.php @@ -226,6 +226,11 @@ switch ($action) { $record->contenthash = $sourcefile->get_contenthash(); $record->filesize = $sourcefile->get_filesize(); } + + // This hook gives the repo a place to do some house cleaning, and update the $reference before it's saved + // to the file store. E.g. transfer ownership of the file to a system account etc. + $reference = $repo->reference_file_selected($reference, $context); + // Check if file exists. if (repository::draftfile_exists($itemid, $saveas_path, $saveas_filename)) { // File name being used, rename it. From 8ece1d70d89f5c38a233ce0aec3595f282ad27b7 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Mon, 6 Mar 2017 14:16:51 +0800 Subject: [PATCH 17/84] MDL-58128 googledocs: Don't copy files When linking - add the moodle account to the file and organise it on the moodle side. Also allow read access with link, but don't make any other changes to the perm/ownership. Part of MDL-58220 --- lang/en/repository.php | 1 + .../lang/en/repository_googledocs.php | 1 + repository/googledocs/lib.php | 99 ++++++++++++------- 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/lang/en/repository.php b/lang/en/repository.php index d76784a5b7b..16f77ddf476 100644 --- a/lang/en/repository.php +++ b/lang/en/repository.php @@ -232,6 +232,7 @@ $string['unknownoriginal'] = 'Unknown'; $string['upload'] = 'Upload this file'; $string['uploading'] = 'Uploading...'; $string['uploadsucc'] = 'The file has been uploaded successfully'; +$string['unknownsource'] = 'Unknown source'; $string['undisclosedsource'] = '(Undisclosed)'; $string['undisclosedreference'] = '(Undisclosed)'; $string['uselatestfile'] = 'Use latest file'; diff --git a/repository/googledocs/lang/en/repository_googledocs.php b/repository/googledocs/lang/en/repository_googledocs.php index e64dabcb73e..cd4c4af3a59 100644 --- a/repository/googledocs/lang/en/repository_googledocs.php +++ b/repository/googledocs/lang/en/repository_googledocs.php @@ -41,6 +41,7 @@ $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.'; +$string['owner'] = 'Owned by: {$a}'; // Deprecated since Moodle 3.3. $string['oauthinfo'] = '

To use this plugin, you must register your site with Google, as described in the documentation Google OAuth 2.0 setup.

As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':

{$a->callbackurl}

Once registered, you will be provided with a client ID and secret which can be used to configure all Google Drive and Picasa plugins.

Please also note that you will have to enable the service \'Drive API\'.

'; diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index bfffc736f92..14fdef85a9b 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -299,7 +299,10 @@ class repository_googledocs extends repository { ); } else { // This is a file. - $link = isset($gfile->webContentLink) ? $gfile->webContentLink : ''; + $link = isset($gfile->webViewLink) ? $gfile->webViewLink : ''; + if (empty($link)) { + $link = isset($gfile->webContentLink) ? $gfile->webContentLink : ''; + } if (isset($gfile->fileExtension)) { // The file has an extension, therefore we can download it. $source = json_encode(['id' => $gfile->id, 'exportformat' => 'download', 'link' => $link]); @@ -521,7 +524,6 @@ class repository_googledocs extends repository { * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) { - // TODO. $source = json_decode($storedfile->get_reference()); if ($source->link) { @@ -605,6 +607,23 @@ class repository_googledocs extends repository { return $client->call('get', $params); } + /** + * Get simple file info for humans. + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are querying. + * + * @return stdClass + */ + protected function get_file_summary(\repository_googledocs\rest $client, $fileid) { + $fields = "id,name,owners"; + $params = [ + 'fileid' => $fileid, + 'fields' => $fields + ]; + return $client->call('get', $params); + } + /** * Update file owner. * @@ -665,7 +684,7 @@ class repository_googledocs extends repository { 'role' => 'writer', 'type' => 'user' ]; - $params = ['fileid' => $fileid]; + $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false']; $response = $client->call('create_permission', $params, json_encode($updateeditor)); if (empty($response->id)) { $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; @@ -783,12 +802,6 @@ class repository_googledocs extends repository { $readshareupdaterequired = true; $ownerupdaterequired = true; foreach ($permissions->permissions as $permission) { - if ($permission->type == 'user' && - $permission->role == 'owner' && - isset($permission->emailAddress) && - $permission->emailAddress == $systemuseremail) { - $ownerupdaterequired = false; - } if ($permission->id == 'anyoneWithLink' && $permission->type == 'anyone' && $permission->role == 'reader' && @@ -797,12 +810,17 @@ class repository_googledocs extends repository { } } + // Add Moodle as writer. + $this->add_writer_to_file($userservice, $source->id, $systemuseremail); + // Now move it to a sensible folder. $contextlist = array_reverse($context->get_parent_contexts(true)); $parentid = 'root'; foreach ($contextlist as $context) { // Make sure a folder exists here. + $foldername = $context->get_context_name(); + $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid); if ($folderid !== false) { $parentid = $folderid; @@ -812,39 +830,44 @@ class repository_googledocs extends repository { } } - // See if we have edit capability before the copy. - $fileinfo = $this->get_file_capabilities($userservice, $source->id); - $canedit = !empty($fileinfo->capabilities->canEdit); - $writerscanshare = !empty($fileinfo->writersCanShare); - - // The owner was not the system user so we have to update the file. - if ($ownerupdaterequired) { - - $worked = $this->update_file_owner($userservice, $source->id, $systemuseremail); - if (!$worked) { - // Updating the owner only works for "google files" like documents etc. For binary - // files we will get here. - $source = $this->copy_file($systemservice, $source->id); - - $readshareupdaterequired = true; - $writerscanshare = true; - - if ($canedit) { - $this->add_writer_to_file($systemservice, $source->id, $useremail); - } - } - - $this->move_file_from_root_to_folder($systemservice, $source->id, $parentid); - } - - if ($writerscanshare) { - // We don't want anyone but Moodle to change the sharing settings. - $this->prevent_writers_from_sharing_file($systemservice, $source->id); - } + $this->move_file_from_root_to_folder($systemservice, $source->id, $parentid); if ($readshareupdaterequired) { $this->set_file_sharing_anyone_with_link_can_read($systemservice, $source->id); } + + // We did not update the reference at all. + return $reference; + } + + /** + * Get human readable file info from a the reference. + * + * @param string $reference + * @param int $filestatus + */ + public function get_reference_details($reference, $filestatus = 0) { + if (empty($reference)) { + return get_string('unknownsource', 'repository'); + } + $source = json_decode($reference); + $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); + + if ($systemauth === false) { + return ''; + } + $systemservice = new repository_googledocs\rest($systemauth); + $info = $this->get_file_summary($systemservice, $source->id); + + $owner = ''; + if (!empty($info->owners[0]->displayName)) { + $owner = $info->owners[0]->displayName; + } + if ($owner) { + return get_string('owner', 'repository_googledocs', $owner); + } else { + return $info->name; + } } /** From 151b0f940966559ba00e124f89ab7c153901f3df Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 7 Mar 2017 22:04:30 +0800 Subject: [PATCH 18/84] MDL-58132 repositories: Controlled link file type This introduces a new "controlled link" file type where the file is not stored in Moodle - but Moodle will control the access permissions on the file. Plugins can "freeze" a filearea which means Moodle will take ownership of all the remote files of this type. When accessing a file, if the "filebrowser" infomation indicates the current user can write to the file, they will be granted temporary write access. Part of MDL-58220 --- backup/backupfilesedit_form.php | 3 +- files/renderer.php | 6 + lang/en/repository.php | 1 + lib/filelib.php | 17 +- lib/filestorage/file_storage.php | 31 +++ lib/filestorage/stored_file.php | 9 + lib/form/editor.php | 4 +- lib/form/filemanager.php | 2 +- lib/upgrade.txt | 5 + mod/assign/assignmentplugin.php | 20 +- mod/assign/lang/en/assign.php | 1 + mod/assign/locallib.php | 19 ++ mod/assign/renderable.php | 1 + mod/assign/renderer.php | 2 +- mod/assign/submission/file/locallib.php | 24 +- mod/assign/submission/onlinetext/locallib.php | 2 +- mod/data/field/file/field.class.php | 4 +- mod/forum/classes/post_form.php | 2 +- mod/forum/lib.php | 5 +- mod/wiki/filesedit.php | 3 +- mod/workshop/locallib.php | 11 +- question/type/essay/renderer.php | 2 +- repository/areafiles/lib.php | 1 + repository/filepicker.js | 14 +- repository/googledocs/classes/rest.php | 8 + repository/googledocs/lib.php | 212 ++++++++++++++++-- repository/lib.php | 17 +- repository/repository_ajax.php | 3 +- .../core/filemanager_selectlayout.mustache | 6 + 29 files changed, 391 insertions(+), 44 deletions(-) diff --git a/backup/backupfilesedit_form.php b/backup/backupfilesedit_form.php index dce09fe9fa0..fa0257717b5 100644 --- a/backup/backupfilesedit_form.php +++ b/backup/backupfilesedit_form.php @@ -30,7 +30,8 @@ class backup_files_edit_form extends moodleform { public function definition() { $mform =& $this->_form; - $options = array('subdirs' => 0, 'maxfiles' => -1, 'accepted_types' => '*', 'return_types' => FILE_INTERNAL | FILE_REFERENCE); + $types = (FILE_INTERNAL | FILE_REFERENCE | FILE_CONTRLLED_LINK); + $options = array('subdirs' => 0, 'maxfiles' => -1, 'accepted_types' => '*', 'return_types' => $types); $mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options); diff --git a/files/renderer.php b/files/renderer.php index 020c24d0baa..f63b336faed 100644 --- a/files/renderer.php +++ b/files/renderer.php @@ -772,6 +772,12 @@ class core_files_renderer extends plugin_renderer_base {
+
+ +
+ +
+
diff --git a/lang/en/repository.php b/lang/en/repository.php index 16f77ddf476..1a89457bfbb 100644 --- a/lang/en/repository.php +++ b/lang/en/repository.php @@ -160,6 +160,7 @@ $string['lostsource'] = 'Error. Source is missing. {$a}'; $string['makefileinternal'] = 'Make a copy of the file'; $string['makefilelink'] = 'Link to the file directly'; $string['makefilereference'] = 'Create an alias/shortcut to the file'; +$string['makefilecontrolledlink'] = 'Create an access controlled link to the file'; $string['manage'] = 'Manage repositories'; $string['manageinstances'] = 'Manage instances'; $string['manageurl'] = 'Manage'; diff --git a/lib/filelib.php b/lib/filelib.php index 8d1d3cf818b..e2bda31f674 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -256,6 +256,21 @@ function file_postupdate_standard_editor($data, $field, array $options, $context return $data; } +/** + * For all files in this file area - walk the file list and copy each to a system owned account, making them read-only. + * + * @category files + * @param stdClass $context context - must already exist + * @param string $component + * @param string $filearea file area name + * @param int $itemid + * @return bool + */ +function file_prevent_changes_to_external_files($contextid, $component, $filearea, $itemid=false) { + $fs = get_file_storage(); + return $fs->prevent_changes_to_external_files($contextid, $component, $filearea, $itemid); +} + /** * Saves text and files modified by Editor formslib element * @@ -813,7 +828,7 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea $options['areamaxbytes'] = FILE_AREA_MAX_BYTES_UNLIMITED; // Unlimited. } $allowreferences = true; - if (isset($options['return_types']) && !($options['return_types'] & FILE_REFERENCE)) { + if (isset($options['return_types']) && !($options['return_types'] & (FILE_REFERENCE | FILE_CONTROLLED_LINK))) { // we assume that if $options['return_types'] is NOT specified, we DO allow references. // this is not exactly right. BUT there are many places in code where filemanager options // are not passed to file_save_draft_area_files() diff --git a/lib/filestorage/file_storage.php b/lib/filestorage/file_storage.php index 1cf24fcd59e..bdc0c6cb100 100644 --- a/lib/filestorage/file_storage.php +++ b/lib/filestorage/file_storage.php @@ -2323,4 +2323,35 @@ class file_storage { $data = array('id' => $referencefileid, 'lastsync' => $lastsync); $DB->update_record('files_reference', (object)$data); } + + /** + * For an entire file area - walk through the files and for each one that is a controlled link, + * call prevent_changes on the repository. Typically this will copy the external file to a system + * account controlled by Moodle, remove all write access and update the file reference. + * + * @param int $contextid + * @param string $component + * @param string $filearea + * @param int $itemid + */ + public function prevent_changes_to_external_files($contextid, $component, $filearea, $itemid = false) { + global $DB; + + $transaction = $DB->start_delegated_transaction(); + + $files = $this->get_area_files($contextid, $component, $filearea, $itemid, 'id', false); + + foreach ($files as $file) { + if ($file->is_external_file()) { + // Note that this function uses a cache, so we don't need to + // double cache these. + $repo = repository::get_repository_by_id($file->get_repository_id(), SYSCONTEXTID); + + // We expect this function to throw exceptions on failure. + $repo->prevent_changes_to_external_file($file); + } + } + $transaction->allow_commit(); + return true; + } } diff --git a/lib/filestorage/stored_file.php b/lib/filestorage/stored_file.php index 192e7b8daf2..92478b61114 100644 --- a/lib/filestorage/stored_file.php +++ b/lib/filestorage/stored_file.php @@ -102,6 +102,15 @@ class stored_file { return !empty($this->repository); } + /** + * Whether or not this is a controlled link. Note that repositories cannot support FILE_REFERENCE and FILE_CONTROLLED_LINK. + * + * @return bool + */ + public function is_controlled_link() { + return $this->is_external_file() && $this->repository->supported_returntypes() & FILE_CONTROLLED_LINK; + } + /** * Update some file record fields * NOTE: Must remain protected diff --git a/lib/form/editor.php b/lib/form/editor.php index 551e30645b5..e391a734508 100644 --- a/lib/form/editor.php +++ b/lib/form/editor.php @@ -58,8 +58,8 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab /** @var array options provided to initalize filepicker */ protected $_options = array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 0, 'changeformat' => 0, 'areamaxbytes' => FILE_AREA_MAX_BYTES_UNLIMITED, 'context' => null, 'noclean' => 0, 'trusttext' => 0, - 'return_types' => 7, 'enable_filemanagement' => true); - // $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE + 'return_types' => 15, 'enable_filemanagement' => true); + // $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK /** @var array values for editor */ protected $_values = array('text'=>null, 'format'=>null, 'itemid'=>null); diff --git a/lib/form/filemanager.php b/lib/form/filemanager.php index 0cbf543ea7e..1c9126fe0c0 100644 --- a/lib/form/filemanager.php +++ b/lib/form/filemanager.php @@ -78,7 +78,7 @@ class MoodleQuickForm_filemanager extends HTML_QuickForm_element implements temp $this->_options['maxbytes'] = get_user_max_upload_file_size($PAGE->context, $CFG->maxbytes, $options['maxbytes']); } if (empty($options['return_types'])) { - $this->_options['return_types'] = (FILE_INTERNAL | FILE_REFERENCE); + $this->_options['return_types'] = (FILE_INTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK); } $this->_type = 'filemanager'; parent::__construct($elementName, $elementLabel, $attributes); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 1f7edcfdb01..b1293039d5e 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -1,6 +1,11 @@ This files describes API changes in core libraries and APIs, information provided here is intended especially for developers. === 3.3 === +* Support added for a new type of external file: FILE_CONTROLLED_LINK. This is an external file that Moodle can control + the permissions. Moodle can make files read-only or grant temporary write access. + To make all the files in file area read only (owned by Moodle) - use file_prevent_changes_to_external_files(). + When accessing a URL, the info from file_browser::get_file_info will be checked to determine if the user has write access, + if they do - the remote file will have access controls set to allow editing. * The method moodleform::after_definition() has been added and can now be used to add some logic to be performed after the form's definition was set. This is useful for intermediate subclasses. * Moodle has support for font-awesome icons. Plugins should use the xxx_get_fontawesome_icon_map callback diff --git a/mod/assign/assignmentplugin.php b/mod/assign/assignmentplugin.php index 8861aece6aa..91c62f1840c 100644 --- a/mod/assign/assignmentplugin.php +++ b/mod/assign/assignmentplugin.php @@ -580,17 +580,26 @@ abstract class assign_plugin { public function get_file_info($browser, $filearea, $itemid, $filepath, $filename) { global $CFG, $DB, $USER; $urlbase = $CFG->wwwroot.'/pluginfile.php'; - + $writeaccess = false; // Permission check on the itemid. if ($this->get_subtype() == 'assignsubmission') { if ($itemid) { - $record = $DB->get_record('assign_submission', array('id'=>$itemid), 'userid', IGNORE_MISSING); + $record = $DB->get_record('assign_submission', array('id'=>$itemid), 'userid,groupid', IGNORE_MISSING); if (!$record) { return null; } - if (!$this->assignment->can_view_submission($record->userid)) { - return null; + if (!empty($record->userid)) { + if (!$this->assignment->can_view_submission($record->userid)) { + return null; + } + $writeaccess = $this->assignment->can_edit_submission($record->userid); + } else { + // Must be a team submission with a group. + if (!$this->assignment->can_view_group_submission($record->groupid)) { + return null; + } + $writeaccess = $this->assignment->can_edit_group_submission($record->groupid); } } } else { @@ -609,6 +618,7 @@ abstract class assign_plugin { $filename))) { return null; } + return new file_info_stored($browser, $this->assignment->get_context(), $storedfile, @@ -616,7 +626,7 @@ abstract class assign_plugin { $filearea, $itemid, true, - true, + $writeaccess, false); } diff --git a/mod/assign/lang/en/assign.php b/mod/assign/lang/en/assign.php index 9a4e07c6c34..46eaeb92d8e 100644 --- a/mod/assign/lang/en/assign.php +++ b/mod/assign/lang/en/assign.php @@ -155,6 +155,7 @@ $string['submissionmodifiedgroup'] = 'The submission has been modified by somebo $string['duedatereached'] = 'The due date for this assignment has now passed'; $string['duedatevalidation'] = 'Due date must be after the allow submissions from date.'; $string['editattemptfeedback'] = 'Edit the grade and feedback for attempt number {$a}.'; +$string['editonline'] = 'Edit online'; $string['editingpreviousfeedbackwarning'] = 'You are editing the feedback for a previous attempt. This is attempt {$a->attemptnumber} out of {$a->totalattempts}.'; $string['editoverride'] = 'Edit override'; $string['editsubmission'] = 'Edit submission'; diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index aec06e8f61e..4deb7ba05d6 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -4360,6 +4360,25 @@ class assign { return false; } + /** + * Perform an access check to see if the current $USER can edit this group submission. + * + * @param int $groupid + * @return bool + */ + public function can_edit_group_submission($groupid) { + global $USER; + + $members = $this->get_submission_group_members($groupid, true); + foreach ($members as $member) { + // If we can edit any members submission, we can edit the submission for the group. + if ($this->can_edit_submission($member->id)) { + return true; + } + } + return false; + } + /** * Perform an access check to see if the current $USER can view this group submission. * diff --git a/mod/assign/renderable.php b/mod/assign/renderable.php index 6ee0e490ce7..8c6435e6a4f 100644 --- a/mod/assign/renderable.php +++ b/mod/assign/renderable.php @@ -911,6 +911,7 @@ class assign_files implements renderable { */ public function preprocess($dir, $filearea, $component) { global $CFG; + foreach ($dir['subdirs'] as $subdir) { $this->preprocess($subdir, $filearea, $component); } diff --git a/mod/assign/renderer.php b/mod/assign/renderer.php index 2abf463a503..a987385578d 100644 --- a/mod/assign/renderer.php +++ b/mod/assign/renderer.php @@ -1420,7 +1420,7 @@ class mod_assign_renderer extends plugin_renderer_base { $result .= '
  • ' . '
    ' . $image . ' ' . $file->fileurl . ' ' . - $plagiarismlinks . + $plagiarismlinks . ' ' . $file->portfoliobutton . '
    ' . '
  • '; } diff --git a/mod/assign/submission/file/locallib.php b/mod/assign/submission/file/locallib.php index a044a03ea7a..ba222f490d7 100644 --- a/mod/assign/submission/file/locallib.php +++ b/mod/assign/submission/file/locallib.php @@ -125,11 +125,11 @@ class assign_submission_file extends assign_submission_plugin { * @return array */ private function get_file_options() { - $fileoptions = array('subdirs'=>1, - 'maxbytes'=>$this->get_config('maxsubmissionsizebytes'), - 'maxfiles'=>$this->get_config('maxfilesubmissions'), - 'accepted_types'=>'*', - 'return_types'=>FILE_INTERNAL); + $fileoptions = array('subdirs' => 1, + 'maxbytes' => $this->get_config('maxsubmissionsizebytes'), + 'maxfiles' => $this->get_config('maxfilesubmissions'), + 'accepted_types' => '*', + 'return_types' => (FILE_INTERNAL | FILE_CONTROLLED_LINK)); if ($fileoptions['maxbytes'] == 0) { // Use module default. $fileoptions['maxbytes'] = get_config('assignsubmission_file', 'maxbytes'); @@ -174,7 +174,6 @@ class assign_submission_file extends assign_submission_plugin { * @return int */ private function count_files($submissionid, $area) { - $fs = get_file_storage(); $files = $fs->get_area_files($this->assignment->get_context()->id, 'assignsubmission_file', @@ -553,6 +552,19 @@ class assign_submission_file extends assign_submission_plugin { ); } + /** + * Make any controlled links in the submission area read-only for the student. + * + * @param stdClass $submission the assign_submission record being submitted. + * @return void + */ + public function submit_for_grading($submission) { + file_prevent_changes_to_external_files($this->assignment->get_context()->id, + 'assignsubmission_file', + ASSIGNSUBMISSION_FILE_FILEAREA, + $submission->id); + } + /** * Return the plugin configs for external functions. * diff --git a/mod/assign/submission/onlinetext/locallib.php b/mod/assign/submission/onlinetext/locallib.php index 8cd05284fd2..c57646022fa 100644 --- a/mod/assign/submission/onlinetext/locallib.php +++ b/mod/assign/submission/onlinetext/locallib.php @@ -174,7 +174,7 @@ class assign_submission_onlinetext extends assign_submission_plugin { 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->assignment->get_course()->maxbytes, 'context' => $this->assignment->get_context(), - 'return_types' => FILE_INTERNAL | FILE_EXTERNAL + 'return_types' => (FILE_INTERNAL | FILE_EXTERNAL | FILE_CONTROLLED_LINK) ); return $editoroptions; } diff --git a/mod/data/field/file/field.class.php b/mod/data/field/file/field.class.php index af455718aac..0263432c21d 100644 --- a/mod/data/field/file/field.class.php +++ b/mod/data/field/file/field.class.php @@ -85,7 +85,7 @@ class data_field_file extends data_field_base { $options->maxfiles = 1; // Limit to one file for the moment, this may be changed if requested as a feature in the future. $options->itemid = $itemid; $options->accepted_types = '*'; - $options->return_types = FILE_INTERNAL; + $options->return_types = FILE_INTERNAL | FILE_CONTROLLED_LINK; $options->context = $PAGE->context; $fm = new form_filemanager($options); @@ -185,6 +185,8 @@ class data_field_file extends data_field_base { $usercontext = context_user::instance($USER->id); $files = $fs->get_area_files($this->context->id, 'mod_data', 'content', $content->id, 'itemid, filepath, filename', false); + file_prevent_changes_to_external_files($this->context->id, 'mod_data', 'content', $content->id); + // We expect no or just one file (maxfiles = 1 option is set for the form_filemanager). if (count($files) == 0) { $content->content = null; diff --git a/mod/forum/classes/post_form.php b/mod/forum/classes/post_form.php index 6ee9586b1a6..a9e8e82e7eb 100644 --- a/mod/forum/classes/post_form.php +++ b/mod/forum/classes/post_form.php @@ -50,7 +50,7 @@ class mod_forum_post_form extends moodleform { 'maxbytes' => $maxbytes, 'maxfiles' => $forum->maxattachments, 'accepted_types' => '*', - 'return_types' => FILE_INTERNAL + 'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK ); } diff --git a/mod/forum/lib.php b/mod/forum/lib.php index 8c5ec559005..cef2dd0e197 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -559,13 +559,16 @@ function forum_cron() { } } + // We need to prevent changes to controlled links in attachments. + $modcontext = context_module::instance($coursemodules[$forumid]->id); + file_prevent_changes_to_external_files($modcontext->id, 'mod_forum', 'attachment', $pid); + // Save the Inbound Message datakey here to reduce DB queries later. $messageinboundgenerator->set_data($pid); $messageinboundhandlers[$pid] = $messageinboundgenerator->fetch_data_key(); // Caching subscribed users of each forum. if (!isset($subscribedusers[$forumid])) { - $modcontext = context_module::instance($coursemodules[$forumid]->id); if ($subusers = \mod_forum\subscriptions::fetch_subscribed_users($forums[$forumid], 0, $modcontext, 'u.*', true)) { foreach ($subusers as $postuser) { diff --git a/mod/wiki/filesedit.php b/mod/wiki/filesedit.php index 5edab9afae1..08378821386 100644 --- a/mod/wiki/filesedit.php +++ b/mod/wiki/filesedit.php @@ -83,7 +83,8 @@ $data = new stdClass(); $data->returnurl = $returnurl; $data->subwikiid = $subwiki->id; $maxbytes = get_max_upload_file_size($CFG->maxbytes, $COURSE->maxbytes); -$options = array('subdirs'=>0, 'maxbytes'=>$maxbytes, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL | FILE_REFERENCE); +$types = FILE_INTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK; +$options = array('subdirs'=>0, 'maxbytes'=>$maxbytes, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>$types); file_prepare_standard_filemanager($data, 'files', $options, $context, 'mod_wiki', 'attachments', $subwiki->id); $mform = new mod_wiki_filesedit_form(null, array('data'=>$data, 'options'=>$options)); diff --git a/mod/workshop/locallib.php b/mod/workshop/locallib.php index 9016190e3af..fc2cfe2b90f 100644 --- a/mod/workshop/locallib.php +++ b/mod/workshop/locallib.php @@ -1843,6 +1843,13 @@ class workshop { workshop_update_grades($workshop); } + if (self::PHASE_ASSESSMENT == $newphase) { + file_prevent_changes_to_external_files($this->context, 'mod_workshop', 'submission_content'); + } + if (self::PHASE_EVALUATION == $newphase) { + file_prevent_changes_to_external_files($this->context, 'mod_workshop', 'overallfeedback_attachment'); + } + $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id)); $this->phase = $newphase; $eventdata = array( @@ -2512,7 +2519,7 @@ class workshop { 'subdirs' => true, 'maxfiles' => $this->nattachments, 'maxbytes' => $this->maxbytes, - 'return_types' => FILE_INTERNAL, + 'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK, ); if ($acceptedtypes = self::normalize_file_extensions($this->submissionfiletypes)) { @@ -2554,7 +2561,7 @@ class workshop { 'subdirs' => 1, 'maxbytes' => $this->overallfeedbackmaxbytes, 'maxfiles' => $this->overallfeedbackfiles, - 'return_types' => FILE_INTERNAL, + 'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK, ); if ($acceptedtypes = self::normalize_file_extensions($this->overallfeedbackfiletypes)) { diff --git a/question/type/essay/renderer.php b/question/type/essay/renderer.php index f8db5516dec..a6352ef2580 100644 --- a/question/type/essay/renderer.php +++ b/question/type/essay/renderer.php @@ -115,7 +115,7 @@ class qtype_essay_renderer extends qtype_renderer { $pickeroptions->itemid = $qa->prepare_response_files_draft_itemid( 'attachments', $options->context->id); $pickeroptions->context = $options->context; - $pickeroptions->return_types = FILE_INTERNAL; + $pickeroptions->return_types = FILE_INTERNAL | FILE_CONTROLLED_LINK; $pickeroptions->itemid = $qa->prepare_response_files_draft_itemid( 'attachments', $options->context->id); diff --git a/repository/areafiles/lib.php b/repository/areafiles/lib.php index 364028b47c7..ac3045aa86f 100644 --- a/repository/areafiles/lib.php +++ b/repository/areafiles/lib.php @@ -106,6 +106,7 @@ class repository_areafiles extends repository { 'author' => $file->get_author(), 'license' => $file->get_license(), 'isref' => $file->is_external_file(), + 'iscontrolledlink' => $file->is_controlled_link(), 'icon' => $OUTPUT->image_url(file_file_icon($file, 24))->out(false), 'thumbnail' => $OUTPUT->image_url(file_file_icon($file, 90))->out(false) ); diff --git a/repository/filepicker.js b/repository/filepicker.js index 3a2ff4edd89..65d58464023 100644 --- a/repository/filepicker.js +++ b/repository/filepicker.js @@ -896,6 +896,9 @@ M.core_filepicker.init = function(Y, options) { if (node.isref) { classname = classname + ' fp-isreference'; } + if (node.iscontrolledlink) { + classname = classname + ' fp-iscontrolledlink'; + } if (node.refcount) { classname = classname + ' fp-hasreferences'; } @@ -1081,7 +1084,7 @@ M.core_filepicker.init = function(Y, options) { selectnode.one('.fp-thumbnail').setContent('').appendChild(imgnode); // filelink is the array of file-link-types available for this repository in this env - var filelinktypes = [2/*FILE_INTERNAL*/,1/*FILE_EXTERNAL*/,4/*FILE_REFERENCE*/]; + var filelinktypes = [2/*FILE_INTERNAL*/,1/*FILE_EXTERNAL*/,4/*FILE_REFERENCE*/,8/*FILE_CONTROLLED_LINK*/]; var filelink = {}, firstfilelink = null, filelinkcount = 0; for (var i in filelinktypes) { var allowed = (return_types & filelinktypes[i]) && @@ -1129,12 +1132,13 @@ M.core_filepicker.init = function(Y, options) { var selectnode = this.selectnode; var getfile = selectnode.one('.fp-select-confirm'); // bind labels with corresponding inputs - selectnode.all('.fp-saveas,.fp-linktype-2,.fp-linktype-1,.fp-linktype-4,.fp-setauthor,.fp-setlicense').each(function (node) { + 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()); }); selectnode.one('.fp-linktype-2 input').setAttrs({value: 2, name: 'linktype'}); selectnode.one('.fp-linktype-1 input').setAttrs({value: 1, name: 'linktype'}); selectnode.one('.fp-linktype-4 input').setAttrs({value: 4, name: 'linktype'}); + selectnode.one('.fp-linktype-8 input').setAttrs({value: 8, name: 'linktype'}); var changelinktype = function(e) { if (e.currentTarget.get('checked')) { var allowinputs = e.currentTarget.get('value') != 1/*FILE_EXTERNAL*/; @@ -1144,7 +1148,7 @@ M.core_filepicker.init = function(Y, options) { }); } }; - selectnode.all('.fp-linktype-2,.fp-linktype-1,.fp-linktype-4').each(function (node) { + selectnode.all('.fp-linktype-2,.fp-linktype-1,.fp-linktype-4,.fp-linktype-8').each(function (node) { node.one('input').on('change', changelinktype, this); }); this.populate_licenses_select(selectnode.one('.fp-setlicense select')); @@ -1182,6 +1186,10 @@ M.core_filepicker.init = function(Y, options) { (this.options.return_types & 4/*FILE_REFERENCE*/) && selectnode.one('.fp-linktype-4 input').get('checked')) { params['usefilereference'] = '1'; + } else if ((return_types & 8/*FILE_CONTROLLED_LINK*/) && + (this.options.return_types & 8/*FILE_CONTROLLED_LINK*/) && + selectnode.one('.fp-linktype-8 input').get('checked')) { + params['usecontrolledlink'] = '1'; } selectnode.addClass('loading'); diff --git a/repository/googledocs/classes/rest.php b/repository/googledocs/classes/rest.php index 38028ba3434..5c5e65a4a6c 100644 --- a/repository/googledocs/classes/rest.php +++ b/repository/googledocs/classes/rest.php @@ -73,6 +73,14 @@ class rest extends \core\oauth2\rest { ], 'response' => 'json' ], + 'delete' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', + 'method' => 'delete', + 'args' => [ + 'fileid' => PARAM_RAW + ], + 'response' => 'json' + ], 'create' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files', 'method' => 'post', diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 14fdef85a9b..c95942033f6 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -26,7 +26,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/repository/lib.php'); -require_once($CFG->libdir . '/google/lib.php'); +require_once($CFG->libdir . '/filebrowser/file_browser.php'); /** * Google Docs Plugin @@ -73,16 +73,21 @@ class repository_googledocs extends repository { /** * Get a cached user authenticated oauth client. * + * @param moodle_url $usecurrenturl - Use this url instead of the repo callback. * @return \core\oauth2\client */ - protected function get_user_oauth_client() { + protected function get_user_oauth_client($overrideurl = false) { if ($this->client) { return $this->client; } - $returnurl = new moodle_url('/repository/repository_callback.php'); - $returnurl->param('callback', 'yes'); - $returnurl->param('repo_id', $this->id); - $returnurl->param('sesskey', sesskey()); + 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); @@ -455,8 +460,6 @@ class repository_googledocs extends repository { /** * Tells how the file can be picked from this repository. * - * Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE. - * * @return int */ public function supported_returntypes() { @@ -466,9 +469,9 @@ class repository_googledocs extends repository { if ($setting == 'internal') { return FILE_INTERNAL; } else if ($setting == 'external') { - return FILE_REFERENCE; + return FILE_CONTROLLED_LINK; } else { - return FILE_REFERENCE | FILE_INTERNAL; + return FILE_CONTROLLED_LINK | FILE_INTERNAL; } } else { return FILE_INTERNAL; @@ -486,7 +489,7 @@ class repository_googledocs extends repository { if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') { return FILE_INTERNAL; } else { - return FILE_REFERENCE; + return FILE_CONTROLLED_LINK; } } @@ -526,14 +529,127 @@ class repository_googledocs extends repository { public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) { $source = json_decode($storedfile->get_reference()); + $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()); + + if ($info->is_writable()) { + // Add the current user as an OAuth writer. + $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); + + if ($systemauth === false) { + $details = 'Cannot connect as system user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $systemservice = new repository_googledocs\rest($systemauth); + + // Get the user oauth so we can get the account to add. + $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(), + $storedfile->get_component(), + $storedfile->get_filearea(), + $storedfile->get_itemid(), + $storedfile->get_filepath(), + $storedfile->get_filename(), + $forcedownload); + $url->param('sesskey', sesskey()); + $userauth = $this->get_user_oauth_client($url); + if (!$userauth->is_logged_in()) { + redirect($userauth->get_login_url()); + } + if ($userauth === false) { + $details = 'Cannot connect as current user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $userinfo = $userauth->get_userinfo(); + $useremail = $userinfo['email']; + + $this->add_temp_writer_to_file($systemservice, $source->id, $useremail); + } + if ($source->link) { - header('Location: ' . $source->link); + redirect($source->link); } else { $details = 'File is missing source link'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } } + /** + * Update an external file so only Moodle has write access to it. + * This function must be implemented by all repositories supporting FILE_CONTROLLED_LINK return types. + * + * Throw exceptions on error and the transaction will be rolled back + * (because it is called on an entire filearea at a time). + * + * @param stored_file $file + */ + public function prevent_changes_to_external_file(stored_file $file) { + global $DB; + + // Copy the file (will make it owned by moodle system account). + // Update the sharing settings on the file. + // Prevent editors from sharing the file. + $source = json_decode($file->get_reference()); + + $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); + + if ($systemauth === false) { + $details = 'Cannot connect as system user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $systemservice = new repository_googledocs\rest($systemauth); + + // Copy the file so we get a snapshot file owned by Moodle. + $newsource = $this->copy_file($systemservice, $source->id); + + // Set the sharing options. + $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id); + $this->prevent_writers_from_sharing_file($systemservice, $newsource->id); + // Delete the original file from the Moodle account. This only deletes it for us (not the original owner). + + $summary = $this->get_file_summary($systemservice, $source->id); + if (!empty($summary->parents[0])) { + $myparent = $summary->parents[0]; + $this->remove_file_parent($systemservice, $source->id, $myparent); + } + // We need to change the source on the existing file now to point to the new id. + $source->id = $newsource->id; + $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : ''; + if (empty($source->link)) { + $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : ''; + } + $reference = json_encode($source); + $file->set_source($reference); + + // We need to update the reference in the file_reference table. + $refid = $file->get_referencefileid(); + $newref = (object) [ + 'id' => $refid, + 'reference' => $reference, + 'referencehash' => sha1($reference) + ]; + $DB->update_record('files_reference', $newref); + + return true; + } + + /** + * Grant write access and redirect to an edit link for the file. + * + * @param stored_file $storedfile the file that contains the reference + */ + public function edit_external_file($storedfile) { + // Grant writer access to this file. + + // Redirect to the file. + $this->send_file($storedfile); + } + /** * List the permissions on a file. * @param \core\oauth2\client $client Authenticated client. @@ -616,7 +732,7 @@ class repository_googledocs extends repository { * @return stdClass */ protected function get_file_summary(\repository_googledocs\rest $client, $fileid) { - $fields = "id,name,owners"; + $fields = "id,name,owners,parents"; $params = [ 'fileid' => $fileid, 'fields' => $fields @@ -670,6 +786,53 @@ class repository_googledocs extends repository { return $fileinfo; } + /** + * Delete a file (for the current user). + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are deleting. + * @return boolean + */ + protected function delete_file($client, $fileid) { + $params = ['fileid' => $fileid]; + $response = $client->call('delete', $params, ' '); + var_dump($response); + if (empty($response->id)) { + $details = 'Cannot delete file: ' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return true; + } + + /** + * Add a writer to the permissions on the file (temporary). + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are updating. + * @param string $email The email of the writer account to add. + * @return boolean + */ + protected function add_temp_writer_to_file($client, $fileid, $email) { + // Expires in 7 days. + $expires = new DateTime(); + $expires->add(new DateInterval("P7D")); + + $updateeditor = [ + 'emailAddress' => $email, + 'role' => 'writer', + 'type' => 'user', + 'expirationTime' => $expires->format(DateTime::RFC3339) + ]; + $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false']; + $response = $client->call('create_permission', $params, json_encode($updateeditor)); + if (empty($response->id)) { + $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return true; + } + + /** * Add a writer to the permissions on the file. * @@ -714,6 +877,27 @@ class repository_googledocs extends repository { return true; } + /** + * Remove parent + * + * @param \core\oauth2\client $client Authenticated client. + * @param string $fileid The file we are updating. + * @param string $folderid The id of the folder we are removing + * @return boolean + */ + protected function remove_file_parent($client, $fileid, $folderid) { + // Set the parent. + $params = [ + 'fileid' => $fileid, 'removeParents' => $folderid + ]; + $response = $client->call('update', $params, ' '); + if (empty($response->id)) { + $details = 'Cannot remove the file parent: ' . $fileid . ', ' . $folderid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return true; + } + /** * Prevent writers from sharing. * @@ -906,7 +1090,7 @@ class repository_googledocs extends repository { $choices = [ FILE_INTERNAL => get_string('internal', 'repository_googledocs'), - FILE_REFERENCE => get_string('external', 'repository_googledocs'), + FILE_CONTROLLED_LINK => get_string('external', 'repository_googledocs'), ]; $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_googledocs'), $choices); diff --git a/repository/lib.php b/repository/lib.php index 6ed4db745bd..f30c5cb7e01 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -30,6 +30,8 @@ require_once($CFG->libdir . '/formslib.php'); define('FILE_EXTERNAL', 1); define('FILE_INTERNAL', 2); define('FILE_REFERENCE', 4); +define('FILE_CONTROLLED_LINK', 8); + define('RENAME_SUFFIX', '_2'); /** @@ -1000,7 +1002,7 @@ abstract class repository implements cacheable_object { * onlyvisible : bool (default true) * type : string return instances of this type only * accepted_types : string|array return instances that contain files of those types (*, web_image, .pdf, ...) - * return_types : int combination of FILE_INTERNAL & FILE_EXTERNAL & FILE_REFERENCE. + * return_types : int combination of FILE_INTERNAL & FILE_EXTERNAL & FILE_REFERENCE & FILE_CONTROLLED_LINK. * 0 means every type. The default is FILE_INTERNAL | FILE_EXTERNAL. * userid : int if specified, instances belonging to other users will not be returned * @@ -2678,6 +2680,19 @@ abstract class repository implements cacheable_object { 'Use repository::sync_reference instead.'); } + /** + * Update an external file so only Moodle has write access to it. + * This function must be implemented by all repositories supporting FILE_CONTROLLED_LINK return types. + * + * Throw exceptions on error and the transaction will be rolled back + * (because it is called on an entire filearea at a time). + * + * @param stored_file $file + */ + public function prevent_changes_to_external_file(stored_file $file) { + return; + } + /** * Performs synchronisation of an external file if the previous one has expired. * diff --git a/repository/repository_ajax.php b/repository/repository_ajax.php index ba703765f1b..20ae194f2fc 100644 --- a/repository/repository_ajax.php +++ b/repository/repository_ajax.php @@ -51,6 +51,7 @@ $saveas_path = optional_param('savepath', '/', PARAM_PATH); // save as file $search_text = optional_param('s', '', PARAM_CLEANHTML); $linkexternal = optional_param('linkexternal', '', PARAM_ALPHA); $usefilereference = optional_param('usefilereference', false, PARAM_BOOL); +$usecontrolledlink = optional_param('usecontrolledlink', false, PARAM_BOOL); list($context, $course, $cm) = get_context_info_array($contextid); require_login($course, false, $cm, false, true); @@ -220,7 +221,7 @@ switch ($action) { } } - if ($usefilereference) { + if ($usefilereference || $usecontrolledlink) { if ($repo->has_moodle_files()) { $sourcefile = repository::get_moodle_file($reference); $record->contenthash = $sourcefile->get_contenthash(); diff --git a/theme/boost/templates/core/filemanager_selectlayout.mustache b/theme/boost/templates/core/filemanager_selectlayout.mustache index 03e89722c37..110a5e816b4 100644 --- a/theme/boost/templates/core/filemanager_selectlayout.mustache +++ b/theme/boost/templates/core/filemanager_selectlayout.mustache @@ -24,6 +24,12 @@ {{#str}}makefilereference, repository{{/str}}
    +
    + +
    From 1dca8d1a1d846ee19139cdc6ced8da4b9331cafe Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 8 Mar 2017 22:03:09 +0800 Subject: [PATCH 19/84] MDL-58168 auth_oauth2: Allow linking other accounts Part of MDL-58220 --- auth/oauth2/classes/api.php | 147 ++++++++++++++++++++++++ auth/oauth2/classes/auth.php | 47 +++++--- auth/oauth2/classes/linked_login.php | 63 ++++++++++ auth/oauth2/classes/output/renderer.php | 96 ++++++++++++++++ auth/oauth2/config.html | 32 ++++++ auth/oauth2/db/access.php | 34 ++++++ auth/oauth2/db/install.xml | 30 +++++ auth/oauth2/db/upgrade.php | 74 ++++++++++++ auth/oauth2/lang/en/auth_oauth2.php | 10 ++ auth/oauth2/lib.php | 19 +++ auth/oauth2/linkedlogins.php | 103 +++++++++++++++++ auth/oauth2/version.php | 2 +- 12 files changed, 640 insertions(+), 17 deletions(-) create mode 100644 auth/oauth2/classes/api.php create mode 100644 auth/oauth2/classes/linked_login.php create mode 100644 auth/oauth2/classes/output/renderer.php create mode 100644 auth/oauth2/config.html create mode 100644 auth/oauth2/db/access.php create mode 100644 auth/oauth2/db/install.xml create mode 100644 auth/oauth2/db/upgrade.php create mode 100644 auth/oauth2/lib.php create mode 100644 auth/oauth2/linkedlogins.php diff --git a/auth/oauth2/classes/api.php b/auth/oauth2/classes/api.php new file mode 100644 index 00000000000..ce02ce39c89 --- /dev/null +++ b/auth/oauth2/classes/api.php @@ -0,0 +1,147 @@ +. + +/** + * Class for loading/storing oauth2 linked logins from the DB. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace auth_oauth2; + +use context_user; +use stdClass; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Static list of api methods for auth oauth2 configuration. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + + /** + * List linked logins + * + * Requires auth/oauth2:managelinkedlogins capability at the user context. + * + * @param int $userid (defaults to $USER->id) + * @return boolean + */ + public static function get_linked_logins($userid = false) { + global $USER; + + if ($userid === false) { + $userid = $USER->id; + } + + if (\core\session\manager::is_loggedinas()) { + throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); + } + + $context = context_user::instance($userid); + require_capability('auth/oauth2:managelinkedlogins', $context); + + return linked_login::get_records(['userid' => $userid]); + } + + /** + * See if there is a match for this username and issuer in the linked_login table. + * + * @param string $username as returned from an oauth client. + * @param \core\oauth2\issuer $issuer + * @return stdClass User record if found. + */ + public static function match_username_to_user($username, $issuer) { + $params = [ + 'issuerid' => $issuer->get('id'), + 'username' => $username + ]; + $match = linked_login::get_record($params); + + if ($match) { + $user = get_complete_user_data('id', $match->get('userid')); + + return $user; + } + return false; + } + + /** + * Link a login to this account. + * + * Requires auth/oauth2:managelinkedlogins capability at the user context. + * + * @param array $userinfo as returned from an oauth client. + * @param \core\oauth2\issuer $issuer + * @param int $userid (defaults to $USER->id) + * @return boolean + */ + public static function link_login($userinfo, $issuer, $userid = false) { + global $USER; + + if ($userid === false) { + $userid = $USER->id; + } + + if (\core\session\manager::is_loggedinas()) { + throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); + } + + $context = context_user::instance($userid); + require_capability('auth/oauth2:managelinkedlogins', $context); + + + $record = new stdClass(); + $record->issuerid = $issuer->get('id'); + $record->username = $userinfo['username']; + $record->email = $userinfo['email']; + $record->userid = $userid; + $existing = linked_login::get_record((array)$record); + if ($existing) { + return $existing; + } + $linkedlogin = new linked_login(0, $record); + return $linkedlogin->create(); + } + + /** + * Delete linked login + * + * Requires auth/oauth2:managelinkedlogins capability at the user context. + * + * @param int $linkedloginid + * @return boolean + */ + public static function delete_linked_login($linkedloginid) { + $login = new linked_login($linkedloginid); + $userid = $login->get('userid'); + + if (\core\session\manager::is_loggedinas()) { + throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); + } + + $context = context_user::instance($userid); + require_capability('auth/oauth2:managelinkedlogins', $context); + + $login->delete(); + } +} diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 22906567a9d..677ec630b2e 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -156,14 +156,8 @@ class auth extends \auth_plugin_base { * @param array $userfields */ public function config_form($config, $err, $userfields) { - echo get_string('plugindescription', 'auth_oauth2'); + include(__DIR__ . "/../config.html"); - // Force all fields updated on login and locked. - - foreach ($userfields as $field) { - set_config('field_updatelocal_' . $field, 'onlogin', 'auth_oauth2'); - set_config('field_lock_' . $field, 'unlockedifempty', 'auth_oauth2'); - } return; } @@ -312,6 +306,14 @@ class auth extends \auth_plugin_base { return true; } + public function process_config($config) { + // Set to defaults if undefined + if (!isset($config->allowlinkedlogins)) { + $config->allowlinkedlogins = false; + } + set_config('allowlinkedlogins', trim($config->allowlinkedlogins), 'auth_oauth2'); + } + /** * Complete the login process after oauth handshake is complete. * @param \core\oauth2\client $client @@ -336,20 +338,33 @@ class auth extends \auth_plugin_base { $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); - if (!empty($userinfo['picture'])) { - $this->set_static_user_picture($userinfo['picture']); - unset($userinfo['picture']); - } + $userwasmapped = false; + if (get_config('auth_oauth2', 'allowlinkedlogins')) { + $mappeduser = api::match_username_to_user($userinfo['username'], $client->get_issuer()); - if (!empty($userinfo['lang'])) { - $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); - if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { - unset($userinfo['lang']); + if ($mappeduser) { + $userinfo = (array) $mappeduser; + $userwasmapped = true; } } + + if (!$userwasmapped) { + if (!empty($userinfo['picture'])) { + $this->set_static_user_picture($userinfo['picture']); + unset($userinfo['picture']); + } + + if (!empty($userinfo['lang'])) { + $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); + if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { + unset($userinfo['lang']); + } + } + } + $this->set_static_user_info($userinfo); - $user = authenticate_user_login($userinfo['username'], ''); + $user = get_complete_user_data('username', $userinfo['username']); if ($user) { complete_user_login($user); diff --git a/auth/oauth2/classes/linked_login.php b/auth/oauth2/classes/linked_login.php new file mode 100644 index 00000000000..93dfd5702cf --- /dev/null +++ b/auth/oauth2/classes/linked_login.php @@ -0,0 +1,63 @@ +. + +/** + * Class for loading/storing issuers from the DB. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace auth_oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing issuer from the DB + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class linked_login extends persistent { + + /** @const TABLE */ + const TABLE = 'auth_oauth2_linked_login'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'issuerid' => array( + 'type' => PARAM_INT + ), + 'userid' => array( + 'type' => PARAM_INT + ), + 'username' => array( + 'type' => PARAM_RAW + ), + 'email' => array( + 'type' => PARAM_RAW + ) + ); + } + +} diff --git a/auth/oauth2/classes/output/renderer.php b/auth/oauth2/classes/output/renderer.php new file mode 100644 index 00000000000..dbe2048810e --- /dev/null +++ b/auth/oauth2/classes/output/renderer.php @@ -0,0 +1,96 @@ +. + +/** + * Output rendering for the plugin. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace auth_oauth2\output; + +use plugin_renderer_base; +use html_table; +use html_table_cell; +use html_table_row; +use html_writer; +use auth\oauth2\linked_login; +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Implements the plugin renderer + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + /** + * This function will render one beautiful table with all the linked_logins. + * + * @param \auth\oauth2\linked_login[] $linkedlogins - list of all linked logins. + * @return string HTML to output. + */ + public function linked_logins_table($linkedlogins) { + global $CFG, $OUTPUT; + + $table = new html_table(); + $table->head = [ + get_string('issuer', 'auth_oauth2'), + get_string('info', 'auth_oauth2'), + get_string('edit'), + ]; + $table->attributes['class'] = 'admintable generaltable'; + $data = []; + + $index = 0; + + foreach ($linkedlogins as $linkedlogin) { + // Issuer. + $issuerid = $linkedlogin->get('issuerid'); + $issuer = \core\oauth2\api::get_issuer($issuerid); + $issuercell = new html_table_cell(s($issuer->get('name'))); + + // Issuer. + $username = $linkedlogin->get('username'); + $email = $linkedlogin->get('email'); + $usernamecell = new html_table_cell(s($email) . ', (' . s($username) . ')'); + + $links = ''; + + // Delete. + $deleteparams = ['linkedloginid' => $linkedlogin->get('id'), 'action' => 'delete', 'sesskey' => sesskey()]; + $deleteurl = new moodle_url('/auth/oauth2/linkedlogins.php', $deleteparams); + $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $links .= ' ' . $deletelink; + + $editcell = new html_table_cell($links); + + $row = new html_table_row([ + $issuercell, + $usernamecell, + $editcell, + ]); + + $data[] = $row; + $index++; + } + $table->data = $data; + return html_writer::table($table); + } +} diff --git a/auth/oauth2/config.html b/auth/oauth2/config.html new file mode 100644 index 00000000000..b78655a748d --- /dev/null +++ b/auth/oauth2/config.html @@ -0,0 +1,32 @@ + +
    + +
    +allowlinkedlogins)) { + $config->allowlinkedlogins = true; +} +?> + + + + + + + +authtype, $userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); + +?> +
    + + + allowlinkedlogins) { echo 'checked="checked"'; } ?> + > + error_text($err['allowlinkedlogins']); } ?> + + +
    diff --git a/auth/oauth2/db/access.php b/auth/oauth2/db/access.php new file mode 100644 index 00000000000..53864726cd9 --- /dev/null +++ b/auth/oauth2/db/access.php @@ -0,0 +1,34 @@ +. + +/** + * Capability definitions for this plugin. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +$capabilities = [ + + 'auth/oauth2:managelinkedlogins' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ), +]; diff --git a/auth/oauth2/db/install.xml b/auth/oauth2/db/install.xml new file mode 100644 index 00000000000..3e4933822df --- /dev/null +++ b/auth/oauth2/db/install.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    diff --git a/auth/oauth2/db/upgrade.php b/auth/oauth2/db/upgrade.php new file mode 100644 index 00000000000..45ebeb90685 --- /dev/null +++ b/auth/oauth2/db/upgrade.php @@ -0,0 +1,74 @@ +. + +/** + * OAuth2 authentication plugin upgrade code + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * @param int $oldversion the version we are upgrading from + * @return bool result + */ +function xmldb_auth_oauth2_upgrade($oldversion) { + global $DB; + + $dbman = $DB->get_manager(); + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + if ($oldversion < 2017030700) { + + // Define table auth_oauth2_linked_login to be created. + $table = new xmldb_table('auth_oauth2_linked_login'); + + // Adding fields to table auth_oauth2_linked_login. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('issuerid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('username', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('email', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table auth_oauth2_linked_login. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('usermodified_key', XMLDB_KEY_FOREIGN, array('usermodified'), 'user', array('id')); + $table->add_key('userid_key', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id')); + $table->add_key('issuerid_key', XMLDB_KEY_FOREIGN, array('issuerid'), 'oauth2_issuer', array('id')); + $table->add_key('uniq_key', XMLDB_KEY_UNIQUE, array('userid', 'issuerid', 'username')); + + // Adding indexes to table auth_oauth2_linked_login. + $table->add_index('search_index', XMLDB_INDEX_NOTUNIQUE, array('issuerid', 'username')); + + // Conditionally launch create table for auth_oauth2_linked_login. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Oauth2 savepoint reached. + upgrade_plugin_savepoint(true, 2017030700, 'auth', 'oauth2'); + } + + return true; +} diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php index ce34fa5aae2..74ee233d0c4 100644 --- a/auth/oauth2/lang/en/auth_oauth2.php +++ b/auth/oauth2/lang/en/auth_oauth2.php @@ -27,3 +27,13 @@ $string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; $string['notloggedin'] = 'The login attempt failed.'; $string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; $string['pluginname'] = 'OAuth 2'; +$string['oauth2:managelinkedlogins'] = 'Manage own linked login accounts'; +$string['linkedlogins'] = 'Linked logins'; +$string['linkedloginshelp'] = 'Help with linked logins.'; +$string['notwhileloggedinas'] = 'Linked logins cannot be managed while logged in as another user.'; +$string['issuer'] = 'OAuth 2 Service'; +$string['info'] = 'External account'; +$string['createnewlinkedlogin'] = 'Link a new account ({$a})'; +$string['allowlinkedlogins'] = 'Allow linked logins'; +$string['allowlinkedloginsdesc'] = 'Linked logins allow users to link their Moodle account to another external account which they can use to login with.'; +$string['createaccountswarning'] = 'This authentication plugin allows users to create accounts on your site. You may want to enable the setting "authpreventaccountcreation" if you use this plugin.'; diff --git a/auth/oauth2/lib.php b/auth/oauth2/lib.php new file mode 100644 index 00000000000..17040112bd3 --- /dev/null +++ b/auth/oauth2/lib.php @@ -0,0 +1,19 @@ +parent->find('useraccount', navigation_node::TYPE_CONTAINER); + $thingnode = $parent->add(get_string('linkedlogins', 'auth_oauth2'), new moodle_url('/auth/oauth2/linkedlogins.php')); + } + } + } +} + diff --git a/auth/oauth2/linkedlogins.php b/auth/oauth2/linkedlogins.php new file mode 100644 index 00000000000..a5ffbb08151 --- /dev/null +++ b/auth/oauth2/linkedlogins.php @@ -0,0 +1,103 @@ +. + +/** + * OAuth 2 Linked login configuration page. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/tablelib.php'); + +$PAGE->set_url('/auth/oauth2/linkedlogins.php'); +$PAGE->set_context(context_user::instance($USER->id)); +$PAGE->set_pagelayout('admin'); +$strheading = get_string('linkedlogins', 'auth_oauth2'); +$PAGE->set_title($strheading); +$PAGE->set_heading($strheading); + +require_login(); + +if (!get_config('auth_oauth2', 'allowlinkedlogins')) { + throw new moodle_exception('Linked logins are disabled.'); +} + +$action = optional_param('action', '', PARAM_ALPHAEXT); +if ($action == 'new') { + require_sesskey(); + $issuerid = required_param('issuerid', PARAM_INT); + $issuer = \core\oauth2\api::get_issuer($issuerid); + + + // We do a login dance with this issuer. + $addparams = ['action' => 'new', 'issuerid' => $issuerid, 'sesskey' => sesskey()]; + $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams); + $client = \core\oauth2\api::get_user_oauth_client($issuer, $addurl); + + if (optional_param('logout', false, PARAM_BOOL)) { + $client->log_out(); + } + + if (!$client->is_logged_in()) { + redirect($client->get_login_url()); + } + + $userinfo = $client->get_userinfo(); + + if (!empty($userinfo)) { + \auth_oauth2\api::link_login($userinfo, $issuer); + redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); + } else { + redirect($PAGE->url, get_string('notloggedin', 'auth_oauth2'), null, \core\output\notification::NOTIFY_ERROR); + } +} else if ($action == 'delete') { + require_sesskey(); + $linkedloginid = required_param('linkedloginid', PARAM_INT); + + auth_oauth2\api::delete_linked_login($linkedloginid); + redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); +} + +$renderer = $PAGE->get_renderer('auth_oauth2'); + +$linkedloginid = optional_param('id', '', PARAM_RAW); +$linkedlogin = null; + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('linkedlogins', 'auth_oauth2')); +echo $OUTPUT->doc_link('Linked_Logins', get_string('linkedloginshelp', 'auth_oauth2')); +$linkedlogins = auth_oauth2\api::get_linked_logins(); + +echo $renderer->linked_logins_table($linkedlogins); + +$issuers = \core\oauth2\api::get_all_issuers(); + +foreach ($issuers as $issuer) { + if (!$issuer->is_authentication_supported()) { + continue; + } + + $addparams = ['action' => 'new', 'issuerid' => $issuer->get('id'), 'sesskey' => sesskey(), 'logout' => true]; + $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams); + echo $renderer->single_button($addurl, get_string('createnewlinkedlogin', 'auth_oauth2', s($issuer->get('name')))); +} +echo $OUTPUT->footer(); + + diff --git a/auth/oauth2/version.php b/auth/oauth2/version.php index 11f8fe144dc..7b2703b2298 100644 --- a/auth/oauth2/version.php +++ b/auth/oauth2/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2017030700; // The current plugin version (Date: YYYYMMDDXX) $plugin->requires = 2016112900; // Requires this Moodle version $plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics) From 5ffaf17dad39b549cdcc0e3e261531970a1b8597 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 9 Mar 2017 09:15:57 +0800 Subject: [PATCH 20/84] MDL-58168 oauth2: Check for new access code before checking scopes Part of MDL-58220 --- lib/oauthlib.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/oauthlib.php b/lib/oauthlib.php index 19771cc2e54..53838c91ab1 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -441,6 +441,13 @@ abstract class oauth2_client extends curl { return false; } + // If we've been passed then authorization code generated by the + // authorization server try and upgrade the token to an access token. + $code = optional_param('oauth2code', null, PARAM_RAW); + if ($code && $this->upgrade_token($code)) { + return true; + } + // We have a token so we are logged in. if (isset($this->accesstoken->token)) { // Check that the access token has all the requested scopes. @@ -455,13 +462,6 @@ abstract class oauth2_client extends curl { return true; } - // If we've been passed then authorization code generated by the - // authorization server try and upgrade the token to an access token. - $code = optional_param('oauth2code', null, PARAM_RAW); - if ($code && $this->upgrade_token($code)) { - return true; - } - return false; } From 5823a27e7e673f71e25a2342b13e7af544ee9925 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 9 Mar 2017 09:38:02 +0800 Subject: [PATCH 21/84] MDL-58131 googledocs: Add manage url -> drive Part of MDL-58220 --- files/renderer.php | 2 +- repository/googledocs/lib.php | 3 +++ .../templates/core/filemanager_modal_generallayout.mustache | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/files/renderer.php b/files/renderer.php index f63b336faed..f5f39d83fb5 100644 --- a/files/renderer.php +++ b/files/renderer.php @@ -601,7 +601,7 @@ class core_files_renderer extends plugin_renderer_base {
    diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index c95942033f6..c62bbdab167 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -224,6 +224,8 @@ class repository_googledocs extends repository { $ret['defaultreturntype'] = $this->default_returntype(); $ret['path'] = $this->build_breadcrumb($path); $ret['list'] = $results; + $ret['manage'] = 'https://drive.google.com/'; + return $ret; } @@ -248,6 +250,7 @@ class repository_googledocs extends repository { $ret['dynload'] = true; $ret['path'] = $this->build_breadcrumb($path); $ret['list'] = $results; + $ret['manage'] = 'https://drive.google.com/'; return $ret; } diff --git a/theme/boost/templates/core/filemanager_modal_generallayout.mustache b/theme/boost/templates/core/filemanager_modal_generallayout.mustache index e3b41e22026..f55f2d03b66 100644 --- a/theme/boost/templates/core/filemanager_modal_generallayout.mustache +++ b/theme/boost/templates/core/filemanager_modal_generallayout.mustache @@ -27,7 +27,7 @@
    From 1a1a09d8f887daae5400f2206d6561d53db08d64 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 10 Mar 2017 10:37:29 +0800 Subject: [PATCH 22/84] MDL-58142 calendar: Add an obvious link to the help docs Part of MDL-58220 --- calendar/classes/export_form.php | 4 +++- calendar/export.php | 1 + lang/en/calendar.php | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/calendar/classes/export_form.php b/calendar/classes/export_form.php index c7a782563d7..6789a8a68df 100644 --- a/calendar/classes/export_form.php +++ b/calendar/classes/export_form.php @@ -42,9 +42,11 @@ class core_calendar_export_form extends moodleform { * @throws coding_exception */ public function definition() { - global $CFG; + global $CFG, $OUTPUT; $mform = $this->_form; + $mform->addElement('html', '

    ' . $OUTPUT->doc_link('calendar/export', get_string('exporthelp', 'calendar'), true) . '

    '); + $export = array(); $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventsall', 'calendar'), 'all'); $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventsrelatedtocourses', 'calendar'), 'courses'); diff --git a/calendar/export.php b/calendar/export.php index 979c89272b9..8c5d175be65 100644 --- a/calendar/export.php +++ b/calendar/export.php @@ -169,4 +169,5 @@ if ($action != 'advanced') { echo $calendarurl; echo $renderer->complete_layout(); + echo $OUTPUT->footer(); diff --git a/lang/en/calendar.php b/lang/en/calendar.php index 9782884e5f2..a24c0f32cb6 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -117,6 +117,7 @@ $string['eventsubscriptioneditwarning'] = 'This calendar event is part of a subs $string['expired'] = 'Expired'; $string['explain_site_timeformat'] = 'You can choose to see times in either 12 or 24 hour format for the whole site. If you choose "default", then the format will be automatically chosen according to the language you use in the site. This setting can be overridden by user preferences.'; $string['export'] = 'Export'; +$string['exporthelp'] = 'How do I subscribe to this calendar from a calendar application (Google/Outlook/Other)?'; $string['exportbutton'] = 'Export'; $string['exportcalendar'] = 'Export calendar'; $string['forcecalendartype'] = 'Force calendar'; From 28dddbc1299ac0f0dbac226f3cf5fcc3ca7f4829 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 10 Mar 2017 12:11:17 +0800 Subject: [PATCH 23/84] MDL-58219 oauth2: Show connected system account info Show the username and email of the connected system account (if it ever requires refreshing - this will help identity the account to re-authorise). Part of MDL-58220 --- admin/tool/oauth2/classes/output/renderer.php | 4 +++- auth/oauth2/db/install.xml | 6 +++--- auth/oauth2/db/upgrade.php | 13 +++++++++++++ auth/oauth2/version.php | 2 +- lib/classes/oauth2/api.php | 5 +++++ lib/classes/oauth2/system_account.php | 6 ++++++ lib/db/install.xml | 4 +++- lib/db/upgrade.php | 2 ++ version.php | 2 +- 9 files changed, 37 insertions(+), 7 deletions(-) mode change 100644 => 100755 auth/oauth2/db/install.xml diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index a9568d8de68..d38d7f00848 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -114,7 +114,9 @@ class renderer extends plugin_renderer_base { // Connected. if ($issuer->is_system_account_connected()) { - $systemauth = $OUTPUT->pix_icon('yes', get_string('systemaccountconnected', 'tool_oauth2'), 'tool_oauth2'); + $systemaccount = \core\oauth2\api::get_system_account($issuer); + $systemauth = s($systemaccount->get('email')) . ' (' . s($systemaccount->get('username')). ') '; + $systemauth .= $OUTPUT->pix_icon('yes', get_string('systemaccountconnected', 'tool_oauth2'), 'tool_oauth2'); } else { $systemauth = $OUTPUT->pix_icon('no', get_string('systemaccountnotconnected', 'tool_oauth2'), 'tool_oauth2'); } diff --git a/auth/oauth2/db/install.xml b/auth/oauth2/db/install.xml old mode 100644 new mode 100755 index 3e4933822df..be895b68d36 --- a/auth/oauth2/db/install.xml +++ b/auth/oauth2/db/install.xml @@ -1,5 +1,5 @@ - @@ -13,7 +13,7 @@ - + @@ -27,4 +27,4 @@ - + \ No newline at end of file diff --git a/auth/oauth2/db/upgrade.php b/auth/oauth2/db/upgrade.php index 45ebeb90685..e117395fec2 100644 --- a/auth/oauth2/db/upgrade.php +++ b/auth/oauth2/db/upgrade.php @@ -70,5 +70,18 @@ function xmldb_auth_oauth2_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2017030700, 'auth', 'oauth2'); } + if ($oldversion < 2017031000) { + + // Changing type of field email on table auth_oauth2_linked_login to text. + $table = new xmldb_table('auth_oauth2_linked_login'); + $field = new xmldb_field('email', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null, 'username'); + + // Launch change of type for field email. + $dbman->change_field_type($table, $field); + + // Oauth2 savepoint reached. + upgrade_plugin_savepoint(true, 2017031000, 'auth', 'oauth2'); + } + return true; } diff --git a/auth/oauth2/version.php b/auth/oauth2/version.php index 7b2703b2298..82fb444a534 100644 --- a/auth/oauth2/version.php +++ b/auth/oauth2/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2017030700; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2017031000; // The current plugin version (Date: YYYYMMDDXX) $plugin->requires = 2016112900; // Requires this Moodle version $plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics) diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 51fd8fca2c4..436e1cc983e 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -715,10 +715,15 @@ class api { if ($systemaccount) { $systemaccount->delete(); } + + $userinfo = $client->get_userinfo(); + $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->refreshtoken = $refreshtoken; $record->grantedscopes = $scopes; + $record->email = $userinfo['email']; + $record->username = $userinfo['username']; $systemaccount = new system_account(0, $record); diff --git a/lib/classes/oauth2/system_account.php b/lib/classes/oauth2/system_account.php index 2f770fc0315..c30b0d02fea 100644 --- a/lib/classes/oauth2/system_account.php +++ b/lib/classes/oauth2/system_account.php @@ -55,6 +55,12 @@ class system_account extends persistent { ), 'grantedscopes' => array( 'type' => PARAM_RAW, + ), + 'email' => array( + 'type' => PARAM_RAW, + ), + 'username' => array( + 'type' => PARAM_RAW, ) ); } diff --git a/lib/db/install.xml b/lib/db/install.xml index 29518ad8246..7610a685c53 100755 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3510,6 +3510,8 @@ + + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 06988419548..5deb30d322b 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2686,6 +2686,8 @@ function xmldb_main_upgrade($oldversion) { $table->add_field('issuerid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); $table->add_field('refreshtoken', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('grantedscopes', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('username', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('email', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); // Adding keys to table oauth2_system_account. $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); diff --git a/version.php b/version.php index 14d302ac2a2..aaed5ce20e3 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017033100.02; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017033100.03; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From 2fad141006a3baf2243bee27ffa02f1ce6aefa3f Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 10 Mar 2017 12:51:00 +0800 Subject: [PATCH 24/84] MDL-58219 oauth2: Fix token upgrade problem with incremental auth Part of MDL-58220 --- lib/oauthlib.php | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/oauthlib.php b/lib/oauthlib.php index 53838c91ab1..e77ce051cad 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -397,6 +397,8 @@ abstract class oauth2_client extends curl { private $refreshtoken = ''; /** var string mocknextresponse string */ private $mocknextresponse = ''; + /** var array $upgradedcodes list of upgraded codes in this request */ + private static $upgradedcodes = []; /** * Returns the auth url for OAuth 2.0 request @@ -441,24 +443,30 @@ abstract class oauth2_client extends curl { return false; } - // If we've been passed then authorization code generated by the - // authorization server try and upgrade the token to an access token. - $code = optional_param('oauth2code', null, PARAM_RAW); - if ($code && $this->upgrade_token($code)) { - return true; - } - // We have a token so we are logged in. if (isset($this->accesstoken->token)) { // Check that the access token has all the requested scopes. + $scopemissing = false; $scopecheck = ' ' . $this->accesstoken->scope . ' '; $requiredscopes = explode(' ', $this->scope); foreach ($requiredscopes as $requiredscope) { if (strpos($scopecheck, ' ' . $requiredscope . ' ') === false) { - return false; + $scopemissing = true; + break; } } + if (!$scopemissing) { + return true; + } + } + + // If we've been passed then authorization code generated by the + // authorization server try and upgrade the token to an access token. + $code = optional_param('oauth2code', null, PARAM_RAW); + // Note - sometimes we may call is_logged_in twice in the same request - we don't want to attempt + // to upgrade the same token twice. + if ($code && !in_array($code, self::$upgradedcodes) && $this->upgrade_token($code)) { return true; } @@ -574,6 +582,7 @@ abstract class oauth2_client extends curl { $accesstoken->scope = $this->scope; } // Also add the scopes. + self::$upgradedcodes[] = $code; $this->store_token($accesstoken); return true; From 6da1c55ba86947c105a7c86998352fdd0c6ddadb Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 10 Mar 2017 13:34:21 +0800 Subject: [PATCH 25/84] MDL-58219 googledocs: Keep original name on copy Also - only add writers to files that have been "claimed" (prevent_changes_to_external_files) Part of MDL-58220 --- repository/googledocs/lib.php | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index c62bbdab167..81668d4bb7b 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -313,7 +313,13 @@ class repository_googledocs extends repository { } if (isset($gfile->fileExtension)) { // The file has an extension, therefore we can download it. - $source = json_encode(['id' => $gfile->id, 'exportformat' => 'download', 'link' => $link]); + $source = json_encode([ + 'id' => $gfile->id, + 'name' => $gfile->name, + 'exportformat' => 'download', + 'link' => $link, + 'claimed' => false + ]); $title = $gfile->name; } else { // The file is probably a Google Doc file, we get the corresponding export link. @@ -356,7 +362,13 @@ class repository_googledocs extends repository { if (empty($title)) { continue; } - $source = json_encode(['id' => $gfile->id, 'exportformat' => $exporttype, 'link' => $link]); + $source = json_encode([ + 'id' => $gfile->id, + 'exportformat' => $exporttype, + 'link' => $link, + 'name' => $gfile->name, + 'claimed' => false + ]); } // Adds the file to the file list. Using the itemId along with the name as key // of the array because Google Drive allows files with identical names. @@ -541,7 +553,7 @@ class repository_googledocs extends repository { $storedfile->get_filepath(), $storedfile->get_filename()); - if ($info->is_writable()) { + if (!empty($source->claimed) && $info->is_writable()) { // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); @@ -608,7 +620,7 @@ class repository_googledocs extends repository { $systemservice = new repository_googledocs\rest($systemauth); // Copy the file so we get a snapshot file owned by Moodle. - $newsource = $this->copy_file($systemservice, $source->id); + $newsource = $this->copy_file($systemservice, $source->id, $source->name); // Set the sharing options. $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id); @@ -626,6 +638,7 @@ class repository_googledocs extends repository { if (empty($source->link)) { $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : ''; } + $source->claimed = true; $reference = json_encode($source); $file->set_source($reference); @@ -772,16 +785,22 @@ class repository_googledocs extends repository { * * @param \core\oauth2\client $client Authenticated client. * @param string $fileid The file we are copying. + * @param string $name The original filename (don't change it). * * @return stdClass file details. */ - protected function copy_file(\repository_googledocs\rest $client, $fileid) { + protected function copy_file(\repository_googledocs\rest $client, $fileid, $name) { $fields = "id,name,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink"; $params = [ 'fileid' => $fileid, - 'fields' => $fields + 'fields' => $fields, ]; - $fileinfo = $client->call('copy', $params, ' '); + // Keep the original name (don't put copy at the end of it). + $copyinfo = []; + if (!empty($name)) { + $copyinfo = [ 'name' => $name ]; + } + $fileinfo = $client->call('copy', $params, json_encode($copyinfo)); if (empty($fileinfo->id)) { $details = 'Cannot copy file:' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); From 39f60f6c008a34f387f929ea3036f2b6171e2eb8 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 10 Mar 2017 14:12:00 +0800 Subject: [PATCH 26/84] MDL-58219 googledocs: Use a cache Speed up folder operations with a simple cache. Part of MDL-58220 --- repository/googledocs/db/caches.php | 41 +++++++++++++++++++ .../lang/en/repository_googledocs.php | 1 + repository/googledocs/lib.php | 10 ++++- repository/googledocs/version.php | 2 +- 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 repository/googledocs/db/caches.php diff --git a/repository/googledocs/db/caches.php b/repository/googledocs/db/caches.php new file mode 100644 index 00000000000..0f554b62631 --- /dev/null +++ b/repository/googledocs/db/caches.php @@ -0,0 +1,41 @@ +. + +/** + * Googledocs repository cache definitions. + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package repository_googledocs + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$definitions = array( + + // Used to store file ids for folders. + // The keys used are full path to the folder, the values are the id in google drive. + // The static acceleration size has been based upon the depths of a single path. + 'folder' => array( + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => false, + 'simpledata' => true, + 'staticacceleration' => true, + 'staticaccelerationsize' => 10, + 'canuselocalstore' => true + ), +); diff --git a/repository/googledocs/lang/en/repository_googledocs.php b/repository/googledocs/lang/en/repository_googledocs.php index cd4c4af3a59..940fadce1e7 100644 --- a/repository/googledocs/lang/en/repository_googledocs.php +++ b/repository/googledocs/lang/en/repository_googledocs.php @@ -47,3 +47,4 @@ $string['owner'] = 'Owned by: {$a}'; $string['oauthinfo'] = '

    To use this plugin, you must register your site with Google, as described in the documentation Google OAuth 2.0 setup.

    As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':

    {$a->callbackurl}

    Once registered, you will be provided with a client ID and secret which can be used to configure all Google Drive and Picasa plugins.

    Please also note that you will have to enable the service \'Drive API\'.

    '; $string['secret'] = 'Secret'; $string['clientid'] = 'Client ID'; +$string['cachedef_folder'] = 'Google File IDs for folders in the system account'; diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 81668d4bb7b..ad2f1219ee2 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -1022,17 +1022,25 @@ class repository_googledocs extends repository { // Now move it to a sensible folder. $contextlist = array_reverse($context->get_parent_contexts(true)); + $cache = cache::make('repository_googledocs', 'folder'); $parentid = 'root'; + $fullpath = 'root'; foreach ($contextlist as $context) { // Make sure a folder exists here. $foldername = $context->get_context_name(); + $fullpath .= '/' . $foldername; - $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid); + $folderid = $cache->get('fullpath'); + if (empty($folderid)) { + $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid); + } if ($folderid !== false) { + $cache->set($fullpath, $folderid); $parentid = $folderid; } else { // Create it. $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid); + $cache->set($fullpath, $parentid); } } diff --git a/repository/googledocs/version.php b/repository/googledocs/version.php index 4aa78df7655..64be4b11666 100644 --- a/repository/googledocs/version.php +++ b/repository/googledocs/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2017030600; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2017031001; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2016112900; // Requires this Moodle version. $plugin->component = 'repository_googledocs'; // Full name of the plugin (used for diagnostics). From 3739559644971c0b05894bd4b64663e1e857e402 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 10 Mar 2017 14:33:51 +0800 Subject: [PATCH 27/84] MDL-58219 workshop: Fix unit tests The bug is caused by the new code to freeze the files in the fileareas when switching phases. Part of MDL-58220 --- mod/workshop/locallib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mod/workshop/locallib.php b/mod/workshop/locallib.php index fc2cfe2b90f..c19a0c02d6b 100644 --- a/mod/workshop/locallib.php +++ b/mod/workshop/locallib.php @@ -1844,10 +1844,10 @@ class workshop { } if (self::PHASE_ASSESSMENT == $newphase) { - file_prevent_changes_to_external_files($this->context, 'mod_workshop', 'submission_content'); + file_prevent_changes_to_external_files($this->context->id, 'mod_workshop', 'submission_content'); } if (self::PHASE_EVALUATION == $newphase) { - file_prevent_changes_to_external_files($this->context, 'mod_workshop', 'overallfeedback_attachment'); + file_prevent_changes_to_external_files($this->context->id, 'mod_workshop', 'overallfeedback_attachment'); } $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id)); From ec504d94f667d6a30c17582d6b3a3a90cb839170 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 10 Mar 2017 14:51:44 +0800 Subject: [PATCH 28/84] MDL-58219 googledocs: Fix upgrade script nesting Part of MDL-58220 --- repository/googledocs/db/upgrade.php | 7 ++++--- repository/googledocs/tests/generator/lib.php | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/repository/googledocs/db/upgrade.php b/repository/googledocs/db/upgrade.php index 9d4643230a1..381571e7233 100644 --- a/repository/googledocs/db/upgrade.php +++ b/repository/googledocs/db/upgrade.php @@ -63,11 +63,12 @@ function xmldb_repository_googledocs_upgrade($oldversion) { set_config('issuerid', $issuer->get('id'), 'googledocs'); } - if ($oldversion < 2017030600) { - set_config('supportedfiles', 'both', 'googledocs'); - } upgrade_plugin_savepoint(true, 2017030500, 'repository', 'googledocs'); } + if ($oldversion < 2017030600) { + set_config('supportedfiles', 'both', 'googledocs'); + upgrade_plugin_savepoint(true, 2017030600, 'repository', 'googledocs'); + } return true; } diff --git a/repository/googledocs/tests/generator/lib.php b/repository/googledocs/tests/generator/lib.php index 0fd3e7d3da8..977bc9c007e 100644 --- a/repository/googledocs/tests/generator/lib.php +++ b/repository/googledocs/tests/generator/lib.php @@ -66,6 +66,12 @@ class repository_googledocs_generator extends testing_repository_generator { if (!isset($record['issuerid'])) { $record['issuerid'] = $issuer->get('id'); } + if (!isset($record['defaultreturntype'])) { + $record['defaultreturntype'] = FILE_INTERNAL; + } + if (!isset($record['supportedreturntypes'])) { + $record['supportedreturntypes'] = 'both'; + } if (!isset($record['documentformat'])) { $record['documentformat'] = 'pdf'; } From dece386586db1dbd2f113aca84c728972db63f3b Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 10 Mar 2017 15:05:52 +0800 Subject: [PATCH 29/84] MDL-58219 oauth2: Fix unit tests because we changed the model We added 2 fields to the model and we needed to update the tests. I also removed some error_log from the rest client (very useful while developing) Part of MDL-58220 --- lib/classes/oauth2/rest.php | 5 ----- lib/tests/oauth2_test.php | 4 +++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/classes/oauth2/rest.php b/lib/classes/oauth2/rest.php index 708ad35ac5d..4a405f234a7 100644 --- a/lib/classes/oauth2/rest.php +++ b/lib/classes/oauth2/rest.php @@ -103,13 +103,8 @@ abstract class rest { $callargs = $rawpost; } - error_log('CALL REST'); - error_log($endpoint); - error_log(json_encode($callargs)); - error_log($method); $this->curl->setHeader('Content-type: application/json'); $response = $this->curl->$method($endpoint, $callargs); - error_log($response); if ($this->curl->errno == 0) { if ($responsetype == 'json') { diff --git a/lib/tests/oauth2_test.php b/lib/tests/oauth2_test.php index fc7097acd8b..1831dc26b3d 100644 --- a/lib/tests/oauth2_test.php +++ b/lib/tests/oauth2_test.php @@ -112,7 +112,9 @@ class core_oauth2_testcase extends advanced_testcase { $data = (object) [ 'issuerid' => $issuer->get('id'), 'refreshtoken' => 'abc', - 'grantedscopes' => $requiredscopes + 'grantedscopes' => $requiredscopes, + 'email' => 'sys@example.com', + 'username' => 'sys' ]; $sys = new \core\oauth2\system_account(0, $data); $sys->create(); From 72fd103adde5710e4e06eb99323fbdf388520a57 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 10 Mar 2017 15:44:19 +0800 Subject: [PATCH 30/84] MDL-58219 cibot: Fixes Fixes for cibot warnings. Part of MDL-58220 --- auth/oauth2/classes/api.php | 1 - auth/oauth2/classes/auth.php | 8 ++- auth/oauth2/classes/linked_login.php | 3 +- auth/oauth2/db/access.php | 3 + auth/oauth2/db/upgrade.php | 2 + auth/oauth2/lib.php | 35 +++++++++++- auth/oauth2/version.php | 6 +- calendar/classes/export_form.php | 2 +- lib/classes/oauth2/api.php | 2 +- lib/classes/oauth2/client.php | 2 +- lib/classes/oauth2/endpoint.php | 3 +- lib/classes/oauth2/issuer.php | 3 +- .../oauth2/refresh_system_tokens_task.php | 8 ++- lib/classes/oauth2/rest.php | 1 + lib/classes/oauth2/system_account.php | 5 +- lib/classes/oauth2/user_field_mapping.php | 3 +- lib/filelib.php | 4 +- lib/form/editor.php | 2 +- lib/oauthlib.php | 16 +++--- mod/assign/assignmentplugin.php | 2 +- mod/wiki/filesedit.php | 2 +- repository/googledocs/db/caches.php | 2 + repository/googledocs/lib.php | 57 ++++++++++--------- 23 files changed, 109 insertions(+), 63 deletions(-) diff --git a/auth/oauth2/classes/api.php b/auth/oauth2/classes/api.php index ce02ce39c89..a7090bdf95b 100644 --- a/auth/oauth2/classes/api.php +++ b/auth/oauth2/classes/api.php @@ -109,7 +109,6 @@ class api { $context = context_user::instance($userid); require_capability('auth/oauth2:managelinkedlogins', $context); - $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 677ec630b2e..b82124e317b 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -164,7 +164,7 @@ class auth extends \auth_plugin_base { /** * Return the userinfo from the oauth handshake. Will only be valid * for the logged in user. - * @param $string username + * @param string $username */ public function get_userinfo($username) { $cached = $this->get_static_user_info(); @@ -306,8 +306,12 @@ class auth extends \auth_plugin_base { return true; } + /** + * Process the config after the form is saved. + * @param stdClass $config + */ public function process_config($config) { - // Set to defaults if undefined + // Set to defaults if undefined. if (!isset($config->allowlinkedlogins)) { $config->allowlinkedlogins = false; } diff --git a/auth/oauth2/classes/linked_login.php b/auth/oauth2/classes/linked_login.php index 93dfd5702cf..7098e4ece5c 100644 --- a/auth/oauth2/classes/linked_login.php +++ b/auth/oauth2/classes/linked_login.php @@ -17,7 +17,7 @@ /** * Class for loading/storing issuers from the DB. * - * @package core_oauth2 + * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -35,7 +35,6 @@ use core\persistent; */ class linked_login extends persistent { - /** @const TABLE */ const TABLE = 'auth_oauth2_linked_login'; /** diff --git a/auth/oauth2/db/access.php b/auth/oauth2/db/access.php index 53864726cd9..3b1f0b88639 100644 --- a/auth/oauth2/db/access.php +++ b/auth/oauth2/db/access.php @@ -21,6 +21,9 @@ * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + +defined('MOODLE_INTERNAL') || die(); + $capabilities = [ 'auth/oauth2:managelinkedlogins' => array( diff --git a/auth/oauth2/db/upgrade.php b/auth/oauth2/db/upgrade.php index e117395fec2..ca1bd5851a7 100644 --- a/auth/oauth2/db/upgrade.php +++ b/auth/oauth2/db/upgrade.php @@ -25,6 +25,8 @@ defined('MOODLE_INTERNAL') || die(); /** + * Upgrade function + * * @param int $oldversion the version we are upgrading from * @return bool result */ diff --git a/auth/oauth2/lib.php b/auth/oauth2/lib.php index 17040112bd3..e0835ad3b95 100644 --- a/auth/oauth2/lib.php +++ b/auth/oauth2/lib.php @@ -1,5 +1,38 @@ . +/** + * Callbacks for auth_oauth2 + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Navigation hook to add to preferences page. + * + * @param navigation_node $useraccount + * @param stdClass $user + * @param context_user $context + * @param stdClass $course + * @param context_course $coursecontext + */ function auth_oauth2_extend_navigation_user_settings(navigation_node $useraccount, stdClass $user, context_user $context, @@ -11,7 +44,7 @@ function auth_oauth2_extend_navigation_user_settings(navigation_node $useraccoun if (get_config('auth_oauth2', 'allowlinkedlogins')) { $parent = $useraccount->parent->find('useraccount', navigation_node::TYPE_CONTAINER); - $thingnode = $parent->add(get_string('linkedlogins', 'auth_oauth2'), new moodle_url('/auth/oauth2/linkedlogins.php')); + $parent->add(get_string('linkedlogins', 'auth_oauth2'), new moodle_url('/auth/oauth2/linkedlogins.php')); } } } diff --git a/auth/oauth2/version.php b/auth/oauth2/version.php index 82fb444a534..0a014690f25 100644 --- a/auth/oauth2/version.php +++ b/auth/oauth2/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2017031000; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2016112900; // Requires this Moodle version -$plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics) +$plugin->version = 2017031000; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2016112900; // Requires this Moodle version. +$plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics). diff --git a/calendar/classes/export_form.php b/calendar/classes/export_form.php index 6789a8a68df..cced8fccb45 100644 --- a/calendar/classes/export_form.php +++ b/calendar/classes/export_form.php @@ -45,7 +45,7 @@ class core_calendar_export_form extends moodleform { global $CFG, $OUTPUT; $mform = $this->_form; - $mform->addElement('html', '

    ' . $OUTPUT->doc_link('calendar/export', get_string('exporthelp', 'calendar'), true) . '

    '); + $mform->addElement('html', $OUTPUT->doc_link('calendar/export', get_string('exporthelp', 'calendar'), true)); $export = array(); $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventsall', 'calendar'), 'all'); diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 436e1cc983e..3ca18ce7034 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -242,7 +242,7 @@ class api { * Get the system account for an installed OAuth service. * Never ever ever expose this to a webservice because it contains the refresh token which grants API access. * - * @param \core\oauth2\issuer $id + * @param \core\oauth2\issuer $issuer * @return \core\oauth2\client */ public static function get_system_account(issuer $issuer) { diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index e216e92c316..1ef887a12c7 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -17,7 +17,7 @@ /** * Configurable oauth2 client class. * - * @package core\oauth2 + * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/lib/classes/oauth2/endpoint.php b/lib/classes/oauth2/endpoint.php index 9832bc0ef9a..921f9a0e8da 100644 --- a/lib/classes/oauth2/endpoint.php +++ b/lib/classes/oauth2/endpoint.php @@ -17,7 +17,7 @@ /** * Class for loading/storing oauth2 endpoints from the DB. * - * @package core_oauth2 + * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -36,7 +36,6 @@ use lang_string; */ class endpoint extends persistent { - /** @const TABLE */ const TABLE = 'oauth2_endpoint'; /** diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 1ff6cef8384..756d36ba804 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -17,7 +17,7 @@ /** * Class for loading/storing issuers from the DB. * - * @package core_oauth2 + * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -35,7 +35,6 @@ use core\persistent; */ class issuer extends persistent { - /** @const TABLE */ const TABLE = 'oauth2_issuer'; /** diff --git a/lib/classes/oauth2/refresh_system_tokens_task.php b/lib/classes/oauth2/refresh_system_tokens_task.php index c6e3db8fc08..22cb45ce610 100644 --- a/lib/classes/oauth2/refresh_system_tokens_task.php +++ b/lib/classes/oauth2/refresh_system_tokens_task.php @@ -21,12 +21,18 @@ * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + namespace core\oauth2; use \core\task\scheduled_task; +defined('MOODLE_INTERNAL') || die(); + /** * Simple task to delete old messaging records. + * @package core + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class refresh_system_tokens_task extends scheduled_task { @@ -47,7 +53,7 @@ class refresh_system_tokens_task extends scheduled_task { $admins = get_admins(); if (empty($admins)) { - return; + return; } foreach ($admins as $admin) { $strparams = ['siteurl' => $CFG->wwwroot, 'issuer' => $issuer->get('name')]; diff --git a/lib/classes/oauth2/rest.php b/lib/classes/oauth2/rest.php index 4a405f234a7..f7049e6f54c 100644 --- a/lib/classes/oauth2/rest.php +++ b/lib/classes/oauth2/rest.php @@ -63,6 +63,7 @@ abstract class rest { * * @param string $functionname * @param array $functionargs + * @param string $rawpost Optional param to include in the body of a post. */ public function call($functionname, $functionargs, $rawpost = false) { $functions = $this->get_api_functions(); diff --git a/lib/classes/oauth2/system_account.php b/lib/classes/oauth2/system_account.php index c30b0d02fea..74aa5c81611 100644 --- a/lib/classes/oauth2/system_account.php +++ b/lib/classes/oauth2/system_account.php @@ -16,10 +16,8 @@ /** * When using OAuth sometimes it makes sense to authenticate as a system user, and not the current user. - * In this case we use a refresh token to get an access token and the system admin must manually authorize the - * system account. * - * @package core_oauth2 + * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -37,7 +35,6 @@ use core\persistent; */ class system_account extends persistent { - /** @const TABLE */ const TABLE = 'oauth2_system_account'; /** diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php index a67a089247e..9751bc9009a 100644 --- a/lib/classes/oauth2/user_field_mapping.php +++ b/lib/classes/oauth2/user_field_mapping.php @@ -17,7 +17,7 @@ /** * Class for loading/storing oauth2 endpoints from the DB. * - * @package core_oauth2 + * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -35,7 +35,6 @@ use core\persistent; */ class user_field_mapping extends persistent { - /** @const TABLE */ const TABLE = 'oauth2_user_field_mapping'; /** @var array $userfields - List of standard Moodle userfields. */ diff --git a/lib/filelib.php b/lib/filelib.php index e2bda31f674..df71196dbba 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -260,7 +260,7 @@ function file_postupdate_standard_editor($data, $field, array $options, $context * For all files in this file area - walk the file list and copy each to a system owned account, making them read-only. * * @category files - * @param stdClass $context context - must already exist + * @param int $contextid context id - must already exist * @param string $component * @param string $filearea file area name * @param int $itemid @@ -3495,7 +3495,7 @@ class curl { $options['CURLOPT_POSTFIELDS'] = $this->_tmp_file_post_params; unset($this->_tmp_file_post_params); } else { - // $params is the raw post data + // The variable $params is the raw post data. $options['CURLOPT_POSTFIELDS'] = $params; } return $this->request($url, $options); diff --git a/lib/form/editor.php b/lib/form/editor.php index e391a734508..69a78127219 100644 --- a/lib/form/editor.php +++ b/lib/form/editor.php @@ -59,7 +59,7 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab protected $_options = array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 0, 'changeformat' => 0, 'areamaxbytes' => FILE_AREA_MAX_BYTES_UNLIMITED, 'context' => null, 'noclean' => 0, 'trusttext' => 0, 'return_types' => 15, 'enable_filemanagement' => true); - // $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK + // 15 is $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK. /** @var array values for editor */ protected $_values = array('text'=>null, 'format'=>null, 'itemid'=>null); diff --git a/lib/oauthlib.php b/lib/oauthlib.php index e77ce051cad..cf62393b61d 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -383,21 +383,21 @@ class oauth_helper { * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class oauth2_client extends curl { - /** var string client identifier issued to the client */ + /** @var string $clientid client identifier issued to the client */ private $clientid = ''; - /** var string The client secret. */ + /** @var string $clientsecret The client secret. */ private $clientsecret = ''; - /** var moodle_url URL to return to after authenticating */ + /** @var moodle_url $returnurl URL to return to after authenticating */ private $returnurl = null; - /** var string scope of the authentication request */ + /** @var string $scope of the authentication request */ protected $scope = ''; - /** var stdClass access token object */ + /** @var stdClass $accesstoken access token object */ private $accesstoken = null; - /** var stdClass refresh token string */ + /** @var string $refreshtoken refresh token string */ private $refreshtoken = ''; - /** var string mocknextresponse string */ + /** @var string $mocknextresponse string */ private $mocknextresponse = ''; - /** var array $upgradedcodes list of upgraded codes in this request */ + /** @var array $upgradedcodes list of upgraded codes in this request */ private static $upgradedcodes = []; /** diff --git a/mod/assign/assignmentplugin.php b/mod/assign/assignmentplugin.php index 91c62f1840c..83b8dfa8b39 100644 --- a/mod/assign/assignmentplugin.php +++ b/mod/assign/assignmentplugin.php @@ -585,7 +585,7 @@ abstract class assign_plugin { if ($this->get_subtype() == 'assignsubmission') { if ($itemid) { - $record = $DB->get_record('assign_submission', array('id'=>$itemid), 'userid,groupid', IGNORE_MISSING); + $record = $DB->get_record('assign_submission', array('id' => $itemid), 'userid,groupid', IGNORE_MISSING); if (!$record) { return null; } diff --git a/mod/wiki/filesedit.php b/mod/wiki/filesedit.php index 08378821386..91ed22e61a2 100644 --- a/mod/wiki/filesedit.php +++ b/mod/wiki/filesedit.php @@ -84,7 +84,7 @@ $data->returnurl = $returnurl; $data->subwikiid = $subwiki->id; $maxbytes = get_max_upload_file_size($CFG->maxbytes, $COURSE->maxbytes); $types = FILE_INTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK; -$options = array('subdirs'=>0, 'maxbytes'=>$maxbytes, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>$types); +$options = array('subdirs' => 0, 'maxbytes' => $maxbytes, 'maxfiles' => -1, 'accepted_types' => '*', 'return_types' => $types); file_prepare_standard_filemanager($data, 'files', $options, $context, 'mod_wiki', 'attachments', $subwiki->id); $mform = new mod_wiki_filesedit_form(null, array('data'=>$data, 'options'=>$options)); diff --git a/repository/googledocs/db/caches.php b/repository/googledocs/db/caches.php index 0f554b62631..a751ed9b8cd 100644 --- a/repository/googledocs/db/caches.php +++ b/repository/googledocs/db/caches.php @@ -25,6 +25,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +defined('MOODLE_INTERNAL') || die(); + $definitions = array( // Used to store file ids for folders. diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index ad2f1219ee2..f482046561f 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -73,7 +73,7 @@ class repository_googledocs extends repository { /** * Get a cached user authenticated oauth client. * - * @param moodle_url $usecurrenturl - Use this url instead of the repo callback. + * @param moodle_url $overrideurl - Use this url instead of the repo callback. * @return \core\oauth2\client */ protected function get_user_oauth_client($overrideurl = false) { @@ -668,9 +668,9 @@ class repository_googledocs extends repository { /** * List the permissions on a file. - * @param \core\oauth2\client $client Authenticated client. - * @param string $fileid The id of the file. * + * @param \repository_googledocs\rest $client Authenticated client. + * @param string $fileid The id of the file. * @return array */ protected function list_file_permissions(\repository_googledocs\rest $client, $fileid) { @@ -681,10 +681,9 @@ class repository_googledocs extends repository { /** * See if a folder exists within a folder * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $foldername The folder we are looking for. * @param string $parentid The parent folder we are looking in. - * * @return string|boolean The file id if it exists or false. */ protected function folder_exists_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) { @@ -704,7 +703,7 @@ class repository_googledocs extends repository { /** * Create a folder within a folder * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $foldername The folder we are creating. * @param string $parentid The parent folder we are creating in. * @@ -725,7 +724,7 @@ class repository_googledocs extends repository { /** * Get capabilities for a file. * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are checking. * * @return stdClass The file info with capabilities. @@ -742,7 +741,7 @@ class repository_googledocs extends repository { /** * Get simple file info for humans. * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are querying. * * @return stdClass @@ -759,8 +758,9 @@ class repository_googledocs extends repository { /** * Update file owner. * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. + * @param string $owneremail * * @return boolean Did it work? */ @@ -783,7 +783,7 @@ class repository_googledocs extends repository { * Copy a file and return the new file details. A side effect of the copy * is that the owner will be the account authenticated with this oauth client. * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are copying. * @param string $name The original filename (don't change it). * @@ -811,14 +811,13 @@ class repository_googledocs extends repository { /** * Delete a file (for the current user). * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are deleting. * @return boolean */ - protected function delete_file($client, $fileid) { + protected function delete_file(\repository_googledocs\rest $client, $fileid) { $params = ['fileid' => $fileid]; $response = $client->call('delete', $params, ' '); - var_dump($response); if (empty($response->id)) { $details = 'Cannot delete file: ' . $fileid; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); @@ -829,12 +828,12 @@ class repository_googledocs extends repository { /** * Add a writer to the permissions on the file (temporary). * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $email The email of the writer account to add. * @return boolean */ - protected function add_temp_writer_to_file($client, $fileid, $email) { + protected function add_temp_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) { // Expires in 7 days. $expires = new DateTime(); $expires->add(new DateInterval("P7D")); @@ -858,12 +857,12 @@ class repository_googledocs extends repository { /** * Add a writer to the permissions on the file. * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $email The email of the writer account to add. * @return boolean */ - protected function add_writer_to_file($client, $fileid, $email) { + protected function add_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) { $updateeditor = [ 'emailAddress' => $email, 'role' => 'writer', @@ -881,12 +880,12 @@ class repository_googledocs extends repository { /** * Move from root to folder * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $folderid The id of the folder we are moving to * @return boolean */ - protected function move_file_from_root_to_folder($client, $fileid, $folderid) { + protected function move_file_from_root_to_folder(\repository_googledocs\rest $client, $fileid, $folderid) { // Set the parent. $params = [ 'fileid' => $fileid, 'addParents' => $folderid, 'removeParents' => 'root' @@ -902,12 +901,12 @@ class repository_googledocs extends repository { /** * Remove parent * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $folderid The id of the folder we are removing * @return boolean */ - protected function remove_file_parent($client, $fileid, $folderid) { + protected function remove_file_parent(\repository_googledocs\rest $client, $fileid, $folderid) { // Set the parent. $params = [ 'fileid' => $fileid, 'removeParents' => $folderid @@ -923,11 +922,11 @@ class repository_googledocs extends repository { /** * Prevent writers from sharing. * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @return boolean */ - protected function prevent_writers_from_sharing_file($client, $fileid) { + protected function prevent_writers_from_sharing_file(\repository_googledocs\rest $client, $fileid) { // We don't want anyone but Moodle to change the sharing settings. $params = [ 'fileid' => $fileid @@ -946,11 +945,11 @@ class repository_googledocs extends repository { /** * Allow anyone with the link to read the file. * - * @param \core\oauth2\client $client Authenticated client. + * @param \repository_googledocs\rest $client Authenticated client. * @param string $fileid The file we are updating. * @return boolean */ - protected function set_file_sharing_anyone_with_link_can_read($client, $fileid) { + protected function set_file_sharing_anyone_with_link_can_read(\repository_googledocs\rest $client, $fileid) { $updateread = [ 'type' => 'anyone', 'role' => 'reader', @@ -1030,7 +1029,7 @@ class repository_googledocs extends repository { $foldername = $context->get_context_name(); $fullpath .= '/' . $foldername; - $folderid = $cache->get('fullpath'); + $folderid = $cache->get('fullpath'); if (empty($folderid)) { $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid); } @@ -1179,7 +1178,11 @@ class repository_googledocs extends repository { } } -// Icon from: http://www.iconspedia.com/icon/google-2706.html. +/** + * Callback to get the required scopes for system account. + * + * @return string + */ function repository_googledocs_oauth2_system_scopes() { return 'https://www.googleapis.com/auth/drive'; } From 141ee541cad11846d3df8e1aa336ceb8fca89a11 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 14 Mar 2017 12:17:30 +0800 Subject: [PATCH 31/84] MDL-58219 repository: Change how controlled links work Files are copied to the system user as soon as they are uploaded. Write access is then controlled when serving links to the file. Part of MDL-58220 --- lib/classes/oauth2/api.php | 2 +- lib/classes/oauth2/rest.php | 3 ++- lib/db/install.xml | 1 - lib/db/upgrade.php | 1 - lib/filelib.php | 24 ++++++----------- lib/filestorage/file_storage.php | 35 ++----------------------- lib/oauthlib.php | 6 +---- lib/upgrade.txt | 3 +-- mod/assign/submission/file/locallib.php | 13 --------- mod/data/field/file/field.class.php | 2 -- mod/forum/lib.php | 2 -- mod/workshop/locallib.php | 7 ----- repository/lib.php | 18 +++---------- repository/repository_ajax.php | 4 --- 14 files changed, 19 insertions(+), 102 deletions(-) diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 3ca18ce7034..c886a1aaecb 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -107,7 +107,7 @@ class api { 'name' => 'alternatename', 'last_name' => 'lastname', 'email' => 'email', - 'id' => 'username', + 'third_party_id' => 'username', 'first_name' => 'firstname', 'picture-data-url' => 'picture', 'link' => 'url', diff --git a/lib/classes/oauth2/rest.php b/lib/classes/oauth2/rest.php index f7049e6f54c..c790429c891 100644 --- a/lib/classes/oauth2/rest.php +++ b/lib/classes/oauth2/rest.php @@ -74,6 +74,7 @@ abstract class rest { $method = $functions[$functionname]['method']; $endpoint = $functions[$functionname]['endpoint']; + $responsetype = $functions[$functionname]['response']; if (!in_array($method, $supportedmethods)) { throw new coding_exception('unsupported api method: ' . $method); @@ -112,7 +113,7 @@ abstract class rest { $json = json_decode($response); if (!empty($json->error)) { - throw new rest_exception($json->error->message, $json->error->code); + throw new rest_exception($json->error->code . ': ' . $json->error->message); } return $json; } diff --git a/lib/db/install.xml b/lib/db/install.xml index 7610a685c53..adb86a4f4ed 100755 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -3531,7 +3531,6 @@ - diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 5deb30d322b..c5878f7b3a0 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2719,7 +2719,6 @@ function xmldb_main_upgrade($oldversion) { // Adding keys to table oauth2_user_field_mapping. $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); $table->add_key('issuerkey', XMLDB_KEY_FOREIGN, array('issuerid'), 'oauth2_issuer', array('id')); - $table->add_key('uniqexternal', XMLDB_KEY_UNIQUE, array('issuerid', 'externalfield')); $table->add_key('uniqinternal', XMLDB_KEY_UNIQUE, array('issuerid', 'internalfield')); // Conditionally launch create table for oauth2_user_field_mapping. diff --git a/lib/filelib.php b/lib/filelib.php index df71196dbba..d9e0b0bb414 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -256,21 +256,6 @@ function file_postupdate_standard_editor($data, $field, array $options, $context return $data; } -/** - * For all files in this file area - walk the file list and copy each to a system owned account, making them read-only. - * - * @category files - * @param int $contextid context id - must already exist - * @param string $component - * @param string $filearea file area name - * @param int $itemid - * @return bool - */ -function file_prevent_changes_to_external_files($contextid, $component, $filearea, $itemid=false) { - $fs = get_file_storage(); - return $fs->prevent_changes_to_external_files($contextid, $component, $filearea, $itemid); -} - /** * Saves text and files modified by Editor formslib element * @@ -968,8 +953,15 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea if ($file->is_external_file()) { $repoid = $file->get_repository_id(); if (!empty($repoid)) { + $context = context::instance_by_id($contextid, MUST_EXIST); + $repo = repository::get_repository_by_id($repoid, $context); + $file_record['repositoryid'] = $repoid; - $file_record['reference'] = $file->get_reference(); + // This hook gives the repo a place to do some house cleaning, and update the $reference before it's saved + // to the file store. E.g. transfer ownership of the file to a system account etc. + $reference = $repo->reference_file_selected($file->get_reference(), $context, $component, $filearea, $itemid); + + $file_record['reference'] = $reference; } } diff --git a/lib/filestorage/file_storage.php b/lib/filestorage/file_storage.php index bdc0c6cb100..7fd1eaaa4ac 100644 --- a/lib/filestorage/file_storage.php +++ b/lib/filestorage/file_storage.php @@ -1128,9 +1128,8 @@ class file_storage { // creating a new file from an existing alias creates new alias implicitly. // here we just check the database consistency. if (!empty($newrecord->repositoryid)) { - if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) { - throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid); - } + // It is OK if the current reference does not exist. It may have been altered by a repository plugin when the files where saved from a draft area. + $newrecord->referencefileid = $this->get_or_create_referencefileid($newrecord->repositoryid, $newrecord->reference); } try { @@ -2324,34 +2323,4 @@ class file_storage { $DB->update_record('files_reference', (object)$data); } - /** - * For an entire file area - walk through the files and for each one that is a controlled link, - * call prevent_changes on the repository. Typically this will copy the external file to a system - * account controlled by Moodle, remove all write access and update the file reference. - * - * @param int $contextid - * @param string $component - * @param string $filearea - * @param int $itemid - */ - public function prevent_changes_to_external_files($contextid, $component, $filearea, $itemid = false) { - global $DB; - - $transaction = $DB->start_delegated_transaction(); - - $files = $this->get_area_files($contextid, $component, $filearea, $itemid, 'id', false); - - foreach ($files as $file) { - if ($file->is_external_file()) { - // Note that this function uses a cache, so we don't need to - // double cache these. - $repo = repository::get_repository_by_id($file->get_repository_id(), SYSCONTEXTID); - - // We expect this function to throw exceptions on failure. - $repo->prevent_changes_to_external_file($file); - } - } - $transaction->allow_commit(); - return true; - } } diff --git a/lib/oauthlib.php b/lib/oauthlib.php index cf62393b61d..c684c9c911a 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -576,11 +576,7 @@ abstract class oauth2_client extends curl { // Expires 10 seconds before actual expiry. $accesstoken->expires = (time() + ($r->expires_in - 10)); } - if (isset($r->scope)) { - $accesstoken->scope = $r->scope; - } else { - $accesstoken->scope = $this->scope; - } + $accesstoken->scope = $this->scope; // Also add the scopes. self::$upgradedcodes[] = $code; $this->store_token($accesstoken); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index b1293039d5e..088454b38bb 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -2,8 +2,7 @@ This files describes API changes in core libraries and APIs, information provided here is intended especially for developers. === 3.3 === * Support added for a new type of external file: FILE_CONTROLLED_LINK. This is an external file that Moodle can control - the permissions. Moodle can make files read-only or grant temporary write access. - To make all the files in file area read only (owned by Moodle) - use file_prevent_changes_to_external_files(). + the permissions. Moodle makes files read-only but can grant temporary write access. When accessing a URL, the info from file_browser::get_file_info will be checked to determine if the user has write access, if they do - the remote file will have access controls set to allow editing. * The method moodleform::after_definition() has been added and can now be used to add some logic diff --git a/mod/assign/submission/file/locallib.php b/mod/assign/submission/file/locallib.php index ba222f490d7..cd54142cb34 100644 --- a/mod/assign/submission/file/locallib.php +++ b/mod/assign/submission/file/locallib.php @@ -552,19 +552,6 @@ class assign_submission_file extends assign_submission_plugin { ); } - /** - * Make any controlled links in the submission area read-only for the student. - * - * @param stdClass $submission the assign_submission record being submitted. - * @return void - */ - public function submit_for_grading($submission) { - file_prevent_changes_to_external_files($this->assignment->get_context()->id, - 'assignsubmission_file', - ASSIGNSUBMISSION_FILE_FILEAREA, - $submission->id); - } - /** * Return the plugin configs for external functions. * diff --git a/mod/data/field/file/field.class.php b/mod/data/field/file/field.class.php index 0263432c21d..251fa7c93dc 100644 --- a/mod/data/field/file/field.class.php +++ b/mod/data/field/file/field.class.php @@ -185,8 +185,6 @@ class data_field_file extends data_field_base { $usercontext = context_user::instance($USER->id); $files = $fs->get_area_files($this->context->id, 'mod_data', 'content', $content->id, 'itemid, filepath, filename', false); - file_prevent_changes_to_external_files($this->context->id, 'mod_data', 'content', $content->id); - // We expect no or just one file (maxfiles = 1 option is set for the form_filemanager). if (count($files) == 0) { $content->content = null; diff --git a/mod/forum/lib.php b/mod/forum/lib.php index cef2dd0e197..a14d1eddb80 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -559,9 +559,7 @@ function forum_cron() { } } - // We need to prevent changes to controlled links in attachments. $modcontext = context_module::instance($coursemodules[$forumid]->id); - file_prevent_changes_to_external_files($modcontext->id, 'mod_forum', 'attachment', $pid); // Save the Inbound Message datakey here to reduce DB queries later. $messageinboundgenerator->set_data($pid); diff --git a/mod/workshop/locallib.php b/mod/workshop/locallib.php index c19a0c02d6b..af0dca3c231 100644 --- a/mod/workshop/locallib.php +++ b/mod/workshop/locallib.php @@ -1843,13 +1843,6 @@ class workshop { workshop_update_grades($workshop); } - if (self::PHASE_ASSESSMENT == $newphase) { - file_prevent_changes_to_external_files($this->context->id, 'mod_workshop', 'submission_content'); - } - if (self::PHASE_EVALUATION == $newphase) { - file_prevent_changes_to_external_files($this->context->id, 'mod_workshop', 'overallfeedback_attachment'); - } - $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id)); $this->phase = $newphase; $eventdata = array( diff --git a/repository/lib.php b/repository/lib.php index f30c5cb7e01..7522ca9247f 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -1290,9 +1290,12 @@ abstract class repository implements cacheable_object { * @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). */ - public function reference_file_selected($reference, $context) { + public function reference_file_selected($reference, $context, $component, $filearea, $itemid) { return $reference; } @@ -2680,19 +2683,6 @@ abstract class repository implements cacheable_object { 'Use repository::sync_reference instead.'); } - /** - * Update an external file so only Moodle has write access to it. - * This function must be implemented by all repositories supporting FILE_CONTROLLED_LINK return types. - * - * Throw exceptions on error and the transaction will be rolled back - * (because it is called on an entire filearea at a time). - * - * @param stored_file $file - */ - public function prevent_changes_to_external_file(stored_file $file) { - return; - } - /** * Performs synchronisation of an external file if the previous one has expired. * diff --git a/repository/repository_ajax.php b/repository/repository_ajax.php index 20ae194f2fc..9cda39912ba 100644 --- a/repository/repository_ajax.php +++ b/repository/repository_ajax.php @@ -228,10 +228,6 @@ switch ($action) { $record->filesize = $sourcefile->get_filesize(); } - // This hook gives the repo a place to do some house cleaning, and update the $reference before it's saved - // to the file store. E.g. transfer ownership of the file to a system account etc. - $reference = $repo->reference_file_selected($reference, $context); - // Check if file exists. if (repository::draftfile_exists($itemid, $saveas_path, $saveas_filename)) { // File name being used, rename it. From 72643dc688a50e67fe2f31769c39f49a86f66b84 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 14 Mar 2017 12:50:09 +0800 Subject: [PATCH 32/84] MDL-58219 googledocs: Update to new model for controlledlinks Part of MDL-58220 --- repository/googledocs/classes/rest.php | 9 - .../lang/en/repository_googledocs.php | 2 +- repository/googledocs/lib.php | 231 +++--------------- 3 files changed, 39 insertions(+), 203 deletions(-) diff --git a/repository/googledocs/classes/rest.php b/repository/googledocs/classes/rest.php index 5c5e65a4a6c..6f46c55c05f 100644 --- a/repository/googledocs/classes/rest.php +++ b/repository/googledocs/classes/rest.php @@ -123,15 +123,6 @@ class rest extends \core\oauth2\rest { ], 'response' => 'json' ], - 'list_permissions' => [ - 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/permissions', - 'method' => 'get', - 'args' => [ - 'fileid' => PARAM_RAW, - 'fields' => PARAM_RAW - ], - 'response' => 'json' - ], ]; } } diff --git a/repository/googledocs/lang/en/repository_googledocs.php b/repository/googledocs/lang/en/repository_googledocs.php index 940fadce1e7..6374a5cca33 100644 --- a/repository/googledocs/lang/en/repository_googledocs.php +++ b/repository/googledocs/lang/en/repository_googledocs.php @@ -42,9 +42,9 @@ $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.'; $string['owner'] = 'Owned by: {$a}'; +$string['cachedef_folder'] = 'Google File IDs for folders in the system account'; // Deprecated since Moodle 3.3. $string['oauthinfo'] = '

    To use this plugin, you must register your site with Google, as described in the documentation Google OAuth 2.0 setup.

    As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':

    {$a->callbackurl}

    Once registered, you will be provided with a client ID and secret which can be used to configure all Google Drive and Picasa plugins.

    Please also note that you will have to enable the service \'Drive API\'.

    '; $string['secret'] = 'Secret'; $string['clientid'] = 'Client ID'; -$string['cachedef_folder'] = 'Google File IDs for folders in the system account'; diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index f482046561f..50894a1432d 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -317,8 +317,7 @@ class repository_googledocs extends repository { 'id' => $gfile->id, 'name' => $gfile->name, 'exportformat' => 'download', - 'link' => $link, - 'claimed' => false + 'link' => $link ]); $title = $gfile->name; } else { @@ -366,8 +365,7 @@ class repository_googledocs extends repository { 'id' => $gfile->id, 'exportformat' => $exporttype, 'link' => $link, - 'name' => $gfile->name, - 'claimed' => false + 'name' => $gfile->name ]); } // Adds the file to the file list. Using the itemId along with the name as key @@ -553,7 +551,7 @@ class repository_googledocs extends repository { $storedfile->get_filepath(), $storedfile->get_filename()); - if (!empty($source->claimed) && $info->is_writable()) { + if ($info->is_writable()) { // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); @@ -594,90 +592,6 @@ class repository_googledocs extends repository { } } - /** - * Update an external file so only Moodle has write access to it. - * This function must be implemented by all repositories supporting FILE_CONTROLLED_LINK return types. - * - * Throw exceptions on error and the transaction will be rolled back - * (because it is called on an entire filearea at a time). - * - * @param stored_file $file - */ - public function prevent_changes_to_external_file(stored_file $file) { - global $DB; - - // Copy the file (will make it owned by moodle system account). - // Update the sharing settings on the file. - // Prevent editors from sharing the file. - $source = json_decode($file->get_reference()); - - $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); - - if ($systemauth === false) { - $details = 'Cannot connect as system user'; - throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); - } - $systemservice = new repository_googledocs\rest($systemauth); - - // Copy the file so we get a snapshot file owned by Moodle. - $newsource = $this->copy_file($systemservice, $source->id, $source->name); - - // Set the sharing options. - $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id); - $this->prevent_writers_from_sharing_file($systemservice, $newsource->id); - // Delete the original file from the Moodle account. This only deletes it for us (not the original owner). - - $summary = $this->get_file_summary($systemservice, $source->id); - if (!empty($summary->parents[0])) { - $myparent = $summary->parents[0]; - $this->remove_file_parent($systemservice, $source->id, $myparent); - } - // We need to change the source on the existing file now to point to the new id. - $source->id = $newsource->id; - $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : ''; - if (empty($source->link)) { - $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : ''; - } - $source->claimed = true; - $reference = json_encode($source); - $file->set_source($reference); - - // We need to update the reference in the file_reference table. - $refid = $file->get_referencefileid(); - $newref = (object) [ - 'id' => $refid, - 'reference' => $reference, - 'referencehash' => sha1($reference) - ]; - $DB->update_record('files_reference', $newref); - - return true; - } - - /** - * Grant write access and redirect to an edit link for the file. - * - * @param stored_file $storedfile the file that contains the reference - */ - public function edit_external_file($storedfile) { - // Grant writer access to this file. - - // Redirect to the file. - $this->send_file($storedfile); - } - - /** - * List the permissions on a file. - * - * @param \repository_googledocs\rest $client Authenticated client. - * @param string $fileid The id of the file. - * @return array - */ - protected function list_file_permissions(\repository_googledocs\rest $client, $fileid) { - $fields = "permissions(id,type,emailAddress,role,allowFileDiscovery,displayName)"; - return $client->call('list_permissions', ['fileid' => $fileid]); - } - /** * See if a folder exists within a folder * @@ -721,23 +635,6 @@ class repository_googledocs extends repository { return $created->id; } - /** - * Get capabilities for a file. - * - * @param \repository_googledocs\rest $client Authenticated client. - * @param string $fileid The file we are checking. - * - * @return stdClass The file info with capabilities. - */ - protected function get_file_capabilities(\repository_googledocs\rest $client, $fileid) { - $fields = "id,capabilities,writersCanShare"; - $params = [ - 'fileid' => $fileid, - 'fields' => $fields - ]; - return $client->call('get', $params); - } - /** * Get simple file info for humans. * @@ -755,30 +652,6 @@ class repository_googledocs extends repository { return $client->call('get', $params); } - /** - * Update file owner. - * - * @param \repository_googledocs\rest $client Authenticated client. - * @param string $fileid The file we are updating. - * @param string $owneremail - * - * @return boolean Did it work? - */ - protected function update_file_owner(\repository_googledocs\rest $client, $fileid, $owneremail) { - $updateowner = [ - 'emailAddress' => $owneremail, - 'role' => 'owner', - 'type' => 'user' - ]; - $params = ['fileid' => $fileid, 'transferOwnership' => 'true']; - try { - $response = $client->call('create_permission', $params, json_encode($updateowner)); - } catch (\core\oauth2\rest_exception $re) { - return false; - } - return !empty($response->id); - } - /** * Copy a file and return the new file details. A side effect of the copy * is that the owner will be the account authenticated with this oauth client. @@ -808,23 +681,6 @@ class repository_googledocs extends repository { return $fileinfo; } - /** - * Delete a file (for the current user). - * - * @param \repository_googledocs\rest $client Authenticated client. - * @param string $fileid The file we are deleting. - * @return boolean - */ - protected function delete_file(\repository_googledocs\rest $client, $fileid) { - $params = ['fileid' => $fileid]; - $response = $client->call('delete', $params, ' '); - if (empty($response->id)) { - $details = 'Cannot delete file: ' . $fileid; - throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); - } - return true; - } - /** * Add a writer to the permissions on the file (temporary). * @@ -898,27 +754,6 @@ class repository_googledocs extends repository { return true; } - /** - * Remove parent - * - * @param \repository_googledocs\rest $client Authenticated client. - * @param string $fileid The file we are updating. - * @param string $folderid The id of the folder we are removing - * @return boolean - */ - protected function remove_file_parent(\repository_googledocs\rest $client, $fileid, $folderid) { - // Set the parent. - $params = [ - 'fileid' => $fileid, 'removeParents' => $folderid - ]; - $response = $client->call('update', $params, ' '); - if (empty($response->id)) { - $details = 'Cannot remove the file parent: ' . $fileid . ', ' . $folderid; - throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); - } - return true; - } - /** * Prevent writers from sharing. * @@ -971,9 +806,12 @@ class repository_googledocs extends repository { * @param string $reference this reference is generated by * repository::get_file_reference() * @param context $context the target context for this new file. - * @return string $modifiedreference (final one before saving to DB) + * @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). */ - public function reference_file_selected($reference, $context) { + public function reference_file_selected($reference, $context, $component, $filearea, $itemid) { // What we need to do here is transfer ownership to the system user (or copy) // then set the permissions so anyone with the share link can view, // finally update the reference to contain the share link if it was not @@ -1000,21 +838,6 @@ class repository_googledocs extends repository { $userservice = new repository_googledocs\rest($userauth); $systemservice = new repository_googledocs\rest($systemauth); - // Get the list of existing permissions so we can see if the owner is already the system account, - // and whether we need to update the link sharing options. - $permissions = $this->list_file_permissions($userservice, $source->id); - - $readshareupdaterequired = true; - $ownerupdaterequired = true; - foreach ($permissions->permissions as $permission) { - if ($permission->id == 'anyoneWithLink' && - $permission->type == 'anyone' && - $permission->role == 'reader' && - $permission->allowFileDiscovery == false) { - $readshareupdaterequired = false; - } - } - // Add Moodle as writer. $this->add_writer_to_file($userservice, $source->id, $systemuseremail); @@ -1024,12 +847,22 @@ class repository_googledocs extends repository { $cache = cache::make('repository_googledocs', 'folder'); $parentid = 'root'; $fullpath = 'root'; + $allfolders = []; foreach ($contextlist as $context) { // Make sure a folder exists here. - $foldername = $context->get_context_name(); + $foldername = clean_param($context->get_context_name(), PARAM_PATH); + $allfolders[] = $foldername; + } + + $allfolders[] = clean_param($component, PARAM_PATH); + $allfolders[] = clean_param($filearea, PARAM_PATH); + $allfolders[] = clean_param($itemid, PARAM_PATH); + + foreach ($allfolders as $foldername) { + // Make sure a folder exists here. $fullpath .= '/' . $foldername; - $folderid = $cache->get('fullpath'); + $folderid = $cache->get($fullpath); if (empty($folderid)) { $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid); } @@ -1043,13 +876,22 @@ class repository_googledocs extends repository { } } - $this->move_file_from_root_to_folder($systemservice, $source->id, $parentid); + // Copy the file so we get a snapshot file owned by Moodle. + $newsource = $this->copy_file($systemservice, $source->id, $source->name); + // Move the copied file to the correct folder. + $this->move_file_from_root_to_folder($systemservice, $newsource->id, $parentid); - if ($readshareupdaterequired) { - $this->set_file_sharing_anyone_with_link_can_read($systemservice, $source->id); + // Set the sharing options. + $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id); + $this->prevent_writers_from_sharing_file($systemservice, $newsource->id); + + $source->id = $newsource->id; + $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : ''; + if (empty($source->link)) { + $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : ''; } + $reference = json_encode($source); - // We did not update the reference at all. return $reference; } @@ -1183,6 +1025,9 @@ class repository_googledocs extends repository { * * @return string */ -function repository_googledocs_oauth2_system_scopes() { - return 'https://www.googleapis.com/auth/drive'; +function repository_googledocs_oauth2_system_scopes(\core\oauth2\issuer $issuer) { + if ($issuer->get('id') == get_config('googledocs', 'issuerid')) { + return 'https://www.googleapis.com/auth/drive'; + } + return ''; } From af28b228929365d1f473c3c6b92df7101cdb4b07 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 14 Mar 2017 14:20:16 +0800 Subject: [PATCH 33/84] MDL-58219 googledocs: Use google file extensions Only rename on export for download. Part of MDL-58220 --- lib/classes/filetypes.php | 5 +++++ repository/googledocs/lib.php | 33 +++++++++++++++++++++++++-------- repository/repository_ajax.php | 4 ++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/lib/classes/filetypes.php b/lib/classes/filetypes.php index b2ac0620be8..23d45bde610 100644 --- a/lib/classes/filetypes.php +++ b/lib/classes/filetypes.php @@ -100,6 +100,11 @@ abstract class core_filetypes { 'gallery' => array('type' => 'application/x-smarttech-notebook', 'icon' => 'archive'), 'galleryitem' => array('type' => 'application/x-smarttech-notebook', 'icon' => 'archive'), 'gallerycollection' => array('type' => 'application/x-smarttech-notebook', 'icon' => 'archive'), + 'gdraw' => array('type' => 'application/vnd.google-apps.drawing', 'icon' => 'image', 'groups' => array('image')), + 'gdoc' => array('type' => 'application/vnd.google-apps.document', 'icon' => 'document', 'groups' => array('document')), + 'gsheet' => array('type' => 'application/vnd.google-apps.spreadsheet', 'icon' => 'spreadsheet', + 'groups' => array('spreadsheet')), + 'gslides' => array('type' => 'application/vnd.google-apps.presentation', 'icon' => 'powerpoint', 'groups' => array('presentation')), 'gif' => array('type' => 'image/gif', 'icon' => 'gif', 'groups' => array('image', 'web_image'), 'string' => 'image'), 'gtar' => array('type' => 'application/x-gtar', 'icon' => 'archive', 'groups' => array('archive'), 'string' => 'archive'), diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 50894a1432d..cbc275ff470 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -331,7 +331,7 @@ class repository_googledocs extends repository { switch ($type){ case 'document': $ext = $config->documentformat; - $title = $gfile->name . '.'. $ext; + $title = $gfile->name . '.gdoc'; if ($ext === 'rtf') { // Moodle user 'text/rtf' as the MIME type for RTF files. // Google uses 'application/rtf' for the same type of file. @@ -343,12 +343,12 @@ class repository_googledocs extends repository { break; case 'presentation': $ext = $config->presentationformat; - $title = $gfile->name . '.'. $ext; + $title = $gfile->name . '.gslides'; $exporttype = $types[$ext]['type']; break; case 'spreadsheet': $ext = $config->spreadsheetformat; - $title = $gfile->name . '.'. $ext; + $title = $gfile->name . '.gsheet'; $exporttype = $types[$ext]['type']; break; case 'drawing': @@ -421,6 +421,7 @@ class repository_googledocs extends repository { $source = json_decode($reference); + $newfilename = false; if ($source->exportformat == 'download') { $params = ['alt' => 'media']; $sourceurl = new moodle_url($base . '/files/' . $source->id, $params); @@ -428,20 +429,36 @@ class repository_googledocs extends repository { } else { $params = ['mimeType' => $source->exportformat]; $sourceurl = new moodle_url($base . '/files/' . $source->id . '/export', $params); + $types = get_mimetypes_array(); + $checktype = $source->exportformat; + if ($checktype == 'application/rtf') { + $checktype = 'text/rtf'; + } + foreach ($types as $extension => $info) { + if ($info['type'] == $checktype) { + $newfilename = $source->name . '.' . $extension; + break; + } + } $source = $sourceurl->out(false); } // We use download_one and not the rest API because it has special timeouts etc. $path = $this->prepare_file($filename); $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5]; - $result = $client->download_one($source, null, $options); + $success = $client->download_one($source, null, $options); - if ($result) { + if ($success) { @chmod($path, $CFG->filepermissions); - return array( + + $result = [ 'path' => $path, - 'url' => $reference - ); + 'url' => $reference, + ]; + if (!empty($newfilename)) { + $result['newfilename'] = $newfilename; + } + return $result; } throw new repository_exception('cannotdownload', 'repository'); } diff --git a/repository/repository_ajax.php b/repository/repository_ajax.php index 9cda39912ba..726486a8e43 100644 --- a/repository/repository_ajax.php +++ b/repository/repository_ajax.php @@ -278,6 +278,10 @@ switch ($action) { } else { // Download file to moodle. $downloadedfile = $repo->get_file($reference, $saveas_filename); + + if (!empty($downloadedfile['newfilename'])) { + $record->filename = $downloadedfile['newfilename']; + } if (empty($downloadedfile['path'])) { $err->error = get_string('cannotdownload', 'repository'); die(json_encode($err)); From ffda3e395c78c053c7c9affdffb95d487dd5d960 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 14 Mar 2017 12:18:27 +0800 Subject: [PATCH 34/84] MDL-58127 skydrive: Upgrades to new oauth2 Support for controlled links workflow. Part of MDL-58220 --- repository/skydrive/classes/access.php | 56 + .../classes/remove_temp_access_task.php | 79 ++ repository/skydrive/classes/rest.php | 159 +++ repository/skydrive/db/caches.php | 14 +- repository/skydrive/db/install.xml | 22 + repository/skydrive/db/tasks.php | 43 + repository/skydrive/db/upgrade.php | 54 + .../skydrive/lang/en/repository_skydrive.php | 22 +- repository/skydrive/lib.php | 987 ++++++++++++++++-- repository/skydrive/microsoftliveapi.php | 245 ----- repository/skydrive/version.php | 2 +- 11 files changed, 1328 insertions(+), 355 deletions(-) create mode 100644 repository/skydrive/classes/access.php create mode 100644 repository/skydrive/classes/remove_temp_access_task.php create mode 100644 repository/skydrive/classes/rest.php create mode 100644 repository/skydrive/db/install.xml create mode 100644 repository/skydrive/db/tasks.php create mode 100644 repository/skydrive/db/upgrade.php delete mode 100644 repository/skydrive/microsoftliveapi.php diff --git a/repository/skydrive/classes/access.php b/repository/skydrive/classes/access.php new file mode 100644 index 00000000000..8c3fd90edc6 --- /dev/null +++ b/repository/skydrive/classes/access.php @@ -0,0 +1,56 @@ +. + +/** + * Class for loading/storing access records from the DB. + * + * @package core + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace repository_skydrive; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing issuer from the DB + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class access extends persistent { + + const TABLE = 'repository_skydrive_access'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'permissionid' => array( + 'type' => PARAM_RAW + ), + 'itemid' => array( + 'type' => PARAM_RAW + ) + ); + } + +} diff --git a/repository/skydrive/classes/remove_temp_access_task.php b/repository/skydrive/classes/remove_temp_access_task.php new file mode 100644 index 00000000000..98247ec93db --- /dev/null +++ b/repository/skydrive/classes/remove_temp_access_task.php @@ -0,0 +1,79 @@ +. + +/** + * A scheduled task. + * + * @package repository_skydrive + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace repository_skydrive; + +use \core\task\scheduled_task; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Simple task to delete temporary permission records. + * @package core + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class remove_temp_access_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('removetempaccesstask', 'repository_skydrive'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + $accessrecords = access::get_records(); + $expires = new DateTime(); + $expires->sub(new DateInterval("P7D")); + $timestamp = $expires->getTimestamp(); + + $issuerid = get_config('repository_skydrive', 'issuerid'); + $issuer = \core\oauth2\api::get_issuer_by_id($issuerid); + + // Add the current user as an OAuth writer. + $systemauth = \core\oauth2\api::get_system_oauth_client($issuer); + + if ($systemauth === false) { + $details = 'Cannot connect as system user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $systemservice = new repository_skydrive\rest($systemauth); + + foreach ($accessrecords as $access) { + if ($access->get('timemodified') < $timestamp) { + $params = ['permissionid' => $access->get('permissionid'), 'itemid' => $access->get('itemid')]; + $systemservice->call('delete_permission', $params); + $access->delete(); + } + } + } + +} diff --git a/repository/skydrive/classes/rest.php b/repository/skydrive/classes/rest.php new file mode 100644 index 00000000000..5a35baa0a33 --- /dev/null +++ b/repository/skydrive/classes/rest.php @@ -0,0 +1,159 @@ +. + +/** + * Microsoft Graph API Rest Interface. + * + * @package repository_skydrive + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace repository_skydrive; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Microsoft Graph API Rest Interface. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class rest extends \core\oauth2\rest { + + /** + * Define the functions of the rest API. + * + * @return array Example: + * [ 'listFiles' => [ 'method' => 'get', 'endpoint' => 'http://...', 'args' => [ 'folder' => PARAM_STRING ] ] ] + */ + public function get_api_functions() { + return [ + 'list' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/{parent}/children', + 'method' => 'get', + 'args' => [ + '$select' => PARAM_RAW, + '$expand' => PARAM_RAW, + 'parent' => PARAM_RAW, + '$skip' => PARAM_INT, + '$skipToken' => PARAM_RAW, + '$count' => PARAM_INT + ], + 'response' => 'json' + ], + 'search' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/{parent}/search(q=\'{search}\')', + 'method' => 'get', + 'args' => [ + 'search' => PARAM_NOTAGS, + '$select' => PARAM_RAW, + 'parent' => PARAM_RAW, + '$skip' => PARAM_INT, + '$skipToken' => PARAM_RAW, + '$count' => PARAM_INT + ], + 'response' => 'json' + ], + 'get' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}', + 'method' => 'get', + 'args' => [ + 'fileid' => PARAM_RAW, + '$select' => PARAM_RAW, + '$expand' => PARAM_RAW + ], + 'response' => 'json' + ], + 'list_permissions' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/permissions', + 'method' => 'get', + 'args' => [ + '$select' => PARAM_RAW, + '$expand' => PARAM_RAW, + 'fileid' => PARAM_RAW, + '$skip' => PARAM_INT, + '$skipToken' => PARAM_RAW, + '$count' => PARAM_INT + ], + 'response' => 'json' + ], + 'create_permission' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/invite', + 'method' => 'post', + 'args' => [ + 'fileid' => PARAM_RAW + ], + 'response' => 'json' + ], + 'get_file_by_path' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/root:/{fullpath}', + 'method' => 'get', + 'args' => [ + 'fullpath' => PARAM_RAW, + '$select' => PARAM_RAW + ], + 'response' => 'json' + ], + 'create_folder' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{parentid}/children', + 'method' => 'post', + 'args' => [ + 'parentid' => PARAM_RAW + ], + 'response' => 'json' + ], + 'create_link' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/createLink', + 'method' => 'post', + 'args' => [ + 'fileid' => PARAM_RAW + ], + 'response' => 'json' + ], + 'get_drive' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive', + 'method' => 'get', + 'args' => [], + 'response' => 'json' + ], + 'delete_file_by_path' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/root:/{fullpath}', + 'method' => 'delete', + 'args' => [ + 'fullpath' => PARAM_RAW, + ], + 'response' => 'json' + ], + 'copy_share' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/shares/{sharetoken}/root/copy', + 'method' => 'post', + 'args' => [ + 'sharetoken' => PARAM_RAW, + ], + 'response' => 'json' + ], + 'delete_permission' => [ + 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/permissions/{permissionid}', + 'method' => 'delete', + 'args' => [ + 'fileid' => PARAM_RAW, + 'permissionid' => PARAM_RAW + ], + 'response' => 'json' + ], + ]; + } +} diff --git a/repository/skydrive/db/caches.php b/repository/skydrive/db/caches.php index 8b61e339ba6..1050c238de2 100644 --- a/repository/skydrive/db/caches.php +++ b/repository/skydrive/db/caches.php @@ -25,7 +25,15 @@ defined('MOODLE_INTERNAL') || die(); $definitions = array( - 'foldername' => array( - 'mode' => cache_store::MODE_SESSION, - ) + // Used to store file ids for folders. + // The keys used are full path to the folder, the values are the id in google drive. + // The static acceleration size has been based upon the depths of a single path. + 'folder' => array( + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => false, + 'simpledata' => true, + 'staticacceleration' => true, + 'staticaccelerationsize' => 10, + 'canuselocalstore' => true + ), ); diff --git a/repository/skydrive/db/install.xml b/repository/skydrive/db/install.xml new file mode 100644 index 00000000000..f61ae0e88df --- /dev/null +++ b/repository/skydrive/db/install.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + +
    +
    +
    diff --git a/repository/skydrive/db/tasks.php b/repository/skydrive/db/tasks.php new file mode 100644 index 00000000000..b55c4ea3bb6 --- /dev/null +++ b/repository/skydrive/db/tasks.php @@ -0,0 +1,43 @@ +. + +/** + * Definition of repository_skydrive scheduled tasks. + * + * The handlers defined on this file are processed and registered into + * the Moodle DB after any install or upgrade operation. All plugins + * support this. + * + * @package repository_skydrive + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/* List of handlers */ + +$tasks = array( + array( + 'classname' => 'repository_skydrive\remove_temp_access_task', + 'blocking' => 0, + 'minute' => 'R', + 'hour' => 'R', + 'day' => '*', + 'dayofweek' => 'R', + 'month' => '*' + ), +); diff --git a/repository/skydrive/db/upgrade.php b/repository/skydrive/db/upgrade.php new file mode 100644 index 00000000000..7a714846fcd --- /dev/null +++ b/repository/skydrive/db/upgrade.php @@ -0,0 +1,54 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +/** + * @param int $oldversion the version we are upgrading from + * @return bool result + */ +function xmldb_repository_skydrive_upgrade($oldversion) { + global $DB; + + $dbman = $DB->get_manager(); + + if ($oldversion < 2017031400) { + + // Define table repository_skydrive_access to be created. + $table = new xmldb_table('repository_skydrive_access'); + + // Adding fields to table repository_skydrive_access. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('permissionid', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('itemid', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table repository_skydrive_access. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('usermodifiedkey', XMLDB_KEY_FOREIGN, array('usermodified'), 'user', array('id')); + + // Conditionally launch create table for repository_skydrive_access. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Skydrive savepoint reached. + upgrade_plugin_savepoint(true, 2017031400, 'repository', 'skydrive'); + } + return true; +} diff --git a/repository/skydrive/lang/en/repository_skydrive.php b/repository/skydrive/lang/en/repository_skydrive.php index 1ed71f48fc5..015647fa9d1 100644 --- a/repository/skydrive/lang/en/repository_skydrive.php +++ b/repository/skydrive/lang/en/repository_skydrive.php @@ -22,10 +22,20 @@ * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['cachedef_foldername'] = 'Folder name cache'; -$string['clientid'] = 'Client ID'; -$string['configplugin'] = 'Configure Microsoft OneDrive'; -$string['oauthinfo'] = '

    To use this plugin, you must register your site with Microsoft.

    As part of the registration process, you will need to enter the following URL as \'Redirect domain\':

    {$a->callbackurl}

    Once registered, you will be provided with a client ID and secret which can be entered here.

    '; +$string['configplugin'] = 'Configure OneDrive plugin'; +$string['skydrive:view'] = 'View OneDrive repository'; $string['pluginname'] = 'Microsoft OneDrive'; -$string['secret'] = 'Secret'; -$string['skydrive:view'] = 'View OneDrive'; +$string['issuer'] = 'OAuth 2 service'; +$string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the OneDrive API. If the services does not exist yet, you might need to create it.'; +$string['servicenotenabled'] = 'Access not configured.'; +$string['oauth2serviceslink'] = 'OAuth 2 Services Configuration'; +$string['searchfor'] = 'Search for {$a}'; +$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.'; +$string['owner'] = 'Owned by: {$a}'; +$string['cachedef_folder'] = 'OneDrive File IDs for folders in the system account'; + diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php index 69c6316454d..41c87dc8f8e 100644 --- a/repository/skydrive/lib.php +++ b/repository/skydrive/lib.php @@ -25,8 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -require_once('microsoftliveapi.php'); - /** * Microsoft skydrive repository plugin. * @@ -36,46 +34,80 @@ require_once('microsoftliveapi.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_skydrive extends repository { - /** @var microsoft_skydrive skydrive oauth2 api helper object */ - private $skydrive = null; + /** + * OAuth 2 client + * @var \core\oauth2\client + */ + private $client = null; /** - * Constructor + * OAuth 2 Issuer + * @var \core\oauth2\issuer + */ + private $issuer = null; + + /** + * Additional scopes required for drive. + */ + const SCOPES = 'files.readwrite.all'; + + /** + * Constructor. * * @param int $repositoryid repository instance id. * @param int|stdClass $context a context id or context object. * @param array $options repository options. + * @param int $readonly indicate this repo is readonly or not. + * @return void */ - public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { - parent::__construct($repositoryid, $context, $options); + public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) { + parent::__construct($repositoryid, $context, $options, $readonly = 0); - $clientid = get_config('skydrive', 'clientid'); - $secret = get_config('skydrive', 'secret'); - $returnurl = new moodle_url('/repository/repository_callback.php'); - $returnurl->param('callback', 'yes'); - $returnurl->param('repo_id', $this->id); - $returnurl->param('sesskey', sesskey()); - - $this->skydrive = new microsoft_skydrive($clientid, $secret, $returnurl); - $this->check_login(); + $this->issuer = \core\oauth2\api::get_issuer(get_config('skydrive', 'issuerid')); } /** - * Checks whether the user is logged in or not. + * Get a cached user authenticated oauth client. * - * @return bool true when logged in + * @param 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; + } + + /** + * Checks whether the user is authenticate or not. + * + * @return bool true when logged in. */ public function check_login() { - return $this->skydrive->is_logged_in(); + $client = $this->get_user_oauth_client(); + return $client->is_logged_in(); } /** - * Print the login form, if required + * Print or return the login form. * - * @return array of login options + * @return void|array for ajax. */ public function print_login() { - $url = $this->skydrive->get_login_url(); + $client = $this->get_user_oauth_client(); + $url = $client->get_login_url(); if ($this->options['ajax']) { $popup = new stdClass(); @@ -88,124 +120,879 @@ class repository_skydrive extends repository { } /** - * Given a path, and perhaps a search, get a list of files. + * Build the breadcrumb from a path. * - * See details on {@link http://docs.moodle.org/dev/Repository_plugins} + * @param string $path to create a breadcrumb from. + * @return array containing name and path of each crumb. + */ + protected function build_breadcrumb($path) { + $bread = explode('/', $path); + $crumbtrail = ''; + foreach ($bread as $crumb) { + list($id, $name) = $this->explode_node_path($crumb); + $name = empty($name) ? $id : $name; + $breadcrumb[] = array( + 'name' => $name, + 'path' => $this->build_node_path($id, $name, $crumbtrail) + ); + $tmp = end($breadcrumb); + $crumbtrail = $tmp['path']; + } + return $breadcrumb; + } + + /** + * Generates a safe path to a node. * - * @param string $path identifier for current path - * @param string $page the page number of file list - * @return array list of files including meta information as specified by parent. + * Typically, a node will be id|Name of the node. + * + * @param string $id of the node. + * @param string $name of the node, will be URL encoded. + * @param string $root to append the node on, must be a result of this function. + * @return string path to the node. + */ + protected function build_node_path($id, $name = '', $root = '') { + $path = $id; + if (!empty($name)) { + $path .= '|' . urlencode($name); + } + if (!empty($root)) { + $path = trim($root, '/') . '/' . $path; + } + return $path; + } + + /** + * Returns information about a node in a path. + * + * @see self::build_node_path() + * @param string $node to extrat information from. + * @return array about the node. + */ + protected function explode_node_path($node) { + if (strpos($node, '|') !== false) { + list($id, $name) = explode('|', $node, 2); + $name = urldecode($name); + } else { + $id = $node; + $name = ''; + } + $id = urldecode($id); + return array( + 0 => $id, + 1 => $name, + 'id' => $id, + 'name' => $name + ); + } + + /** + * List the files and folders. + * + * @param string $path path to browse. + * @param string $page page to browse. + * @return array of result. */ public function get_listing($path='', $page = '') { - $ret = array(); - $ret['dynload'] = true; - $ret['nosearch'] = true; - $ret['manage'] = 'https://skydrive.live.com/'; - - $fileslist = $this->skydrive->get_file_list($path); - // Filter list for accepted types. Hopefully this will be done by core some day. - $fileslist = array_filter($fileslist, array($this, 'filter')); - $ret['list'] = $fileslist; - - // Generate path bar, always start with the plugin name. - $ret['path'] = array(); - $ret['path'][] = array('name'=> $this->name, 'path'=>''); - - // Now add each level folder. - $trail = ''; - if (!empty($path)) { - $parts = explode('/', $path); - foreach ($parts as $folderid) { - if (!empty($folderid)) { - $trail .= ('/'.$folderid); - $ret['path'][] = array('name' => $this->skydrive->get_folder_name($folderid), - 'path' => $trail); - } - } + if (empty($path)) { + $path = $this->build_node_path('root', get_string('pluginname', 'repository_skydrive')); } + // We analyse the path to extract what to browse. + $trail = explode('/', $path); + $uri = array_pop($trail); + list($id, $name) = $this->explode_node_path($uri); + + // Handle the special keyword 'search', which we defined in self::search() so that + // we could set up a breadcrumb in the search results. In any other case ID would be + // 'root' which is a special keyword, or a parent (folder) ID. + if ($id === 'search') { + $q = $name; + $id = 'root'; + + // Append the active path for search. + $str = get_string('searchfor', 'repository_skydrive', $searchtext); + $path = $this->build_node_path('search', $str, $path); + } + + // Query the Drive. + $parent = $id; + if ($parent != 'root') { + $parent = 'items/' . $parent; + } + $q = ''; + $results = $this->query($q, $path, $parent); + + $ret = []; + $ret['dynload'] = true; + $ret['path'] = $this->build_breadcrumb($path); + $ret['list'] = $results; + $ret['manage'] = 'https://www.office.com/'; return $ret; } /** - * Downloads a repository file and saves to a path. + * Search throughout the Google Drive. * - * @param string $id identifier of file - * @param string $filename to save file as - * @return array with keys: - * path: internal location of the file - * url: URL to the source + * @param string $searchtext text to search for. + * @param int $page search page. + * @return array of results. */ - public function get_file($id, $filename = '') { - $path = $this->prepare_file($filename); - return $this->skydrive->download_file($id, $path); + public function search($searchtext, $page = 0) { + $path = $this->build_node_path('root', get_string('pluginname', 'repository_skydrive')); + $str = get_string('searchfor', 'repository_skydrive', $searchtext); + $path = $this->build_node_path('search', $str, $path); + + // Query the Drive. + $parent = 'root'; + $results = $this->query($searchtext, $path, 'root'); + + $ret = []; + $ret['dynload'] = true; + $ret['path'] = $this->build_breadcrumb($path); + $ret['list'] = $results; + $ret['manage'] = 'https://www.office.com/'; + return $ret; } /** - * Return names of the options to display in the repository form + * Query Google Drive for files and folders using a search query. * - * @return array of option names + * Documentation about the query format can be found here: + * https://developers.google.com/drive/search-parameters + * + * This returns a list of files and folders with their details as they should be + * formatted and returned by functions such as get_listing() or search(). + * + * @param string $q search query as expected by the Google API. + * @param string $path parent path of the current files, will not be used for the query. + * @param int $page page. + * @return array of files and folders. */ - public static function get_type_option_names() { - return array('clientid', 'secret', 'pluginname'); + protected function query($q, $path = null, $parent = null, $page = 0) { + global $OUTPUT; + + $files = []; + $folders = []; + $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,thumbnails"; + $params = ['$select' => $fields, '$expand' => 'thumbnails', 'parent' => $parent]; + + try { + // Retrieving files and folders. + $client = $this->get_user_oauth_client(); + $service = new repository_skydrive\rest($client); + + if (!empty($q)) { + $params['search'] = urlencode($q); + + // MS does not return thumbnails on a search. + unset($params['$expand']); + $response = $service->call('search', $params); + } else { + $response = $service->call('list', $params); + } + } catch (Exception $e) { + if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) { + throw new repository_exception('servicenotenabled', 'repository_skydrive'); + } else { + throw $e; + } + } + + $remotefiles = isset($response->value) ? $response->value : []; + foreach ($remotefiles as $remotefile) { + if (!empty($remotefile->folder)) { + // This is a folder. + $folders[$remotefile->id] = [ + 'title' => $remotefile->name, + 'path' => $this->build_node_path($remotefile->id, $remotefile->name, $path), + 'date' => strtotime($remotefile->lastModifiedDateTime), + 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(64))->out(false), + 'thumbnail_height' => 64, + 'thumbnail_width' => 64, + 'children' => [] + ]; + } else { + // We can download all other file types. + $title = $remotefile->name; + $source = json_encode([ + 'id' => $remotefile->id, + 'name' => $remotefile->name, + 'link' => $remotefile->webUrl + ]); + + // Adds the file to the file list. Using the itemId along with the name as key + // of the array because Google Drive allows files with identical names. + $thumb = ''; + $thumbwidth = 0; + $thumbheight = 0; + $extendedinfoerr = false; + + if (empty($remotefile->thumbnails)) { + // Try and get it directly from the item. + $params = ['fileid' => $remotefile->id, '$select' => $fields, '$expand' => 'thumbnails']; + try { + $response = $service->call('get', $params); + $remotefile = $response; + } catch (Exception $e) { + // This is not a failure condition - we just could not get extended info about the file. + $extendedinfoerr = true; + } + } + + if (!empty($remotefile->thumbnails)) { + $thumbs = $remotefile->thumbnails; + if (count($thumbs)) { + $first = reset($thumbs); + if (!empty($first->medium) && !empty($first->medium->url)) { + $thumb = $first->medium->url; + $thumbwidth = min($first->medium->width, 64); + $thumbheight = min($first->medium->height, 64); + } + } + } + + $files[$remotefile->id] = [ + 'title' => $title, + 'source' => $source, + 'date' => strtotime($remotefile->lastModifiedDateTime), + 'size' => isset($remotefile->size) ? $remotefile->size : null, + 'thumbnail' => $thumb, + 'thumbnail_height' => $thumbwidth, + 'thumbnail_width' => $thumbheight, + ]; + } + } + + // Filter and order the results. + $files = array_filter($files, [$this, 'filter']); + core_collator::ksort($files, core_collator::SORT_NATURAL); + core_collator::ksort($folders, core_collator::SORT_NATURAL); + return array_merge(array_values($folders), array_values($files)); } /** - * Setup repistory form. + * Logout. * - * @param moodleform $mform Moodle form (passed by reference) - * @param string $classname repository class name - */ - public static function type_config_form($mform, $classname = 'repository') { - $a = new stdClass; - $a->callbackurl = microsoft_skydrive::callback_url()->out(false); - $mform->addElement('static', null, '', get_string('oauthinfo', 'repository_skydrive', $a)); - - parent::type_config_form($mform); - $strrequired = get_string('required'); - $mform->addElement('text', 'clientid', get_string('clientid', 'repository_skydrive')); - $mform->addElement('text', 'secret', get_string('secret', 'repository_skydrive')); - $mform->addRule('clientid', $strrequired, 'required', null, 'client'); - $mform->addRule('secret', $strrequired, 'required', null, 'client'); - $mform->setType('clientid', PARAM_RAW_TRIMMED); - $mform->setType('secret', PARAM_RAW_TRIMMED); - } - - /** - * Logout from repository instance and return - * login form. - * - * @return page to display + * @return string */ public function logout() { - $this->skydrive->log_out(); - return $this->print_login(); + $client = $this->get_user_oauth_client(); + $client->log_out(); + return parent::logout(); } /** - * This repository doesn't support global search. + * Get a file. * - * @return bool if supports global search + * @param string $reference reference of the file. + * @param string $file name to save the file to. + * @return string JSON encoded array of information about the file. */ - public function global_search() { - return false; + public function get_file($reference, $filename = '') { + global $CFG; + + $client = $this->get_user_oauth_client(); + $base = 'https://graph.microsoft.com/v1.0/'; + + $sourceinfo = json_decode($reference); + $sourceurl = new moodle_url($base . 'me/drive/items/' . $sourceinfo->id . '/content'); + $source = $sourceurl->out(false); + + // We use download_one and not the rest API because it has special timeouts etc. + $path = $this->prepare_file($filename); + $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5]; + $result = $client->download_one($source, null, $options); + + if ($result) { + @chmod($path, $CFG->filepermissions); + return array( + 'path' => $path, + 'url' => $reference + ); + } + throw new repository_exception('cannotdownload', 'repository'); } /** - * This repoistory supports any filetype. + * Prepare file reference information. * - * @return string '*' means this repository support any files + * We are using this method to clean up the source to make sure that it + * is a valid source. + * + * @param string $source of the file. + * @return string file reference. + */ + public function get_file_reference($source) { + // We could do some magic upgrade code here. + return $source; + } + + /** + * What kind of files will be in this repository? + * + * @return array return '*' means this repository support any files, otherwise + * return mimetypes of files, it can be an array */ public function supported_filetypes() { return '*'; } /** - * This repostiory only supports internal files + * Tells how the file can be picked from this repository. * - * @return int return type bitmask supported + * @return int */ public function supported_returntypes() { - return FILE_INTERNAL; + // We can only support references if the system account is connected. + if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) { + $setting = get_config('skydrive', 'supportedreturntypes'); + if ($setting == 'internal') { + return FILE_INTERNAL; + } else if ($setting == 'external') { + return FILE_CONTROLLED_LINK; + } else { + return FILE_CONTROLLED_LINK | FILE_INTERNAL; + } + } else { + return FILE_INTERNAL; + } + } + + /** + * Which return type should be selected by default. + * + * @return int + */ + public function default_returntype() { + $setting = get_config('skydrive', 'defaultreturntype'); + $supported = get_config('skydrive', 'supportedreturntypes'); + if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') { + return FILE_INTERNAL; + } else { + 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('issuerid', 'pluginname', 'defaultreturntype', 'supportedreturntypes'); + } + + /** + * Store the access token. + */ + public function callback() { + $client = $this->get_user_oauth_client(); + // 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(); + } + + /** + * Repository method to serve the referenced file + * + * @see send_stored_file + * + * @param stored_file $storedfile the file that contains the reference + * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime) + * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only + * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin + * @param array $options additional options affecting the file serving + */ + public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) { + $source = json_decode($storedfile->get_reference()); + + $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()); + + if ($info->is_writable()) { + // Add the current user as an OAuth writer. + $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); + + if ($systemauth === false) { + $details = 'Cannot connect as system user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $systemservice = new repository_skydrive\rest($systemauth); + + // Get the user oauth so we can get the account to add. + $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(), + $storedfile->get_component(), + $storedfile->get_filearea(), + $storedfile->get_itemid(), + $storedfile->get_filepath(), + $storedfile->get_filename(), + $forcedownload); + $url->param('sesskey', sesskey()); + $userauth = $this->get_user_oauth_client($url); + if (!$userauth->is_logged_in()) { + redirect($userauth->get_login_url()); + } + if ($userauth === false) { + $details = 'Cannot connect as current user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $userinfo = $userauth->get_userinfo(); + $useremail = $userinfo['email']; + + $this->add_temp_writer_to_file($systemservice, $source->id, $useremail); + } + + if ($source->link) { + redirect($source->link); + } else { + $details = 'File is missing source link'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + } + + /** + * List the permissions on a file. + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $fileid The id of the file. + * @return array + */ + protected function list_file_permissions(\repository_skydrive\rest $client, $fileid) { + $fields = "id,roles,link,grantedTo"; + return $client->call('list_permissions', ['fileid' => $fileid, '$select' => $fields]); + } + + /** + * See if a folder exists within a folder + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $fullpath + * @return string|boolean The file id if it exists or false. + */ + protected function get_file_id_by_path(\repository_skydrive\rest $client, $fullpath) { + $fields = "id"; + try { + $response = $client->call('get_file_by_path', ['fullpath' => $fullpath, '$select' => $fields]); + } catch (\core\oauth2\rest_exception $re) { + return false; + } + return $response->id; + } + + /** + * Delete a file by full path. + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $fullpath + * @return boolean + */ + protected function delete_file_by_path(\repository_skydrive\rest $client, $fullpath) { + try { + $response = $client->call('delete_file_by_path', ['fullpath' => $fullpath]); + } catch (\core\oauth2\rest_exception $re) { + return false; + } + return true; + } + + + /** + * Get a file summary by full path. + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $fullpath + * @return stdClass + */ + protected function get_file_summary_by_path(\repository_skydrive\rest $client, $fullpath) { + $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,createdByUser"; + $response = $client->call('get_file_by_path', ['fullpath' => $fullpath, '$select' => $fields]); + if (empty($response->id)) { + $details = 'Cannot get file summary:' . $fullpath; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return $response; + } + + /** + * Create a folder within a folder + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $foldername The folder we are creating. + * @param string $parentid The parent folder we are creating in. + * + * @return string The file id of the new folder. + */ + protected function create_folder_in_folder(\repository_skydrive\rest $client, $foldername, $parentid) { + $params = ['parentid' => $parentid]; + $folder = [ 'name' => $foldername, 'folder' => [ 'childCount' => 0 ]]; + $created = $client->call('create_folder', $params, json_encode($folder)); + if (empty($created->id)) { + $details = 'Cannot create folder:' . $foldername; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return $created->id; + } + + /** + * Get simple file info for humans. + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $fileid The file we are querying. + * + * @return stdClass + */ + protected function get_file_summary(\repository_skydrive\rest $client, $fileid) { + $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,createdByUser"; + $response = $client->call('get', ['fileid' => $fileid, '$select' => $fields]); + return $response; + } + + /** + * Get the id of this users root drive. + * + * @param \repository_skydrive\rest $client Authenticated client. + * + * @return string id + */ + protected function get_root_drive_id(\repository_skydrive\rest $client) { + $response = $client->call('get_drive', []); + + if (empty($response->id)) { + $details = 'Cannot get driveid'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return $response->id; + } + + /** + * Add a writer to the permissions on the file (temporary). + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $fileid The file we are updating. + * @param string $email The email of the writer account to add. + * @return boolean + */ + protected function add_temp_writer_to_file(\repository_skydrive\rest $client, $fileid, $email) { + // Expires in 7 days. + $expires = new DateTime(); + $expires->add(new DateInterval("P7D")); + + $updateeditor = [ + 'recipients' => [[ 'email' => $email ]], + 'roles' => ['write'], + 'requireSignIn' => true, + 'sendInvitation' => false + ]; + $params = ['fileid' => $fileid]; + $response = $client->call('create_permission', $params, json_encode($updateeditor)); + if (empty($response->value[0]->id)) { + $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + // Store the permission id in the DB. Scheduled task will remove this permission after 7 days. + if ($access = repository_skydrive\access::get_record(['permissionid' => $response->value[0]->id, 'itemid' => $fileid ])) { + // Update the timemodified. + $access->update(); + } else { + $record = (object) [ 'permissionid' => $response->value[0]->id, 'itemid' => $fileid ]; + $access = new repository_skydrive\access(0, $record); + $access->create(); + } + return true; + } + + /** + * Add a writer to the permissions on the file. + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $fileid The file we are updating. + * @param string $userid The userid of the writer account to add. + * @return boolean + */ + protected function add_writer_to_file(\repository_skydrive\rest $client, $fileid, $useremail) { + $updateeditor = [ + 'recipients' => [ [ 'email' => $useremail ] ], + 'roles' => ['write'], + 'requireSignIn' => true, + 'sendInvitation' => false + ]; + $params = [ 'fileid' => $fileid ]; + $response = $client->call('create_permission', $params, json_encode($updateeditor)); + if (empty($response->value)) { + $details = 'Cannot add user ' . $useremail . ' as a writer for document: ' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return true; + } + + /** + * Allow anyone with the link to read the file. + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $fileid The file we are updating. + * @return boolean + */ + protected function set_file_sharing_anyone_with_link_can_read(\repository_skydrive\rest $client, $fileid) { + $updateread = [ + 'type' => 'view', + 'scope' => 'anonymous' + ]; + $params = ['fileid' => $fileid]; + $response = $client->call('create_link', $params, json_encode($updateread)); + if (empty($response->link)) { + $details = 'Cannot update link sharing for the document: ' . $fileid; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + return true; + } + + /** + * Get share info. + * + * @param \repository_skydrive\rest $client Authenticated client. + * @param string $sharetoken The share we are querying. + * @return stdClass + */ + protected function copy_share(\repository_skydrive\rest $client, $sharetoken, $newdrive, $parentid) { + $folder = [ + 'parentReference' => ['id' => $parentid, 'driveId' => $newdrive] + ]; + $params = ['sharetoken' => $sharetoken]; + $response = $client->call('copy_share', $params, json_encode($folder)); + return true; + } + + /** + * From MS docs - to get a share token from a url, do this: + * Reference: https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/shares_get + * To access a sharing URL using the shares API, the URL needs to be transformed into a sharing token. + * To transform a URL into a sharing token: + * Base64 encode the sharing URL. + * Convert the base64 encoded data to unpadded base64url format by: + * Trim trailing = characeters from the string. + * Replace unsafe URL characters with an equivelent character; replace / with _ and + with -. + * Append u! to the beginning of the string. + * + * @param string sharingUrl + * @return string sharingtoken + */ + protected function get_share_token($shareurl) { + return 'u!' . str_replace(['/', '+'], ['_', '-'], rtrim(base64_encode($shareurl), '=')); + } + + /** + * Called when a file is selected as a "link". + * Invoked at MOODLE/repository/repository_ajax.php + * + * @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 $modifiedreference (final one before saving to DB) + */ + public function reference_file_selected($reference, $context, $component, $filearea, $itemid) { + // What we need to do here is transfer ownership to the system user (or copy) + // then set the permissions so anyone with the share link can view, + // finally update the reference to contain the share link if it was not + // already there (and point to new file id if we copied). + var_dump($reference); + $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); + + if ($systemauth === false) { + $details = 'Cannot connect as system user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $systemuserinfo = $systemauth->get_userinfo(); + $systemuseremail = $systemuserinfo['email']; + + $source = json_decode($reference); + + $userauth = $this->get_user_oauth_client(); + if ($userauth === false) { + $details = 'Cannot connect as current user'; + throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); + } + $userinfo = $userauth->get_userinfo(); + $useremail = $userinfo['email']; + + $userservice = new repository_skydrive\rest($userauth); + $systemservice = new repository_skydrive\rest($systemauth); + + // Get the list of existing permissions so we can see if the owner is already the system account, + // and whether we need to update the link sharing options. + $permissions = $this->list_file_permissions($userservice, $source->id); + + $readshareupdaterequired = true; + $ownerupdaterequired = true; + foreach ($permissions->value as $permission) { + if (!empty($permission->link)) { + if ($permission->link->scope == 'anonymous' && + $permission->link->type == 'view') { + $shareurl = $permission->link->webUrl; + $readshareupdaterequired = false; + break; + } + } + } + + // Add Moodle as writer. + $this->add_writer_to_file($userservice, $source->id, $systemuseremail); + + // Now copy it to a sensible folder. + $contextlist = array_reverse($context->get_parent_contexts(true)); + + $cache = cache::make('repository_skydrive', 'folder'); + $parentid = 'root'; + $fullpath = ''; + $allfolders = []; + foreach ($contextlist as $context) { + // Make sure a folder exists here. + $foldername = urlencode(clean_param($context->get_context_name(), PARAM_PATH)); + $allfolders[] = $foldername; + } + + $allfolders[] = urlencode(clean_param($component, PARAM_PATH)); + $allfolders[] = urlencode(clean_param($filearea, PARAM_PATH)); + $allfolders[] = urlencode(clean_param($itemid, PARAM_PATH)); + + foreach ($allfolders as $foldername) { + if ($fullpath) { + $fullpath .= '/'; + } + $fullpath .= $foldername; + + $folderid = $cache->get($fullpath); + if (empty($folderid)) { + $folderid = $this->get_file_id_by_path($systemservice, $fullpath); + } + if ($folderid !== false) { + $cache->set($fullpath, $folderid); + $parentid = $folderid; + } else { + // Create it. + $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid); + $cache->set($fullpath, $parentid); + } + } + + // Get the users drive id. + $newdrive = $this->get_root_drive_id($systemservice); + + if ($readshareupdaterequired) { + $response = $this->set_file_sharing_anyone_with_link_can_read($userservice, $source->id); + $shareurl = $response->value->webUrl; + } + + // Turn the share url into a sharing token. + $sharetoken = $this->get_share_token($shareurl); + + // Delete any existing file at this path. + $path = $fullpath . '/' . $source->name; + $this->delete_file_by_path($systemservice, $path); + + // Copy the file so we have a backup. + $this->copy_share($systemservice, $sharetoken, $newdrive, $parentid); + + $summary = $this->get_file_summary_by_path($systemservice, $path); + + // Update the details in the file reference before it is saved. + $source->id = $summary->id; + $source->link = $summary->webUrl; + + $reference = json_encode($source); + + return $reference; + } + + /** + * Get human readable file info from the reference. + * + * @param string $reference + * @param int $filestatus + */ + public function get_reference_details($reference, $filestatus = 0) { + if (empty($reference)) { + return get_string('unknownsource', 'repository'); + } + $source = json_decode($reference); + $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); + + if ($systemauth === false) { + return ''; + } + $systemservice = new repository_skydrive\rest($systemauth); + $info = $this->get_file_summary($systemservice, $source->id); + + $owner = ''; + if (!empty($info->createdByUser->displayName)) { + $owner = $info->createdByUser->displayName; + } + if ($owner) { + return get_string('owner', 'repository_skydrive', $owner); + } else { + return $info->name; + } + } + + /** + * Edit/Create Admin Settings Moodle form. + * + * @param moodleform $mform Moodle form (passed by reference). + * @param string $classname repository class name. + */ + public static function type_config_form($mform, $classname = 'repository') { + $url = new moodle_url('/admin/tool/oauth2/issuers.php'); + $url = $url->out(); + + $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_skydrive', $url)); + + parent::type_config_form($mform); + $options = []; + $issuers = \core\oauth2\api::get_all_issuers(); + + foreach ($issuers as $issuer) { + $options[$issuer->get('id')] = s($issuer->get('name')); + } + + $strrequired = get_string('required'); + + $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_skydrive'), $options); + $mform->addHelpButton('issuerid', 'issuer', 'repository_skydrive'); + $mform->addRule('issuerid', $strrequired, 'required', null, 'client'); + + $mform->addElement('static', null, '', get_string('fileoptions', 'repository_skydrive')); + $choices = [ + 'internal' => get_string('internal', 'repository_skydrive'), + 'external' => get_string('external', 'repository_skydrive'), + 'both' => get_string('both', 'repository_skydrive') + ]; + $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_skydrive'), $choices); + + $choices = [ + FILE_INTERNAL => get_string('internal', 'repository_skydrive'), + FILE_CONTROLLED_LINK => get_string('external', 'repository_skydrive'), + ]; + $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_skydrive'), $choices); } } + +/** + * Callback to get the required scopes for system account. + * + * @return string + */ +function repository_skydrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) { + if ($issuer->get('id') == get_config('skydrive', 'issuerid')) { + return repository_skydrive::SCOPES; + } + return ''; +} diff --git a/repository/skydrive/microsoftliveapi.php b/repository/skydrive/microsoftliveapi.php deleted file mode 100644 index 5fc52274eae..00000000000 --- a/repository/skydrive/microsoftliveapi.php +++ /dev/null @@ -1,245 +0,0 @@ -. - -/** - * Functions for operating with the skydrive API - * - * @package repository_skydrive - * @copyright 2012 Lancaster University Network Services Ltd - * @author Dan Poltawski - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir.'/oauthlib.php'); - -/** - * A helper class to access microsoft live resources using the api. - * - * This uses the microsfot API defined in - * http://msdn.microsoft.com/en-us/library/hh243648.aspx - * - * @package repository_skydrive - * @copyright 2012 Lancaster University Network Services Ltd - * @author Dan Poltawski - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class microsoft_skydrive extends oauth2_client { - /** @var string OAuth 2.0 scope */ - const SCOPE = 'wl.skydrive'; - /** @var string Base url to access API */ - const API = 'https://apis.live.net/v5.0'; - /** @var cache_session cache of foldernames */ - var $foldernamecache = null; - - /** - * Construct a skydrive request object - * - * @param string $clientid client id for OAuth 2.0 provided by microsoft - * @param string $clientsecret secret for OAuth 2.0 provided by microsoft - * @param moodle_url $returnurl url to return to after succseful auth - */ - public function __construct($clientid, $clientsecret, $returnurl) { - parent::__construct($clientid, $clientsecret, $returnurl, self::SCOPE); - // Make a session cache - $this->foldernamecache = cache::make('repository_skydrive', 'foldername'); - } - - /** - * Returns the auth url for OAuth 2.0 request - * @return string the auth url - */ - protected function auth_url() { - return 'https://login.live.com/oauth20_authorize.srf'; - } - - /** - * Returns the token url for OAuth 2.0 request - * @return string the auth url - */ - protected function token_url() { - return 'https://login.live.com/oauth20_token.srf'; - } - - /** - * Post request. - * - * Overridden to convert the data to a string, else curl will set the wrong headers. - * - * @param string $url The URL. - * @param array|string $params The parameters. - * @param array $options The options. - * @return bool - */ - public function post($url, $params = '', $options = array()) { - return parent::post($url, format_postdata_for_curlcall($params), $options); - } - - /** - * Downloads a file to a file from skydrive using authenticated request - * - * @param string $id id of file - * @param string $path path to save file to - * @return array stucture for repository download_file - */ - public function download_file($id, $path) { - $url = self::API."/${id}/content"; - // Microsoft live redirects to the real download location.. - $this->setopt(array('CURLOPT_FOLLOWLOCATION' => true, 'CURLOPT_MAXREDIRS' => 3)); - $content = $this->get($url); - file_put_contents($path, $content); - return array('path'=>$path, 'url'=>$url); - } - - /** - * Returns a folder name property for a given folderid. - * - * @param string $folderid the folder id which is passed - * @return mixed folder name or false in case of error - */ - public function get_folder_name($folderid) { - if (empty($folderid)) { - throw new coding_exception('Empty folderid passed to get_folder_name'); - } - - // Cache based on oauthtoken and folderid. - $cachekey = $this->folder_cache_key($folderid); - - if ($foldername = $this->foldernamecache->get($cachekey)) { - return $foldername; - } - - $url = self::API."/{$folderid}"; - $ret = json_decode($this->get($url)); - if (isset($ret->error)) { - $this->log_out(); - return false; - } - - $this->foldernamecache->set($cachekey, $ret->name); - return $ret->name; - } - - /** - * Returns a list of files the user has formated for files api - * - * @param string $path the path which we are in - * @return mixed Array of files formated for fileapoi - */ - public function get_file_list($path = '') { - global $OUTPUT; - - $precedingpath = ''; - if (empty($path)) { - $url = self::API."/me/skydrive/files/"; - } else { - $parts = explode('/', $path); - $currentfolder = array_pop($parts); - $url = self::API."/{$currentfolder}/files/"; - } - - $ret = json_decode($this->get($url)); - - if (isset($ret->error)) { - $this->log_out(); - return false; - } - - $files = array(); - - foreach ($ret->data as $file) { - switch($file->type) { - case 'folder': - case 'album': - // Cache the foldername for future requests. - $cachekey = $this->folder_cache_key($file->id); - $this->foldernamecache->set($cachekey, $file->name); - - $files[] = array( - 'title' => $file->name, - 'path' => $path.'/'.$file->id, - 'size' => 0, - 'date' => strtotime($file->updated_time), - 'thumbnail' => $OUTPUT->image_url(file_folder_icon(90))->out(false), - 'children' => array(), - ); - break; - case 'photo': - $files[] = array( - 'title' => $file->name, - 'size' => $file->size, - 'date' => strtotime($file->updated_time), - 'thumbnail' => $OUTPUT->image_url(file_extension_icon($file->name, 90))->out(false), - 'realthumbnail' => $file->picture, - 'source' => $file->id, - 'url' => $file->link, - 'image_height' => $file->height, - 'image_width' => $file->width, - 'author' => $file->from->name, - ); - break; - case 'video': - $files[] = array( - 'title' => $file->name, - 'size' => $file->size, - 'date' => strtotime($file->updated_time), - 'thumbnail' => $OUTPUT->image_url(file_extension_icon($file->name, 90))->out(false), - 'realthumbnail' => $file->picture, - 'source' => $file->id, - 'url' => $file->link, - 'author' => $file->from->name, - ); - break; - case 'audio': - $files[] = array( - 'title' => $file->name, - 'size' => $file->size, - 'date' => strtotime($file->updated_time), - 'thumbnail' => $OUTPUT->image_url(file_extension_icon($file->name, 90))->out(false), - 'source' => $file->id, - 'url' => $file->link, - 'author' => $file->from->name, - ); - break; - case 'file': - $files[] = array( - 'title' => $file->name, - 'size' => $file->size, - 'date' => strtotime($file->updated_time), - 'thumbnail' => $OUTPUT->image_url(file_extension_icon($file->name, 90))->out(false), - 'source' => $file->id, - 'url' => $file->link, - 'author' => $file->from->name, - ); - break; - } - } - return $files; - } - - /** - * Returns a key for foldernane cache - * - * @param string $folderid the folder id which is to be cached - * @return string the cache key to use - */ - private function folder_cache_key($folderid) { - // Cache based on oauthtoken and folderid. - return $this->get_tokenname().'_'.$folderid; - } -} diff --git a/repository/skydrive/version.php b/repository/skydrive/version.php index 8afca1d1f90..a3995847071 100644 --- a/repository/skydrive/version.php +++ b/repository/skydrive/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2017031400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2016112900; // Requires this Moodle version. $plugin->component = 'repository_skydrive'; // Full name of the plugin (used for diagnostics). From 6e0d700de83af2589570895353e0bf45b718ad0e Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 14 Mar 2017 15:41:56 +0800 Subject: [PATCH 35/84] MDL-58220 assign: Only show write access to teachers In filebrowser api - this is also used by repositories using controlled links. --- mod/assign/assignmentplugin.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mod/assign/assignmentplugin.php b/mod/assign/assignmentplugin.php index 83b8dfa8b39..22c43f82cf2 100644 --- a/mod/assign/assignmentplugin.php +++ b/mod/assign/assignmentplugin.php @@ -582,6 +582,7 @@ abstract class assign_plugin { $urlbase = $CFG->wwwroot.'/pluginfile.php'; $writeaccess = false; // Permission check on the itemid. + $assignment = $this->assignment; if ($this->get_subtype() == 'assignsubmission') { if ($itemid) { @@ -590,16 +591,19 @@ abstract class assign_plugin { return null; } if (!empty($record->userid)) { - if (!$this->assignment->can_view_submission($record->userid)) { + if (!$assignment->can_view_submission($record->userid)) { return null; } - $writeaccess = $this->assignment->can_edit_submission($record->userid); + + // We only report write access for teachers. + $writeaccess = $assignment->can_grade() && $assignment->can_edit_submission($record->userid); } else { // Must be a team submission with a group. - if (!$this->assignment->can_view_group_submission($record->groupid)) { + if (!$assignment->can_view_group_submission($record->groupid)) { return null; } - $writeaccess = $this->assignment->can_edit_group_submission($record->groupid); + // We only report write access for teachers. + $writeaccess = $assignment->can_grade() && $assignment->can_edit_group_submission($record->groupid); } } } else { @@ -610,7 +614,7 @@ abstract class assign_plugin { $fs = get_file_storage(); $filepath = is_null($filepath) ? '/' : $filepath; $filename = is_null($filename) ? '.' : $filename; - if (!($storedfile = $fs->get_file($this->assignment->get_context()->id, + if (!($storedfile = $fs->get_file($assignment->get_context()->id, $this->get_subtype() . '_' . $this->get_type(), $filearea, $itemid, @@ -620,7 +624,7 @@ abstract class assign_plugin { } return new file_info_stored($browser, - $this->assignment->get_context(), + $assignment->get_context(), $storedfile, $urlbase, $filearea, From eca128bf4786a5bd55614525a8fba0be73beaac8 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 14 Mar 2017 16:39:25 +0800 Subject: [PATCH 36/84] MDL-58220 oauth2: Global enable/disable for issuers. --- admin/tool/oauth2/classes/output/renderer.php | 14 ++++++++ admin/tool/oauth2/issuers.php | 11 +++++++ admin/tool/oauth2/lang/en/tool_oauth2.php | 2 ++ auth/oauth2/classes/auth.php | 3 +- lib/classes/oauth2/api.php | 33 +++++++++++++++++++ lib/classes/oauth2/issuer.php | 4 +++ lib/db/install.xml | 1 + lib/db/upgrade.php | 1 + repository/googledocs/lib.php | 18 ++++++++++ repository/skydrive/lib.php | 13 ++++++++ 10 files changed, 99 insertions(+), 1 deletion(-) diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index d38d7f00848..7a6fe3aaa68 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -154,6 +154,20 @@ class renderer extends plugin_renderer_base { $deleteurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['id' => $issuer->get('id'), 'action' => 'delete']); $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); $links .= ' ' . $deletelink; + // Enable / Disable. + if ($issuer->get('enabled')) { + // Disable. + $disableparams = ['id' => $issuer->get('id'), 'sesskey' => sesskey(), 'action' => 'disable']; + $disableurl = new moodle_url('/admin/tool/oauth2/issuers.php', $disableparams); + $disablelink = html_writer::link($disableurl, $OUTPUT->pix_icon('t/hide', get_string('disable'))); + $links .= ' ' . $disablelink; + } else { + // Enable. + $enableparams = ['id' => $issuer->get('id'), 'sesskey' => sesskey(), 'action' => 'enable']; + $enableurl = new moodle_url('/admin/tool/oauth2/issuers.php', $enableparams); + $enablelink = html_writer::link($enableurl, $OUTPUT->pix_icon('t/show', get_string('enable'))); + $links .= ' ' . $enablelink; + } if (!$last) { // Move down. $params = ['id' => $issuer->get('id'), 'action' => 'movedown', 'sesskey' => sesskey()]; diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php index b8ada448e0a..7da2c480973 100644 --- a/admin/tool/oauth2/issuers.php +++ b/admin/tool/oauth2/issuers.php @@ -111,6 +111,17 @@ if ($mform && $mform->is_cancelled()) { $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); redirect($editurl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } +} else if ($action == 'enable') { + + require_sesskey(); + core\oauth2\api::enable_issuer($issuerid); + redirect($PAGE->url, get_string('issuerenabled', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); + +} else if ($action == 'disable') { + + require_sesskey(); + core\oauth2\api::disable_issuer($issuerid); + redirect($PAGE->url, get_string('issuerdisabled', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); } else if ($action == 'delete') { diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 2ee10daa14c..7c751416b51 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -84,6 +84,8 @@ $string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer $string['deleteendpointconfirm'] = 'Are you sure you want to delete the endpoint "{$a->endpoint}" for issuer "{$a->issuer}"? Any plugins relying on this endpoint will stop working.'; $string['deleteuserfieldmappingconfirm'] = 'Are you sure you want to delete the user field mapping for issuer "{$a}"?'; $string['issuerdeleted'] = 'Identity issuer deleted'; +$string['issuerenabled'] = 'Identity issuer enabled'; +$string['issuerdisabled'] = 'Identity issuer disabled'; $string['endpointdeleted'] = 'Endpoint deleted'; $string['userfieldmappingdeleted'] = 'User field mapping deleted'; $string['connectsystemaccount'] = 'Connect to a system account'; diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index b82124e317b..832d1f0398b 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -180,7 +180,8 @@ class auth extends \auth_plugin_base { * @return boolean */ private function is_ready_for_login_page(\core\oauth2\issuer $issuer) { - return !empty($issuer->get('clientid')) && + return $issuer->get('enabled') && + !empty($issuer->get('clientid')) && !empty($issuer->get('clientsecret')) && $issuer->is_authentication_supported() && !empty($issuer->get('showonloginpage')); diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index c886a1aaecb..02320cefc1b 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -616,6 +616,39 @@ class api { return $result; } + /** + * Disable an identity issuer. + * + * Requires moodle/site:config capability at the system context. + * + * @param int $id The id of the identity issuer to enable. + * @return boolean + */ + public static function disable_issuer($id) { + require_capability('moodle/site:config', context_system::instance()); + $issuer = new issuer($id); + + $issuer->set('enabled', 0); + return $issuer->update(); + } + + + /** + * Enable an identity issuer. + * + * Requires moodle/site:config capability at the system context. + * + * @param int $id The id of the identity issuer to enable. + * @return boolean + */ + public static function enable_issuer($id) { + require_capability('moodle/site:config', context_system::instance()); + $issuer = new issuer($id); + + $issuer->set('enabled', 1); + return $issuer->update(); + } + /** * Delete an identity issuer. * diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 756d36ba804..6ef3ca1d0b4 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -64,6 +64,10 @@ class issuer extends persistent { 'type' => PARAM_URL, 'default' => '' ), + 'enabled' => array( + 'type' => PARAM_BOOL, + 'default' => true + ), 'showonloginpage' => array( 'type' => PARAM_BOOL, 'default' => false diff --git a/lib/db/install.xml b/lib/db/install.xml index adb86a4f4ed..63545257b2e 100755 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -3494,6 +3494,7 @@ + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index c5878f7b3a0..29eaf7471bd 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2632,6 +2632,7 @@ function xmldb_main_upgrade($oldversion) { $table->add_field('loginparamsoffline', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('scopessupported', XMLDB_TYPE_TEXT, null, null, null, null, null); $table->add_field('showonloginpage', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('enabled', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); // Adding keys to table oauth2_issuer. diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index cbc275ff470..eb9d83988a1 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -201,6 +201,10 @@ class repository_googledocs extends repository { if (empty($path)) { $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs')); } + if (!$this->issuer->get('enabled')) { + // Empty list of files for disabled repository. + return ['dynload' => false, 'list' => [], 'nologin' => true]; + } // We analyse the path to extract what to browse. $trail = explode('/', $path); @@ -416,6 +420,10 @@ class repository_googledocs extends repository { public function get_file($reference, $filename = '') { global $CFG; + if (!$this->issuer->get('enabled')) { + throw new repository_exception('cannotdownload', 'repository'); + } + $client = $this->get_user_oauth_client(); $base = 'https://www.googleapis.com/drive/v3'; @@ -557,6 +565,10 @@ class repository_googledocs extends repository { * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) { + if (!$this->issuer->get('enabled')) { + throw new repository_exception('cannotdownload', 'repository'); + } + $source = json_decode($storedfile->get_reference()); $fb = get_file_browser(); @@ -829,6 +841,9 @@ class repository_googledocs extends repository { * @return string updated reference (final one before it's saved to db). */ public function reference_file_selected($reference, $context, $component, $filearea, $itemid) { + if (!$this->issuer->get('enabled')) { + throw new repository_exception('cannotdownload', 'repository'); + } // What we need to do here is transfer ownership to the system user (or copy) // then set the permissions so anyone with the share link can view, // finally update the reference to contain the share link if it was not @@ -919,6 +934,9 @@ class repository_googledocs extends repository { * @param int $filestatus */ public function get_reference_details($reference, $filestatus = 0) { + if (!$this->issuer->get('enabled')) { + throw new repository_exception('cannotdownload', 'repository'); + } if (empty($reference)) { return get_string('unknownsource', 'repository'); } diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php index 41c87dc8f8e..d7b56a31482 100644 --- a/repository/skydrive/lib.php +++ b/repository/skydrive/lib.php @@ -198,6 +198,11 @@ class repository_skydrive extends repository { $path = $this->build_node_path('root', get_string('pluginname', 'repository_skydrive')); } + if (!$this->issuer->get('enabled')) { + // Empty list of files for disabled repository. + return ['dynload' => false, 'list' => [], 'nologin' => true]; + } + // We analyse the path to extract what to browse. $trail = explode('/', $path); $uri = array_pop($trail); @@ -392,6 +397,10 @@ class repository_skydrive extends repository { public function get_file($reference, $filename = '') { global $CFG; + if (!$this->issuer->get('enabled')) { + throw new repository_exception('cannotdownload', 'repository'); + } + $client = $this->get_user_oauth_client(); $base = 'https://graph.microsoft.com/v1.0/'; @@ -505,6 +514,10 @@ class repository_skydrive extends repository { * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) { + if (!$this->issuer->get('enabled')) { + throw new repository_exception('cannotdownload', 'repository'); + } + $source = json_decode($storedfile->get_reference()); $fb = get_file_browser(); From 979d1f66dd1e15f64e941525c299bf7a272eb2e2 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 15 Mar 2017 10:13:07 +0800 Subject: [PATCH 37/84] MDL-58220 auth_oauth2: Fix new account creation This was not allowing new accounts to be registered. --- auth/oauth2/classes/auth.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 832d1f0398b..b2fa8f6626e 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -76,6 +76,7 @@ class auth extends \auth_plugin_base { if ($verifyusername == $username) { return true; } + return false; } /** @@ -369,7 +370,7 @@ class auth extends \auth_plugin_base { $this->set_static_user_info($userinfo); - $user = get_complete_user_data('username', $userinfo['username']); + $user = authenticate_user_login($userinfo['username'], ''); if ($user) { complete_user_login($user); From c21a66e40af7e24bec5bc984304c2f3ee3453f52 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 15 Mar 2017 10:48:20 +0800 Subject: [PATCH 38/84] MDL-58220 auth_oauth2: Restrict logins to a set of domains This is set on the issuer, so you can have different lists for each oauth2 provider. --- admin/tool/oauth2/classes/form/issuer.php | 8 +++++++ admin/tool/oauth2/lang/en/tool_oauth2.php | 3 +++ auth/oauth2/classes/auth.php | 9 ++++++-- lib/classes/oauth2/issuer.php | 27 +++++++++++++++++++++++ lib/db/upgrade.php | 17 +++++++------- version.php | 2 +- 6 files changed, 55 insertions(+), 11 deletions(-) diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index 80a44820af0..b3356a1e411 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -99,6 +99,11 @@ class issuer extends persistent { $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2'); + // Allowed Domains. + $mform->addElement('text', 'alloweddomains', get_string('issueralloweddomains', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addRule('alloweddomains', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); + $mform->addHelpButton('alloweddomains', 'issueralloweddomains', 'tool_oauth2'); + // Image. $mform->addElement('text', 'image', get_string('issuerimage', 'tool_oauth2'), 'maxlength="1024"'); $mform->addRule('image', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); @@ -114,6 +119,9 @@ class issuer extends persistent { $mform->addElement('hidden', 'action', 'edit'); $mform->setType('action', PARAM_RAW); + $mform->addElement('hidden', 'enabled', $endpoint->get('enabled')); + $mform->setType('enabled', PARAM_BOOL); + $mform->addElement('hidden', 'id', $endpoint->get('id')); $mform->setType('id', PARAM_INT); diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 7c751416b51..63edd815de0 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -51,6 +51,9 @@ $string['issuerloginparams'] = 'Additional parameters included in a login reques $string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the users basic profile.'; $string['issuerloginparamsoffline'] = 'Additional parameters included in a login request for offline access.'; $string['issuerloginparamsoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Google requires the additional params: "access_type=offline&prompt=consent" these parameters should be in url query parameter format.'; +$string['issueralloweddomains'] = 'Login domains'; +$string['issueralloweddomains_help'] = 'If set, this setting is a comma separated list of domains that logins will be restricted to when using this provider.'; +$string['issueralloweddomains_link'] = 'OAuth_2_login_domains'; $string['issuershowonloginpage'] = 'Show on login page.'; $string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to login with accounts from this issuer.'; $string['issuerbehaviour'] = 'Behaviour'; diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index b2fa8f6626e..82e58e24311 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -368,9 +368,14 @@ class auth extends \auth_plugin_base { } } - $this->set_static_user_info($userinfo); + $issuer = $client->get_issuer(); - $user = authenticate_user_login($userinfo['username'], ''); + $user = false; + if ($issuer->is_valid_login_domain($userinfo['email'])) { + + $this->set_static_user_info($userinfo); + $user = authenticate_user_login($userinfo['username'], ''); + } if ($user) { complete_user_login($user); diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 6ef3ca1d0b4..58709a922ca 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -93,6 +93,10 @@ class issuer extends persistent { 'type' => PARAM_RAW, 'default' => '' ), + 'alloweddomains' => array( + 'type' => PARAM_RAW, + 'default' => '' + ), 'sortorder' => array( 'type' => PARAM_INT, 'default' => 0, @@ -128,6 +132,29 @@ class issuer extends persistent { return false; } + /** + * Perform matching against the list of allowed login domains for this issuer. + * @return boolean + */ + public function is_valid_login_domain($email) { + if (empty($this->get('alloweddomains'))) { + return true; + } + $validdomains = explode(',', $this->get('alloweddomains')); + + list($unused, $emaildomain) = explode('@', $email, 2); + + foreach ($validdomains as $checkdomain) { + $checkdomain = \core_text::strtolower(trim($checkdomain)); + + if ((\core_text::strlen($checkdomain) == \core_text::strlen($emaildomain)) && + (\core_text::strpos($checkdomain, $emaildomain) === 0)) { + return true; + } + } + return false; + } + /** * Does this OAuth service support user authentication? * @return boolean diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 29eaf7471bd..99798247d77 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2611,7 +2611,7 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2017031400.00); } - if ($oldversion < 2017032400.21) { + if ($oldversion < 2017033100.01) { // Define table oauth2_issuer to be created. $table = new xmldb_table('oauth2_issuer'); @@ -2630,6 +2630,7 @@ function xmldb_main_upgrade($oldversion) { $table->add_field('loginscopesoffline', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('loginparams', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('loginparamsoffline', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('alloweddomains', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('scopessupported', XMLDB_TYPE_TEXT, null, null, null, null, null); $table->add_field('showonloginpage', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); $table->add_field('enabled', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); @@ -2644,10 +2645,10 @@ function xmldb_main_upgrade($oldversion) { } // Main savepoint reached. - upgrade_main_savepoint(true, 2017032400.21); + upgrade_main_savepoint(true, 2017033100.01); } - if ($oldversion < 2017032400.22) { + if ($oldversion < 2017033100.02) { // Define table oauth2_endpoint to be created. $table = new xmldb_table('oauth2_endpoint'); @@ -2671,10 +2672,10 @@ function xmldb_main_upgrade($oldversion) { } // Main savepoint reached. - upgrade_main_savepoint(true, 2017032400.22); + upgrade_main_savepoint(true, 2017033100.02); } - if ($oldversion < 2017032400.23) { + if ($oldversion < 2017033100.03) { // Define table oauth2_system_account to be created. $table = new xmldb_table('oauth2_system_account'); @@ -2700,10 +2701,10 @@ function xmldb_main_upgrade($oldversion) { } // Main savepoint reached. - upgrade_main_savepoint(true, 2017032400.23); + upgrade_main_savepoint(true, 2017033100.03); } - if ($oldversion < 2017033100.01) { + if ($oldversion < 2017033100.04) { // Define table oauth2_user_field_mapping to be created. $table = new xmldb_table('oauth2_user_field_mapping'); @@ -2728,7 +2729,7 @@ function xmldb_main_upgrade($oldversion) { } // Main savepoint reached. - upgrade_main_savepoint(true, 2017033100.01); + upgrade_main_savepoint(true, 2017033100.04); } return true; diff --git a/version.php b/version.php index aaed5ce20e3..b45e6e930a9 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017033100.03; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017033100.04; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. From 738c4a2a73462c24137cb5d76f71968764485e2f Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 15 Mar 2017 14:04:49 +0800 Subject: [PATCH 39/84] MDL-58220 oauth2: More help buttons Add help for system account connections and discovery. --- admin/tool/oauth2/classes/output/renderer.php | 3 +++ admin/tool/oauth2/lang/en/tool_oauth2.php | 2 ++ 2 files changed, 5 insertions(+) diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index 7a6fe3aaa68..43633c1dab8 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -110,6 +110,8 @@ class renderer extends plugin_renderer_base { $discovered = '-'; } } + $discovered .= ' ' . $OUTPUT->help_icon('discovered', 'tool_oauth2'); + $discoverystatuscell = new html_table_cell($discovered); // Connected. @@ -128,6 +130,7 @@ class renderer extends plugin_renderer_base { $authlink = html_writer::link($authurl, $icon); $systemauth .= ' ' . $authlink; } + $systemauth .= ' ' . $OUTPUT->help_icon('systemaccountconnected', 'tool_oauth2'); $systemauthstatuscell = new html_table_cell($systemauth); diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 63edd815de0..4d697b16c3a 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -23,6 +23,8 @@ */ $string['pluginname'] = 'OAuth 2 Services'; +$string['discovered_help'] = 'Discovered means that the OAuth2 endpoints could be automatically determined from the base url for the OAuth service. Not all services are required to be "discovered", but if they are not, then the endpoints and user mapping information will need to be entered manually.'; +$string['systemaccountconnected_help'] = 'System accounts are used to provide advanced functionality for plugins. They are not required for login functionality only, but other plugins using the OAuth service may offer a reduced set of features if the system account has not been connected. For example repositories cannot support "controlled links" without a system account to perform file operations.'; $string['editissuer'] = 'Edit identity issuer: {$a}'; $string['editendpoint'] = 'Edit endpoint: {$a->endpoint} for issuer {$a->issuer}'; $string['endpointsforissuer'] = 'Endpoints for issuer: {$a}'; From 092304a3decc86a4277dbc7e5e45943f00219a9e Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 15 Mar 2017 15:07:41 +0800 Subject: [PATCH 40/84] MDL-58220 oauth2: Cibot fixes Missing phpdocs. --- lib/classes/oauth2/issuer.php | 2 ++ repository/googledocs/lib.php | 1 + repository/skydrive/classes/access.php | 3 ++- .../skydrive/classes/remove_temp_access_task.php | 2 +- repository/skydrive/db/upgrade.php | 1 + repository/skydrive/lib.php | 13 ++++++++----- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 58709a922ca..5bbb9e8d049 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -134,6 +134,8 @@ class issuer extends persistent { /** * Perform matching against the list of allowed login domains for this issuer. + * + * @param string $email The email to check. * @return boolean */ public function is_valid_login_domain($email) { diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index eb9d83988a1..5137d519e32 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -1058,6 +1058,7 @@ class repository_googledocs extends repository { /** * Callback to get the required scopes for system account. * + * @param \core\oauth2\issuer $issuer * @return string */ function repository_googledocs_oauth2_system_scopes(\core\oauth2\issuer $issuer) { diff --git a/repository/skydrive/classes/access.php b/repository/skydrive/classes/access.php index 8c3fd90edc6..35a9bdd1d9a 100644 --- a/repository/skydrive/classes/access.php +++ b/repository/skydrive/classes/access.php @@ -17,7 +17,7 @@ /** * Class for loading/storing access records from the DB. * - * @package core + * @package repository_skydrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -30,6 +30,7 @@ use core\persistent; /** * Class for loading/storing issuer from the DB * + * @package repository_skydrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/repository/skydrive/classes/remove_temp_access_task.php b/repository/skydrive/classes/remove_temp_access_task.php index 98247ec93db..b7f928449bf 100644 --- a/repository/skydrive/classes/remove_temp_access_task.php +++ b/repository/skydrive/classes/remove_temp_access_task.php @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); /** * Simple task to delete temporary permission records. - * @package core + * @package repository_skydrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/repository/skydrive/db/upgrade.php b/repository/skydrive/db/upgrade.php index 7a714846fcd..a9922abe2f1 100644 --- a/repository/skydrive/db/upgrade.php +++ b/repository/skydrive/db/upgrade.php @@ -18,6 +18,7 @@ defined('MOODLE_INTERNAL') || die(); /** * @param int $oldversion the version we are upgrading from + * @package repository_skydrive * @return bool result */ function xmldb_repository_skydrive_upgrade($oldversion) { diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php index d7b56a31482..89c3b174559 100644 --- a/repository/skydrive/lib.php +++ b/repository/skydrive/lib.php @@ -271,6 +271,7 @@ class repository_skydrive extends repository { * * @param string $q search query as expected by the Google API. * @param string $path parent path of the current files, will not be used for the query. + * @param string $parent Parent id. * @param int $page page. * @return array of files and folders. */ @@ -391,7 +392,7 @@ class repository_skydrive extends repository { * Get a file. * * @param string $reference reference of the file. - * @param string $file name to save the file to. + * @param string $filename filename to save the file to. * @return string JSON encoded array of information about the file. */ public function get_file($reference, $filename = '') { @@ -726,7 +727,7 @@ class repository_skydrive extends repository { * * @param \repository_skydrive\rest $client Authenticated client. * @param string $fileid The file we are updating. - * @param string $userid The userid of the writer account to add. + * @param string $useremail The user email of the writer account to add. * @return boolean */ protected function add_writer_to_file(\repository_skydrive\rest $client, $fileid, $useremail) { @@ -767,10 +768,12 @@ class repository_skydrive extends repository { } /** - * Get share info. + * Copy a shared file to a new folder. * * @param \repository_skydrive\rest $client Authenticated client. * @param string $sharetoken The share we are querying. + * @param string $newdrive Id of the drive to copy to. + * @param string $parentid Id of the folder to copy to. * @return stdClass */ protected function copy_share(\repository_skydrive\rest $client, $sharetoken, $newdrive, $parentid) { @@ -793,8 +796,8 @@ class repository_skydrive extends repository { * Replace unsafe URL characters with an equivelent character; replace / with _ and + with -. * Append u! to the beginning of the string. * - * @param string sharingUrl - * @return string sharingtoken + * @param string $shareurl + * @return string The sharing token */ protected function get_share_token($shareurl) { return 'u!' . str_replace(['/', '+'], ['_', '-'], rtrim(base64_encode($shareurl), '=')); From fa78244d17d8b0acaed91b7e7b9f9d79f59e0ab0 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 15 Mar 2017 16:29:25 +0800 Subject: [PATCH 41/84] MDL-58220 oauth2: Fix unit tests and add more --- lib/classes/oauth2/issuer.php | 6 ++- lib/tests/oauth2_test.php | 72 ++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 5bbb9e8d049..3f56d7aedc1 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -144,7 +144,11 @@ class issuer extends persistent { } $validdomains = explode(',', $this->get('alloweddomains')); - list($unused, $emaildomain) = explode('@', $email, 2); + $parts = explode('@', $email, 2); + $emaildomain = ''; + if (count($parts) > 1) { + $emaildomain = $parts[1]; + } foreach ($validdomains as $checkdomain) { $checkdomain = \core_text::strtolower(trim($checkdomain)); diff --git a/lib/tests/oauth2_test.php b/lib/tests/oauth2_test.php index 1831dc26b3d..a8b50c11449 100644 --- a/lib/tests/oauth2_test.php +++ b/lib/tests/oauth2_test.php @@ -74,7 +74,10 @@ class core_oauth2_testcase extends advanced_testcase { $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); $same = \core\oauth2\api::get_issuer($issuer->get('id')); - $this->assertEquals($issuer, $same); + + foreach ($same->properties_definition() as $name => $def) { + $this->assertTrue($issuer->get($name) == $same->get($name)); + } $endpoints = \core\oauth2\api::get_endpoints($issuer); $same = \core\oauth2\api::get_endpoint($endpoints[0]->get('id')); @@ -132,4 +135,71 @@ class core_oauth2_testcase extends advanced_testcase { $client = \core\oauth2\api::get_system_oauth_client($issuer); $this->assertTrue($client->is_logged_in()); } + + /** + * Tests we can enable and disable an issuer. + */ + public function test_enable_disable_issuer() { + global $SESSION; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); + + $issuerid = $issuer->get('id'); + + \core\oauth2\api::enable_issuer($issuerid); + $check = \core\oauth2\api::get_issuer($issuer->get('id')); + $this->assertTrue((boolean)$check->get('enabled')); + + \core\oauth2\api::enable_issuer($issuerid); + $check = \core\oauth2\api::get_issuer($issuer->get('id')); + $this->assertTrue((boolean)$check->get('enabled')); + + \core\oauth2\api::disable_issuer($issuerid); + $check = \core\oauth2\api::get_issuer($issuer->get('id')); + $this->assertFalse((boolean)$check->get('enabled')); + + \core\oauth2\api::enable_issuer($issuerid); + $check = \core\oauth2\api::get_issuer($issuer->get('id')); + $this->assertTrue((boolean)$check->get('enabled')); + } + + /** + * Test the alloweddomains for an issuer. + */ + public function test_issuer_alloweddomains() { + global $SESSION; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); + + $issuer->set('alloweddomains', ''); + + // Anything is allowed when domain is empty. + $this->assertTrue($issuer->is_valid_login_domain('')); + $this->assertTrue($issuer->is_valid_login_domain('a@b')); + $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com')); + + $issuer->set('alloweddomains', 'example.com'); + + // One domain - must match exactly - no substrings etc. + $this->assertFalse($issuer->is_valid_login_domain('')); + $this->assertFalse($issuer->is_valid_login_domain('a@b')); + $this->assertFalse($issuer->is_valid_login_domain('longer.example@example')); + $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com')); + + $issuer->set('alloweddomains', 'example.com,example.net'); + // Multiple domains - must match any exactly - no substrings etc. + $this->assertFalse($issuer->is_valid_login_domain('')); + $this->assertFalse($issuer->is_valid_login_domain('a@b')); + $this->assertFalse($issuer->is_valid_login_domain('longer.example@example')); + $this->assertFalse($issuer->is_valid_login_domain('invalid@email@example.net')); + $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.net')); + $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com')); + } + } From d5bb9f1ffca8eb4110881f989421f0a09e9d88b0 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 21 Mar 2017 13:16:29 +0800 Subject: [PATCH 42/84] MDL-58334 repositories: Offline downloads Support an optional param for offline downloads for repositories supporting external links (googledrive and skydrive). Part of MDL-58220 --- lib/filelib.php | 70 ++++++++++++++++++----------------- pluginfile.php | 5 ++- repository/googledocs/lib.php | 12 +++++- repository/skydrive/lib.php | 9 +++-- 4 files changed, 57 insertions(+), 39 deletions(-) diff --git a/lib/filelib.php b/lib/filelib.php index d9e0b0bb414..0c314f547ba 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -3856,9 +3856,11 @@ class curl_cache { * @param string $relativepath * @param bool $forcedownload * @param null|string $preview the preview mode, defaults to serving the original file + * @param boolean $offline If offline is requested - don't serve a redirect to an external file, return a file suitable for viewing + * offline (e.g. mobile app). * @todo MDL-31088 file serving improments */ -function file_pluginfile($relativepath, $forcedownload, $preview = null) { +function file_pluginfile($relativepath, $forcedownload, $preview = null, $offline = false) { global $DB, $CFG, $USER; // relative path must start with '/' if (!$relativepath) { @@ -3882,6 +3884,8 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { $fs = get_file_storage(); + $sendfileoptions = ['preview' => $preview, 'offline' => $offline]; + // ======================================================================================================================== if ($component === 'blog') { // Blog file serving @@ -3934,7 +3938,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { send_file_not_found(); } - send_stored_file($file, 10*60, 0, true, array('preview' => $preview)); // download MUST be forced - security! + send_stored_file($file, 10*60, 0, true, $sendfileoptions); // download MUST be forced - security! // ======================================================================================================================== } else if ($component === 'grade') { @@ -3951,7 +3955,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else if ($filearea === 'feedback' and $context->contextlevel == CONTEXT_COURSE) { //TODO: nobody implemented this yet in grade edit form!! @@ -3968,7 +3972,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else { send_file_not_found(); } @@ -3989,7 +3993,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, true, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, true, $sendfileoptions); } else { send_file_not_found(); @@ -4011,14 +4015,14 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else if ($filearea === 'userbadge' and $context->contextlevel == CONTEXT_USER) { if (!$file = $fs->get_file($context->id, 'badges', 'userbadge', $badge->id, '/', $filename.'.png')) { send_file_not_found(); } \core\session\manager::write_close(); - send_stored_file($file, 60*60, 0, true, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, true, $sendfileoptions); } // ======================================================================================================================== } else if ($component === 'calendar') { @@ -4045,7 +4049,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_USER) { @@ -4073,7 +4077,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 0, 0, true, array('preview' => $preview)); + send_stored_file($file, 0, 0, true, $sendfileoptions); } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_COURSE) { @@ -4120,7 +4124,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else { send_file_not_found(); @@ -4174,7 +4178,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { send_file($imagefile, basename($imagefile), 60*60*24*14); } - $options = array('preview' => $preview); + $options = $sendfileoptions; if (empty($CFG->forcelogin) && empty($CFG->forceloginforprofileimage)) { // Profile images should be cache-able by both browsers and proxies according // to $CFG->forcelogin and $CFG->forceloginforprofileimage. @@ -4200,7 +4204,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 0, 0, true, array('preview' => $preview)); // must force download - security! + send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security! } else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_USER) { @@ -4247,7 +4251,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 0, 0, true, array('preview' => $preview)); // must force download - security! + send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security! } else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_COURSE) { $userid = (int)array_shift($args); @@ -4285,7 +4289,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 0, 0, true, array('preview' => $preview)); // must force download - security! + send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security! } else if ($filearea === 'backup' and $context->contextlevel == CONTEXT_USER) { require_login(); @@ -4306,7 +4310,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 0, 0, true, array('preview' => $preview)); // must force download - security! + send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security! } else { send_file_not_found(); @@ -4339,7 +4343,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else { send_file_not_found(); } @@ -4362,7 +4366,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else if ($filearea === 'section') { if ($CFG->forcelogin) { @@ -4384,7 +4388,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else { send_file_not_found(); @@ -4413,7 +4417,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { if (($file = $fs->get_file($cohortcontext->id, 'cohort', 'description', $cohort->id, $filepath, $filename)) && !$file->is_directory()) { \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60 * 60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60 * 60, 0, $forcedownload, $sendfileoptions); } } @@ -4445,7 +4449,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else if ($filearea === 'icon') { $filename = array_pop($args); @@ -4460,7 +4464,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, false, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, false, $sendfileoptions); } else { send_file_not_found(); @@ -4485,7 +4489,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else { send_file_not_found(); @@ -4504,7 +4508,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 0, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 0, 0, $forcedownload, $sendfileoptions); } else if ($filearea === 'section' and $context->contextlevel == CONTEXT_COURSE) { require_login($course); @@ -4519,7 +4523,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else if ($filearea === 'activity' and $context->contextlevel == CONTEXT_MODULE) { require_login($course, false, $cm); @@ -4532,7 +4536,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } else if ($filearea === 'automated' and $context->contextlevel == CONTEXT_COURSE) { // Backup files that were generated by the automated backup systems. @@ -4547,7 +4551,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 0, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 0, 0, $forcedownload, $sendfileoptions); } else { send_file_not_found(); @@ -4556,7 +4560,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { // ======================================================================================================================== } else if ($component === 'question') { require_once($CFG->libdir . '/questionlib.php'); - question_pluginfile($course, $context, 'question', $filearea, $args, $forcedownload); + question_pluginfile($course, $context, 'question', $filearea, $args, $forcedownload, $sendfileoptions); send_file_not_found(); // ======================================================================================================================== @@ -4593,7 +4597,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } \core\session\manager::write_close(); // Unlock session during file serving. - send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview)); + send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions); } // ======================================================================================================================== @@ -4625,17 +4629,17 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } // finally send the file - send_stored_file($file, null, 0, false, array('preview' => $preview)); + send_stored_file($file, null, 0, false, $sendfileoptions); } $filefunction = $component.'_pluginfile'; $filefunctionold = $modname.'_pluginfile'; if (function_exists($filefunction)) { // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found" - $filefunction($course, $cm, $context, $filearea, $args, $forcedownload, array('preview' => $preview)); + $filefunction($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions); } else if (function_exists($filefunctionold)) { // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found" - $filefunctionold($course, $cm, $context, $filearea, $args, $forcedownload, array('preview' => $preview)); + $filefunctionold($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions); } send_file_not_found(); @@ -4676,7 +4680,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { $filefunction = $component.'_pluginfile'; if (function_exists($filefunction)) { // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found" - $filefunction($course, $birecord, $context, $filearea, $args, $forcedownload, array('preview' => $preview)); + $filefunction($course, $birecord, $context, $filearea, $args, $forcedownload, $sendfileoptions); } send_file_not_found(); @@ -4697,7 +4701,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { $filefunction = $component.'_pluginfile'; if (function_exists($filefunction)) { // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found" - $filefunction($course, $cm, $context, $filearea, $args, $forcedownload, array('preview' => $preview)); + $filefunction($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions); } send_file_not_found(); diff --git a/pluginfile.php b/pluginfile.php index 8cc087786dd..9148b624447 100644 --- a/pluginfile.php +++ b/pluginfile.php @@ -33,5 +33,8 @@ require_once('lib/filelib.php'); $relativepath = get_file_argument(); $forcedownload = optional_param('forcedownload', 0, PARAM_BOOL); $preview = optional_param('preview', null, PARAM_ALPHANUM); +// Offline means download the file from the repository and serve it, even if it was an external link. +// The repository may have to export the file to an offline format. +$offline = optional_param('offline', 0, PARAM_BOOL); -file_pluginfile($relativepath, $forcedownload, $preview); +file_pluginfile($relativepath, $forcedownload, $preview, $offline); diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 5137d519e32..0cafa7b0f56 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -580,7 +580,7 @@ class repository_googledocs extends repository { $storedfile->get_filepath(), $storedfile->get_filename()); - if ($info->is_writable()) { + if (empty($options['offline']) && $info->is_writable()) { // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); @@ -613,7 +613,15 @@ class repository_googledocs extends repository { $this->add_temp_writer_to_file($systemservice, $source->id, $useremail); } - if ($source->link) { + if (!empty($options['offline'])) { + $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename()); + + $filename = $storedfile->get_filename(); + if (isset($downloaded['newfilename'])) { + $filename = $downloaded['newfilename']; + } + send_file($downloaded['path'], $filename, $lifetime, $filter, false, $forcedownload, '', false, $options); + } else if ($source->link) { redirect($source->link); } else { $details = 'File is missing source link'; diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php index 89c3b174559..569526c58ec 100644 --- a/repository/skydrive/lib.php +++ b/repository/skydrive/lib.php @@ -530,7 +530,7 @@ class repository_skydrive extends repository { $storedfile->get_filepath(), $storedfile->get_filename()); - if ($info->is_writable()) { + if (empty($options['offline']) && $info->is_writable()) { // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); @@ -563,7 +563,11 @@ class repository_skydrive extends repository { $this->add_temp_writer_to_file($systemservice, $source->id, $useremail); } - if ($source->link) { + if (!empty($options['offline'])) { + $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename()); + $filename = $storedfile->get_filename(); + send_file($downloaded['path'], $filename, $lifetime, $filter, false, $forcedownload, '', false, $options); + } else if ($source->link) { redirect($source->link); } else { $details = 'File is missing source link'; @@ -820,7 +824,6 @@ class repository_skydrive extends repository { // then set the permissions so anyone with the share link can view, // finally update the reference to contain the share link if it was not // already there (and point to new file id if we copied). - var_dump($reference); $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { From 28b592d5a6957a4e9d0642f246da945ffa599f6e Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 21 Mar 2017 16:43:23 +0800 Subject: [PATCH 43/84] MDL-58338 oauth2: Force email confirmation New accounts and linking a login to an existing account MUST go through email verification. We cannot trust the emails we get from oauth providers have been confirmed. Part of MDL-58220 --- auth/oauth2/classes/api.php | 194 +++++++++++++++++++++++++-- auth/oauth2/classes/auth.php | 151 +++++++++++++++++---- auth/oauth2/classes/linked_login.php | 6 + auth/oauth2/config.html | 20 --- auth/oauth2/confirm-account.php | 91 +++++++++++++ auth/oauth2/confirm-linkedlogin.php | 78 +++++++++++ auth/oauth2/db/install.xml | 6 +- auth/oauth2/db/upgrade.php | 22 +++ auth/oauth2/lang/en/auth_oauth2.php | 45 +++++++ auth/oauth2/version.php | 2 +- lib/classes/user.php | 23 ++++ 11 files changed, 573 insertions(+), 65 deletions(-) create mode 100644 auth/oauth2/confirm-account.php create mode 100644 auth/oauth2/confirm-linkedlogin.php diff --git a/auth/oauth2/classes/api.php b/auth/oauth2/classes/api.php index a7090bdf95b..58028a9f573 100644 --- a/auth/oauth2/classes/api.php +++ b/auth/oauth2/classes/api.php @@ -26,6 +26,7 @@ namespace auth_oauth2; use context_user; use stdClass; use moodle_exception; +use moodle_url; defined('MOODLE_INTERNAL') || die(); @@ -60,7 +61,7 @@ class api { $context = context_user::instance($userid); require_capability('auth/oauth2:managelinkedlogins', $context); - return linked_login::get_records(['userid' => $userid]); + return linked_login::get_records(['userid' => $userid, 'confirmtoken' => '']); } /** @@ -75,14 +76,7 @@ class api { 'issuerid' => $issuer->get('id'), 'username' => $username ]; - $match = linked_login::get_record($params); - - if ($match) { - $user = get_complete_user_data('id', $match->get('userid')); - - return $user; - } - return false; + return linked_login::get_record($params); } /** @@ -93,9 +87,10 @@ class api { * @param array $userinfo as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @param int $userid (defaults to $USER->id) - * @return boolean + * @param bool $skippermissions During signup we need to set this before the user is setup for capability checks. + * @return bool */ - public static function link_login($userinfo, $issuer, $userid = false) { + public static function link_login($userinfo, $issuer, $userid = false, $skippermissions = false) { global $USER; if ($userid === false) { @@ -107,21 +102,194 @@ class api { } $context = context_user::instance($userid); - require_capability('auth/oauth2:managelinkedlogins', $context); + if (!$skippermissions) { + require_capability('auth/oauth2:managelinkedlogins', $context); + } $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; - $record->email = $userinfo['email']; $record->userid = $userid; $existing = linked_login::get_record((array)$record); if ($existing) { + $existing->set('confirmtoken', ''); + $existing->update(); return $existing; } + $record->email = $userinfo['email']; + $record->confirmtoken = ''; $linkedlogin = new linked_login(0, $record); return $linkedlogin->create(); } + /** + * Send an email with a link to confirm linking this account. + * + * @param array $userinfo as returned from an oauth client. + * @param \core\oauth2\issuer $issuer + * @param int $userid (defaults to $USER->id) + * @return bool + */ + public static function send_confirm_link_login_email($userinfo, $issuer, $userid) { + + $record = new stdClass(); + $record->issuerid = $issuer->get('id'); + $record->username = $userinfo['username']; + $record->userid = $userid; + $existing = linked_login::get_record((array)$record); + if ($existing) { + return false; + } + $record->email = $userinfo['email']; + $record->confirmtoken = random_string(32); + $expires = new \DateTime('NOW'); + $expires->add(new \DateInterval('PT30M')); + $record->confirmtokenexpires = $expires->getTimestamp(); + + $linkedlogin = new linked_login(0, $record); + $linkedlogin->create(); + + // Construct the email. + $site = get_site(); + $supportuser = \core_user::get_support_user(); + $user = get_complete_user_data('id', $userid); + + $data = new stdClass(); + $data->fullname = fullname($user); + $data->sitename = format_string($site->fullname); + $data->admin = generate_email_signoff(); + $data->issuername = format_string($issuer->get('name')); + $data->linkedemail = format_string($linkedlogin->get('email')); + + $subject = get_string('confirmlinkedloginemailsubject', 'auth_oauth2', format_string($site->fullname)); + + $params = [ + 'token' => $linkedlogin->get('confirmtoken'), + 'userid' => $userid, + 'username' => $userinfo['username'], + 'issuerid' => $issuer->get('id'), + ]; + $confirmationurl = new moodle_url('/auth/oauth2/confirm-linkedlogin.php', $params); + + // Remove data parameter just in case it was included in the confirmation so we can add it manually later. + $data->link = $confirmationurl->out(); + + $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data); + $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true); + + $user->mailformat = 1; // Always send HTML version as well. + + // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. + return email_to_user($user, $supportuser, $subject, $message, $messagehtml); + } + + /** + * Look for a waiting confirmation token, and if we find a match - confirm it. + * + * @param int $userid + * @param string $username + * @param int $issuerid + * @param string $token + * @return boolean True if we linked. + */ + public static function confirm_link_login($userid, $username, $issuerid, $token) { + if (empty($token) || empty($userid) || empty($issuerid) || empty($username)) { + return false; + } + $params = [ + 'userid' => $userid, + 'username' => $username, + 'issuerid' => $issuerid, + 'confirmtoken' => $token, + ]; + + $login = linked_login::get_record($params); + if (empty($login)) { + return false; + } + $expires = $login->get('confirmtokenexpires'); + if (time() > $expires) { + $login->delete(); + return; + } + $login->set('confirmtokenexpires', 0); + $login->set('confirmtoken', ''); + $login->update(); + return true; + } + + /** + * Send an email with a link to confirm creating this account. + * + * @param array $userinfo as returned from an oauth client. + * @param \core\oauth2\issuer $issuer + * @param int $userid (defaults to $USER->id) + * @return bool + */ + public static function send_confirm_account_email($userinfo, $issuer) { + global $CFG, $DB; + require_once($CFG->dirroot.'/user/profile/lib.php'); + require_once($CFG->dirroot.'/user/lib.php'); + + $user = new stdClass(); + $user->username = $userinfo['username']; + $user->email = $userinfo['email']; + $user->auth = 'oauth2'; + $user->mnethostid = $CFG->mnet_localhost_id; + $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : ''; + $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : ''; + $user->url = isset($userinfo['url']) ? $userinfo['url'] : ''; + $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : ''; + $user->secret = random_string(15); + + $user->password = ''; + // This user is not confirmed. + $user->confirmed = 0; + + $user->id = user_create_user($user, false, true); + + // The linked account is pre-confirmed. + $record = new stdClass(); + $record->issuerid = $issuer->get('id'); + $record->username = $userinfo['username']; + $record->userid = $user->id; + $record->email = $userinfo['email']; + $record->confirmtoken = ''; + $record->confirmtokenexpires = 0; + + $linkedlogin = new linked_login(0, $record); + $linkedlogin->create(); + + // Construct the email. + $site = get_site(); + $supportuser = \core_user::get_support_user(); + $user = get_complete_user_data('id', $user->id); + + $data = new stdClass(); + $data->fullname = fullname($user); + $data->sitename = format_string($site->fullname); + $data->admin = generate_email_signoff(); + + $subject = get_string('confirmaccountemailsubject', 'auth_oauth2', format_string($site->fullname)); + + $params = [ + 'token' => $user->secret, + 'username' => $userinfo['username'] + ]; + $confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params); + + $data->link = $confirmationurl->out(); + + $message = get_string('confirmaccountemail', 'auth_oauth2', $data); + $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true); + + $user->mailformat = 1; // Always send HTML version as well. + + // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. + email_to_user($user, $supportuser, $subject, $message, $messagehtml); + return $user; + } + /** * Delete linked login * diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 82e58e24311..951d8757d77 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die(); use pix_icon; use moodle_url; use core_text; +use context_system; use stdClass; use core\oauth2\issuer; use core\oauth2\client; @@ -105,7 +106,7 @@ class auth extends \auth_plugin_base { * @return bool true means automatically copy data from ext to user table */ public function is_synchronised_with_external() { - return true; + return false; } /** @@ -309,15 +310,45 @@ class auth extends \auth_plugin_base { } /** - * Process the config after the form is saved. - * @param stdClass $config + * Confirm the new user as registered. + * + * @param string $username + * @param string $confirmsecret */ - public function process_config($config) { - // Set to defaults if undefined. - if (!isset($config->allowlinkedlogins)) { - $config->allowlinkedlogins = false; + function user_confirm($username, $confirmsecret) { + global $DB; + $user = get_complete_user_data('username', $username); + + if (!empty($user)) { + if ($user->auth != $this->authtype) { + return AUTH_CONFIRM_ERROR; + + } else if ($user->secret == $confirmsecret && $user->confirmed) { + return AUTH_CONFIRM_ALREADY; + + } else if ($user->secret == $confirmsecret) { // They have provided the secret key to get in + $DB->set_field("user", "confirmed", 1, array("id"=>$user->id)); + return AUTH_CONFIRM_OK; + } + } else { + return AUTH_CONFIRM_ERROR; } - set_config('allowlinkedlogins', trim($config->allowlinkedlogins), 'auth_oauth2'); + } + + /** + * Print a page showing that a confirm email was sent with instructions. + * + * @param string title + * @param string message + */ + public function print_confirm_required($title, $message) { + global $PAGE, $OUTPUT, $CFG; + + $PAGE->navbar->add($title); + $PAGE->set_title($title); + $PAGE->set_heading($PAGE->course->fullname); + echo $OUTPUT->header(); + notice($message, "$CFG->httpswwwroot/index.php"); } /** @@ -327,7 +358,7 @@ class auth extends \auth_plugin_base { * @return none Either redirects or throws an exception */ public function complete_login(client $client, $redirecturl) { - global $CFG, $SESSION; + global $CFG, $SESSION, $PAGE; $userinfo = $client->get_userinfo(); @@ -336,7 +367,7 @@ class auth extends \auth_plugin_base { $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } - if (empty($userinfo['username'])) { + if (empty($userinfo['username']) || empty($userinfo['email'])) { $errormsg = get_string('notloggedin', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); @@ -344,38 +375,100 @@ class auth extends \auth_plugin_base { $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); + // Once we get here we have the user info from oauth. $userwasmapped = false; - if (get_config('auth_oauth2', 'allowlinkedlogins')) { - $mappeduser = api::match_username_to_user($userinfo['username'], $client->get_issuer()); - if ($mappeduser) { + // Clean and remember the picture / lang. + if (!empty($userinfo['picture'])) { + $this->set_static_user_picture($userinfo['picture']); + unset($userinfo['picture']); + } + + if (!empty($userinfo['lang'])) { + $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); + if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { + unset($userinfo['lang']); + } + } + + // First we try and find a defined mapping. + $linkedlogin = api::match_username_to_user($userinfo['username'], $client->get_issuer()); + + if (!empty($linkedlogin) && empty($linkedlogin->get('confirmtoken'))) { + $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid')); + + if ($mappeduser && $mappeduser->confirmed) { $userinfo = (array) $mappeduser; $userwasmapped = true; + } else { + $errormsg = get_string('confirmationpending', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } + } else if (!empty($linkedlogin)) { + $errormsg = get_string('confirmationpending', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); + } + $issuer = $client->get_issuer(); + if (!$issuer->is_valid_login_domain($userinfo['email'])) { + $errormsg = get_string('notloggedin', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } if (!$userwasmapped) { - if (!empty($userinfo['picture'])) { - $this->set_static_user_picture($userinfo['picture']); - unset($userinfo['picture']); - } + // No defined mapping - we need to see if there is an existing account with the same email. - if (!empty($userinfo['lang'])) { - $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); - if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { - unset($userinfo['lang']); + $moodleuser = \core_user::get_user_by_email($userinfo['email']); + if (!empty($moodleuser)) { + $PAGE->set_url('/auth/oauth2/confirm-link-login.php'); + $PAGE->set_context(context_system::instance()); + + \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id); + // Request to link to existing account. + $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2'); + $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email); + $this->print_confirm_required($emailconfirm, $message); + exit(); + + } else { + // This is a new account. + $exists = \core_user::get_user_by_username($userinfo['username']); + // Creating a new user? + if ($exists) { + + // The username exists but the emails don't match. Refuse to continue. + $errormsg = get_string('accountexists', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } + + if (email_is_not_allowed($userinfo['email'])) { + // The username exists but the emails don't match. Refuse to continue. + $errormsg = get_string('emailnotallowed', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); + } + + $PAGE->set_url('/auth/oauth2/confirm-account.php'); + $PAGE->set_context(context_system::instance()); + + // Create a new (unconfirmed account) and send an email to confirm it. + $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer); + + $this->update_picture($user); + $emailconfirm = get_string('emailconfirm'); + $message = get_string('emailconfirmsent', '', $userinfo['email']); + $this->print_confirm_required($emailconfirm, $message); + exit(); + } } - $issuer = $client->get_issuer(); - - $user = false; - if ($issuer->is_valid_login_domain($userinfo['email'])) { - - $this->set_static_user_info($userinfo); - $user = authenticate_user_login($userinfo['username'], ''); - } + // If we got to here - we must have found a real user account that is confirmed. + $this->set_static_user_info($userinfo); + $user = authenticate_user_login($userinfo['username'], ''); if ($user) { complete_user_login($user); diff --git a/auth/oauth2/classes/linked_login.php b/auth/oauth2/classes/linked_login.php index 7098e4ece5c..e4a56ca600b 100644 --- a/auth/oauth2/classes/linked_login.php +++ b/auth/oauth2/classes/linked_login.php @@ -55,6 +55,12 @@ class linked_login extends persistent { ), 'email' => array( 'type' => PARAM_RAW + ), + 'confirmtoken' => array( + 'type' => PARAM_RAW + ), + 'confirmtokenexpires' => array( + 'type' => PARAM_INT ) ); } diff --git a/auth/oauth2/config.html b/auth/oauth2/config.html index b78655a748d..e7ce6066b3d 100644 --- a/auth/oauth2/config.html +++ b/auth/oauth2/config.html @@ -2,28 +2,8 @@
    -allowlinkedlogins)) { - $config->allowlinkedlogins = true; -} -?> - - - - -authtype, $userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); diff --git a/auth/oauth2/confirm-account.php b/auth/oauth2/confirm-account.php new file mode 100644 index 00000000000..54e338327f5 --- /dev/null +++ b/auth/oauth2/confirm-account.php @@ -0,0 +1,91 @@ +. + +/** + * Confirm self oauth2 user. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../../config.php'); +require_once($CFG->libdir . '/authlib.php'); + +$usersecret = required_param('token', PARAM_RAW); +$username = required_param('username', PARAM_USERNAME); +$redirect = optional_param('redirect', '', PARAM_LOCALURL); // Where to redirect the browser once the user has been confirmed. + +$PAGE->set_url('/auth/oauth2/confirm-account.php'); +$PAGE->set_context(context_system::instance()); + +$auth = get_auth_plugin('oauth2'); + +$confirmed = $auth->user_confirm($username, $usersecret); + +if ($confirmed == AUTH_CONFIRM_ALREADY) { + $user = get_complete_user_data('username', $username); + $PAGE->navbar->add(get_string("alreadyconfirmed")); + $PAGE->set_title(get_string("alreadyconfirmed")); + $PAGE->set_heading($COURSE->fullname); + echo $OUTPUT->header(); + echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); + echo "

    ".get_string("alreadyconfirmed")."

    \n"; + echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); + echo $OUTPUT->box_end(); + echo $OUTPUT->footer(); + exit; + +} else if ($confirmed == AUTH_CONFIRM_OK) { + + // The user has confirmed successfully, let's log them in. + + if (!$user = get_complete_user_data('username', $username)) { + print_error('cannotfinduser', '', '', s($username)); + } + + if (!$user->suspended) { + complete_user_login($user); + + \core\session\manager::apply_concurrent_login_limit($user->id, session_id()); + + // Check where to go, $redirect has a higher preference. + if (empty($redirect) and !empty($SESSION->wantsurl) ) { + $redirect = $SESSION->wantsurl; + unset($SESSION->wantsurl); + } + + if (!empty($redirect)) { + redirect($redirect); + } + } + + $PAGE->navbar->add(get_string("confirmed")); + $PAGE->set_title(get_string("confirmed")); + $PAGE->set_heading($COURSE->fullname); + echo $OUTPUT->header(); + echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); + echo "

    ".get_string("thanks").", ". fullname($USER) . "

    \n"; + echo "

    ".get_string("confirmed")."

    \n"; + echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); + echo $OUTPUT->box_end(); + echo $OUTPUT->footer(); + exit; +} else { + print_error('invalidconfirmdata'); +} + +redirect("$CFG->wwwroot/"); diff --git a/auth/oauth2/confirm-linkedlogin.php b/auth/oauth2/confirm-linkedlogin.php new file mode 100644 index 00000000000..6dc69a2066c --- /dev/null +++ b/auth/oauth2/confirm-linkedlogin.php @@ -0,0 +1,78 @@ +. + +/** + * Confirm self oauth2 user. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../../config.php'); +require_once($CFG->libdir . '/authlib.php'); + +$token = required_param('token', PARAM_RAW); +$username = required_param('username', PARAM_USERNAME); +$userid = required_param('userid', PARAM_INT); +$issuerid = required_param('issuerid', PARAM_INT); +$redirect = optional_param('redirect', '', PARAM_LOCALURL); // Where to redirect the browser once the user has been confirmed. + +$PAGE->set_url('/auth/oauth2/confirm-linkedlogin.php'); +$PAGE->set_context(context_system::instance()); + +$confirmed = \auth_oauth2\api::confirm_link_login($userid, $username, $issuerid, $token); + +if ($confirmed) { + + // The user has confirmed successfully, let's log them in. + + if (!$user = get_complete_user_data('username', $username)) { + print_error('cannotfinduser', '', '', s($username)); + } + + if (!$user->suspended) { + complete_user_login($user); + + \core\session\manager::apply_concurrent_login_limit($user->id, session_id()); + + // Check where to go, $redirect has a higher preference. + if (empty($redirect) and !empty($SESSION->wantsurl) ) { + $redirect = $SESSION->wantsurl; + unset($SESSION->wantsurl); + } + + if (!empty($redirect)) { + redirect($redirect); + } + } + + $PAGE->navbar->add(get_string("confirmed")); + $PAGE->set_title(get_string("confirmed")); + $PAGE->set_heading($COURSE->fullname); + echo $OUTPUT->header(); + echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); + echo "

    ".get_string("thanks").", ". fullname($USER) . "

    \n"; + echo "

    ".get_string("confirmed")."

    \n"; + echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); + echo $OUTPUT->box_end(); + echo $OUTPUT->footer(); + exit; +} else { + print_error('invalidconfirmdata'); +} + +redirect("$CFG->wwwroot/"); diff --git a/auth/oauth2/db/install.xml b/auth/oauth2/db/install.xml index be895b68d36..09cecf41aa6 100755 --- a/auth/oauth2/db/install.xml +++ b/auth/oauth2/db/install.xml @@ -1,5 +1,5 @@ - @@ -14,6 +14,8 @@ + + @@ -27,4 +29,4 @@
    - - - allowlinkedlogins) { echo 'checked="checked"'; } ?> - > - error_text($err['allowlinkedlogins']); } ?> - - -
    -
    \ No newline at end of file + diff --git a/auth/oauth2/db/upgrade.php b/auth/oauth2/db/upgrade.php index ca1bd5851a7..71dd0a5e1a9 100644 --- a/auth/oauth2/db/upgrade.php +++ b/auth/oauth2/db/upgrade.php @@ -85,5 +85,27 @@ function xmldb_auth_oauth2_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2017031000, 'auth', 'oauth2'); } + if ($oldversion < 2017032300) { + + // Define field confirmtoken to be added to auth_oauth2_linked_login. + $table = new xmldb_table('auth_oauth2_linked_login'); + $field = new xmldb_field('confirmtoken', XMLDB_TYPE_CHAR, '64', null, XMLDB_NOTNULL, null, null, 'email'); + + // Conditionally launch add field confirmtoken. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('confirmtokenexpires', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'confirmtoken'); + + // Conditionally launch add field confirmtokenexpires. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Oauth2 savepoint reached. + upgrade_plugin_savepoint(true, 2017032300, 'auth', 'oauth2'); + } + return true; } diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php index 74ee233d0c4..f4395bf4e36 100644 --- a/auth/oauth2/lang/en/auth_oauth2.php +++ b/auth/oauth2/lang/en/auth_oauth2.php @@ -27,13 +27,58 @@ $string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; $string['notloggedin'] = 'The login attempt failed.'; $string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; $string['pluginname'] = 'OAuth 2'; +$string['emailconfirmlink'] = 'Link your accounts'; +$string['emailconfirmlinksent'] = '

    An existing account was found with this email address but it is not linked yet.

    +

    The accounts must be linked before you can login.

    +

    An email should have been sent to your address at {$a}

    +

    It contains easy instructions to link your accounts.

    +

    If you continue to have difficulty, contact the site administrator.

    '; $string['oauth2:managelinkedlogins'] = 'Manage own linked login accounts'; $string['linkedlogins'] = 'Linked logins'; +$string['accountexists'] = 'A user already exists on this site with this username. If this is your account, login manually and link this link from your preferences page.'; $string['linkedloginshelp'] = 'Help with linked logins.'; $string['notwhileloggedinas'] = 'Linked logins cannot be managed while logged in as another user.'; $string['issuer'] = 'OAuth 2 Service'; $string['info'] = 'External account'; $string['createnewlinkedlogin'] = 'Link a new account ({$a})'; +$string['confirmationpending'] = 'This account is pending email confirmation.'; +$string['emailnotallowed'] = 'The email address is not permitted at this site.'; $string['allowlinkedlogins'] = 'Allow linked logins'; $string['allowlinkedloginsdesc'] = 'Linked logins allow users to link their Moodle account to another external account which they can use to login with.'; $string['createaccountswarning'] = 'This authentication plugin allows users to create accounts on your site. You may want to enable the setting "authpreventaccountcreation" if you use this plugin.'; +$string['selfregistrationdisabled'] = 'No matching account could be found on this site, and this site does not allow self registration.'; +$string['confirmlinkedloginemail'] = 'Hi {$a->fullname}, + +A request has been made to link the {$a->issuername} login +{$a->linkedemail} to your account at \'{$a->sitename}\' +using your email address. + +To confirm this request and link these logins, please go to this web address: + +{$a->link} + +In most mail programs, this should appear as a blue link +which you can just click on. If that doesn\'t work, +then cut and paste the address into the address +line at the top of your web browser window. + +If you need help, please contact the site administrator, +{$a->admin}'; +$string['confirmlinkedloginemailsubject'] = '{$a}: linked login confirmation'; +$string['confirmaccountemail'] = 'Hi {$a->fullname}, + +A new account has been requested at \'{$a->sitename}\' +using your email address. + +To confirm your new account, please go to this web address: + +{$a->link} + +In most mail programs, this should appear as a blue link +which you can just click on. If that doesn\'t work, +then cut and paste the address into the address +line at the top of your web browser window. + +If you need help, please contact the site administrator, +{$a->admin}'; +$string['confirmaccountemailsubject'] = '{$a}: account confirmation'; diff --git a/auth/oauth2/version.php b/auth/oauth2/version.php index 0a014690f25..2ff2041e851 100644 --- a/auth/oauth2/version.php +++ b/auth/oauth2/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2017031000; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2017032300; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2016112900; // Requires this Moodle version. $plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics). diff --git a/lib/classes/user.php b/lib/classes/user.php index 8ae89dcc87e..26cb195df67 100644 --- a/lib/classes/user.php +++ b/lib/classes/user.php @@ -100,6 +100,29 @@ class core_user { } } + /** + * Return user object from db based on their email. + * + * @param string $email The email of the user searched. + * @param string $fields A comma separated list of user fields to be returned, support and noreply user. + * @param int $mnethostid The id of the remote host. + * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found; + * IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended); + * MUST_EXIST means throw an exception if no user record or multiple records found. + * @return stdClass|bool user record if found, else false. + * @throws dml_exception if user record not found and respective $strictness is set. + */ + public static function get_user_by_email($email, $fields = '*', $mnethostid = null, $strictness = IGNORE_MISSING) { + global $DB, $CFG; + + // Because we use the username as the search criteria, we must also restrict our search based on mnet host. + if (empty($mnethostid)) { + // If empty, we restrict to local users. + $mnethostid = $CFG->mnet_localhost_id; + } + + return $DB->get_record('user', array('email' => $email, 'mnethostid' => $mnethostid), $fields, $strictness); + } /** * Return user object from db based on their username. From 99e3c347f961031a2c849d290c2780b0c2a69c7f Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 23 Mar 2017 16:53:07 +0800 Subject: [PATCH 44/84] MDL-58220 oauth2: Minor cleanups Fixes spotted by Jun in peer review. --- lib/classes/oauth2/api.php | 2 +- lib/classes/oauth2/issuer.php | 1 + lib/tests/oauth2_test.php | 4 ---- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 02320cefc1b..0c70f42e0f7 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -621,7 +621,7 @@ class api { * * Requires moodle/site:config capability at the system context. * - * @param int $id The id of the identity issuer to enable. + * @param int $id The id of the identity issuer to disable. * @return boolean */ public static function disable_issuer($id) { diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 3f56d7aedc1..aea47205704 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -150,6 +150,7 @@ class issuer extends persistent { $emaildomain = $parts[1]; } + $emaildomain = \core_text::strtolower(trim($emaildomain)); foreach ($validdomains as $checkdomain) { $checkdomain = \core_text::strtolower(trim($checkdomain)); diff --git a/lib/tests/oauth2_test.php b/lib/tests/oauth2_test.php index a8b50c11449..50022826d2f 100644 --- a/lib/tests/oauth2_test.php +++ b/lib/tests/oauth2_test.php @@ -140,8 +140,6 @@ class core_oauth2_testcase extends advanced_testcase { * Tests we can enable and disable an issuer. */ public function test_enable_disable_issuer() { - global $SESSION; - $this->resetAfterTest(); $this->setAdminUser(); @@ -170,8 +168,6 @@ class core_oauth2_testcase extends advanced_testcase { * Test the alloweddomains for an issuer. */ public function test_issuer_alloweddomains() { - global $SESSION; - $this->resetAfterTest(); $this->setAdminUser(); From 13b449f4a56490b6cef22b976ce286c8e12ac036 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 24 Mar 2017 15:34:15 +0800 Subject: [PATCH 45/84] MDL-58220 oauth2: cibot cleanups --- auth/oauth2/classes/api.php | 1 - auth/oauth2/classes/auth.php | 10 +++++----- auth/oauth2/lang/en/auth_oauth2.php | 30 ++++++++++++++--------------- lib/classes/filetypes.php | 3 ++- lib/classes/oauth2/rest.php | 1 + lib/filestorage/file_storage.php | 3 ++- repository/skydrive/db/upgrade.php | 2 ++ repository/skydrive/lib.php | 1 + 8 files changed, 28 insertions(+), 23 deletions(-) diff --git a/auth/oauth2/classes/api.php b/auth/oauth2/classes/api.php index 58028a9f573..793fe895c66 100644 --- a/auth/oauth2/classes/api.php +++ b/auth/oauth2/classes/api.php @@ -131,7 +131,6 @@ class api { * @return bool */ public static function send_confirm_link_login_email($userinfo, $issuer, $userid) { - $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 951d8757d77..af536865861 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -315,7 +315,7 @@ class auth extends \auth_plugin_base { * @param string $username * @param string $confirmsecret */ - function user_confirm($username, $confirmsecret) { + public function user_confirm($username, $confirmsecret) { global $DB; $user = get_complete_user_data('username', $username); @@ -326,8 +326,8 @@ class auth extends \auth_plugin_base { } else if ($user->secret == $confirmsecret && $user->confirmed) { return AUTH_CONFIRM_ALREADY; - } else if ($user->secret == $confirmsecret) { // They have provided the secret key to get in - $DB->set_field("user", "confirmed", 1, array("id"=>$user->id)); + } else if ($user->secret == $confirmsecret) { // They have provided the secret key to get in. + $DB->set_field("user", "confirmed", 1, array("id" => $user->id)); return AUTH_CONFIRM_OK; } } else { @@ -338,8 +338,8 @@ class auth extends \auth_plugin_base { /** * Print a page showing that a confirm email was sent with instructions. * - * @param string title - * @param string message + * @param string $title + * @param string $message */ public function print_confirm_required($title, $message) { global $PAGE, $OUTPUT, $CFG; diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php index f4395bf4e36..53f850e7714 100644 --- a/auth/oauth2/lang/en/auth_oauth2.php +++ b/auth/oauth2/lang/en/auth_oauth2.php @@ -65,20 +65,20 @@ line at the top of your web browser window. If you need help, please contact the site administrator, {$a->admin}'; $string['confirmlinkedloginemailsubject'] = '{$a}: linked login confirmation'; -$string['confirmaccountemail'] = 'Hi {$a->fullname}, - -A new account has been requested at \'{$a->sitename}\' -using your email address. - -To confirm your new account, please go to this web address: - -{$a->link} - -In most mail programs, this should appear as a blue link -which you can just click on. If that doesn\'t work, -then cut and paste the address into the address -line at the top of your web browser window. - -If you need help, please contact the site administrator, +$string['confirmaccountemail'] = 'Hi {$a->fullname}, + +A new account has been requested at \'{$a->sitename}\' +using your email address. + +To confirm your new account, please go to this web address: + +{$a->link} + +In most mail programs, this should appear as a blue link +which you can just click on. If that doesn\'t work, +then cut and paste the address into the address +line at the top of your web browser window. + +If you need help, please contact the site administrator, {$a->admin}'; $string['confirmaccountemailsubject'] = '{$a}: account confirmation'; diff --git a/lib/classes/filetypes.php b/lib/classes/filetypes.php index 23d45bde610..97ff4568271 100644 --- a/lib/classes/filetypes.php +++ b/lib/classes/filetypes.php @@ -104,7 +104,8 @@ abstract class core_filetypes { 'gdoc' => array('type' => 'application/vnd.google-apps.document', 'icon' => 'document', 'groups' => array('document')), 'gsheet' => array('type' => 'application/vnd.google-apps.spreadsheet', 'icon' => 'spreadsheet', 'groups' => array('spreadsheet')), - 'gslides' => array('type' => 'application/vnd.google-apps.presentation', 'icon' => 'powerpoint', 'groups' => array('presentation')), + 'gslides' => array('type' => 'application/vnd.google-apps.presentation', 'icon' => 'powerpoint', + 'groups' => array('presentation')), 'gif' => array('type' => 'image/gif', 'icon' => 'gif', 'groups' => array('image', 'web_image'), 'string' => 'image'), 'gtar' => array('type' => 'application/x-gtar', 'icon' => 'archive', 'groups' => array('archive'), 'string' => 'archive'), diff --git a/lib/classes/oauth2/rest.php b/lib/classes/oauth2/rest.php index c790429c891..b4a78732999 100644 --- a/lib/classes/oauth2/rest.php +++ b/lib/classes/oauth2/rest.php @@ -64,6 +64,7 @@ abstract class rest { * @param string $functionname * @param array $functionargs * @param string $rawpost Optional param to include in the body of a post. + * @return string|object */ public function call($functionname, $functionargs, $rawpost = false) { $functions = $this->get_api_functions(); diff --git a/lib/filestorage/file_storage.php b/lib/filestorage/file_storage.php index 7fd1eaaa4ac..1b26eba8860 100644 --- a/lib/filestorage/file_storage.php +++ b/lib/filestorage/file_storage.php @@ -1128,7 +1128,8 @@ class file_storage { // creating a new file from an existing alias creates new alias implicitly. // here we just check the database consistency. if (!empty($newrecord->repositoryid)) { - // It is OK if the current reference does not exist. It may have been altered by a repository plugin when the files where saved from a draft area. + // It is OK if the current reference does not exist. It may have been altered by a repository plugin when the files + // where saved from a draft area. $newrecord->referencefileid = $this->get_or_create_referencefileid($newrecord->repositoryid, $newrecord->reference); } diff --git a/repository/skydrive/db/upgrade.php b/repository/skydrive/db/upgrade.php index a9922abe2f1..0439640d1f1 100644 --- a/repository/skydrive/db/upgrade.php +++ b/repository/skydrive/db/upgrade.php @@ -17,6 +17,8 @@ defined('MOODLE_INTERNAL') || die(); /** + * Upgrade this plugin. + * * @param int $oldversion the version we are upgrading from * @package repository_skydrive * @return bool result diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php index 569526c58ec..d3ca1b16cf8 100644 --- a/repository/skydrive/lib.php +++ b/repository/skydrive/lib.php @@ -1007,6 +1007,7 @@ class repository_skydrive extends repository { /** * Callback to get the required scopes for system account. * + * @param \core\oauth2\issuer $issuer * @return string */ function repository_skydrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) { From f59d81f487d7f4189c0736fc4d1c4260e6102081 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 09:41:33 +0800 Subject: [PATCH 46/84] MDL-58220 block_login: Update icon rendering of idp list --- blocks/login/block_login.php | 9 +++++++-- lib/upgrade.txt | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/blocks/login/block_login.php b/blocks/login/block_login.php index c08835d6e94..4f5fe467b50 100644 --- a/blocks/login/block_login.php +++ b/blocks/login/block_login.php @@ -114,8 +114,13 @@ class block_login extends block_base { $this->content->text .= '
    ' . get_string('potentialidps', 'auth') . '
    '; $this->content->text .= ''; $this->content->text .= ''; diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 088454b38bb..3b0f4188c8b 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -1,6 +1,8 @@ This files describes API changes in core libraries and APIs, information provided here is intended especially for developers. === 3.3 === +* The information returned by the idp list has changed. This is usually only rendered by the login page and login block. + The icon attribute is removed and an iconurl attribute has been added. * Support added for a new type of external file: FILE_CONTROLLED_LINK. This is an external file that Moodle can control the permissions. Moodle makes files read-only but can grant temporary write access. When accessing a URL, the info from file_browser::get_file_info will be checked to determine if the user has write access, From bfc60d386ddb6f5811f011043b4184afd118c199 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 10:27:19 +0800 Subject: [PATCH 47/84] MDL-58220 auth_oauth: return false for non-oauth Always return false for non-oauth logins. --- auth/oauth2/classes/auth.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index af536865861..d3ba71c172f 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -73,6 +73,10 @@ class auth extends \auth_plugin_base { */ public function user_login($username, $password) { $cached = $this->get_static_user_info(); + if (empty($cached)) { + // This means we were called as part of a normal login flow - without using oauth. + return false; + } $verifyusername = $cached['username']; if ($verifyusername == $username) { return true; From 4a32445dc9db3e4bb368cf91911fb53adb3518e8 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 10:51:03 +0800 Subject: [PATCH 48/84] MDL-58220 oauth2: use PARAM_RAW_TRIMMED Client ID and secret and copy/pasted from elsewhere so trim them. --- lib/classes/oauth2/issuer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index aea47205704..785d5db60f5 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -53,11 +53,11 @@ class issuer extends persistent { 'default' => null ), 'clientid' => array( - 'type' => PARAM_RAW, + 'type' => PARAM_RAW_TRIMMED, 'default' => '' ), 'clientsecret' => array( - 'type' => PARAM_RAW, + 'type' => PARAM_RAW_TRIMMED, 'default' => '' ), 'baseurl' => array( From 818e789a00bbb3e31a57a073184971454eaca23b Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 10:51:44 +0800 Subject: [PATCH 49/84] MDL-58220 oauth2: Remove extra step Show create google/facebook/office365 buttons on the main page instead of hiding them behind a click. --- admin/tool/oauth2/issuers.php | 40 +++++++++-------------- admin/tool/oauth2/lang/en/tool_oauth2.php | 1 - 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php index 7da2c480973..1115ae21465 100644 --- a/admin/tool/oauth2/issuers.php +++ b/admin/tool/oauth2/issuers.php @@ -66,7 +66,6 @@ if ($mform && $mform->is_cancelled()) { } else if ($action == 'edit') { if ($data = $mform->get_data()) { - try { if (!empty($data->id)) { core\oauth2\api::update_issuer($data); @@ -89,28 +88,12 @@ if ($mform && $mform->is_cancelled()) { } } else if ($action == 'edittemplate') { - $type = optional_param('type', '', PARAM_ALPHA); - if (empty($type)) { - echo $OUTPUT->header(); - echo $OUTPUT->heading(get_string('createfromtemplate', 'tool_oauth2')); - echo '

    ' . get_string('createfromtemplatedesc', 'tool_oauth2') . '

    '; - $params = ['action' => 'edittemplate', 'type' => 'google', 'sesskey' => sesskey()]; - $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); - echo $renderer->single_button($addurl, get_string('createnewgoogleissuer', 'tool_oauth2')); - $params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey()]; - $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); - echo $renderer->single_button($addurl, get_string('createnewmicrosoftissuer', 'tool_oauth2')); - $params = ['action' => 'edittemplate', 'type' => 'facebook', 'sesskey' => sesskey()]; - $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); - echo $renderer->single_button($addurl, get_string('createnewfacebookissuer', 'tool_oauth2')); - echo $OUTPUT->footer(); - } else { - require_sesskey(); - $issuer = core\oauth2\api::create_standard_issuer($type); - $params = ['action' => 'edit', 'id' => $issuer->get('id')]; - $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); - redirect($editurl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); - } + $type = required_param('type', PARAM_ALPHA); + require_sesskey(); + $issuer = core\oauth2\api::create_standard_issuer($type); + $params = ['action' => 'edit', 'id' => $issuer->get('id')]; + $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + redirect($editurl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } else if ($action == 'enable') { require_sesskey(); @@ -173,10 +156,17 @@ if ($mform && $mform->is_cancelled()) { $issuers = core\oauth2\api::get_all_issuers(); echo $renderer->issuers_table($issuers); + $params = ['action' => 'edittemplate', 'type' => 'google', 'sesskey' => sesskey()]; + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + echo $renderer->single_button($addurl, get_string('createnewgoogleissuer', 'tool_oauth2')); + $params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey()]; + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + echo $renderer->single_button($addurl, get_string('createnewmicrosoftissuer', 'tool_oauth2')); + $params = ['action' => 'edittemplate', 'type' => 'facebook', 'sesskey' => sesskey()]; + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + echo $renderer->single_button($addurl, get_string('createnewfacebookissuer', 'tool_oauth2')); $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']); echo $renderer->single_button($addurl, get_string('createnewissuer', 'tool_oauth2')); - $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edittemplate']); - echo $renderer->single_button($addurl, get_string('createnewstandardissuer', 'tool_oauth2')); echo $OUTPUT->footer(); } diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 4d697b16c3a..73f3120222a 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -82,7 +82,6 @@ $string['createnewissuer'] = 'Create new custom service'; $string['createnewgoogleissuer'] = 'Create new Google service'; $string['createnewmicrosoftissuer'] = 'Create new Microsoft service'; $string['createnewfacebookissuer'] = 'Create new Facebook service'; -$string['createnewstandardissuer'] = 'Create service from a template'; $string['createnewendpoint'] = 'Create new endpoint for issuer "{$a}"'; $string['createnewuserfieldmapping'] = 'Create new user field mapping for issuer "{$a}"'; $string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.'; From 5b926a6a09b3c564f7f15e58e6bfb410339fee91 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 10:56:10 +0800 Subject: [PATCH 50/84] MDL-58220 oauth2: Move help icons to table header In the Site Administration -> Server -> OAuth 2 Services page. --- admin/tool/oauth2/classes/output/renderer.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index 43633c1dab8..b0a32f22009 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -55,8 +55,8 @@ class renderer extends plugin_renderer_base { get_string('name'), get_string('configuredstatus', 'tool_oauth2'), get_string('loginissuer', 'tool_oauth2'), - get_string('discoverystatus', 'tool_oauth2'), - get_string('systemauthstatus', 'tool_oauth2'), + get_string('discoverystatus', 'tool_oauth2') . ' ' . $OUTPUT->help_icon('discovered', 'tool_oauth2'), + get_string('systemauthstatus', 'tool_oauth2') . ' ' . $OUTPUT->help_icon('systemaccountconnected', 'tool_oauth2'), get_string('edit'), ]; $table->attributes['class'] = 'admintable generaltable'; @@ -110,7 +110,6 @@ class renderer extends plugin_renderer_base { $discovered = '-'; } } - $discovered .= ' ' . $OUTPUT->help_icon('discovered', 'tool_oauth2'); $discoverystatuscell = new html_table_cell($discovered); @@ -130,7 +129,6 @@ class renderer extends plugin_renderer_base { $authlink = html_writer::link($authurl, $icon); $systemauth .= ' ' . $authlink; } - $systemauth .= ' ' . $OUTPUT->help_icon('systemaccountconnected', 'tool_oauth2'); $systemauthstatuscell = new html_table_cell($systemauth); From ae596d4a5438794d855d8e6c8d634c291214a455 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:02:35 +0800 Subject: [PATCH 51/84] MDL-58220 auth_oauth2: Remove allow linked logins This was already removed - I just missed some spots. --- auth/oauth2/lang/en/auth_oauth2.php | 2 -- auth/oauth2/lib.php | 6 ++---- auth/oauth2/linkedlogins.php | 4 ---- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php index 53f850e7714..fb8f3a97be3 100644 --- a/auth/oauth2/lang/en/auth_oauth2.php +++ b/auth/oauth2/lang/en/auth_oauth2.php @@ -43,8 +43,6 @@ $string['info'] = 'External account'; $string['createnewlinkedlogin'] = 'Link a new account ({$a})'; $string['confirmationpending'] = 'This account is pending email confirmation.'; $string['emailnotallowed'] = 'The email address is not permitted at this site.'; -$string['allowlinkedlogins'] = 'Allow linked logins'; -$string['allowlinkedloginsdesc'] = 'Linked logins allow users to link their Moodle account to another external account which they can use to login with.'; $string['createaccountswarning'] = 'This authentication plugin allows users to create accounts on your site. You may want to enable the setting "authpreventaccountcreation" if you use this plugin.'; $string['selfregistrationdisabled'] = 'No matching account could be found on this site, and this site does not allow self registration.'; $string['confirmlinkedloginemail'] = 'Hi {$a->fullname}, diff --git a/auth/oauth2/lib.php b/auth/oauth2/lib.php index e0835ad3b95..422e4a088d4 100644 --- a/auth/oauth2/lib.php +++ b/auth/oauth2/lib.php @@ -42,10 +42,8 @@ function auth_oauth2_extend_navigation_user_settings(navigation_node $useraccoun if (!\core\session\manager::is_loggedinas()) { if (has_capability('auth/oauth2:managelinkedlogins', $context)) { - if (get_config('auth_oauth2', 'allowlinkedlogins')) { - $parent = $useraccount->parent->find('useraccount', navigation_node::TYPE_CONTAINER); - $parent->add(get_string('linkedlogins', 'auth_oauth2'), new moodle_url('/auth/oauth2/linkedlogins.php')); - } + $parent = $useraccount->parent->find('useraccount', navigation_node::TYPE_CONTAINER); + $parent->add(get_string('linkedlogins', 'auth_oauth2'), new moodle_url('/auth/oauth2/linkedlogins.php')); } } } diff --git a/auth/oauth2/linkedlogins.php b/auth/oauth2/linkedlogins.php index a5ffbb08151..12285306a5a 100644 --- a/auth/oauth2/linkedlogins.php +++ b/auth/oauth2/linkedlogins.php @@ -35,10 +35,6 @@ $PAGE->set_heading($strheading); require_login(); -if (!get_config('auth_oauth2', 'allowlinkedlogins')) { - throw new moodle_exception('Linked logins are disabled.'); -} - $action = optional_param('action', '', PARAM_ALPHAEXT); if ($action == 'new') { require_sesskey(); From 4ab80291e7c519431478e706f031b0b7a85a880a Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:07:17 +0800 Subject: [PATCH 52/84] MDL-58220 tool_oauth2: forms cleanup Remove extra field length attributes and fix type of action fields. --- admin/tool/oauth2/classes/form/endpoint.php | 6 +++--- admin/tool/oauth2/classes/form/issuer.php | 20 +++++++++---------- .../classes/form/user_field_mapping.php | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/admin/tool/oauth2/classes/form/endpoint.php b/admin/tool/oauth2/classes/form/endpoint.php index 35aa835fcc8..ac66ed79c5a 100644 --- a/admin/tool/oauth2/classes/form/endpoint.php +++ b/admin/tool/oauth2/classes/form/endpoint.php @@ -55,19 +55,19 @@ class endpoint extends persistent { $mform->addElement('header', 'generalhdr', get_string('general')); // Name. - $mform->addElement('text', 'name', get_string('endpointname', 'tool_oauth2'), 'maxlength="255"'); + $mform->addElement('text', 'name', get_string('endpointname', 'tool_oauth2')); $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('name', 'endpointname', 'tool_oauth2'); // Url. - $mform->addElement('text', 'url', get_string('endpointurl', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addElement('text', 'url', get_string('endpointurl', 'tool_oauth2')); $mform->addRule('url', null, 'required', null, 'client'); $mform->addRule('url', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); $mform->addHelpButton('url', 'endpointurl', 'tool_oauth2'); $mform->addElement('hidden', 'action', 'edit'); - $mform->setType('action', PARAM_RAW); + $mform->setType('action', PARAM_ALPHA); $mform->addElement('hidden', 'issuerid', $endpoint->get('issuerid')); $mform->setType('issuerid', PARAM_INT); diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index b3356a1e411..e1e3f0bdf74 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -55,52 +55,52 @@ class issuer extends persistent { $mform->addElement('header', 'generalhdr', get_string('general')); // Name. - $mform->addElement('text', 'name', get_string('issuername', 'tool_oauth2'), 'maxlength="255"'); + $mform->addElement('text', 'name', get_string('issuername', 'tool_oauth2')); $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('name', 'issuername', 'tool_oauth2'); // Client ID. - $mform->addElement('text', 'clientid', get_string('issuerclientid', 'tool_oauth2'), 'maxlength="255"'); + $mform->addElement('text', 'clientid', get_string('issuerclientid', 'tool_oauth2')); $mform->addRule('clientid', null, 'required', null, 'client'); $mform->addRule('clientid', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('clientid', 'issuerclientid', 'tool_oauth2'); // Client Secret. - $mform->addElement('text', 'clientsecret', get_string('issuerclientsecret', 'tool_oauth2'), 'maxlength="255"'); + $mform->addElement('text', 'clientsecret', get_string('issuerclientsecret', 'tool_oauth2')); $mform->addRule('clientsecret', null, 'required', null, 'client'); $mform->addRule('clientsecret', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('clientsecret', 'issuerclientsecret', 'tool_oauth2'); // Login scopes. - $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2'), 'maxlength="255"'); + $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2')); $mform->addRule('loginscopes', null, 'required', null, 'client'); $mform->addRule('loginscopes', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('loginscopes', 'issuerloginscopes', 'tool_oauth2'); // Login scopes offline. - $mform->addElement('text', 'loginscopesoffline', get_string('issuerloginscopesoffline', 'tool_oauth2'), 'maxlength="255"'); + $mform->addElement('text', 'loginscopesoffline', get_string('issuerloginscopesoffline', 'tool_oauth2')); $mform->addRule('loginscopesoffline', null, 'required', null, 'client'); $mform->addRule('loginscopesoffline', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('loginscopesoffline', 'issuerloginscopesoffline', 'tool_oauth2'); // Login params. - $mform->addElement('text', 'loginparams', get_string('issuerloginparams', 'tool_oauth2'), 'maxlength="255"'); + $mform->addElement('text', 'loginparams', get_string('issuerloginparams', 'tool_oauth2')); $mform->addRule('loginparams', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('loginparams', 'issuerloginparams', 'tool_oauth2'); // Login params offline. - $mform->addElement('text', 'loginparamsoffline', get_string('issuerloginparamsoffline', 'tool_oauth2'), 'maxlength="255"'); + $mform->addElement('text', 'loginparamsoffline', get_string('issuerloginparamsoffline', 'tool_oauth2')); $mform->addRule('loginparamsoffline', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('loginparamsoffline', 'issuerloginparamsoffline', 'tool_oauth2'); // Base Url. - $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2')); $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2'); // Allowed Domains. - $mform->addElement('text', 'alloweddomains', get_string('issueralloweddomains', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addElement('text', 'alloweddomains', get_string('issueralloweddomains', 'tool_oauth2')); $mform->addRule('alloweddomains', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); $mform->addHelpButton('alloweddomains', 'issueralloweddomains', 'tool_oauth2'); @@ -117,7 +117,7 @@ class issuer extends persistent { $mform->setType('sortorder', PARAM_INT); $mform->addElement('hidden', 'action', 'edit'); - $mform->setType('action', PARAM_RAW); + $mform->setType('action', PARAM_ALPHA); $mform->addElement('hidden', 'enabled', $endpoint->get('enabled')); $mform->setType('enabled', PARAM_BOOL); diff --git a/admin/tool/oauth2/classes/form/user_field_mapping.php b/admin/tool/oauth2/classes/form/user_field_mapping.php index fff419d9ab5..debd380ed8a 100644 --- a/admin/tool/oauth2/classes/form/user_field_mapping.php +++ b/admin/tool/oauth2/classes/form/user_field_mapping.php @@ -55,7 +55,7 @@ class user_field_mapping extends persistent { $mform->addElement('header', 'generalhdr', get_string('general')); // External. - $mform->addElement('text', 'externalfield', get_string('userfieldexternalfield', 'tool_oauth2'), 'maxlength="255"'); + $mform->addElement('text', 'externalfield', get_string('userfieldexternalfield', 'tool_oauth2')); $mform->addRule('externalfield', null, 'required', null, 'client'); $mform->addRule('externalfield', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('externalfield', 'userfieldexternalfield', 'tool_oauth2'); @@ -66,7 +66,7 @@ class user_field_mapping extends persistent { $mform->addHelpButton('internalfield', 'userfieldinternalfield', 'tool_oauth2'); $mform->addElement('hidden', 'action', 'edit'); - $mform->setType('action', PARAM_RAW); + $mform->setType('action', PARAM_ALPHA); $mform->addElement('hidden', 'issuerid', $userfieldmapping->get('issuerid')); $mform->setConstant('issuerid', $this->_customdata['issuerid']); From 8d90d294603aca1cb03cc7a17e48af5b683eb90e Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:18:57 +0800 Subject: [PATCH 53/84] MDL-58220 oauth2: Use iputils to check domain Allows wild cards and more strict checking against spec. --- lib/classes/oauth2/issuer.php | 12 ++---------- lib/tests/oauth2_test.php | 8 ++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 785d5db60f5..fa48a612480 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -142,6 +142,7 @@ class issuer extends persistent { if (empty($this->get('alloweddomains'))) { return true; } + $validdomains = explode(',', $this->get('alloweddomains')); $parts = explode('@', $email, 2); @@ -150,16 +151,7 @@ class issuer extends persistent { $emaildomain = $parts[1]; } - $emaildomain = \core_text::strtolower(trim($emaildomain)); - foreach ($validdomains as $checkdomain) { - $checkdomain = \core_text::strtolower(trim($checkdomain)); - - if ((\core_text::strlen($checkdomain) == \core_text::strlen($emaildomain)) && - (\core_text::strpos($checkdomain, $emaildomain) === 0)) { - return true; - } - } - return false; + return \core\ip_utils::is_domain_in_allowed_list($emaildomain, $validdomains); } /** diff --git a/lib/tests/oauth2_test.php b/lib/tests/oauth2_test.php index 50022826d2f..676d6ec44fc 100644 --- a/lib/tests/oauth2_test.php +++ b/lib/tests/oauth2_test.php @@ -196,6 +196,14 @@ class core_oauth2_testcase extends advanced_testcase { $this->assertFalse($issuer->is_valid_login_domain('invalid@email@example.net')); $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.net')); $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com')); + + $issuer->set('alloweddomains', '*.example.com'); + // Wildcard. + $this->assertFalse($issuer->is_valid_login_domain('')); + $this->assertFalse($issuer->is_valid_login_domain('a@b')); + $this->assertFalse($issuer->is_valid_login_domain('longer.example@example')); + $this->assertFalse($issuer->is_valid_login_domain('longer.example@example.com')); + $this->assertTrue($issuer->is_valid_login_domain('longer.example@sub.example.com')); } } From 5aa0f0ae48dbfded87a12cf26a8c1830b802a2fe Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:20:22 +0800 Subject: [PATCH 54/84] MDl-58220 auth_oauth2: Skip upgrade for new plugin --- auth/oauth2/db/upgrade.php | 69 -------------------------------------- 1 file changed, 69 deletions(-) diff --git a/auth/oauth2/db/upgrade.php b/auth/oauth2/db/upgrade.php index 71dd0a5e1a9..c64c63312e7 100644 --- a/auth/oauth2/db/upgrade.php +++ b/auth/oauth2/db/upgrade.php @@ -38,74 +38,5 @@ function xmldb_auth_oauth2_upgrade($oldversion) { // Automatically generated Moodle v3.2.0 release upgrade line. // Put any upgrade step following this. - if ($oldversion < 2017030700) { - - // Define table auth_oauth2_linked_login to be created. - $table = new xmldb_table('auth_oauth2_linked_login'); - - // Adding fields to table auth_oauth2_linked_login. - $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); - $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('issuerid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('username', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); - $table->add_field('email', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); - - // Adding keys to table auth_oauth2_linked_login. - $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $table->add_key('usermodified_key', XMLDB_KEY_FOREIGN, array('usermodified'), 'user', array('id')); - $table->add_key('userid_key', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id')); - $table->add_key('issuerid_key', XMLDB_KEY_FOREIGN, array('issuerid'), 'oauth2_issuer', array('id')); - $table->add_key('uniq_key', XMLDB_KEY_UNIQUE, array('userid', 'issuerid', 'username')); - - // Adding indexes to table auth_oauth2_linked_login. - $table->add_index('search_index', XMLDB_INDEX_NOTUNIQUE, array('issuerid', 'username')); - - // Conditionally launch create table for auth_oauth2_linked_login. - if (!$dbman->table_exists($table)) { - $dbman->create_table($table); - } - - // Oauth2 savepoint reached. - upgrade_plugin_savepoint(true, 2017030700, 'auth', 'oauth2'); - } - - if ($oldversion < 2017031000) { - - // Changing type of field email on table auth_oauth2_linked_login to text. - $table = new xmldb_table('auth_oauth2_linked_login'); - $field = new xmldb_field('email', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null, 'username'); - - // Launch change of type for field email. - $dbman->change_field_type($table, $field); - - // Oauth2 savepoint reached. - upgrade_plugin_savepoint(true, 2017031000, 'auth', 'oauth2'); - } - - if ($oldversion < 2017032300) { - - // Define field confirmtoken to be added to auth_oauth2_linked_login. - $table = new xmldb_table('auth_oauth2_linked_login'); - $field = new xmldb_field('confirmtoken', XMLDB_TYPE_CHAR, '64', null, XMLDB_NOTNULL, null, null, 'email'); - - // Conditionally launch add field confirmtoken. - if (!$dbman->field_exists($table, $field)) { - $dbman->add_field($table, $field); - } - - $field = new xmldb_field('confirmtokenexpires', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'confirmtoken'); - - // Conditionally launch add field confirmtokenexpires. - if (!$dbman->field_exists($table, $field)) { - $dbman->add_field($table, $field); - } - - // Oauth2 savepoint reached. - upgrade_plugin_savepoint(true, 2017032300, 'auth', 'oauth2'); - } - return true; } From 5b0b35c096c4702cb73e344c611a880656400435 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:25:04 +0800 Subject: [PATCH 55/84] MDL-58220 oauth2: Add is_configured to issuer Saves repeated checks for clientid and clientsecret. --- admin/tool/oauth2/classes/output/renderer.php | 2 +- auth/oauth2/classes/auth.php | 4 +--- lib/classes/oauth2/issuer.php | 12 ++++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index b0a32f22009..274931001ce 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -85,7 +85,7 @@ class renderer extends plugin_renderer_base { $namecell->header = true; // Configured. - if (!empty($issuer->get('clientid')) && !empty($issuer->get('clientsecret'))) { + if ($issuer->is_configured()) { $configured = $OUTPUT->pix_icon('yes', get_string('configured', 'tool_oauth2'), 'tool_oauth2'); } else { $configured = $OUTPUT->pix_icon('no', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2'); diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index d3ba71c172f..0bdb7c90851 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -187,9 +187,7 @@ class auth extends \auth_plugin_base { */ private function is_ready_for_login_page(\core\oauth2\issuer $issuer) { return $issuer->get('enabled') && - !empty($issuer->get('clientid')) && - !empty($issuer->get('clientsecret')) && - $issuer->is_authentication_supported() && + $issuer->is_configured() && !empty($issuer->get('showonloginpage')); } diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index fa48a612480..6acc77bc3b5 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -162,6 +162,15 @@ class issuer extends persistent { return (!empty($this->get_endpoint_url('userinfo'))); } + /** + * Return true if this issuer looks like it has been configured. + * + * @return boolean + */ + public function is_configured() { + return (!empty($this->get('clientid')) && !empty($this->get('clientsecret'))); + } + /** * Does this OAuth service support system authentication? * @return boolean @@ -175,6 +184,9 @@ class issuer extends persistent { * @return boolean */ public function is_system_account_connected() { + if (!$this->is_configured()) { + return false; + } $sys = system_account::get_record(['issuerid' => $this->get('id')]); if (!empty($sys) and !empty($sys->get('refreshtoken'))) { return true; From 7949b3b2ac4922902a4824f63f0f2b61dd59f523 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:28:10 +0800 Subject: [PATCH 56/84] MDL-58220 oauth2: Remove is_system_account_setup_supported This was left from an earlier branch but no longer useful. --- admin/tool/oauth2/classes/output/renderer.php | 12 +++++------- lib/classes/oauth2/issuer.php | 8 -------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index 274931001ce..4e6d762b07e 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -122,13 +122,11 @@ class renderer extends plugin_renderer_base { $systemauth = $OUTPUT->pix_icon('no', get_string('systemaccountnotconnected', 'tool_oauth2'), 'tool_oauth2'); } - if ($issuer->is_system_account_setup_supported()) { - $params = ['id' => $issuer->get('id'), 'action' => 'auth']; - $authurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); - $icon = $OUTPUT->pix_icon('auth', get_string('connectsystemaccount', 'tool_oauth2'), 'tool_oauth2'); - $authlink = html_writer::link($authurl, $icon); - $systemauth .= ' ' . $authlink; - } + $params = ['id' => $issuer->get('id'), 'action' => 'auth']; + $authurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + $icon = $OUTPUT->pix_icon('auth', get_string('connectsystemaccount', 'tool_oauth2'), 'tool_oauth2'); + $authlink = html_writer::link($authurl, $icon); + $systemauth .= ' ' . $authlink; $systemauthstatuscell = new html_table_cell($systemauth); diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 6acc77bc3b5..e58b07c1b89 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -171,14 +171,6 @@ class issuer extends persistent { return (!empty($this->get('clientid')) && !empty($this->get('clientsecret'))); } - /** - * Does this OAuth service support system authentication? - * @return boolean - */ - public function is_system_account_setup_supported() { - return true; - } - /** * Do we have a refresh token for a system account? * @return boolean From c45d4b6a73d64414b3030fbde28aa558507c7258 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:31:20 +0800 Subject: [PATCH 57/84] MDL-58220 oauth2: Change $OUTPUT to $this --- admin/tool/oauth2/classes/output/renderer.php | 52 +++++++++---------- auth/oauth2/classes/output/renderer.php | 4 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index 4e6d762b07e..5ae79d8f390 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -48,15 +48,15 @@ class renderer extends plugin_renderer_base { * @return string HTML to output. */ public function issuers_table($issuers) { - global $CFG, $OUTPUT; + global $CFG; $table = new html_table(); $table->head = [ get_string('name'), get_string('configuredstatus', 'tool_oauth2'), get_string('loginissuer', 'tool_oauth2'), - get_string('discoverystatus', 'tool_oauth2') . ' ' . $OUTPUT->help_icon('discovered', 'tool_oauth2'), - get_string('systemauthstatus', 'tool_oauth2') . ' ' . $OUTPUT->help_icon('systemaccountconnected', 'tool_oauth2'), + get_string('discoverystatus', 'tool_oauth2') . ' ' . $this->help_icon('discovered', 'tool_oauth2'), + get_string('systemauthstatus', 'tool_oauth2') . ' ' . $this->help_icon('systemaccountconnected', 'tool_oauth2'), get_string('edit'), ]; $table->attributes['class'] = 'admintable generaltable'; @@ -86,26 +86,26 @@ class renderer extends plugin_renderer_base { // Configured. if ($issuer->is_configured()) { - $configured = $OUTPUT->pix_icon('yes', get_string('configured', 'tool_oauth2'), 'tool_oauth2'); + $configured = $this->pix_icon('yes', get_string('configured', 'tool_oauth2'), 'tool_oauth2'); } else { - $configured = $OUTPUT->pix_icon('no', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2'); + $configured = $this->pix_icon('no', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2'); } $configuredstatuscell = new html_table_cell($configured); // Login issuer. if (!empty($issuer->get('showonloginpage'))) { - $loginissuer = $OUTPUT->pix_icon('yes', get_string('loginissuer', 'tool_oauth2'), 'tool_oauth2'); + $loginissuer = $this->pix_icon('yes', get_string('loginissuer', 'tool_oauth2'), 'tool_oauth2'); } else { - $loginissuer = $OUTPUT->pix_icon('no', get_string('notloginissuer', 'tool_oauth2'), 'tool_oauth2'); + $loginissuer = $this->pix_icon('no', get_string('notloginissuer', 'tool_oauth2'), 'tool_oauth2'); } $loginissuerstatuscell = new html_table_cell($loginissuer); // Discovered. if (!empty($issuer->get('scopessupported'))) { - $discovered = $OUTPUT->pix_icon('yes', get_string('discovered', 'tool_oauth2'), 'tool_oauth2'); + $discovered = $this->pix_icon('yes', get_string('discovered', 'tool_oauth2'), 'tool_oauth2'); } else { if (!empty($issuer->get_endpoint_url('discovery'))) { - $discovered = $OUTPUT->pix_icon('no', get_string('notdiscovered', 'tool_oauth2'), 'tool_oauth2'); + $discovered = $this->pix_icon('no', get_string('notdiscovered', 'tool_oauth2'), 'tool_oauth2'); } else { $discovered = '-'; } @@ -117,14 +117,14 @@ class renderer extends plugin_renderer_base { if ($issuer->is_system_account_connected()) { $systemaccount = \core\oauth2\api::get_system_account($issuer); $systemauth = s($systemaccount->get('email')) . ' (' . s($systemaccount->get('username')). ') '; - $systemauth .= $OUTPUT->pix_icon('yes', get_string('systemaccountconnected', 'tool_oauth2'), 'tool_oauth2'); + $systemauth .= $this->pix_icon('yes', get_string('systemaccountconnected', 'tool_oauth2'), 'tool_oauth2'); } else { - $systemauth = $OUTPUT->pix_icon('no', get_string('systemaccountnotconnected', 'tool_oauth2'), 'tool_oauth2'); + $systemauth = $this->pix_icon('no', get_string('systemaccountnotconnected', 'tool_oauth2'), 'tool_oauth2'); } $params = ['id' => $issuer->get('id'), 'action' => 'auth']; $authurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); - $icon = $OUTPUT->pix_icon('auth', get_string('connectsystemaccount', 'tool_oauth2'), 'tool_oauth2'); + $icon = $this->pix_icon('auth', get_string('connectsystemaccount', 'tool_oauth2'), 'tool_oauth2'); $authlink = html_writer::link($authurl, $icon); $systemauth .= ' ' . $authlink; @@ -133,52 +133,52 @@ class renderer extends plugin_renderer_base { $links = ''; // Action links. $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['id' => $issuer->get('id'), 'action' => 'edit']); - $editlink = html_writer::link($editurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))); + $editlink = html_writer::link($editurl, $this->pix_icon('t/edit', get_string('edit'))); $links .= ' ' . $editlink; // Endpoints. $editendpointsurl = new moodle_url('/admin/tool/oauth2/endpoints.php', ['issuerid' => $issuer->get('id')]); $str = get_string('editendpoints', 'tool_oauth2'); - $editendpointlink = html_writer::link($editendpointsurl, $OUTPUT->pix_icon('t/viewdetails', $str)); + $editendpointlink = html_writer::link($editendpointsurl, $this->pix_icon('t/viewdetails', $str)); $links .= ' ' . $editendpointlink; // User field mapping. $params = ['issuerid' => $issuer->get('id')]; $edituserfieldmappingsurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', $params); $str = get_string('edituserfieldmappings', 'tool_oauth2'); - $edituserfieldmappinglink = html_writer::link($edituserfieldmappingsurl, $OUTPUT->pix_icon('t/user', $str)); + $edituserfieldmappinglink = html_writer::link($edituserfieldmappingsurl, $this->pix_icon('t/user', $str)); $links .= ' ' . $edituserfieldmappinglink; // Delete. $deleteurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['id' => $issuer->get('id'), 'action' => 'delete']); - $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $deletelink = html_writer::link($deleteurl, $this->pix_icon('t/delete', get_string('delete'))); $links .= ' ' . $deletelink; // Enable / Disable. if ($issuer->get('enabled')) { // Disable. $disableparams = ['id' => $issuer->get('id'), 'sesskey' => sesskey(), 'action' => 'disable']; $disableurl = new moodle_url('/admin/tool/oauth2/issuers.php', $disableparams); - $disablelink = html_writer::link($disableurl, $OUTPUT->pix_icon('t/hide', get_string('disable'))); + $disablelink = html_writer::link($disableurl, $this->pix_icon('t/hide', get_string('disable'))); $links .= ' ' . $disablelink; } else { // Enable. $enableparams = ['id' => $issuer->get('id'), 'sesskey' => sesskey(), 'action' => 'enable']; $enableurl = new moodle_url('/admin/tool/oauth2/issuers.php', $enableparams); - $enablelink = html_writer::link($enableurl, $OUTPUT->pix_icon('t/show', get_string('enable'))); + $enablelink = html_writer::link($enableurl, $this->pix_icon('t/show', get_string('enable'))); $links .= ' ' . $enablelink; } if (!$last) { // Move down. $params = ['id' => $issuer->get('id'), 'action' => 'movedown', 'sesskey' => sesskey()]; $movedownurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); - $movedownlink = html_writer::link($movedownurl, $OUTPUT->pix_icon('t/down', get_string('movedown'))); + $movedownlink = html_writer::link($movedownurl, $this->pix_icon('t/down', get_string('movedown'))); $links .= ' ' . $movedownlink; } if (!$first) { // Move up. $params = ['id' => $issuer->get('id'), 'action' => 'moveup', 'sesskey' => sesskey()]; $moveupurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); - $moveuplink = html_writer::link($moveupurl, $OUTPUT->pix_icon('t/up', get_string('moveup'))); + $moveuplink = html_writer::link($moveupurl, $this->pix_icon('t/up', get_string('moveup'))); $links .= ' ' . $moveuplink; } @@ -208,7 +208,7 @@ class renderer extends plugin_renderer_base { * @return string HTML to output. */ public function endpoints_table($endpoints, $issuerid) { - global $CFG, $OUTPUT; + global $CFG; $table = new html_table(); $table->head = [ @@ -235,13 +235,13 @@ class renderer extends plugin_renderer_base { // Action links. $editparams = ['issuerid' => $issuerid, 'endpointid' => $endpoint->get('id'), 'action' => 'edit']; $editurl = new moodle_url('/admin/tool/oauth2/endpoints.php', $editparams); - $editlink = html_writer::link($editurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))); + $editlink = html_writer::link($editurl, $this->pix_icon('t/edit', get_string('edit'))); $links .= ' ' . $editlink; // Delete. $deleteparams = ['issuerid' => $issuerid, 'endpointid' => $endpoint->get('id'), 'action' => 'delete']; $deleteurl = new moodle_url('/admin/tool/oauth2/endpoints.php', $deleteparams); - $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $deletelink = html_writer::link($deleteurl, $this->pix_icon('t/delete', get_string('delete'))); $links .= ' ' . $deletelink; $editcell = new html_table_cell($links); @@ -267,7 +267,7 @@ class renderer extends plugin_renderer_base { * @return string HTML to output. */ public function user_field_mappings_table($userfieldmappings, $issuerid) { - global $CFG, $OUTPUT; + global $CFG; $table = new html_table(); $table->head = [ @@ -293,13 +293,13 @@ class renderer extends plugin_renderer_base { // Action links. $editparams = ['issuerid' => $issuerid, 'userfieldmappingid' => $userfieldmapping->get('id'), 'action' => 'edit']; $editurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', $editparams); - $editlink = html_writer::link($editurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))); + $editlink = html_writer::link($editurl, $this->pix_icon('t/edit', get_string('edit'))); $links .= ' ' . $editlink; // Delete. $deleteparams = ['issuerid' => $issuerid, 'userfieldmappingid' => $userfieldmapping->get('id'), 'action' => 'delete']; $deleteurl = new moodle_url('/admin/tool/oauth2/userfieldmappings.php', $deleteparams); - $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $deletelink = html_writer::link($deleteurl, $this->pix_icon('t/delete', get_string('delete'))); $links .= ' ' . $deletelink; $editcell = new html_table_cell($links); diff --git a/auth/oauth2/classes/output/renderer.php b/auth/oauth2/classes/output/renderer.php index dbe2048810e..12435603891 100644 --- a/auth/oauth2/classes/output/renderer.php +++ b/auth/oauth2/classes/output/renderer.php @@ -47,7 +47,7 @@ class renderer extends plugin_renderer_base { * @return string HTML to output. */ public function linked_logins_table($linkedlogins) { - global $CFG, $OUTPUT; + global $CFG; $table = new html_table(); $table->head = [ @@ -76,7 +76,7 @@ class renderer extends plugin_renderer_base { // Delete. $deleteparams = ['linkedloginid' => $linkedlogin->get('id'), 'action' => 'delete', 'sesskey' => sesskey()]; $deleteurl = new moodle_url('/auth/oauth2/linkedlogins.php', $deleteparams); - $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $deletelink = html_writer::link($deleteurl, $this->pix_icon('t/delete', get_string('delete'))); $links .= ' ' . $deletelink; $editcell = new html_table_cell($links); From 440df5fb7fe17c42eee9c8005543c7a55d9678e2 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:33:51 +0800 Subject: [PATCH 58/84] MDL-58220 oauth2: quote image attributes --- admin/tool/oauth2/classes/output/renderer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index 5ae79d8f390..b838d0af482 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -79,7 +79,7 @@ class renderer extends plugin_renderer_base { $name = $issuer->get('name'); $image = $issuer->get('image'); if ($image) { - $name = ' ' . s($name); + $name = ' ' . s($name); } $namecell = new html_table_cell($name); $namecell->header = true; From 68ecf7635b8a490381cb1d9325f45dcd940e31a2 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:36:33 +0800 Subject: [PATCH 59/84] MDL-58220 oauth2: tighten the param types These come from external sources - but still should not contain crazy stuff. --- lib/classes/oauth2/endpoint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/classes/oauth2/endpoint.php b/lib/classes/oauth2/endpoint.php index 921f9a0e8da..21451966180 100644 --- a/lib/classes/oauth2/endpoint.php +++ b/lib/classes/oauth2/endpoint.php @@ -49,7 +49,7 @@ class endpoint extends persistent { 'type' => PARAM_INT ), 'name' => array( - 'type' => PARAM_RAW, + 'type' => PARAM_ALPHANUMEXT, ), 'url' => array( 'type' => PARAM_URL, From d9fbe3146cfc2efff8d41ad78e28a877e1f5a43e Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 11:58:45 +0800 Subject: [PATCH 60/84] MDL-58220 oauth2: Use the same list of user fields Authentication has a hard coded list of valid internal user fields - but they are in a class variable. We need them in oauth user_field_mapping so we need to move them to a central place and call them from oauth2 and auth. --- lib/authlib.php | 21 +----------------- lib/classes/oauth2/user_field_mapping.php | 27 +++++++++-------------- lib/classes/user.php | 25 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/lib/authlib.php b/lib/authlib.php index 09aa59d0584..0fea5319156 100644 --- a/lib/authlib.php +++ b/lib/authlib.php @@ -102,26 +102,7 @@ class auth_plugin_base { * The fields we can lock and update from/to external authentication backends * @var array */ - var $userfields = array( - 'firstname', - 'lastname', - 'email', - 'city', - 'country', - 'lang', - 'description', - 'url', - 'idnumber', - 'institution', - 'department', - 'phone1', - 'phone2', - 'address', - 'firstnamephonetic', - 'lastnamephonetic', - 'middlename', - 'alternatename' - ); + var $userfields = \core_user::AUTHSYNCFIELDS; /** * Moodle custom fields to sync with. diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php index 9751bc9009a..8586edc061d 100644 --- a/lib/classes/oauth2/user_field_mapping.php +++ b/lib/classes/oauth2/user_field_mapping.php @@ -37,21 +37,14 @@ class user_field_mapping extends persistent { const TABLE = 'oauth2_user_field_mapping'; - /** @var array $userfields - List of standard Moodle userfields. */ - private static $userfields = [ - 'firstname', - 'middlename', - 'lastname', - 'email', - 'username', - 'idnumber', - 'url', - 'alternatename', - 'picture', - 'address', - 'phone', - 'lang' - ]; + /** + * Return the list of valid internal user fields. + * + * @return array + */ + private static function get_user_fields() { + return array_merge(\core_user::AUTHSYNCFIELDS, ['picture']); + } /** * Return the definition of the properties of this model. @@ -68,7 +61,7 @@ class user_field_mapping extends persistent { ), 'internalfield' => array( 'type' => PARAM_ALPHANUMEXT, - 'choices' => self::$userfields, + 'choices' => self::get_user_fields() ) ); } @@ -79,6 +72,6 @@ class user_field_mapping extends persistent { * @return array */ public function get_internalfield_list() { - return array_combine(self::$userfields, self::$userfields); + return array_combine(self::get_user_fields(), self::get_user_fields()); } } diff --git a/lib/classes/user.php b/lib/classes/user.php index 26cb195df67..9c95cc2132c 100644 --- a/lib/classes/user.php +++ b/lib/classes/user.php @@ -58,6 +58,30 @@ class core_user { */ const MAILDISPLAY_COURSE_MEMBERS_ONLY = 2; + /** + * List of fields that can be synched/locked during authentication. + */ + const AUTHSYNCFIELDS = [ + 'firstname', + 'lastname', + 'email', + 'city', + 'country', + 'lang', + 'description', + 'url', + 'idnumber', + 'institution', + 'department', + 'phone1', + 'phone2', + 'address', + 'firstnamephonetic', + 'lastnamephonetic', + 'middlename', + 'alternatename' + ]; + /** @var stdClass keep record of noreply user */ public static $noreplyuser = false; @@ -887,4 +911,5 @@ class core_user { return $value; } } + } From e25362a7dbb90796d194e187afaa4db6c923ff12 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 12:01:29 +0800 Subject: [PATCH 61/84] MDL-58220 auth: $OUTPUT -> $output Don't use global $OUTPUT when there is a more specific renderer. --- auth/classes/output/login.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/auth/classes/output/login.php b/auth/classes/output/login.php index bebfed86915..421171a5631 100644 --- a/auth/classes/output/login.php +++ b/auth/classes/output/login.php @@ -121,13 +121,12 @@ class login implements renderable, templatable { } public function export_for_template(renderer_base $output) { - global $CFG, $OUTPUT; + global $CFG; $identityproviders = array_map(function($idp) use ($output) { - global $OUTPUT; if (!empty($idp['icon'])) { - $idp['iconurl'] = $OUTPUT->pix_url($idp['icon']->key, $idp['icon']->component); + $idp['iconurl'] = $output->pix_url($idp['icon']->key, $idp['icon']->component); } else if ($idp['iconurl'] instanceof moodle_url) { $idp['iconurl'] = $idp['iconurl']->out(false); } From 1a911be57bae2905bb036d1a532f92482451c33e Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 12:03:07 +0800 Subject: [PATCH 62/84] MDL-58220 oauth2: Fix docs in test file (and remove unused global $SESSION). --- lib/tests/oauth2_test.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/tests/oauth2_test.php b/lib/tests/oauth2_test.php index 676d6ec44fc..0f19b6fc0d6 100644 --- a/lib/tests/oauth2_test.php +++ b/lib/tests/oauth2_test.php @@ -25,16 +25,16 @@ defined('MOODLE_INTERNAL') || die(); /** - * Tests for myprofilelib apis. + * Tests for oauth2 apis (\core\oauth2\*). * * @package core - * @copyright 2015 onwards Ankit agarwal + * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ class core_oauth2_testcase extends advanced_testcase { /** - * Tests the core_myprofile_navigation() function as an admin viewing a user's course profile. + * Tests the crud operations on oauth2 issuers. */ public function test_create_and_delete_standard_issuers() { $this->resetAfterTest(); @@ -103,8 +103,6 @@ class core_oauth2_testcase extends advanced_testcase { * Tests we can get a logged in oauth client for a system account. */ public function test_get_system_oauth_client() { - global $SESSION; - $this->resetAfterTest(); $this->setAdminUser(); From 0b9bb5bdc485c1f7eeddaeac6d86585657349240 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 28 Mar 2017 12:07:29 +0800 Subject: [PATCH 63/84] MDL-58220 oauth2: Alphabetise lang strings. --- admin/tool/oauth2/lang/en/tool_oauth2.php | 136 +++++++++++----------- 1 file changed, 65 insertions(+), 71 deletions(-) diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 73f3120222a..8cfa6d1dc12 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -22,84 +22,78 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['pluginname'] = 'OAuth 2 Services'; -$string['discovered_help'] = 'Discovered means that the OAuth2 endpoints could be automatically determined from the base url for the OAuth service. Not all services are required to be "discovered", but if they are not, then the endpoints and user mapping information will need to be entered manually.'; -$string['systemaccountconnected_help'] = 'System accounts are used to provide advanced functionality for plugins. They are not required for login functionality only, but other plugins using the OAuth service may offer a reduced set of features if the system account has not been connected. For example repositories cannot support "controlled links" without a system account to perform file operations.'; -$string['editissuer'] = 'Edit identity issuer: {$a}'; -$string['editendpoint'] = 'Edit endpoint: {$a->endpoint} for issuer {$a->issuer}'; -$string['endpointsforissuer'] = 'Endpoints for issuer: {$a}'; -$string['edituserfieldmapping'] = 'Edit user field mapping for issuer {$a}'; -$string['userfieldmappingsforissuer'] = 'User field mappings for issuer: {$a}'; -$string['issuers'] = 'Issuers'; -$string['endpointname'] = 'Name'; -$string['endpointname_help'] = 'Key used to search for this endpoint. Must end with "_endpoint".'; -$string['endpointurl'] = 'Url'; -$string['endpointurl_help'] = 'URL for this endpoint. Must use https:// protocol.'; -$string['issuername'] = 'Name'; -$string['issuername_help'] = 'Name of the identity issuer. May be displayed on login page.'; -$string['issuerimage'] = 'Logo URL'; -$string['issuerimage_help'] = 'An image url used to show a logo for this issuer. May be displayed on login page.'; -$string['issuerclientid'] = 'Client Id'; -$string['issuerclientid_help'] = 'The OAuth client ID for this issuer.'; -$string['issuerclientsecret'] = 'Client Secret'; -$string['issuerclientsecret_help'] = 'The OAuth client secret for this issuer.'; -$string['issuerbaseurl'] = 'Service base url'; -$string['issuerbaseurl_help'] = 'Base url used to access the service.'; -$string['issuerloginscopes'] = 'Scopes included in a login request.'; -$string['issuerloginscopes_help'] = 'Some systems require additional scopes for a login request in order to read the users basic profile. The standard scopes for an OpenID Connect compliant system are "openid profile email".'; -$string['issuerloginscopesoffline'] = 'Scopes included in a login request for offline access.'; -$string['issuerloginscopesoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Microsoft requires an additional scope "offline_access"'; -$string['issuerloginparams'] = 'Additional parameters included in a login request.'; -$string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the users basic profile.'; -$string['issuerloginparamsoffline'] = 'Additional parameters included in a login request for offline access.'; -$string['issuerloginparamsoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Google requires the additional params: "access_type=offline&prompt=consent" these parameters should be in url query parameter format.'; -$string['issueralloweddomains'] = 'Login domains'; -$string['issueralloweddomains_help'] = 'If set, this setting is a comma separated list of domains that logins will be restricted to when using this provider.'; -$string['issueralloweddomains_link'] = 'OAuth_2_login_domains'; -$string['issuershowonloginpage'] = 'Show on login page.'; -$string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to login with accounts from this issuer.'; -$string['issuerbehaviour'] = 'Behaviour'; -$string['issuerbehaviour_help'] = 'Choose from one of the supported behaviours. - -* OAuth 2.0 - OAuth 2.0 API with no authentication -* OpenID Connect - Standards based OAuth 2.0 API with Authentication -* Microsoft OAuth 2.0 - Non-standard OAuth 2.0 combined with Microsoft Graph API'; -$string['savechanges'] = 'Save changes'; -$string['configuredstatus'] = 'Configured'; -$string['discoverystatus'] = 'Discovery'; -$string['systemauthstatus'] = 'System account connected'; +$string['authconfirm'] = 'This action will grant permanent API access to Moodle for the authenticated account. This is intended to be used as a system account for managing files owned by Moodle.'; +$string['authconnected'] = 'The system account is now connected for offline access'; +$string['authnotconnected'] = 'The system account was not connected for offline access'; $string['configured'] = 'Configured'; -$string['editendpoints'] = 'Configure endpoints'; -$string['edituserfieldmappings'] = 'Configure user field mappings'; -$string['notconfigured'] = 'Not configured'; -$string['discovered'] = 'Service discovery successful'; -$string['notdiscovered'] = 'Service discovery not successful'; -$string['loginissuer'] = 'Allow login'; -$string['notloginissuer'] = 'Do not allow login'; -$string['systemaccountconnected'] = 'System account connected'; -$string['systemaccountnotconnected'] = 'System account not connected'; -$string['createnewissuer'] = 'Create new custom service'; -$string['createnewgoogleissuer'] = 'Create new Google service'; -$string['createnewmicrosoftissuer'] = 'Create new Microsoft service'; -$string['createnewfacebookissuer'] = 'Create new Facebook service'; +$string['configuredstatus'] = 'Configured'; +$string['connectsystemaccount'] = 'Connect to a system account'; +$string['createfromtemplate'] = 'Create an OAuth 2 service from a template'; +$string['createfromtemplatedesc'] = 'Choose one of the OAuth 2 service template below to create an OAuth service with a valid configuration for one of the known service types. This will create the OAuth 2 service, with all the correct end points and parameters required for authentication, but you will still need to enter the client ID and secret for the new service before it can be used.'; $string['createnewendpoint'] = 'Create new endpoint for issuer "{$a}"'; +$string['createnewfacebookissuer'] = 'Create new Facebook service'; +$string['createnewgoogleissuer'] = 'Create new Google service'; +$string['createnewissuer'] = 'Create new custom service'; +$string['createnewmicrosoftissuer'] = 'Create new Microsoft service'; $string['createnewuserfieldmapping'] = 'Create new user field mapping for issuer "{$a}"'; $string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.'; $string['deleteendpointconfirm'] = 'Are you sure you want to delete the endpoint "{$a->endpoint}" for issuer "{$a->issuer}"? Any plugins relying on this endpoint will stop working.'; $string['deleteuserfieldmappingconfirm'] = 'Are you sure you want to delete the user field mapping for issuer "{$a}"?'; -$string['issuerdeleted'] = 'Identity issuer deleted'; -$string['issuerenabled'] = 'Identity issuer enabled'; -$string['issuerdisabled'] = 'Identity issuer disabled'; +$string['discovered_help'] = 'Discovered means that the OAuth2 endpoints could be automatically determined from the base url for the OAuth service. Not all services are required to be "discovered", but if they are not, then the endpoints and user mapping information will need to be entered manually.'; +$string['discovered'] = 'Service discovery successful'; +$string['discoverystatus'] = 'Discovery'; +$string['editendpoint'] = 'Edit endpoint: {$a->endpoint} for issuer {$a->issuer}'; +$string['editendpoints'] = 'Configure endpoints'; +$string['editissuer'] = 'Edit identity issuer: {$a}'; +$string['edituserfieldmapping'] = 'Edit user field mapping for issuer {$a}'; +$string['edituserfieldmappings'] = 'Configure user field mappings'; $string['endpointdeleted'] = 'Endpoint deleted'; -$string['userfieldmappingdeleted'] = 'User field mapping deleted'; -$string['connectsystemaccount'] = 'Connect to a system account'; -$string['authconfirm'] = 'This action will grant permanent API access to Moodle for the authenticated account. This is intended to be used as a system account for managing files owned by Moodle.'; -$string['authconnected'] = 'The system account is now connected for offline access'; -$string['authnotconnected'] = 'The system account was not connected for offline access'; +$string['endpointname_help'] = 'Key used to search for this endpoint. Must end with "_endpoint".'; +$string['endpointname'] = 'Name'; +$string['endpointsforissuer'] = 'Endpoints for issuer: {$a}'; +$string['endpointurl_help'] = 'URL for this endpoint. Must use https:// protocol.'; +$string['endpointurl'] = 'Url'; +$string['issueralloweddomains_help'] = 'If set, this setting is a comma separated list of domains that logins will be restricted to when using this provider.'; +$string['issueralloweddomains_link'] = 'OAuth_2_login_domains'; +$string['issueralloweddomains'] = 'Login domains'; +$string['issuerbaseurl_help'] = 'Base url used to access the service.'; +$string['issuerbaseurl'] = 'Service base url'; +$string['issuerclientid'] = 'Client Id'; +$string['issuerclientid_help'] = 'The OAuth client ID for this issuer.'; +$string['issuerclientsecret'] = 'Client Secret'; +$string['issuerclientsecret_help'] = 'The OAuth client secret for this issuer.'; +$string['issuerdeleted'] = 'Identity issuer deleted'; +$string['issuerdisabled'] = 'Identity issuer disabled'; +$string['issuerenabled'] = 'Identity issuer enabled'; +$string['issuerimage_help'] = 'An image url used to show a logo for this issuer. May be displayed on login page.'; +$string['issuerimage'] = 'Logo URL'; +$string['issuerloginparams'] = 'Additional parameters included in a login request.'; +$string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the users basic profile.'; +$string['issuerloginparamsoffline'] = 'Additional parameters included in a login request for offline access.'; +$string['issuerloginparamsoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Google requires the additional params: "access_type=offline&prompt=consent" these parameters should be in url query parameter format.'; +$string['issuerloginscopes_help'] = 'Some systems require additional scopes for a login request in order to read the users basic profile. The standard scopes for an OpenID Connect compliant system are "openid profile email".'; +$string['issuerloginscopesoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Microsoft requires an additional scope "offline_access"'; +$string['issuerloginscopesoffline'] = 'Scopes included in a login request for offline access.'; +$string['issuerloginscopes'] = 'Scopes included in a login request.'; +$string['issuername_help'] = 'Name of the identity issuer. May be displayed on login page.'; +$string['issuername'] = 'Name'; +$string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to login with accounts from this issuer.'; +$string['issuershowonloginpage'] = 'Show on login page.'; +$string['issuers'] = 'Issuers'; +$string['loginissuer'] = 'Allow login'; +$string['notconfigured'] = 'Not configured'; +$string['notdiscovered'] = 'Service discovery not successful'; +$string['notloginissuer'] = 'Do not allow login'; +$string['pluginname'] = 'OAuth 2 Services'; +$string['savechanges'] = 'Save changes'; +$string['serviceshelp'] = 'Service provider setup instructions: (Google, Facebook, Microsoft).'; +$string['systemaccountconnected_help'] = 'System accounts are used to provide advanced functionality for plugins. They are not required for login functionality only, but other plugins using the OAuth service may offer a reduced set of features if the system account has not been connected. For example repositories cannot support "controlled links" without a system account to perform file operations.'; +$string['systemaccountconnected'] = 'System account connected'; +$string['systemaccountnotconnected'] = 'System account not connected'; +$string['systemauthstatus'] = 'System account connected'; $string['userfieldexternalfield'] = 'External field name'; $string['userfieldexternalfield_help'] = 'Name of the field provided by the external OAuth system.'; -$string['userfieldinternalfield'] = 'Internal field name'; $string['userfieldinternalfield_help'] = 'Name of the Moodle user field that should be mapped from the external field.'; -$string['createfromtemplate'] = 'Create an OAuth 2 service from a template'; -$string['createfromtemplatedesc'] = 'Choose one of the OAuth 2 service template below to create an OAuth service with a valid configuration for one of the known service types. This will create the OAuth 2 service, with all the correct end points and parameters required for authentication, but you will still need to enter the client ID and secret for the new service before it can be used.'; -$string['serviceshelp'] = 'Service provider setup instructions: (Google, Facebook, Microsoft).'; +$string['userfieldinternalfield'] = 'Internal field name'; +$string['userfieldmappingdeleted'] = 'User field mapping deleted'; +$string['userfieldmappingsforissuer'] = 'User field mappings for issuer: {$a}'; From 6f8a03f196aa474ef9b64c5d363a01c56ec1677c Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 29 Mar 2017 13:58:59 +0800 Subject: [PATCH 64/84] MDL-58220 skydrive: Upgrade from old settings Upgrade the oauth2 settings to an oauth2 issuer. --- repository/skydrive/db/upgrade.php | 21 +++++++++++++++++++++ repository/skydrive/version.php | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/repository/skydrive/db/upgrade.php b/repository/skydrive/db/upgrade.php index 0439640d1f1..ece122d0948 100644 --- a/repository/skydrive/db/upgrade.php +++ b/repository/skydrive/db/upgrade.php @@ -53,5 +53,26 @@ function xmldb_repository_skydrive_upgrade($oldversion) { // Skydrive savepoint reached. upgrade_plugin_savepoint(true, 2017031400, 'repository', 'skydrive'); } + if ($oldversion < 2017032800) { + $clientid = get_config('clientid', 'skydrive'); + $secret = get_config('secret', 'skydrive'); + + // Update from repo config to use an OAuth service. + if (!empty($clientid) && !empty($secret)) { + $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); + + $issuer->set('clientid', $clientid); + $issuer->set('secret', $secret); + + $issuer->update(); + + set_config('issuerid', $issuer->get('id'), 'skydrive'); + } + upgrade_plugin_savepoint(true, 2017032800, 'repository', 'skydrive'); + } + if ($oldversion < 2017032900) { + set_config('supportedfiles', 'both', 'skydrive'); + upgrade_plugin_savepoint(true, 2017032900, 'repository', 'skydrive'); + } return true; } diff --git a/repository/skydrive/version.php b/repository/skydrive/version.php index a3995847071..41d0adb8f3a 100644 --- a/repository/skydrive/version.php +++ b/repository/skydrive/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2017031400; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2017032900; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2016112900; // Requires this Moodle version. $plugin->component = 'repository_skydrive'; // Full name of the plugin (used for diagnostics). From 9165e83831b966837baa9fd62a8de3a48f04c722 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 29 Mar 2017 14:28:17 +0800 Subject: [PATCH 65/84] MDL-58220 unittests: Fix errors found in unittests --- lib/classes/oauth2/api.php | 4 ++-- lib/classes/oauth2/user_field_mapping.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index 0c70f42e0f7..c8c4f6c8400 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -167,7 +167,7 @@ class api { 'userPrincipalName' => 'username', 'displayName' => 'alternatename', 'officeLocation' => 'address', - 'mobilePhone' => 'phone', + 'mobilePhone' => 'phone1', 'preferredLanguage' => 'lang' ]; foreach ($mapping as $external => $internal) { @@ -430,7 +430,7 @@ class api { 'nickname' => 'alternatename', 'picture' => 'picture', 'address' => 'address', - 'phone' => 'phone', + 'phone' => 'phone1', 'locale' => 'lang' ]; foreach ($mapping as $external => $internal) { diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php index 8586edc061d..33b203a5843 100644 --- a/lib/classes/oauth2/user_field_mapping.php +++ b/lib/classes/oauth2/user_field_mapping.php @@ -43,7 +43,7 @@ class user_field_mapping extends persistent { * @return array */ private static function get_user_fields() { - return array_merge(\core_user::AUTHSYNCFIELDS, ['picture']); + return array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username']); } /** From eb47ad4a6e83121b200a061312ca550bb1e78c02 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 29 Mar 2017 14:41:09 +0800 Subject: [PATCH 66/84] MDL-58220 repository: More docs / comments More docs / comments for the reference_file_selected function used by controlled links. --- repository/googledocs/lib.php | 24 +++++++++++++++++------- repository/lib.php | 8 +++++++- repository/skydrive/lib.php | 13 ++++++++++++- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 0cafa7b0f56..96c6f3701fe 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -840,6 +840,9 @@ class repository_googledocs extends repository { * Called when a file is selected as a "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 + * (when the file has really really been selected. + * * @param string $reference this reference is generated by * repository::get_file_reference() * @param context $context the target context for this new file. @@ -849,32 +852,36 @@ class repository_googledocs extends repository { * @return string updated reference (final one before it's saved to db). */ public function reference_file_selected($reference, $context, $component, $filearea, $itemid) { - if (!$this->issuer->get('enabled')) { - throw new repository_exception('cannotdownload', 'repository'); - } // What we need to do here is transfer ownership to the system user (or copy) // then set the permissions so anyone with the share link can view, // finally update the reference to contain the share link if it was not // already there (and point to new file id if we copied). + + + // Check this issuer is enabled. + if (!$this->issuer->get('enabled')) { + throw new repository_exception('cannotdownload', 'repository'); + } + + // Get a system oauth client and a user oauth client. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } + // Get the system user email so we can share the file with this user. $systemuserinfo = $systemauth->get_userinfo(); $systemuseremail = $systemuserinfo['email']; - $source = json_decode($reference); - $userauth = $this->get_user_oauth_client(); if ($userauth === false) { $details = 'Cannot connect as current user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } - $userinfo = $userauth->get_userinfo(); - $useremail = $userinfo['email']; + // Get the details from the reference. + $source = json_decode($reference); $userservice = new repository_googledocs\rest($userauth); $systemservice = new repository_googledocs\rest($systemauth); @@ -898,6 +905,8 @@ class repository_googledocs extends repository { $allfolders[] = clean_param($filearea, PARAM_PATH); $allfolders[] = clean_param($itemid, PARAM_PATH); + // Variable $allfolders is the full path we want to put the file in - so walk it and create each folder. + foreach ($allfolders as $foldername) { // Make sure a folder exists here. $fullpath .= '/' . $foldername; @@ -925,6 +934,7 @@ class repository_googledocs extends repository { $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id); $this->prevent_writers_from_sharing_file($systemservice, $newsource->id); + // Update the returned reference so that the stored_file in moodle points to the newly copied file. $source->id = $newsource->id; $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : ''; if (empty($source->link)) { diff --git a/repository/lib.php b/repository/lib.php index 7522ca9247f..7c0fa0b2da1 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -1285,7 +1285,13 @@ abstract class repository implements cacheable_object { /** * reference_file_selected - * Invoked at MOODLE/repository/repository_ajax.php + * + * This function is called when a controlled link file is selected in a file picker and the form is + * saved. The expected behaviour for repositories supporting controlled links is to + * - copy the file to the moodle system account + * - put it in a folder that reflects the context it is being used + * - make sure the sharing permissions are correct (read-only with the link) + * - return a new reference string pointing to the newly copied file. * * @param string $reference this reference is generated by * repository::get_file_reference() diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php index d3ca1b16cf8..4d9e0ab69c7 100644 --- a/repository/skydrive/lib.php +++ b/repository/skydrive/lib.php @@ -811,6 +811,11 @@ class repository_skydrive extends repository { * Called when a file is selected as a "link". * Invoked at MOODLE/repository/repository_ajax.php * + * What should happen here is that the file should be copied to a new file owned by the moodle system user. + * It should be organised in a folder based on the file context. + * It's sharing permissions should allow read access with the link. + * The returned reference should point to the newly copied file - not the original. + * * @param string $reference this reference is generated by * repository::get_file_reference() * @param context $context the target context for this new file. @@ -824,6 +829,8 @@ class repository_skydrive extends repository { // then set the permissions so anyone with the share link can view, // finally update the reference to contain the share link if it was not // already there (and point to new file id if we copied). + + // Get a system and a user oauth client. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); if ($systemauth === false) { @@ -883,6 +890,8 @@ class repository_skydrive extends repository { $allfolders[] = urlencode(clean_param($filearea, PARAM_PATH)); $allfolders[] = urlencode(clean_param($itemid, PARAM_PATH)); + // Variable $allfolders now has the complete path we want to store the file in. + // Create each folder in $allfolders under the system account. foreach ($allfolders as $foldername) { if ($fullpath) { $fullpath .= '/'; @@ -918,7 +927,9 @@ class repository_skydrive extends repository { $path = $fullpath . '/' . $source->name; $this->delete_file_by_path($systemservice, $path); - // Copy the file so we have a backup. + // Copy the file to the moodle account. + // Note this method (copying via a share link) is the only way to copy a file in + // office 365 from one user to another. $this->copy_share($systemservice, $sharetoken, $newdrive, $parentid); $summary = $this->get_file_summary_by_path($systemservice, $path); From ba3b0145ff4d14171074abaca2ad35e084279d37 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 29 Mar 2017 14:45:34 +0800 Subject: [PATCH 67/84] MDL-58220 repository_skydrive: Remove references to google --- repository/skydrive/db/caches.php | 2 +- repository/skydrive/lib.php | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/repository/skydrive/db/caches.php b/repository/skydrive/db/caches.php index 1050c238de2..e00a85e9735 100644 --- a/repository/skydrive/db/caches.php +++ b/repository/skydrive/db/caches.php @@ -26,7 +26,7 @@ defined('MOODLE_INTERNAL') || die(); $definitions = array( // Used to store file ids for folders. - // The keys used are full path to the folder, the values are the id in google drive. + // The keys used are full path to the folder, the values are the id in office 365. // The static acceleration size has been based upon the depths of a single path. 'folder' => array( 'mode' => cache_store::MODE_APPLICATION, diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php index 4d9e0ab69c7..8ba65535b1b 100644 --- a/repository/skydrive/lib.php +++ b/repository/skydrive/lib.php @@ -237,7 +237,7 @@ class repository_skydrive extends repository { } /** - * Search throughout the Google Drive. + * Search throughout the OneDrive * * @param string $searchtext text to search for. * @param int $page search page. @@ -261,15 +261,16 @@ class repository_skydrive extends repository { } /** - * Query Google Drive for files and folders using a search query. + * Query OneDrive for files and folders using a search query. * * Documentation about the query format can be found here: - * https://developers.google.com/drive/search-parameters + * https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/driveitem + * https://developer.microsoft.com/en-us/graph/docs/overview/query_parameters * * This returns a list of files and folders with their details as they should be * formatted and returned by functions such as get_listing() or search(). * - * @param string $q search query as expected by the Google API. + * @param string $q search query as expected by the Graph API. * @param string $path parent path of the current files, will not be used for the query. * @param string $parent Parent id. * @param int $page page. @@ -327,8 +328,6 @@ class repository_skydrive extends repository { 'link' => $remotefile->webUrl ]); - // Adds the file to the file list. Using the itemId along with the name as key - // of the array because Google Drive allows files with identical names. $thumb = ''; $thumbwidth = 0; $thumbheight = 0; From 3605eb9a9e03fef0702b37a8dd01b9dc207973c9 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 29 Mar 2017 16:24:30 +0800 Subject: [PATCH 68/84] MDL-58220 tool_oauth2: Disable short forms All of these forms only have one section so we should disable short forms. --- admin/tool/oauth2/classes/form/endpoint.php | 2 -- admin/tool/oauth2/classes/form/issuer.php | 4 +--- admin/tool/oauth2/classes/form/user_field_mapping.php | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/admin/tool/oauth2/classes/form/endpoint.php b/admin/tool/oauth2/classes/form/endpoint.php index ac66ed79c5a..d033f3b4c56 100644 --- a/admin/tool/oauth2/classes/form/endpoint.php +++ b/admin/tool/oauth2/classes/form/endpoint.php @@ -52,8 +52,6 @@ class endpoint extends persistent { $mform = $this->_form; $endpoint = $this->get_persistent(); - $mform->addElement('header', 'generalhdr', get_string('general')); - // Name. $mform->addElement('text', 'name', get_string('endpointname', 'tool_oauth2')); $mform->addRule('name', null, 'required', null, 'client'); diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index e1e3f0bdf74..1e929ac4920 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -47,13 +47,11 @@ class issuer extends persistent { * Define the form - called by parent constructor */ public function definition() { - global $PAGE; + global $PAGE, $OUTPUT; $mform = $this->_form; $endpoint = $this->get_persistent(); - $mform->addElement('header', 'generalhdr', get_string('general')); - // Name. $mform->addElement('text', 'name', get_string('issuername', 'tool_oauth2')); $mform->addRule('name', null, 'required', null, 'client'); diff --git a/admin/tool/oauth2/classes/form/user_field_mapping.php b/admin/tool/oauth2/classes/form/user_field_mapping.php index debd380ed8a..aa118e82a34 100644 --- a/admin/tool/oauth2/classes/form/user_field_mapping.php +++ b/admin/tool/oauth2/classes/form/user_field_mapping.php @@ -52,8 +52,6 @@ class user_field_mapping extends persistent { $mform = $this->_form; $userfieldmapping = $this->get_persistent(); - $mform->addElement('header', 'generalhdr', get_string('general')); - // External. $mform->addElement('text', 'externalfield', get_string('userfieldexternalfield', 'tool_oauth2')); $mform->addRule('externalfield', null, 'required', null, 'client'); From bc55c88ba002c1b3faa6bf6477ce6c1e79ca0437 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 29 Mar 2017 16:25:52 +0800 Subject: [PATCH 69/84] MDL-58220 tool_oauth2: Lang string fix --- admin/tool/oauth2/lang/en/tool_oauth2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 8cfa6d1dc12..b6fc97ba0b5 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -39,7 +39,7 @@ $string['createnewuserfieldmapping'] = 'Create new user field mapping for issuer $string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.'; $string['deleteendpointconfirm'] = 'Are you sure you want to delete the endpoint "{$a->endpoint}" for issuer "{$a->issuer}"? Any plugins relying on this endpoint will stop working.'; $string['deleteuserfieldmappingconfirm'] = 'Are you sure you want to delete the user field mapping for issuer "{$a}"?'; -$string['discovered_help'] = 'Discovered means that the OAuth2 endpoints could be automatically determined from the base url for the OAuth service. Not all services are required to be "discovered", but if they are not, then the endpoints and user mapping information will need to be entered manually.'; +$string['discovered_help'] = 'Discovery means that the OAuth2 endpoints could be automatically determined from the base url for the OAuth service. Not all services are required to be "discovered", but if they are not, then the endpoints and user mapping information will need to be entered manually.'; $string['discovered'] = 'Service discovery successful'; $string['discoverystatus'] = 'Discovery'; $string['editendpoint'] = 'Edit endpoint: {$a->endpoint} for issuer {$a->issuer}'; From 4dc53b15fd44b37c75b0b882aab46622477aadd0 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 29 Mar 2017 16:51:10 +0800 Subject: [PATCH 70/84] MDL-58220 oauth2: More prominent help link --- admin/tool/oauth2/classes/form/issuer.php | 2 ++ admin/tool/oauth2/lang/en/tool_oauth2.php | 1 + 2 files changed, 3 insertions(+) diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index 1e929ac4920..16db966f504 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -52,6 +52,8 @@ class issuer extends persistent { $mform = $this->_form; $endpoint = $this->get_persistent(); + $mform->addElement('html', $OUTPUT->page_doc_link(get_string('issuersetup', 'tool_oauth2'))); + // Name. $mform->addElement('text', 'name', get_string('issuername', 'tool_oauth2')); $mform->addRule('name', null, 'required', null, 'client'); diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index b6fc97ba0b5..558eafa943a 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -53,6 +53,7 @@ $string['endpointname'] = 'Name'; $string['endpointsforissuer'] = 'Endpoints for issuer: {$a}'; $string['endpointurl_help'] = 'URL for this endpoint. Must use https:// protocol.'; $string['endpointurl'] = 'Url'; +$string['issuersetup'] = 'Detailed instructions on configuring the common OAuth 2 Services'; $string['issueralloweddomains_help'] = 'If set, this setting is a comma separated list of domains that logins will be restricted to when using this provider.'; $string['issueralloweddomains_link'] = 'OAuth_2_login_domains'; $string['issueralloweddomains'] = 'Login domains'; From 9c2baf096b1c0f161671c587397e129ec78a0619 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 30 Mar 2017 11:45:46 +0800 Subject: [PATCH 71/84] MDL-58220 unittests: Fix randomly failing unit test The scheduled task unit tests does not handle 'R' fields properly. We don't need it here. --- lib/db/tasks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/db/tasks.php b/lib/db/tasks.php index 4366f69fb45..bdb87087fd5 100644 --- a/lib/db/tasks.php +++ b/lib/db/tasks.php @@ -350,7 +350,7 @@ $tasks = array( array( 'classname' => 'core\oauth2\refresh_system_tokens_task', 'blocking' => 0, - 'minute' => 'R', + 'minute' => '30', 'hour' => '*', 'day' => '*', 'dayofweek' => '*', From 5afb4f0e50a79aa016f745e651a007033c6b391e Mon Sep 17 00:00:00 2001 From: Dan Poltawski Date: Wed, 29 Mar 2017 11:22:19 +0100 Subject: [PATCH 72/84] MDL-58220 repository: prevent repo breaking when issuer doesn't exist --- repository/googledocs/lib.php | 6 +++++- repository/skydrive/lib.php | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 96c6f3701fe..402aff31279 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -67,7 +67,11 @@ class repository_googledocs extends repository { public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) { parent::__construct($repositoryid, $context, $options, $readonly = 0); - $this->issuer = \core\oauth2\api::get_issuer(get_config('googledocs', 'issuerid')); + try { + $this->issuer = \core\oauth2\api::get_issuer(get_config('googledocs', 'issuerid')); + } catch (dml_missing_record_exception $e) { + $this->disabled = true; + } } /** diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php index 8ba65535b1b..a731a023796 100644 --- a/repository/skydrive/lib.php +++ b/repository/skydrive/lib.php @@ -63,7 +63,11 @@ class repository_skydrive extends repository { public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) { parent::__construct($repositoryid, $context, $options, $readonly = 0); - $this->issuer = \core\oauth2\api::get_issuer(get_config('skydrive', 'issuerid')); + try { + $this->issuer = \core\oauth2\api::get_issuer(get_config('skydrive', 'issuerid')); + } catch (dml_missing_record_exception $e) { + $this->disabled = true; + } } /** From 33536fb2b7d3b8746c4e06fb2b88100aa6feb423 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 30 Mar 2017 12:17:50 +0800 Subject: [PATCH 73/84] MDL-58220 repositories: missing/disabled issuer This makes the code checking for a missing / disabled issuer a bit cleaner. --- repository/googledocs/lib.php | 8 ++++++-- repository/skydrive/lib.php | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 402aff31279..3520254d96b 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -72,6 +72,10 @@ class repository_googledocs extends repository { } catch (dml_missing_record_exception $e) { $this->disabled = true; } + + if ($this->issuer && !$this->issuer->get('enabled')) { + $this->disabled = true; + } } /** @@ -863,7 +867,7 @@ class repository_googledocs extends repository { // Check this issuer is enabled. - if (!$this->issuer->get('enabled')) { + if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } @@ -956,7 +960,7 @@ class repository_googledocs extends repository { * @param int $filestatus */ public function get_reference_details($reference, $filestatus = 0) { - if (!$this->issuer->get('enabled')) { + if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } if (empty($reference)) { diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php index a731a023796..45a4b6f334b 100644 --- a/repository/skydrive/lib.php +++ b/repository/skydrive/lib.php @@ -68,6 +68,10 @@ class repository_skydrive extends repository { } catch (dml_missing_record_exception $e) { $this->disabled = true; } + + if ($this->issuer && !$this->issuer->get('enabled')) { + $this->disabled = true; + } } /** @@ -202,7 +206,7 @@ class repository_skydrive extends repository { $path = $this->build_node_path('root', get_string('pluginname', 'repository_skydrive')); } - if (!$this->issuer->get('enabled')) { + if ($this->disabled) { // Empty list of files for disabled repository. return ['dynload' => false, 'list' => [], 'nologin' => true]; } @@ -401,7 +405,7 @@ class repository_skydrive extends repository { public function get_file($reference, $filename = '') { global $CFG; - if (!$this->issuer->get('enabled')) { + if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } @@ -518,7 +522,7 @@ class repository_skydrive extends repository { * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) { - if (!$this->issuer->get('enabled')) { + if ($this->disabled) { throw new repository_exception('cannotdownload', 'repository'); } From 0931acf96b890dbb95169bc42ba877e060e6e068 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 30 Mar 2017 13:01:18 +0800 Subject: [PATCH 74/84] MDL-58220 repository: Move skydrive -> onedrive --- .../{skydrive => onedrive}/classes/access.php | 0 .../classes/remove_temp_access_task.php | 0 repository/{skydrive => onedrive}/classes/rest.php | 0 repository/{skydrive => onedrive}/db/access.php | 0 repository/{skydrive => onedrive}/db/caches.php | 0 repository/{skydrive => onedrive}/db/install.xml | 0 repository/{skydrive => onedrive}/db/tasks.php | 0 repository/{skydrive => onedrive}/db/upgrade.php | 0 .../lang/en/repository_skydrive.php | 0 repository/{skydrive => onedrive}/lib.php | 0 repository/{skydrive => onedrive}/pix/icon.png | Bin repository/{skydrive => onedrive}/version.php | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename repository/{skydrive => onedrive}/classes/access.php (100%) rename repository/{skydrive => onedrive}/classes/remove_temp_access_task.php (100%) rename repository/{skydrive => onedrive}/classes/rest.php (100%) rename repository/{skydrive => onedrive}/db/access.php (100%) rename repository/{skydrive => onedrive}/db/caches.php (100%) rename repository/{skydrive => onedrive}/db/install.xml (100%) rename repository/{skydrive => onedrive}/db/tasks.php (100%) rename repository/{skydrive => onedrive}/db/upgrade.php (100%) rename repository/{skydrive => onedrive}/lang/en/repository_skydrive.php (100%) rename repository/{skydrive => onedrive}/lib.php (100%) rename repository/{skydrive => onedrive}/pix/icon.png (100%) rename repository/{skydrive => onedrive}/version.php (100%) diff --git a/repository/skydrive/classes/access.php b/repository/onedrive/classes/access.php similarity index 100% rename from repository/skydrive/classes/access.php rename to repository/onedrive/classes/access.php diff --git a/repository/skydrive/classes/remove_temp_access_task.php b/repository/onedrive/classes/remove_temp_access_task.php similarity index 100% rename from repository/skydrive/classes/remove_temp_access_task.php rename to repository/onedrive/classes/remove_temp_access_task.php diff --git a/repository/skydrive/classes/rest.php b/repository/onedrive/classes/rest.php similarity index 100% rename from repository/skydrive/classes/rest.php rename to repository/onedrive/classes/rest.php diff --git a/repository/skydrive/db/access.php b/repository/onedrive/db/access.php similarity index 100% rename from repository/skydrive/db/access.php rename to repository/onedrive/db/access.php diff --git a/repository/skydrive/db/caches.php b/repository/onedrive/db/caches.php similarity index 100% rename from repository/skydrive/db/caches.php rename to repository/onedrive/db/caches.php diff --git a/repository/skydrive/db/install.xml b/repository/onedrive/db/install.xml similarity index 100% rename from repository/skydrive/db/install.xml rename to repository/onedrive/db/install.xml diff --git a/repository/skydrive/db/tasks.php b/repository/onedrive/db/tasks.php similarity index 100% rename from repository/skydrive/db/tasks.php rename to repository/onedrive/db/tasks.php diff --git a/repository/skydrive/db/upgrade.php b/repository/onedrive/db/upgrade.php similarity index 100% rename from repository/skydrive/db/upgrade.php rename to repository/onedrive/db/upgrade.php diff --git a/repository/skydrive/lang/en/repository_skydrive.php b/repository/onedrive/lang/en/repository_skydrive.php similarity index 100% rename from repository/skydrive/lang/en/repository_skydrive.php rename to repository/onedrive/lang/en/repository_skydrive.php diff --git a/repository/skydrive/lib.php b/repository/onedrive/lib.php similarity index 100% rename from repository/skydrive/lib.php rename to repository/onedrive/lib.php diff --git a/repository/skydrive/pix/icon.png b/repository/onedrive/pix/icon.png similarity index 100% rename from repository/skydrive/pix/icon.png rename to repository/onedrive/pix/icon.png diff --git a/repository/skydrive/version.php b/repository/onedrive/version.php similarity index 100% rename from repository/skydrive/version.php rename to repository/onedrive/version.php From 9b8a36e882431f1e8f8e1e6510c2c06919013385 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 30 Mar 2017 13:01:39 +0800 Subject: [PATCH 75/84] MDL-58220 repository_skydrive: put back existing Restore the existing skydrive repo from integration / master. Because we cannot upgrade cleanly - it's better not to touch the existing repo. --- repository/skydrive/db/access.php | 33 +++ repository/skydrive/db/caches.php | 31 +++ .../skydrive/lang/en/repository_skydrive.php | 31 +++ repository/skydrive/lib.php | 211 +++++++++++++++ repository/skydrive/microsoftliveapi.php | 245 ++++++++++++++++++ repository/skydrive/pix/icon.png | Bin 0 -> 1594 bytes repository/skydrive/version.php | 30 +++ 7 files changed, 581 insertions(+) create mode 100644 repository/skydrive/db/access.php create mode 100644 repository/skydrive/db/caches.php create mode 100644 repository/skydrive/lang/en/repository_skydrive.php create mode 100644 repository/skydrive/lib.php create mode 100644 repository/skydrive/microsoftliveapi.php create mode 100644 repository/skydrive/pix/icon.png create mode 100644 repository/skydrive/version.php diff --git a/repository/skydrive/db/access.php b/repository/skydrive/db/access.php new file mode 100644 index 00000000000..6fe7ea25da2 --- /dev/null +++ b/repository/skydrive/db/access.php @@ -0,0 +1,33 @@ +. + +/** + * Capability definitions for skydrive repository + * + * @package repository_skydrive + * @copyright 2012 Lancaster University Network Services Ltd + * @author Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +$capabilities = array( + 'repository/skydrive:view' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ) +); diff --git a/repository/skydrive/db/caches.php b/repository/skydrive/db/caches.php new file mode 100644 index 00000000000..8b61e339ba6 --- /dev/null +++ b/repository/skydrive/db/caches.php @@ -0,0 +1,31 @@ +. + +/** + * Cache definitions. + * + * @package repository_skydrive + * @copyright 2013 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$definitions = array( + 'foldername' => array( + 'mode' => cache_store::MODE_SESSION, + ) +); diff --git a/repository/skydrive/lang/en/repository_skydrive.php b/repository/skydrive/lang/en/repository_skydrive.php new file mode 100644 index 00000000000..1ed71f48fc5 --- /dev/null +++ b/repository/skydrive/lang/en/repository_skydrive.php @@ -0,0 +1,31 @@ +. + +/** + * Language file definitions for skydrive repository + * + * @package repository_skydrive + * @copyright 2012 Lancaster University Network Services Ltd + * @author Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +$string['cachedef_foldername'] = 'Folder name cache'; +$string['clientid'] = 'Client ID'; +$string['configplugin'] = 'Configure Microsoft OneDrive'; +$string['oauthinfo'] = '

    To use this plugin, you must register your site with Microsoft.

    As part of the registration process, you will need to enter the following URL as \'Redirect domain\':

    {$a->callbackurl}

    Once registered, you will be provided with a client ID and secret which can be entered here.

    '; +$string['pluginname'] = 'Microsoft OneDrive'; +$string['secret'] = 'Secret'; +$string['skydrive:view'] = 'View OneDrive'; diff --git a/repository/skydrive/lib.php b/repository/skydrive/lib.php new file mode 100644 index 00000000000..69c6316454d --- /dev/null +++ b/repository/skydrive/lib.php @@ -0,0 +1,211 @@ +. + +/** + * Microsoft Live Skydrive Repository Plugin + * + * @package repository_skydrive + * @copyright 2012 Lancaster University Network Services Ltd + * @author Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once('microsoftliveapi.php'); + +/** + * Microsoft skydrive repository plugin. + * + * @package repository_skydrive + * @copyright 2012 Lancaster University Network Services Ltd + * @author Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class repository_skydrive extends repository { + /** @var microsoft_skydrive skydrive oauth2 api helper object */ + private $skydrive = null; + + /** + * Constructor + * + * @param int $repositoryid repository instance id. + * @param int|stdClass $context a context id or context object. + * @param array $options repository options. + */ + public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { + parent::__construct($repositoryid, $context, $options); + + $clientid = get_config('skydrive', 'clientid'); + $secret = get_config('skydrive', 'secret'); + $returnurl = new moodle_url('/repository/repository_callback.php'); + $returnurl->param('callback', 'yes'); + $returnurl->param('repo_id', $this->id); + $returnurl->param('sesskey', sesskey()); + + $this->skydrive = new microsoft_skydrive($clientid, $secret, $returnurl); + $this->check_login(); + } + + /** + * Checks whether the user is logged in or not. + * + * @return bool true when logged in + */ + public function check_login() { + return $this->skydrive->is_logged_in(); + } + + /** + * Print the login form, if required + * + * @return array of login options + */ + public function print_login() { + $url = $this->skydrive->get_login_url(); + + if ($this->options['ajax']) { + $popup = new stdClass(); + $popup->type = 'popup'; + $popup->url = $url->out(false); + return array('login' => array($popup)); + } else { + echo ''.get_string('login', 'repository').''; + } + } + + /** + * Given a path, and perhaps a search, get a list of files. + * + * See details on {@link http://docs.moodle.org/dev/Repository_plugins} + * + * @param string $path identifier for current path + * @param string $page the page number of file list + * @return array list of files including meta information as specified by parent. + */ + public function get_listing($path='', $page = '') { + $ret = array(); + $ret['dynload'] = true; + $ret['nosearch'] = true; + $ret['manage'] = 'https://skydrive.live.com/'; + + $fileslist = $this->skydrive->get_file_list($path); + // Filter list for accepted types. Hopefully this will be done by core some day. + $fileslist = array_filter($fileslist, array($this, 'filter')); + $ret['list'] = $fileslist; + + // Generate path bar, always start with the plugin name. + $ret['path'] = array(); + $ret['path'][] = array('name'=> $this->name, 'path'=>''); + + // Now add each level folder. + $trail = ''; + if (!empty($path)) { + $parts = explode('/', $path); + foreach ($parts as $folderid) { + if (!empty($folderid)) { + $trail .= ('/'.$folderid); + $ret['path'][] = array('name' => $this->skydrive->get_folder_name($folderid), + 'path' => $trail); + } + } + } + + return $ret; + } + + /** + * Downloads a repository file and saves to a path. + * + * @param string $id identifier of file + * @param string $filename to save file as + * @return array with keys: + * path: internal location of the file + * url: URL to the source + */ + public function get_file($id, $filename = '') { + $path = $this->prepare_file($filename); + return $this->skydrive->download_file($id, $path); + } + + /** + * Return names of the options to display in the repository form + * + * @return array of option names + */ + public static function get_type_option_names() { + return array('clientid', 'secret', 'pluginname'); + } + + /** + * Setup repistory form. + * + * @param moodleform $mform Moodle form (passed by reference) + * @param string $classname repository class name + */ + public static function type_config_form($mform, $classname = 'repository') { + $a = new stdClass; + $a->callbackurl = microsoft_skydrive::callback_url()->out(false); + $mform->addElement('static', null, '', get_string('oauthinfo', 'repository_skydrive', $a)); + + parent::type_config_form($mform); + $strrequired = get_string('required'); + $mform->addElement('text', 'clientid', get_string('clientid', 'repository_skydrive')); + $mform->addElement('text', 'secret', get_string('secret', 'repository_skydrive')); + $mform->addRule('clientid', $strrequired, 'required', null, 'client'); + $mform->addRule('secret', $strrequired, 'required', null, 'client'); + $mform->setType('clientid', PARAM_RAW_TRIMMED); + $mform->setType('secret', PARAM_RAW_TRIMMED); + } + + /** + * Logout from repository instance and return + * login form. + * + * @return page to display + */ + public function logout() { + $this->skydrive->log_out(); + return $this->print_login(); + } + + /** + * This repository doesn't support global search. + * + * @return bool if supports global search + */ + public function global_search() { + return false; + } + + /** + * This repoistory supports any filetype. + * + * @return string '*' means this repository support any files + */ + public function supported_filetypes() { + return '*'; + } + + /** + * This repostiory only supports internal files + * + * @return int return type bitmask supported + */ + public function supported_returntypes() { + return FILE_INTERNAL; + } +} diff --git a/repository/skydrive/microsoftliveapi.php b/repository/skydrive/microsoftliveapi.php new file mode 100644 index 00000000000..5fc52274eae --- /dev/null +++ b/repository/skydrive/microsoftliveapi.php @@ -0,0 +1,245 @@ +. + +/** + * Functions for operating with the skydrive API + * + * @package repository_skydrive + * @copyright 2012 Lancaster University Network Services Ltd + * @author Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/oauthlib.php'); + +/** + * A helper class to access microsoft live resources using the api. + * + * This uses the microsfot API defined in + * http://msdn.microsoft.com/en-us/library/hh243648.aspx + * + * @package repository_skydrive + * @copyright 2012 Lancaster University Network Services Ltd + * @author Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class microsoft_skydrive extends oauth2_client { + /** @var string OAuth 2.0 scope */ + const SCOPE = 'wl.skydrive'; + /** @var string Base url to access API */ + const API = 'https://apis.live.net/v5.0'; + /** @var cache_session cache of foldernames */ + var $foldernamecache = null; + + /** + * Construct a skydrive request object + * + * @param string $clientid client id for OAuth 2.0 provided by microsoft + * @param string $clientsecret secret for OAuth 2.0 provided by microsoft + * @param moodle_url $returnurl url to return to after succseful auth + */ + public function __construct($clientid, $clientsecret, $returnurl) { + parent::__construct($clientid, $clientsecret, $returnurl, self::SCOPE); + // Make a session cache + $this->foldernamecache = cache::make('repository_skydrive', 'foldername'); + } + + /** + * Returns the auth url for OAuth 2.0 request + * @return string the auth url + */ + protected function auth_url() { + return 'https://login.live.com/oauth20_authorize.srf'; + } + + /** + * Returns the token url for OAuth 2.0 request + * @return string the auth url + */ + protected function token_url() { + return 'https://login.live.com/oauth20_token.srf'; + } + + /** + * Post request. + * + * Overridden to convert the data to a string, else curl will set the wrong headers. + * + * @param string $url The URL. + * @param array|string $params The parameters. + * @param array $options The options. + * @return bool + */ + public function post($url, $params = '', $options = array()) { + return parent::post($url, format_postdata_for_curlcall($params), $options); + } + + /** + * Downloads a file to a file from skydrive using authenticated request + * + * @param string $id id of file + * @param string $path path to save file to + * @return array stucture for repository download_file + */ + public function download_file($id, $path) { + $url = self::API."/${id}/content"; + // Microsoft live redirects to the real download location.. + $this->setopt(array('CURLOPT_FOLLOWLOCATION' => true, 'CURLOPT_MAXREDIRS' => 3)); + $content = $this->get($url); + file_put_contents($path, $content); + return array('path'=>$path, 'url'=>$url); + } + + /** + * Returns a folder name property for a given folderid. + * + * @param string $folderid the folder id which is passed + * @return mixed folder name or false in case of error + */ + public function get_folder_name($folderid) { + if (empty($folderid)) { + throw new coding_exception('Empty folderid passed to get_folder_name'); + } + + // Cache based on oauthtoken and folderid. + $cachekey = $this->folder_cache_key($folderid); + + if ($foldername = $this->foldernamecache->get($cachekey)) { + return $foldername; + } + + $url = self::API."/{$folderid}"; + $ret = json_decode($this->get($url)); + if (isset($ret->error)) { + $this->log_out(); + return false; + } + + $this->foldernamecache->set($cachekey, $ret->name); + return $ret->name; + } + + /** + * Returns a list of files the user has formated for files api + * + * @param string $path the path which we are in + * @return mixed Array of files formated for fileapoi + */ + public function get_file_list($path = '') { + global $OUTPUT; + + $precedingpath = ''; + if (empty($path)) { + $url = self::API."/me/skydrive/files/"; + } else { + $parts = explode('/', $path); + $currentfolder = array_pop($parts); + $url = self::API."/{$currentfolder}/files/"; + } + + $ret = json_decode($this->get($url)); + + if (isset($ret->error)) { + $this->log_out(); + return false; + } + + $files = array(); + + foreach ($ret->data as $file) { + switch($file->type) { + case 'folder': + case 'album': + // Cache the foldername for future requests. + $cachekey = $this->folder_cache_key($file->id); + $this->foldernamecache->set($cachekey, $file->name); + + $files[] = array( + 'title' => $file->name, + 'path' => $path.'/'.$file->id, + 'size' => 0, + 'date' => strtotime($file->updated_time), + 'thumbnail' => $OUTPUT->image_url(file_folder_icon(90))->out(false), + 'children' => array(), + ); + break; + case 'photo': + $files[] = array( + 'title' => $file->name, + 'size' => $file->size, + 'date' => strtotime($file->updated_time), + 'thumbnail' => $OUTPUT->image_url(file_extension_icon($file->name, 90))->out(false), + 'realthumbnail' => $file->picture, + 'source' => $file->id, + 'url' => $file->link, + 'image_height' => $file->height, + 'image_width' => $file->width, + 'author' => $file->from->name, + ); + break; + case 'video': + $files[] = array( + 'title' => $file->name, + 'size' => $file->size, + 'date' => strtotime($file->updated_time), + 'thumbnail' => $OUTPUT->image_url(file_extension_icon($file->name, 90))->out(false), + 'realthumbnail' => $file->picture, + 'source' => $file->id, + 'url' => $file->link, + 'author' => $file->from->name, + ); + break; + case 'audio': + $files[] = array( + 'title' => $file->name, + 'size' => $file->size, + 'date' => strtotime($file->updated_time), + 'thumbnail' => $OUTPUT->image_url(file_extension_icon($file->name, 90))->out(false), + 'source' => $file->id, + 'url' => $file->link, + 'author' => $file->from->name, + ); + break; + case 'file': + $files[] = array( + 'title' => $file->name, + 'size' => $file->size, + 'date' => strtotime($file->updated_time), + 'thumbnail' => $OUTPUT->image_url(file_extension_icon($file->name, 90))->out(false), + 'source' => $file->id, + 'url' => $file->link, + 'author' => $file->from->name, + ); + break; + } + } + return $files; + } + + /** + * Returns a key for foldernane cache + * + * @param string $folderid the folder id which is to be cached + * @return string the cache key to use + */ + private function folder_cache_key($folderid) { + // Cache based on oauthtoken and folderid. + return $this->get_tokenname().'_'.$folderid; + } +} diff --git a/repository/skydrive/pix/icon.png b/repository/skydrive/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..186d49c06db364b3e5d39afd35eb41e2b3eb2b20 GIT binary patch literal 1594 zcmV-A2F3Y_P)6e4Ax8>6+b0IfB>EaJq+ldR$%!D7FTBFM;?PHb6EIo6 z2sHK0a9a*XxE&S|Ml>EVno#hepDlH$C(Apk0X_#B5hvt=U?$6JTsO;YBsHW)CeTj* zVCKQS7N_1fFdQrgm$$=lPpFk@C=U*uWj=AimwyxR+~P3I4zB=3j|Fn?M`L(kB(l%K zpBDCc>kb_?XI@-5=@z0gBeOb_(myUN)_wj|NI2xd5W&C~(n2i2LVT|5qo_@bw{-8b z)ATROSp0oo{KmR#D_y-y#pbaq&DJ~XzX|Bty%WLCpN7u-`0a%oKD4cR;IgWpJHU16 z;grWrhdWL?wf4FDrwi_)xT3yl$b4aFo>@`c*<^wk8LcQp22h#;(F6z^1tv67oe9LI zC)d`dq)&KxA5`!qa?yEc4B3@F&N6Beuw{M%-Vq<{Han+2l)oVE87%RIa($_IAkDWyD-P})^mia_Xz?c2=6Ds4jSb1fS* zC)`gbPVb_lXVfSYgz(<3+4%O@0^}M|C?R2_04WI}6a?b{5u(Xhe%qm+`p_IVs}QSx z8cSBZ@EbefLu)ff!gYTT4}tl%+?O^@fVAP-W)f24YGQkbQO2HaFVh9{hOs!o@%@oD zy!WMf*qlqjXP7`hNqbX(l!z*>zjoZJw;wTm^JlQf<68lvegkmd`uyQxmwwU_a+och z00h!Zlr>lrVIS>ti$jBnCI(z^q}&XSO?Tn^`2~bX<6|!;@wu)U=qt5Ba06%s(i(2Q zlwBGdT>9k49vS<+XTmqWi1t!Gx4NSxwp?+h8d4*$@r8{ouUk~WB-!T(_~edeU&F!x z;bVSm1gFg!#PPE#$OnWkbS3b^fhp)O$G`#ydL;Wne)~-~Abn^Gz9M|v&Ek<2@;6RI z_dikCMM(3WkVAWDty{T)!!>~^Sd5x1X2OOeu<-*97Bn9P7!z=H9rIf@KmY&$ literal 0 HcmV?d00001 diff --git a/repository/skydrive/version.php b/repository/skydrive/version.php new file mode 100644 index 00000000000..8afca1d1f90 --- /dev/null +++ b/repository/skydrive/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version details for skydrive repository + * + * @package repository_skydrive + * @copyright 2012 Lancaster University Network Services Ltd + * @author Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2016112900; // Requires this Moodle version. +$plugin->component = 'repository_skydrive'; // Full name of the plugin (used for diagnostics). From e518ea794959e787a8a0fc5aafe24d40253f122b Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 30 Mar 2017 13:08:27 +0800 Subject: [PATCH 76/84] MDL-58220 repository_onedrive: rename from skydrive Update all references from skydrive to onedrive in the code / lang strings. --- repository/onedrive/classes/access.php | 8 +- .../classes/remove_temp_access_task.php | 12 +- repository/onedrive/classes/rest.php | 4 +- repository/onedrive/db/access.php | 6 +- repository/onedrive/db/caches.php | 2 +- repository/onedrive/db/install.xml | 4 +- repository/onedrive/db/tasks.php | 6 +- repository/onedrive/db/upgrade.php | 50 +------- ...y_skydrive.php => repository_onedrive.php} | 0 repository/onedrive/lib.php | 116 +++++++++--------- repository/onedrive/version.php | 6 +- .../skydrive/lang/en/repository_skydrive.php | 6 +- 12 files changed, 87 insertions(+), 133 deletions(-) rename repository/onedrive/lang/en/{repository_skydrive.php => repository_onedrive.php} (100%) diff --git a/repository/onedrive/classes/access.php b/repository/onedrive/classes/access.php index 35a9bdd1d9a..10800cd26dd 100644 --- a/repository/onedrive/classes/access.php +++ b/repository/onedrive/classes/access.php @@ -17,11 +17,11 @@ /** * Class for loading/storing access records from the DB. * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace repository_skydrive; +namespace repository_onedrive; defined('MOODLE_INTERNAL') || die(); @@ -30,13 +30,13 @@ use core\persistent; /** * Class for loading/storing issuer from the DB * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class access extends persistent { - const TABLE = 'repository_skydrive_access'; + const TABLE = 'repository_onedrive_access'; /** * Return the definition of the properties of this model. diff --git a/repository/onedrive/classes/remove_temp_access_task.php b/repository/onedrive/classes/remove_temp_access_task.php index b7f928449bf..d6189d42aeb 100644 --- a/repository/onedrive/classes/remove_temp_access_task.php +++ b/repository/onedrive/classes/remove_temp_access_task.php @@ -17,12 +17,12 @@ /** * A scheduled task. * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace repository_skydrive; +namespace repository_onedrive; use \core\task\scheduled_task; @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); /** * Simple task to delete temporary permission records. - * @package repository_skydrive + * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -42,7 +42,7 @@ class remove_temp_access_task extends scheduled_task { * @return string */ public function get_name() { - return get_string('removetempaccesstask', 'repository_skydrive'); + return get_string('removetempaccesstask', 'repository_onedrive'); } /** @@ -55,7 +55,7 @@ class remove_temp_access_task extends scheduled_task { $expires->sub(new DateInterval("P7D")); $timestamp = $expires->getTimestamp(); - $issuerid = get_config('repository_skydrive', 'issuerid'); + $issuerid = get_config('repository_onedrive', 'issuerid'); $issuer = \core\oauth2\api::get_issuer_by_id($issuerid); // Add the current user as an OAuth writer. @@ -65,7 +65,7 @@ class remove_temp_access_task extends scheduled_task { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } - $systemservice = new repository_skydrive\rest($systemauth); + $systemservice = new repository_onedrive\rest($systemauth); foreach ($accessrecords as $access) { if ($access->get('timemodified') < $timestamp) { diff --git a/repository/onedrive/classes/rest.php b/repository/onedrive/classes/rest.php index 5a35baa0a33..47fac12e577 100644 --- a/repository/onedrive/classes/rest.php +++ b/repository/onedrive/classes/rest.php @@ -17,11 +17,11 @@ /** * Microsoft Graph API Rest Interface. * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace repository_skydrive; +namespace repository_onedrive; defined('MOODLE_INTERNAL') || die(); diff --git a/repository/onedrive/db/access.php b/repository/onedrive/db/access.php index 6fe7ea25da2..2a1eec0609e 100644 --- a/repository/onedrive/db/access.php +++ b/repository/onedrive/db/access.php @@ -15,15 +15,15 @@ // along with Moodle. If not, see . /** - * Capability definitions for skydrive repository + * Capability definitions for onedrive repository * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2012 Lancaster University Network Services Ltd * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $capabilities = array( - 'repository/skydrive:view' => array( + 'repository/onedrive:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( diff --git a/repository/onedrive/db/caches.php b/repository/onedrive/db/caches.php index e00a85e9735..23f15b0d481 100644 --- a/repository/onedrive/db/caches.php +++ b/repository/onedrive/db/caches.php @@ -17,7 +17,7 @@ /** * Cache definitions. * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2013 Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/repository/onedrive/db/install.xml b/repository/onedrive/db/install.xml index f61ae0e88df..3c56b317681 100644 --- a/repository/onedrive/db/install.xml +++ b/repository/onedrive/db/install.xml @@ -1,10 +1,10 @@ - - +
    diff --git a/repository/onedrive/db/tasks.php b/repository/onedrive/db/tasks.php index b55c4ea3bb6..1a30f8d6e6a 100644 --- a/repository/onedrive/db/tasks.php +++ b/repository/onedrive/db/tasks.php @@ -15,13 +15,13 @@ // along with Moodle. If not, see . /** - * Definition of repository_skydrive scheduled tasks. + * Definition of repository_onedrive scheduled tasks. * * The handlers defined on this file are processed and registered into * the Moodle DB after any install or upgrade operation. All plugins * support this. * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -32,7 +32,7 @@ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( - 'classname' => 'repository_skydrive\remove_temp_access_task', + 'classname' => 'repository_onedrive\remove_temp_access_task', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', diff --git a/repository/onedrive/db/upgrade.php b/repository/onedrive/db/upgrade.php index ece122d0948..e6d78b6fc15 100644 --- a/repository/onedrive/db/upgrade.php +++ b/repository/onedrive/db/upgrade.php @@ -20,59 +20,13 @@ defined('MOODLE_INTERNAL') || die(); * Upgrade this plugin. * * @param int $oldversion the version we are upgrading from - * @package repository_skydrive + * @package repository_onedrive * @return bool result */ -function xmldb_repository_skydrive_upgrade($oldversion) { +function xmldb_repository_onedrive_upgrade($oldversion) { global $DB; $dbman = $DB->get_manager(); - if ($oldversion < 2017031400) { - - // Define table repository_skydrive_access to be created. - $table = new xmldb_table('repository_skydrive_access'); - - // Adding fields to table repository_skydrive_access. - $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); - $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('permissionid', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); - $table->add_field('itemid', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); - - // Adding keys to table repository_skydrive_access. - $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $table->add_key('usermodifiedkey', XMLDB_KEY_FOREIGN, array('usermodified'), 'user', array('id')); - - // Conditionally launch create table for repository_skydrive_access. - if (!$dbman->table_exists($table)) { - $dbman->create_table($table); - } - - // Skydrive savepoint reached. - upgrade_plugin_savepoint(true, 2017031400, 'repository', 'skydrive'); - } - if ($oldversion < 2017032800) { - $clientid = get_config('clientid', 'skydrive'); - $secret = get_config('secret', 'skydrive'); - - // Update from repo config to use an OAuth service. - if (!empty($clientid) && !empty($secret)) { - $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); - - $issuer->set('clientid', $clientid); - $issuer->set('secret', $secret); - - $issuer->update(); - - set_config('issuerid', $issuer->get('id'), 'skydrive'); - } - upgrade_plugin_savepoint(true, 2017032800, 'repository', 'skydrive'); - } - if ($oldversion < 2017032900) { - set_config('supportedfiles', 'both', 'skydrive'); - upgrade_plugin_savepoint(true, 2017032900, 'repository', 'skydrive'); - } return true; } diff --git a/repository/onedrive/lang/en/repository_skydrive.php b/repository/onedrive/lang/en/repository_onedrive.php similarity index 100% rename from repository/onedrive/lang/en/repository_skydrive.php rename to repository/onedrive/lang/en/repository_onedrive.php diff --git a/repository/onedrive/lib.php b/repository/onedrive/lib.php index 45a4b6f334b..4d5e8d28242 100644 --- a/repository/onedrive/lib.php +++ b/repository/onedrive/lib.php @@ -17,7 +17,7 @@ /** * Microsoft Live Skydrive Repository Plugin * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2012 Lancaster University Network Services Ltd * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -26,14 +26,14 @@ defined('MOODLE_INTERNAL') || die(); /** - * Microsoft skydrive repository plugin. + * Microsoft onedrive repository plugin. * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2012 Lancaster University Network Services Ltd * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class repository_skydrive extends repository { +class repository_onedrive extends repository { /** * OAuth 2 client * @var \core\oauth2\client @@ -64,7 +64,7 @@ class repository_skydrive extends repository { parent::__construct($repositoryid, $context, $options, $readonly = 0); try { - $this->issuer = \core\oauth2\api::get_issuer(get_config('skydrive', 'issuerid')); + $this->issuer = \core\oauth2\api::get_issuer(get_config('onedrive', 'issuerid')); } catch (dml_missing_record_exception $e) { $this->disabled = true; } @@ -203,7 +203,7 @@ class repository_skydrive extends repository { */ public function get_listing($path='', $page = '') { if (empty($path)) { - $path = $this->build_node_path('root', get_string('pluginname', 'repository_skydrive')); + $path = $this->build_node_path('root', get_string('pluginname', 'repository_onedrive')); } if ($this->disabled) { @@ -224,7 +224,7 @@ class repository_skydrive extends repository { $id = 'root'; // Append the active path for search. - $str = get_string('searchfor', 'repository_skydrive', $searchtext); + $str = get_string('searchfor', 'repository_onedrive', $searchtext); $path = $this->build_node_path('search', $str, $path); } @@ -252,8 +252,8 @@ class repository_skydrive extends repository { * @return array of results. */ public function search($searchtext, $page = 0) { - $path = $this->build_node_path('root', get_string('pluginname', 'repository_skydrive')); - $str = get_string('searchfor', 'repository_skydrive', $searchtext); + $path = $this->build_node_path('root', get_string('pluginname', 'repository_onedrive')); + $str = get_string('searchfor', 'repository_onedrive', $searchtext); $path = $this->build_node_path('search', $str, $path); // Query the Drive. @@ -295,7 +295,7 @@ class repository_skydrive extends repository { try { // Retrieving files and folders. $client = $this->get_user_oauth_client(); - $service = new repository_skydrive\rest($client); + $service = new repository_onedrive\rest($client); if (!empty($q)) { $params['search'] = urlencode($q); @@ -308,7 +308,7 @@ class repository_skydrive extends repository { } } catch (Exception $e) { if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) { - throw new repository_exception('servicenotenabled', 'repository_skydrive'); + throw new repository_exception('servicenotenabled', 'repository_onedrive'); } else { throw $e; } @@ -463,7 +463,7 @@ class repository_skydrive extends repository { public function supported_returntypes() { // We can only support references if the system account is connected. if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) { - $setting = get_config('skydrive', 'supportedreturntypes'); + $setting = get_config('onedrive', 'supportedreturntypes'); if ($setting == 'internal') { return FILE_INTERNAL; } else if ($setting == 'external') { @@ -482,8 +482,8 @@ class repository_skydrive extends repository { * @return int */ public function default_returntype() { - $setting = get_config('skydrive', 'defaultreturntype'); - $supported = get_config('skydrive', 'supportedreturntypes'); + $setting = get_config('onedrive', 'defaultreturntype'); + $supported = get_config('onedrive', 'supportedreturntypes'); if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') { return FILE_INTERNAL; } else { @@ -545,7 +545,7 @@ class repository_skydrive extends repository { $details = 'Cannot connect as system user'; throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } - $systemservice = new repository_skydrive\rest($systemauth); + $systemservice = new repository_onedrive\rest($systemauth); // Get the user oauth so we can get the account to add. $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(), @@ -585,11 +585,11 @@ class repository_skydrive extends repository { /** * List the permissions on a file. * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $fileid The id of the file. * @return array */ - protected function list_file_permissions(\repository_skydrive\rest $client, $fileid) { + protected function list_file_permissions(\repository_onedrive\rest $client, $fileid) { $fields = "id,roles,link,grantedTo"; return $client->call('list_permissions', ['fileid' => $fileid, '$select' => $fields]); } @@ -597,11 +597,11 @@ class repository_skydrive extends repository { /** * See if a folder exists within a folder * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $fullpath * @return string|boolean The file id if it exists or false. */ - protected function get_file_id_by_path(\repository_skydrive\rest $client, $fullpath) { + protected function get_file_id_by_path(\repository_onedrive\rest $client, $fullpath) { $fields = "id"; try { $response = $client->call('get_file_by_path', ['fullpath' => $fullpath, '$select' => $fields]); @@ -614,11 +614,11 @@ class repository_skydrive extends repository { /** * Delete a file by full path. * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $fullpath * @return boolean */ - protected function delete_file_by_path(\repository_skydrive\rest $client, $fullpath) { + protected function delete_file_by_path(\repository_onedrive\rest $client, $fullpath) { try { $response = $client->call('delete_file_by_path', ['fullpath' => $fullpath]); } catch (\core\oauth2\rest_exception $re) { @@ -631,11 +631,11 @@ class repository_skydrive extends repository { /** * Get a file summary by full path. * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $fullpath * @return stdClass */ - protected function get_file_summary_by_path(\repository_skydrive\rest $client, $fullpath) { + protected function get_file_summary_by_path(\repository_onedrive\rest $client, $fullpath) { $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,createdByUser"; $response = $client->call('get_file_by_path', ['fullpath' => $fullpath, '$select' => $fields]); if (empty($response->id)) { @@ -648,13 +648,13 @@ class repository_skydrive extends repository { /** * Create a folder within a folder * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $foldername The folder we are creating. * @param string $parentid The parent folder we are creating in. * * @return string The file id of the new folder. */ - protected function create_folder_in_folder(\repository_skydrive\rest $client, $foldername, $parentid) { + protected function create_folder_in_folder(\repository_onedrive\rest $client, $foldername, $parentid) { $params = ['parentid' => $parentid]; $folder = [ 'name' => $foldername, 'folder' => [ 'childCount' => 0 ]]; $created = $client->call('create_folder', $params, json_encode($folder)); @@ -668,12 +668,12 @@ class repository_skydrive extends repository { /** * Get simple file info for humans. * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $fileid The file we are querying. * * @return stdClass */ - protected function get_file_summary(\repository_skydrive\rest $client, $fileid) { + protected function get_file_summary(\repository_onedrive\rest $client, $fileid) { $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,createdByUser"; $response = $client->call('get', ['fileid' => $fileid, '$select' => $fields]); return $response; @@ -682,11 +682,11 @@ class repository_skydrive extends repository { /** * Get the id of this users root drive. * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * * @return string id */ - protected function get_root_drive_id(\repository_skydrive\rest $client) { + protected function get_root_drive_id(\repository_onedrive\rest $client) { $response = $client->call('get_drive', []); if (empty($response->id)) { @@ -699,12 +699,12 @@ class repository_skydrive extends repository { /** * Add a writer to the permissions on the file (temporary). * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $email The email of the writer account to add. * @return boolean */ - protected function add_temp_writer_to_file(\repository_skydrive\rest $client, $fileid, $email) { + protected function add_temp_writer_to_file(\repository_onedrive\rest $client, $fileid, $email) { // Expires in 7 days. $expires = new DateTime(); $expires->add(new DateInterval("P7D")); @@ -722,12 +722,12 @@ class repository_skydrive extends repository { throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); } // Store the permission id in the DB. Scheduled task will remove this permission after 7 days. - if ($access = repository_skydrive\access::get_record(['permissionid' => $response->value[0]->id, 'itemid' => $fileid ])) { + if ($access = repository_onedrive\access::get_record(['permissionid' => $response->value[0]->id, 'itemid' => $fileid ])) { // Update the timemodified. $access->update(); } else { $record = (object) [ 'permissionid' => $response->value[0]->id, 'itemid' => $fileid ]; - $access = new repository_skydrive\access(0, $record); + $access = new repository_onedrive\access(0, $record); $access->create(); } return true; @@ -736,12 +736,12 @@ class repository_skydrive extends repository { /** * Add a writer to the permissions on the file. * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $fileid The file we are updating. * @param string $useremail The user email of the writer account to add. * @return boolean */ - protected function add_writer_to_file(\repository_skydrive\rest $client, $fileid, $useremail) { + protected function add_writer_to_file(\repository_onedrive\rest $client, $fileid, $useremail) { $updateeditor = [ 'recipients' => [ [ 'email' => $useremail ] ], 'roles' => ['write'], @@ -760,11 +760,11 @@ class repository_skydrive extends repository { /** * Allow anyone with the link to read the file. * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $fileid The file we are updating. * @return boolean */ - protected function set_file_sharing_anyone_with_link_can_read(\repository_skydrive\rest $client, $fileid) { + protected function set_file_sharing_anyone_with_link_can_read(\repository_onedrive\rest $client, $fileid) { $updateread = [ 'type' => 'view', 'scope' => 'anonymous' @@ -781,13 +781,13 @@ class repository_skydrive extends repository { /** * Copy a shared file to a new folder. * - * @param \repository_skydrive\rest $client Authenticated client. + * @param \repository_onedrive\rest $client Authenticated client. * @param string $sharetoken The share we are querying. * @param string $newdrive Id of the drive to copy to. * @param string $parentid Id of the folder to copy to. * @return stdClass */ - protected function copy_share(\repository_skydrive\rest $client, $sharetoken, $newdrive, $parentid) { + protected function copy_share(\repository_onedrive\rest $client, $sharetoken, $newdrive, $parentid) { $folder = [ 'parentReference' => ['id' => $parentid, 'driveId' => $newdrive] ]; @@ -857,8 +857,8 @@ class repository_skydrive extends repository { $userinfo = $userauth->get_userinfo(); $useremail = $userinfo['email']; - $userservice = new repository_skydrive\rest($userauth); - $systemservice = new repository_skydrive\rest($systemauth); + $userservice = new repository_onedrive\rest($userauth); + $systemservice = new repository_onedrive\rest($systemauth); // Get the list of existing permissions so we can see if the owner is already the system account, // and whether we need to update the link sharing options. @@ -883,7 +883,7 @@ class repository_skydrive extends repository { // Now copy it to a sensible folder. $contextlist = array_reverse($context->get_parent_contexts(true)); - $cache = cache::make('repository_skydrive', 'folder'); + $cache = cache::make('repository_onedrive', 'folder'); $parentid = 'root'; $fullpath = ''; $allfolders = []; @@ -966,7 +966,7 @@ class repository_skydrive extends repository { if ($systemauth === false) { return ''; } - $systemservice = new repository_skydrive\rest($systemauth); + $systemservice = new repository_onedrive\rest($systemauth); $info = $this->get_file_summary($systemservice, $source->id); $owner = ''; @@ -974,7 +974,7 @@ class repository_skydrive extends repository { $owner = $info->createdByUser->displayName; } if ($owner) { - return get_string('owner', 'repository_skydrive', $owner); + return get_string('owner', 'repository_onedrive', $owner); } else { return $info->name; } @@ -990,7 +990,7 @@ class repository_skydrive extends repository { $url = new moodle_url('/admin/tool/oauth2/issuers.php'); $url = $url->out(); - $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_skydrive', $url)); + $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_onedrive', $url)); parent::type_config_form($mform); $options = []; @@ -1002,23 +1002,23 @@ class repository_skydrive extends repository { $strrequired = get_string('required'); - $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_skydrive'), $options); - $mform->addHelpButton('issuerid', 'issuer', 'repository_skydrive'); + $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_onedrive'), $options); + $mform->addHelpButton('issuerid', 'issuer', 'repository_onedrive'); $mform->addRule('issuerid', $strrequired, 'required', null, 'client'); - $mform->addElement('static', null, '', get_string('fileoptions', 'repository_skydrive')); + $mform->addElement('static', null, '', get_string('fileoptions', 'repository_onedrive')); $choices = [ - 'internal' => get_string('internal', 'repository_skydrive'), - 'external' => get_string('external', 'repository_skydrive'), - 'both' => get_string('both', 'repository_skydrive') + 'internal' => get_string('internal', 'repository_onedrive'), + 'external' => get_string('external', 'repository_onedrive'), + 'both' => get_string('both', 'repository_onedrive') ]; - $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_skydrive'), $choices); + $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_onedrive'), $choices); $choices = [ - FILE_INTERNAL => get_string('internal', 'repository_skydrive'), - FILE_CONTROLLED_LINK => get_string('external', 'repository_skydrive'), + FILE_INTERNAL => get_string('internal', 'repository_onedrive'), + FILE_CONTROLLED_LINK => get_string('external', 'repository_onedrive'), ]; - $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_skydrive'), $choices); + $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_onedrive'), $choices); } } @@ -1028,9 +1028,9 @@ class repository_skydrive extends repository { * @param \core\oauth2\issuer $issuer * @return string */ -function repository_skydrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) { - if ($issuer->get('id') == get_config('skydrive', 'issuerid')) { - return repository_skydrive::SCOPES; +function repository_onedrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) { + if ($issuer->get('id') == get_config('onedrive', 'issuerid')) { + return repository_onedrive::SCOPES; } return ''; } diff --git a/repository/onedrive/version.php b/repository/onedrive/version.php index 41d0adb8f3a..93f8c247a07 100644 --- a/repository/onedrive/version.php +++ b/repository/onedrive/version.php @@ -15,9 +15,9 @@ // along with Moodle. If not, see . /** - * Version details for skydrive repository + * Version details for onedrive repository * - * @package repository_skydrive + * @package repository_onedrive * @copyright 2012 Lancaster University Network Services Ltd * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -27,4 +27,4 @@ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2017032900; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2016112900; // Requires this Moodle version. -$plugin->component = 'repository_skydrive'; // Full name of the plugin (used for diagnostics). +$plugin->component = 'repository_onedrive'; // Full name of the plugin (used for diagnostics). diff --git a/repository/skydrive/lang/en/repository_skydrive.php b/repository/skydrive/lang/en/repository_skydrive.php index 1ed71f48fc5..3bb843b0215 100644 --- a/repository/skydrive/lang/en/repository_skydrive.php +++ b/repository/skydrive/lang/en/repository_skydrive.php @@ -24,8 +24,8 @@ */ $string['cachedef_foldername'] = 'Folder name cache'; $string['clientid'] = 'Client ID'; -$string['configplugin'] = 'Configure Microsoft OneDrive'; +$string['configplugin'] = 'Configure Microsoft SkyDrive'; $string['oauthinfo'] = '

    To use this plugin, you must register your site with Microsoft.

    As part of the registration process, you will need to enter the following URL as \'Redirect domain\':

    {$a->callbackurl}

    Once registered, you will be provided with a client ID and secret which can be entered here.

    '; -$string['pluginname'] = 'Microsoft OneDrive'; +$string['pluginname'] = 'Microsoft SkyDrive'; $string['secret'] = 'Secret'; -$string['skydrive:view'] = 'View OneDrive'; +$string['skydrive:view'] = 'View SkyDrive'; From 86a5f1efe114a1688507b001fd8ceb65f4e01bf3 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 30 Mar 2017 13:16:30 +0800 Subject: [PATCH 77/84] MDL-58220 repository_skydrive: Add deprecation warning --- repository/skydrive/lang/en/repository_skydrive.php | 1 + repository/skydrive/lib.php | 4 ++++ repository/upgrade.txt | 3 +++ 3 files changed, 8 insertions(+) diff --git a/repository/skydrive/lang/en/repository_skydrive.php b/repository/skydrive/lang/en/repository_skydrive.php index 3bb843b0215..ea63162fb0c 100644 --- a/repository/skydrive/lang/en/repository_skydrive.php +++ b/repository/skydrive/lang/en/repository_skydrive.php @@ -29,3 +29,4 @@ $string['oauthinfo'] = '

    To use this plugin, you must register your site callbackurl = microsoft_skydrive::callback_url()->out(false); $mform->addElement('static', null, '', get_string('oauthinfo', 'repository_skydrive', $a)); + $mform->addElement('static', null, '', $OUTPUT->notification(get_string('deprecatedwarning', 'repository_skydrive', $a))); + parent::type_config_form($mform); $strrequired = get_string('required'); $mform->addElement('text', 'clientid', get_string('clientid', 'repository_skydrive')); diff --git a/repository/upgrade.txt b/repository/upgrade.txt index 27814a84e54..2fccc91777e 100644 --- a/repository/upgrade.txt +++ b/repository/upgrade.txt @@ -3,6 +3,9 @@ information provided here is intended especially for developers. Full details of the repository API are available on Moodle docs: http://docs.moodle.org/dev/Repository_API +=== 3.3 === +The skydrive repository is deprecated - please migrate to the newer onedrive repository. + === 3.2 === * The method repository::uses_post_requests() has been deprecated and must not be used anymore. From e7688f559a2a5c6e69c1f0cab3e4a17a4d05ed86 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 30 Mar 2017 15:42:41 +0800 Subject: [PATCH 78/84] MDL-58220 onedrive: Add import from skydrive If the skydrive repo exists - show a button on the config page for the onedrive repo to "steal" all the files from the other repo and disable it. --- repository/onedrive/importskydrive.php | 57 +++++++++++++ .../onedrive/lang/en/repository_onedrive.php | 5 ++ repository/onedrive/lib.php | 82 +++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 repository/onedrive/importskydrive.php diff --git a/repository/onedrive/importskydrive.php b/repository/onedrive/importskydrive.php new file mode 100644 index 00000000000..6eeeb13de7d --- /dev/null +++ b/repository/onedrive/importskydrive.php @@ -0,0 +1,57 @@ +. + +/** + * Import files from skydrive. + * + * @package repository_onedrive + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); + +$PAGE->set_url('/repository/onedrive/importskydrive.php'); +$PAGE->set_context(context_system::instance()); +$strheading = get_string('importskydrivefiles', 'repository_onedrive'); +$PAGE->set_title($strheading); +$PAGE->set_heading($strheading); + +require_login(); + +require_capability('moodle/site:config', context_system::instance()); + +$confirm = optional_param('confirm', false, PARAM_BOOL); + +if ($confirm) { + require_sesskey(); + require_once($CFG->dirroot . '/repository/lib.php'); + require_once($CFG->dirroot . '/repository/onedrive/lib.php'); + + if (repository_onedrive::import_skydrive_files()) { + $mesg = get_string('skydrivefilesimported', 'repository_onedrive'); + redirect(new moodle_url('/admin/repository.php'), $mesg, null, \core\output\notification::NOTIFY_SUCCESS); + } else { + $mesg = get_string('skydrivefilesnotimported', 'repository_onedrive'); + redirect(new moodle_url('/admin/repository.php'), $mesg, null, \core\output\notification::NOTIFY_ERROR); + } +} else { + $continueurl = new moodle_url('/repository/onedrive/importskydrive.php', ['confirm' => true]); + $cancelurl = new moodle_url('/admin/repository.php'); + echo $OUTPUT->header(); + echo $OUTPUT->confirm(get_string('confirmimportskydrive', 'repository_onedrive'), $continueurl, $cancelurl); + echo $OUTPUT->footer(); +} diff --git a/repository/onedrive/lang/en/repository_onedrive.php b/repository/onedrive/lang/en/repository_onedrive.php index 015647fa9d1..46f32672beb 100644 --- a/repository/onedrive/lang/en/repository_onedrive.php +++ b/repository/onedrive/lang/en/repository_onedrive.php @@ -25,6 +25,7 @@ $string['configplugin'] = 'Configure OneDrive plugin'; $string['skydrive:view'] = 'View OneDrive repository'; $string['pluginname'] = 'Microsoft OneDrive'; +$string['importskydrivefiles'] = 'Import files from Microsoft SkyDrive repository'; $string['issuer'] = 'OAuth 2 service'; $string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the OneDrive API. If the services does not exist yet, you might need to create it.'; $string['servicenotenabled'] = 'Access not configured.'; @@ -33,9 +34,13 @@ $string['searchfor'] = 'Search for {$a}'; $string['internal'] = 'Internal (files stored in Moodle)'; $string['external'] = 'External (only links stored in Moodle)'; $string['both'] = 'Internal and External'; +$string['skydrivefilesexist'] = 'Files found in the Microsoft SkyDrive repository. This repository is deprecated by Microsoft - the files can be automatically imported to this Microsoft OneDrive repository.'; $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.'; $string['owner'] = 'Owned by: {$a}'; $string['cachedef_folder'] = 'OneDrive File IDs for folders in the system account'; +$string['confirmimportskydrive'] = 'Are you sure you want to import all files from the "Microsoft SkyDrive" repository to the "Microsoft OneDrive" repository? As long as the Microsoft OneDrive repository is already configured and working - all imported files will continue working as before. There is no way to undo these changes.'; +$string['skydrivefilesimported'] = 'All files were imported from the Microsoft SkyDrive repository.'; +$string['skydrivefilesnotimported'] = 'Some files could not be imported from the Microsoft SkyDrive repository.'; diff --git a/repository/onedrive/lib.php b/repository/onedrive/lib.php index 4d5e8d28242..5e79d9bca96 100644 --- a/repository/onedrive/lib.php +++ b/repository/onedrive/lib.php @@ -980,6 +980,77 @@ class repository_onedrive extends repository { } } + /** + * Return true if any instances of the skydrive repo exist - and we can import them. + * + * @return bool + */ + public static function can_import_skydrive_files() { + global $DB; + + $skydrive = $DB->get_record('repository', ['type' => 'skydrive'], 'id', IGNORE_MISSING); + $onedrive = $DB->get_record('repository', ['type' => 'onedrive'], 'id', IGNORE_MISSING); + + if (empty($skydrive) || empty($onedrive)) { + return false; + } + + $ready = true; + try { + $issuer = \core\oauth2\api::get_issuer(get_config('onedrive', 'issuerid')); + if (!$issuer->get('enabled')) { + $ready = false; + } + if (!$issuer->is_configured()) { + $ready = false; + } + } catch (dml_missing_record_exception $e) { + $ready = false; + } + if (!$ready) { + return false; + } + + $sql = "SELECT count('x') + FROM {repository_instances} i, {repository} r + WHERE r.type=:plugin AND r.id=i.typeid"; + $params = array('plugin' => 'skydrive'); + return $DB->count_records_sql($sql, $params) > 0; + } + + /** + * Import all the files that were created with the skydrive repo to this repo. + * + * @return bool + */ + public static function import_skydrive_files() { + global $DB; + + if (!self::can_import_skydrive_files()) { + return false; + } + // Should only be one of each. + $skydrivetype = repository::get_type_by_typename('skydrive'); + + $skydriveinstances = repository::get_instances(['type' => 'skydrive']); + $skydriveinstance = reset($skydriveinstances); + $onedriveinstances = repository::get_instances(['type' => 'onedrive']); + $onedriveinstance = reset($onedriveinstances); + + // Update all file references. + $DB->set_field('files_reference', 'repositoryid', $onedriveinstance->id, ['repositoryid' => $skydriveinstance->id]); + + // Delete and disable the skydrive repo. + $skydrivetype->delete(); + core_plugin_manager::reset_caches(); + + $sql = "SELECT count('x') + FROM {repository_instances} i, {repository} r + WHERE r.type=:plugin AND r.id=i.typeid"; + $params = array('plugin' => 'skydrive'); + return $DB->count_records_sql($sql, $params) == 0; + } + /** * Edit/Create Admin Settings Moodle form. * @@ -987,11 +1058,21 @@ class repository_onedrive extends repository { * @param string $classname repository class name. */ public static function type_config_form($mform, $classname = 'repository') { + global $OUTPUT; + $url = new moodle_url('/admin/tool/oauth2/issuers.php'); $url = $url->out(); $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_onedrive', $url)); + if (self::can_import_skydrive_files()) { + $notice = get_string('skydrivefilesexist', 'repository_onedrive'); + $url = new moodle_url('/repository/onedrive/importskydrive.php'); + $attrs = ['class' => 'btn btn-primary']; + $button = $OUTPUT->action_link($url, get_string('importskydrivefiles', 'repository_onedrive'), null, $attrs); + $mform->addElement('static', null, '', $OUTPUT->notification($notice) . $button); + } + parent::type_config_form($mform); $options = []; $issuers = \core\oauth2\api::get_all_issuers(); @@ -1019,6 +1100,7 @@ class repository_onedrive extends repository { FILE_CONTROLLED_LINK => get_string('external', 'repository_onedrive'), ]; $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_onedrive'), $choices); + } } From 7f15866006b3a7cb3423b7b604dd8a8a972c0706 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 31 Mar 2017 10:06:51 +0800 Subject: [PATCH 79/84] MDL-58220 oauth2: Use email as default username And give better error messages. --- auth/oauth2/classes/auth.php | 11 +++-- auth/oauth2/lang/en/auth_oauth2.php | 76 +++++++++++++++-------------- lib/classes/oauth2/api.php | 5 +- lib/classes/oauth2/client.php | 4 ++ 4 files changed, 50 insertions(+), 46 deletions(-) diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 0bdb7c90851..bd1fbf7c45b 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -365,12 +365,12 @@ class auth extends \auth_plugin_base { $userinfo = $client->get_userinfo(); if (!$userinfo) { - $errormsg = get_string('notloggedin', 'auth_oauth2'); + $errormsg = get_string('loginerror_nouserinfo', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } if (empty($userinfo['username']) || empty($userinfo['email'])) { - $errormsg = get_string('notloggedin', 'auth_oauth2'); + $errormsg = get_string('loginerror_userincomplete', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } @@ -414,7 +414,7 @@ class auth extends \auth_plugin_base { } $issuer = $client->get_issuer(); if (!$issuer->is_valid_login_domain($userinfo['email'])) { - $errormsg = get_string('notloggedin', 'auth_oauth2'); + $errormsg = get_string('notloggedindebug', 'auth_oauth2', get_string('loginerror_invaliddomain', 'auth_oauth2')); $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } @@ -448,7 +448,8 @@ class auth extends \auth_plugin_base { if (email_is_not_allowed($userinfo['email'])) { // The username exists but the emails don't match. Refuse to continue. - $errormsg = get_string('emailnotallowed', 'auth_oauth2'); + $reason = get_string('loginerror_invaliddomain', 'auth_oauth2'); + $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason); $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } @@ -477,7 +478,7 @@ class auth extends \auth_plugin_base { $this->update_picture($user); redirect($redirecturl); } - $errormsg = get_string('notloggedin', 'auth_oauth2'); + $errormsg = get_string('notloggedindebug', 'auth_oauth2', get_string('loginerror_authenticationfailed', 'auth_oauth2')); $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); } diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php index fb8f3a97be3..5e8fc620263 100644 --- a/auth/oauth2/lang/en/auth_oauth2.php +++ b/auth/oauth2/lang/en/auth_oauth2.php @@ -22,29 +22,27 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['accountexists'] = 'A user already exists on this site with this username. If this is your account, login manually and link this link from your preferences page.'; $string['auth_oauth2description'] = 'OAuth 2 standards based authentication'; $string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; -$string['notloggedin'] = 'The login attempt failed.'; -$string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; -$string['pluginname'] = 'OAuth 2'; -$string['emailconfirmlink'] = 'Link your accounts'; -$string['emailconfirmlinksent'] = '

    An existing account was found with this email address but it is not linked yet.

    -

    The accounts must be linked before you can login.

    -

    An email should have been sent to your address at {$a}

    -

    It contains easy instructions to link your accounts.

    -

    If you continue to have difficulty, contact the site administrator.

    '; -$string['oauth2:managelinkedlogins'] = 'Manage own linked login accounts'; -$string['linkedlogins'] = 'Linked logins'; -$string['accountexists'] = 'A user already exists on this site with this username. If this is your account, login manually and link this link from your preferences page.'; -$string['linkedloginshelp'] = 'Help with linked logins.'; -$string['notwhileloggedinas'] = 'Linked logins cannot be managed while logged in as another user.'; -$string['issuer'] = 'OAuth 2 Service'; -$string['info'] = 'External account'; -$string['createnewlinkedlogin'] = 'Link a new account ({$a})'; +$string['confirmaccountemail'] = 'Hi {$a->fullname}, + +A new account has been requested at \'{$a->sitename}\' +using your email address. + +To confirm your new account, please go to this web address: + +{$a->link} + +In most mail programs, this should appear as a blue link +which you can just click on. If that doesn\'t work, +then cut and paste the address into the address +line at the top of your web browser window. + +If you need help, please contact the site administrator, +{$a->admin}'; +$string['confirmaccountemailsubject'] = '{$a}: account confirmation'; $string['confirmationpending'] = 'This account is pending email confirmation.'; -$string['emailnotallowed'] = 'The email address is not permitted at this site.'; -$string['createaccountswarning'] = 'This authentication plugin allows users to create accounts on your site. You may want to enable the setting "authpreventaccountcreation" if you use this plugin.'; -$string['selfregistrationdisabled'] = 'No matching account could be found on this site, and this site does not allow self registration.'; $string['confirmlinkedloginemail'] = 'Hi {$a->fullname}, A request has been made to link the {$a->issuername} login @@ -63,20 +61,24 @@ line at the top of your web browser window. If you need help, please contact the site administrator, {$a->admin}'; $string['confirmlinkedloginemailsubject'] = '{$a}: linked login confirmation'; -$string['confirmaccountemail'] = 'Hi {$a->fullname}, - -A new account has been requested at \'{$a->sitename}\' -using your email address. - -To confirm your new account, please go to this web address: - -{$a->link} - -In most mail programs, this should appear as a blue link -which you can just click on. If that doesn\'t work, -then cut and paste the address into the address -line at the top of your web browser window. - -If you need help, please contact the site administrator, -{$a->admin}'; -$string['confirmaccountemailsubject'] = '{$a}: account confirmation'; +$string['createaccountswarning'] = 'This authentication plugin allows users to create accounts on your site. You may want to enable the setting "authpreventaccountcreation" if you use this plugin.'; +$string['createnewlinkedlogin'] = 'Link a new account ({$a})'; +$string['emailconfirmlink'] = 'Link your accounts'; +$string['emailconfirmlinksent'] = '

    An existing account was found with this email address but it is not linked yet.

    +

    The accounts must be linked before you can login.

    +

    An email should have been sent to your address at {$a}

    +

    It contains easy instructions to link your accounts.

    +

    If you continue to have difficulty, contact the site administrator.

    '; +$string['info'] = 'External account'; +$string['issuer'] = 'OAuth 2 Service'; +$string['linkedlogins'] = 'Linked logins'; +$string['linkedloginshelp'] = 'Help with linked logins.'; +$string['loginerror_userincomplete'] = 'The user information returned did not contain a username and email address. The OAuth 2 service may be configured incorrectly.'; +$string['loginerror_nouserinfo'] = 'No user information was returned. The OAuth 2 service may be configured incorrectly.'; +$string['loginerror_invaliddomain'] = 'The email address is not allowed at this site.'; +$string['loginerror_authenticationfailed'] = 'The authentication process failed.'; +$string['notloggedindebug'] = 'The login attempt failed. Reason: {$a}'; +$string['notwhileloggedinas'] = 'Linked logins cannot be managed while logged in as another user.'; +$string['oauth2:managelinkedlogins'] = 'Manage own linked login accounts'; +$string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; +$string['pluginname'] = 'OAuth 2'; diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index c8c4f6c8400..d7c0cad7d42 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -107,7 +107,6 @@ class api { 'name' => 'alternatename', 'last_name' => 'lastname', 'email' => 'email', - 'third_party_id' => 'username', 'first_name' => 'firstname', 'picture-data-url' => 'picture', 'link' => 'url', @@ -163,8 +162,7 @@ class api { $mapping = [ 'givenName' => 'firstname', 'surname' => 'lastname', - 'mail' => 'email', - 'userPrincipalName' => 'username', + 'userPrincipalName' => 'email', 'displayName' => 'alternatename', 'officeLocation' => 'address', 'mobilePhone' => 'phone1', @@ -425,7 +423,6 @@ class api { 'middle_name' => 'middlename', 'family_name' => 'lastname', 'email' => 'email', - 'sub' => 'username', 'website' => 'url', 'nickname' => 'alternatename', 'picture' => 'picture', diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index 1ef887a12c7..67b23f40605 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -269,6 +269,10 @@ class client extends \oauth2_client { } } + if (empty($user->username) && !empty($user->email)) { + $user->username = $user->email; + } + if (!empty($user->picture)) { $user->picture = download_file_content($user->picture, null, null, false, 10, 10, true, null, false); } else { From 512e681a3be34f1109202826bbce8b2a0d80a4f8 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 31 Mar 2017 10:31:53 +0800 Subject: [PATCH 80/84] MDL-58220 oauth2: Don't login as deleted users Also prevent sesskey error on first page of new logins. --- auth/oauth2/classes/api.php | 10 +++++++++- auth/oauth2/login.php | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/auth/oauth2/classes/api.php b/auth/oauth2/classes/api.php index 793fe895c66..952d3d36325 100644 --- a/auth/oauth2/classes/api.php +++ b/auth/oauth2/classes/api.php @@ -76,7 +76,15 @@ class api { 'issuerid' => $issuer->get('id'), 'username' => $username ]; - return linked_login::get_record($params); + $result = linked_login::get_record($params); + + if ($result) { + $user = \core_user::get_user($result->get('userid')); + if (!empty($user) && !$user->deleted) { + return $result; + } + } + return false; } /** diff --git a/auth/oauth2/login.php b/auth/oauth2/login.php index eba8b62b3d2..d1d8f79446e 100644 --- a/auth/oauth2/login.php +++ b/auth/oauth2/login.php @@ -25,7 +25,7 @@ require_once('../../config.php'); $issuerid = required_param('id', PARAM_INT); -$wantsurl = new moodle_url(optional_param('wantsurl', '/', PARAM_URL)); +$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_URL)); require_sesskey(); From 7b9f5b9986ea0922d099a76f933619c5e2a1c10d Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 31 Mar 2017 10:36:17 +0800 Subject: [PATCH 81/84] MDL-58220 oauth2: Cleanup on user delete Delete the linked logins for this user account when it is deleted. --- auth/oauth2/lib.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/auth/oauth2/lib.php b/auth/oauth2/lib.php index 422e4a088d4..2463db0be83 100644 --- a/auth/oauth2/lib.php +++ b/auth/oauth2/lib.php @@ -48,3 +48,12 @@ function auth_oauth2_extend_navigation_user_settings(navigation_node $useraccoun } } +/** + * Callback to remove linked logins for deleted users. + * + * @param stdClass $user + */ +function auth_oauth2_pre_user_delete($user) { + global $DB; + $DB->delete_records(auth_oauth2\linked_login::TABLE, ['userid' => $user->id]); +} From 14cfd280d35b00583d237094e40bacb623aedf4c Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 31 Mar 2017 10:48:15 +0800 Subject: [PATCH 82/84] MDL-58220 oauth2: Sanity check user pictures --- lib/classes/oauth2/client.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php index 67b23f40605..5f2b8c2870a 100644 --- a/lib/classes/oauth2/client.php +++ b/lib/classes/oauth2/client.php @@ -282,6 +282,18 @@ class client extends \oauth2_client { } } + if (!empty($user->picture)) { + // If it doesn't look like a picture lets unset it. + if (function_exists('imagecreatefromstring')) { + $img = @imagecreatefromstring($user->picture); + if (empty($img)) { + unset($user->picture); + } else { + imagedestroy($img); + } + } + } + return (array)$user; } } From 1d43165a5a5cc416f73bba3f9102e25650b712cc Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 31 Mar 2017 11:13:43 +0800 Subject: [PATCH 83/84] MDL-58220 oauth2: Provide template specific help links E.g. separate pages for Microsoft / Google. --- admin/tool/oauth2/classes/form/issuer.php | 14 ++++++++++---- admin/tool/oauth2/issuers.php | 13 +++++++++---- admin/tool/oauth2/lang/en/tool_oauth2.php | 3 ++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index 16db966f504..3a0b64d3cf5 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -50,9 +50,15 @@ class issuer extends persistent { global $PAGE, $OUTPUT; $mform = $this->_form; - $endpoint = $this->get_persistent(); + $issuer = $this->get_persistent(); - $mform->addElement('html', $OUTPUT->page_doc_link(get_string('issuersetup', 'tool_oauth2'))); + $docslink = optional_param('docslink', '', PARAM_ALPHAEXT); + if ($docslink) { + $name = s($issuer->get('name')); + $mform->addElement('html', $OUTPUT->doc_link($docslink, get_string('issuersetuptype', 'tool_oauth2', $name))); + } else { + $mform->addElement('html', $OUTPUT->page_doc_link(get_string('issuersetup', 'tool_oauth2'))); + } // Name. $mform->addElement('text', 'name', get_string('issuername', 'tool_oauth2')); @@ -119,10 +125,10 @@ class issuer extends persistent { $mform->addElement('hidden', 'action', 'edit'); $mform->setType('action', PARAM_ALPHA); - $mform->addElement('hidden', 'enabled', $endpoint->get('enabled')); + $mform->addElement('hidden', 'enabled', $issuer->get('enabled')); $mform->setType('enabled', PARAM_BOOL); - $mform->addElement('hidden', 'id', $endpoint->get('id')); + $mform->addElement('hidden', 'id', $issuer->get('id')); $mform->setType('id', PARAM_INT); $this->add_action_buttons(true, get_string('savechanges', 'tool_oauth2')); diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php index 1115ae21465..5bb4c4f9dc9 100644 --- a/admin/tool/oauth2/issuers.php +++ b/admin/tool/oauth2/issuers.php @@ -89,9 +89,10 @@ if ($mform && $mform->is_cancelled()) { } else if ($action == 'edittemplate') { $type = required_param('type', PARAM_ALPHA); + $docs = required_param('docslink', PARAM_ALPHAEXT); require_sesskey(); $issuer = core\oauth2\api::create_standard_issuer($type); - $params = ['action' => 'edit', 'id' => $issuer->get('id')]; + $params = ['action' => 'edit', 'id' => $issuer->get('id'), 'docslink' => $docs]; $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); redirect($editurl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } else if ($action == 'enable') { @@ -156,13 +157,17 @@ if ($mform && $mform->is_cancelled()) { $issuers = core\oauth2\api::get_all_issuers(); echo $renderer->issuers_table($issuers); - $params = ['action' => 'edittemplate', 'type' => 'google', 'sesskey' => sesskey()]; + $docs = 'admin/tool/oauth2/issuers/google'; + $params = ['action' => 'edittemplate', 'type' => 'google', 'sesskey' => sesskey(), 'docslink' => $docs]; $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); echo $renderer->single_button($addurl, get_string('createnewgoogleissuer', 'tool_oauth2')); - $params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey()]; + $docs = 'admin/tool/oauth2/issuers/microsoft'; + $params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey(), 'docslink' => $docs]; $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); echo $renderer->single_button($addurl, get_string('createnewmicrosoftissuer', 'tool_oauth2')); - $params = ['action' => 'edittemplate', 'type' => 'facebook', 'sesskey' => sesskey()]; + $docs = 'admin/tool/oauth2/issuers/facebook'; + $params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey(), 'docslink' => $docs]; + $params = ['action' => 'edittemplate', 'type' => 'facebook', 'sesskey' => sesskey(), 'docslink' => $docs]; $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); echo $renderer->single_button($addurl, get_string('createnewfacebookissuer', 'tool_oauth2')); $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']); diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index 558eafa943a..d2c9a2a8736 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -54,6 +54,7 @@ $string['endpointsforissuer'] = 'Endpoints for issuer: {$a}'; $string['endpointurl_help'] = 'URL for this endpoint. Must use https:// protocol.'; $string['endpointurl'] = 'Url'; $string['issuersetup'] = 'Detailed instructions on configuring the common OAuth 2 Services'; +$string['issuersetuptype'] = 'Detailed instructions on setting up the {$a} OAuth 2 provider'; $string['issueralloweddomains_help'] = 'If set, this setting is a comma separated list of domains that logins will be restricted to when using this provider.'; $string['issueralloweddomains_link'] = 'OAuth_2_login_domains'; $string['issueralloweddomains'] = 'Login domains'; @@ -87,7 +88,7 @@ $string['notdiscovered'] = 'Service discovery not successful'; $string['notloginissuer'] = 'Do not allow login'; $string['pluginname'] = 'OAuth 2 Services'; $string['savechanges'] = 'Save changes'; -$string['serviceshelp'] = 'Service provider setup instructions: (Google, Facebook, Microsoft).'; +$string['serviceshelp'] = 'Service provider setup instructions.'; $string['systemaccountconnected_help'] = 'System accounts are used to provide advanced functionality for plugins. They are not required for login functionality only, but other plugins using the OAuth service may offer a reduced set of features if the system account has not been connected. For example repositories cannot support "controlled links" without a system account to perform file operations.'; $string['systemaccountconnected'] = 'System account connected'; $string['systemaccountnotconnected'] = 'System account not connected'; From aa89bf2e6c0ca31101bc2864043cb722a610b8b4 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 31 Mar 2017 12:50:11 +0800 Subject: [PATCH 84/84] MDL-58220 repo: Dont expect file info for any file Some plugins do not implement the file info callback so expect and handle null in this case. --- repository/googledocs/lib.php | 2 +- repository/onedrive/lib.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 3520254d96b..1ce0e1bdc15 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -588,7 +588,7 @@ class repository_googledocs extends repository { $storedfile->get_filepath(), $storedfile->get_filename()); - if (empty($options['offline']) && $info->is_writable()) { + if (empty($options['offline']) && !empty($info) && $info->is_writable()) { // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer); diff --git a/repository/onedrive/lib.php b/repository/onedrive/lib.php index 5e79d9bca96..73118a34230 100644 --- a/repository/onedrive/lib.php +++ b/repository/onedrive/lib.php @@ -537,7 +537,7 @@ class repository_onedrive extends repository { $storedfile->get_filepath(), $storedfile->get_filename()); - if (empty($options['offline']) && $info->is_writable()) { + if (empty($options['offline']) && !empty($info) && $info->is_writable()) { // Add the current user as an OAuth writer. $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);