Skip to content
Merged
92 changes: 65 additions & 27 deletions admin/contactmergefailure.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,49 +27,87 @@
require_once($CFG->libdir . '/tablelib.php');

$id = required_param('id', PARAM_INT);
admin_externalpage_setup('enrolsettingsarlocontactmergefailure',
null, ['id' => $id], '/enrol/arlo/admin/contactmergefailure.php');
$action = optional_param('action', '', PARAM_ALPHA);
admin_externalpage_setup(
'enrolsettingsarlocontactmergefailure',
null,
['id' => $id],
'/enrol/arlo/admin/contactmergefailure.php'
);
$registration = new \enrol_arlo\local\persistent\registration_persistent($id);
$contact = $registration->get_contact();
$event = $registration->get_event();
$onlineactivity = $registration->get_online_activity();
$code = ($event) ? $event->get('code') : $onlineactivity->get('code');
$returnurl = new moodle_url('/enrol/arlo/admin/unsuccessfulenrolments.php');
$output = $PAGE->get_renderer('enrol_arlo');
// Check for failed contact merge requests first.
$contactmergerequests = \enrol_arlo\local\persistent\contact_merge_request_persistent::get_records(
['destinationcontactid' => $contact->get('sourceid'), 'mergefailed' => 1]
);
if (!$contactmergerequests) {
redirect($returnurl);
}
// Just deal with first.
$contactmergerequest = reset($contactmergerequests);
$sourcecontact = $contactmergerequest->get_source_contact();
$destinationcontact = $contactmergerequest->get_destination_contact();
// When both contacts resolve to the same Moodle user there is nothing to merge,
// so the merge request can be safely marked as complete.
$sameuser = $sourcecontact && $destinationcontact
&& $sourcecontact->get('userid') > 0
&& $sourcecontact->get('userid') == $destinationcontact->get('userid');
if ($action === 'markcomplete' && confirm_sesskey()) {
if (!$sameuser || !enrol_is_enabled('arlo')) {
throw new moodle_exception('invalidrecord');
}
// Mark the merge request as applied.
$contactmergerequest->set('active', 0);
$contactmergerequest->set('mergefailed', 0);
$contactmergerequest->update();
// Re-attempt the enrolment now that the merge request is resolved.
$plugin = \enrol_arlo\api::get_enrolment_plugin();
$enrolmentinstance = $plugin::get_instance_record($registration->get('enrolid'), MUST_EXIST);
$result = \enrol_arlo\local\job\memberships_job::process_enrolment_registration(
$enrolmentinstance,
$registration
);
$message = ($result) ? get_string('success') : get_string('failed', 'enrol_arlo');
redirect($returnurl, $message, 1);
}
echo $OUTPUT->header();
$params = [
'fullname' => $contact->get('firstname') . ' ' . $contact->get('lastname'),
'code' => $code
'code' => $code,
];
$heading = get_string('unsuccessfulenrolmentof', 'enrol_arlo', $params);
echo $OUTPUT->heading(format_string($heading), 3);
// Check for failed contact merge requests first.
$contactmergerequests = \enrol_arlo\local\persistent\contact_merge_request_persistent::get_records(
['destinationcontactid' => $contact->get('sourceid'), 'mergefailed' => 1]
);
if ($contactmergerequests) {
// Just deal with first.
$contactmergerequest = reset($contactmergerequests);
$sourcecontact = new \enrol_arlo\output\contact($contactmergerequest->get_source_contact(), 'source');
$destinationcontact = new \enrol_arlo\output\contact($contactmergerequest->get_destination_contact(), 'destination');
echo $OUTPUT->heading(get_string('contactmergerequestfailure', 'enrol_arlo'), 3);
echo html_writer::start_div('container');
echo html_writer::start_div('row');
echo html_writer::start_div('col-sm-6');
echo $output->render($sourcecontact);
echo html_writer::end_div();
echo html_writer::start_div('col-sm-6');
echo $output->render($destinationcontact);
echo html_writer::end_div();
echo html_writer::end_div();
echo html_writer::end_div();
} else {
redirect($returnurl);
$sourcecontactoutput = new \enrol_arlo\output\contact($sourcecontact, 'source');
$destinationcontactoutput = new \enrol_arlo\output\contact($destinationcontact, 'destination');
echo $OUTPUT->heading(get_string('contactmergerequestfailure', 'enrol_arlo'), 3);
echo html_writer::start_div('container');
echo html_writer::start_div('row');
echo html_writer::start_div('col-sm-6');
echo $output->render($sourcecontactoutput);
echo html_writer::end_div();
echo html_writer::start_div('col-sm-6');
echo $output->render($destinationcontactoutput);
echo html_writer::end_div();
echo html_writer::end_div();
echo html_writer::end_div();
if ($sameuser) {
echo $OUTPUT->notification(get_string('contactmergerequestsameuser', 'enrol_arlo'), 'info');
$markcompleteurl = new moodle_url($PAGE->url, ['action' => 'markcomplete', 'sesskey' => sesskey()]);
$markcompletebutton = new single_button($markcompleteurl, get_string('markmergecomplete', 'enrol_arlo'), 'post');
$markcompletebutton->add_confirm_action(get_string('markmergecompleteconfirm', 'enrol_arlo'));
echo $OUTPUT->render($markcompletebutton);
}
echo html_writer::start_div('row float-right');
echo html_writer::start_tag('h4');
echo $OUTPUT->action_link($returnurl,
get_string('returntounsucessfulenrolments', 'enrol_arlo'));
echo $OUTPUT->action_link(
$returnurl,
get_string('returntounsucessfulenrolments', 'enrol_arlo')
);
echo html_writer::end_tag('h4');
echo html_writer::end_div();
echo $OUTPUT->footer();
4 changes: 4 additions & 0 deletions classes/Arlo/AuthAPI/XmlDeserializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class XmlDeserializer {
* @var bool
*/
private $ignoreMissingClasses = true;
/**
* @var int
*/
private $loadOptions;

/**
* XmlDeserializer constructor.
Expand Down
16 changes: 16 additions & 0 deletions classes/local/client.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ class client {
/** @var $httpclient \GuzzleHttp\Client */
protected $httpclient;

/** @var mixed Guzzle handler override used only under PHPUnit. */
protected static $testhandler = null;

/**
* Inject a Guzzle handler (e.g. a MockHandler stack) for unit testing. Has no effect
* outside PHPUnit. Pass null to reset.
*
* @param mixed $handler
*/
public static function set_test_handler($handler) {
static::$testhandler = $handler;
}

/**
* Construct a guzzle client setup with basic authentication and appropriate
* options and headers set.
Expand All @@ -62,6 +75,9 @@ public static function get_instance($headers = []) {
]
];
$config['headers'] = array_merge($config['headers'], $headers);
if (defined('PHPUNIT_TEST') && PHPUNIT_TEST && !is_null(static::$testhandler)) {
$config['handler'] = static::$testhandler;
}
$client = new static();
$client->httpclient = new \GuzzleHttp\Client($config);
return $client;
Expand Down
32 changes: 29 additions & 3 deletions classes/local/job/contacts_job.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,34 @@ public function run() {
$uri->setPagingTop(250);
$uri->setResourcePath($jobpersistent->get('endpoint'));
$uri->addExpand('Registration/Contact');
$filter = "Contact/LastModifiedDateTime gt datetime('". $jobpersistent->get('lastsourcetimemodified') ."')";
$lastsourcetimemodified = $jobpersistent->get('lastsourcetimemodified');
// Page using a (LastModifiedDateTime, ContactID) keyset cursor. The ContactID
// tiebreaker matches the other sync jobs and stops the cursor stalling when more
// than one page of contacts share the same LastModifiedDateTime.
$filter = "(Contact/LastModifiedDateTime gt datetime('" . $lastsourcetimemodified . "'))";
if ($jobpersistent->get('lastsourceid')) {
$filter .= " OR (Contact/LastModifiedDateTime eq datetime('" . $lastsourcetimemodified . "')";
$filter .= " AND Contact/ContactID gt " . $jobpersistent->get('lastsourceid') . ")";
}
$uri->setFilterBy($filter);
$uri->setOrderBy('Contact/LastModifiedDateTime ASC');
$uri->setOrderBy('Contact/LastModifiedDateTime ASC,Contact/ContactID ASC');
$request = new Request('GET', $uri->output(true));
$response = client::get_instance()->send_request($request);
$collection = response_processor::process($response);
if ($collection->count() > 0) {
$cursortimebefore = $jobpersistent->get('lastsourcetimemodified');
$cursoridbefore = $jobpersistent->get('lastsourceid');
foreach ($collection as $resource) {
try {
// No need to process cancelled registrations.
// No need to process cancelled registrations, but still advance the
// paging cursor so a page made up entirely of cancelled registrations
// does not stall pagination and re-request the same page forever.
if ($resource->Status == RegistrationStatus::CANCELLED) {
$cancelledcontact = $resource->getContact();
if (!empty($cancelledcontact) && !empty($cancelledcontact->LastModifiedDateTime)) {
$jobpersistent->set('lastsourceid', $cancelledcontact->ContactID);
$jobpersistent->set('lastsourcetimemodified', $cancelledcontact->LastModifiedDateTime);
}
continue;
}
$contactresource = $resource->getContact();
Expand Down Expand Up @@ -218,6 +235,15 @@ public function run() {
}
// See if need to get another page of records.
$hasnext = (bool) $collection->hasNext();
// Safety net: if Arlo reports more pages but our paging cursor did not move
// while processing this page, stop instead of re-requesting the identical page
// forever (prevents runaway polling of the Arlo API).
if ($hasnext
&& $jobpersistent->get('lastsourcetimemodified') === $cursortimebefore
&& $jobpersistent->get('lastsourceid') == $cursoridbefore) {
$this->add_error(get_string('pagingnoprogress', 'enrol_arlo'));
$hasnext = false;
}
}
}
return true;
Expand Down
6 changes: 6 additions & 0 deletions lang/en/enrol_arlo.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
$string['apipassword_desc'] = 'Forgot your password? <a title="{$a->title}" href="{$a->url}" target="_blank">Learn how to reset it.</a>';
$string['arloauthconfig'] = 'Arlo authentication method';
$string['arloauthconfig_desc'] = 'Select the authentication method to use for Arlo enrolments.';
$string['enable_arlo_auth_config'] = 'Enable Arlo authentication configuration';
$string['enable_arlo_auth_config_desc'] = 'Show options for selecting the authentication method used for Arlo enrolments and its password handling.';
$string['arloconnection'] = 'Arlo connection';
$string['arlosettings'] = 'Arlo settings';
$string['arlocontact'] = 'Arlo contact';
Expand All @@ -81,7 +83,10 @@
$string['codeprimary'] = 'Code primary';
$string['contactmergerequestfailure'] = 'Failure to apply contact merge request';
$string['contactmergerequestfailures'] = 'Contact merge request failures';
$string['contactmergerequestsameuser'] = 'Both contacts are linked to the same Moodle user account, probably due to a user merge inside Moodle or a change of email address. There is nothing to merge, so it is safe to mark this merge as complete.';
$string['contactmergefailurereport'] = 'Contact merge report';
$string['markmergecomplete'] = 'Mark merge as complete';
$string['markmergecompleteconfirm'] = 'Both contacts are linked to the same Moodle user, so there is nothing to merge. Mark this merge request as complete?';
$string['communications'] = 'Communications';
$string['completed'] = 'Completed';
$string['courseid'] = 'Course ID';
Expand Down Expand Up @@ -331,6 +336,7 @@
$string['contactresourcemissing'] = 'Contact resource missing from Registration.';
$string['contactrecordmissing'] = 'Contact record missing.';
$string['noassociateduser'] = 'No associated Moodle user account.';
$string['pagingnoprogress'] = 'Paging halted: the sync cursor did not advance while more pages were reported. Stopped to avoid re-requesting the same page.';
$string['unsuccessfulenrolment'] = 'Unsuccessful enrolment';
$string['unsuccessfulenrolments'] = 'Unsuccessful enrolments';
$string['unsuccessfulenrolmentscount'] = 'Unsuccessful enrolments: {$a}';
Expand Down
30 changes: 16 additions & 14 deletions lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ public function edit_instance_form($instance, MoodleQuickForm $mform, $context)
$eventoptions = [
$persistent->get('sourceguid') => $persistent->get('code')
];
$mform->addElement('select', 'arloevent', get_string('event', 'enrol_arlo'),
$mform->addElement('autocomplete', 'arloevent', get_string('event', 'enrol_arlo'),
$eventoptions);
$mform->setConstant('arloevent', $instance->customchar3);
$mform->hardFreeze('arloevent', $instance->customchar3);
Expand All @@ -723,7 +723,7 @@ public function edit_instance_form($instance, MoodleQuickForm $mform, $context)
$eventoptions = [
$persistent->get('sourceguid') => $persistent->get('code')
];
$mform->addElement('select', 'arloonlineactivity', get_string('onlineactivity',
$mform->addElement('autocomplete', 'arloonlineactivity', get_string('onlineactivity',
'enrol_arlo'), $eventoptions);
$mform->setConstant('arloonlineactivity', $instance->customchar3);
$mform->hardFreeze('arloonlineactivity', $instance->customchar3);
Expand All @@ -739,19 +739,21 @@ public function edit_instance_form($instance, MoodleQuickForm $mform, $context)
redirect($redirect, get_string('noeventsoractivitiesfound', 'enrol_arlo'), 1);
}
// Type options.
array_unshift($typeoptions, get_string('choose') . '...');
$typeoptions = ['' => get_string('choose') . '...'] + $typeoptions;
$mform->addElement('select', 'arlotype', get_string('type', 'enrol_arlo'), $typeoptions);
// Event selector.
array_unshift($eventoptions, get_string('choose') . '...');
$mform->addElement('select', 'arloevent', get_string('event', 'enrol_arlo'), $eventoptions);
$mform->disabledIf('arloevent', 'arlotype', 'eq', arlo_type::ONLINEACTIVITY);
$mform->disabledIf('arloevent', 'arlotype', 'eq', 0);
// Online Activity selector.
array_unshift($onlineactivityoptions, get_string('choose') . '...');
$mform->addElement('select', 'arloonlineactivity',
get_string('onlineactivity', 'enrol_arlo'), $onlineactivityoptions);
$mform->disabledIf('arloonlineactivity', 'arlotype', 'eq', arlo_type::EVENT);
$mform->disabledIf('arloonlineactivity', 'arlotype', 'eq', 0);
// Event selector. Shown only when the Event type is selected.
$eventoptions = ['' => ''] + $eventoptions;
$mform->addElement('autocomplete', 'arloevent', get_string('event', 'enrol_arlo'), $eventoptions);
$mform->hideIf('arloevent', 'arlotype', 'neq', arlo_type::EVENT);
// Online Activity selector. Shown only when the Online Activity type is selected.
$onlineactivityoptions = ['' => ''] + $onlineactivityoptions;
$mform->addElement(
'autocomplete',
'arloonlineactivity',
get_string('onlineactivity', 'enrol_arlo'),
$onlineactivityoptions
);
$mform->hideIf('arloonlineactivity', 'arlotype', 'neq', arlo_type::ONLINEACTIVITY);
}
// Settings that are editable be instance new or existing.
$options = $this->get_status_options();
Expand Down
6 changes: 5 additions & 1 deletion settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
get_string('webhookstatus', 'enrol_arlo'),
new moodle_url('/enrol/arlo/admin/webhook_status.php')));

$name = get_string('enable_arlo_auth_config', 'enrol_arlo');
$description = get_string('enable_arlo_auth_config_desc', 'enrol_arlo');
$settings->add(new admin_setting_configcheckbox('enable_arlo_auth_config', $name, $description, 0));

if (!empty($CFG->enable_arlo_auth_config)) {
$name = get_string('arloauthconfig', 'enrol_arlo');
$description = get_string('arloauthconfig_desc', 'enrol_arlo');
Expand All @@ -190,7 +194,7 @@
// Some auth methods doesn't use a password inside Moodle, so it may be better to disable the force password change feature.
$name = get_string('disableforcepasswordchange', 'enrol_arlo');
$description = get_string('disableforcepasswordchange_desc', 'enrol_arlo');
$settings->add(new admin_setting_configcheckbox('enrol_arlo/disableforcepasswordchange', $name, $description, 0));
$settings->add(new admin_setting_configcheckbox('enrol_arlo/disableforcepasswordchange', $name, $description, 1));
}
$settings = null;
}
Loading
Loading