From 632295d782db897eb5f1faf799c0616e76624684 Mon Sep 17 00:00:00 2001 From: Marc-Alexandre Ghaly Date: Mon, 27 Jun 2022 17:18:19 -0400 Subject: [PATCH] Issue #167: curl step --- classes/local/step/connector_curl.php | 162 +++++++++++++++++++++++++- lang/en/tool_dataflows.php | 12 ++ version.php | 4 +- 3 files changed, 174 insertions(+), 4 deletions(-) diff --git a/classes/local/step/connector_curl.php b/classes/local/step/connector_curl.php index bc897f81..840386be 100644 --- a/classes/local/step/connector_curl.php +++ b/classes/local/step/connector_curl.php @@ -27,18 +27,176 @@ */ class connector_curl extends connector_step { + /** @var int[] number of input flows (min, max). */ + protected $inputflows = [0, 0]; + + /** @var int[] number of output connectors (min, max). */ + protected $inputconnectors = [0, 1]; + /** @var bool whether or not this step type (potentially) contains a side effect or not */ protected $hassideeffect = true; + /** @var int Time after which curl request is aborted */ + protected $timeout = 60; + + /** + * Return the definition of the fields available in this form. + * + * @return array + */ + protected static function form_define_fields(): array { + return [ + 'curl' => ['type' => PARAM_URL], + 'destination' => ['type' => PARAM_PATH], + 'headers' => ['type' => PARAM_RAW], + 'method' => ['type' => PARAM_TEXT], + 'rawpostdata' => ['type' => PARAM_RAW], + 'sideeffects' => ['type' => PARAM_RAW], + 'timeout' => ['type' => PARAM_INT], + ]; + } + + /** + * Allows each step type to determine a list of optional/required form + * inputs for their configuration + * + * It's recommended you prefix the additional config related fields to avoid + * conflicts with any existing fields. + * + * @param \MoodleQuickForm &$mform + */ + public function form_add_custom_inputs(\MoodleQuickForm &$mform) { + $mform->addElement('text', 'config_curl', get_string('connector_curl:curl', 'tool_dataflows'), + ['cols' => 50, 'rows' => 7]); + $mform->addElement('text', 'config_destination', get_string('connector_curl:destination', 'tool_dataflows')); + $mform->addHelpButton('config_destination', 'connector_curl:destination', 'tool_dataflows'); + $mform->addElement('textarea', 'config_headers', get_string('connector_curl:headers', 'tool_dataflows'), + ['cols' => 50, 'rows' => 7]); + $mform->addElement('select', 'config_method', get_string('connector_curl:method', 'tool_dataflows'), + ['get' => 'GET', 'post' => 'POST', 'put' => 'PUT']); + $mform->addElement('textarea' , 'config_rawpostdata', get_string('connector_curl:rawpostdata', 'tool_dataflows'), + ['cols' => 50, 'rows' => 7]); + $mform->addElement('checkbox', 'config_sideeffects', get_string('connector_curl:sideeffects', 'tool_dataflows'), + get_string('yes')); + $mform->hideIf('config_rawpostdata', 'config_method', 'eq', 'get'); + $mform->disabledIf('config_rawpostdata', 'config_method', 'eq', 'get'); + $mform->addElement('text', 'config_timeout', get_string('connector_curl:timeout', 'tool_dataflows')); + $mform->addHelpButton('config_timeout', 'connector_curl:timeout', 'tool_dataflows'); + } + + /** + * Validate the configuration settings. + * + * @param object $config + * @return true|\lang_string[] true if valid, an array of errors otherwise + */ + public function validate_config($config) { + $errors = []; + if (empty($config->curl)) { + $errors['config_curl'] = get_string('config_field_missing', 'tool_dataflows', 'curl', true); + } + if (empty($config->rawpostdata) && ($config->method === 'put' || $config->method === 'post')) { + $errors['config_rawpostdata'] = get_string('config_field_missing', 'tool_dataflows', 'rawpostdata', true); + } + return empty($errors) ? true : $errors; + } + /** * Executes the step * - * TODO: This will perform a curl call + * Performs a curl call according to given parameters. * * @return bool Returns true if successful, false otherwise. */ public function execute(): bool { - // TODO: Implement. + global $CFG; + // Get variables. + $config = $this->enginestep->stepdef->config; + $isdryrun = $this->enginestep->engine->isdryrun; + $method = $config->method; + $postdata = null; + $result = null; + + if (!empty($config->timeout)) { + $this->timeout = (int) $config->timeout; + } + + $options = ['CURLOPT_TIMEOUT' => $this->timeout]; + + if ($method == 'post') { + $options['CURLOPT_POST'] = 1; + } + + if ($method == 'put') { + $options['CURLOPT_PUT'] = 1; + } + + $curl = new \curl(); + + if (!empty($config->sideeffects)) { + $this->hassideeffect = true; + } else { + $this->hassideeffect = false; + } + + // Provided a header is specified add header to request. + if (!empty($config->headers)) { + $headers = $config->headers; + if (!is_array($headers)) { + $headers = json_decode($headers, true); + } + $curl->setHeader($headers); + } + + if (!empty($config->rawpostdata)) { + $postdata = json_decode($config->rawpostdata, true); + } + + // Perform call or download/call. + if (!empty($config->destination) && !$isdryrun) { + // Download the response. + if ($config->destination[0] === '/') { + $config->destination = substr($config->destination, 1); + } + if (isset($postdata)) { + $options['CURLOPT_POSTFIELDS'] = $postdata; + } + $file = fopen($CFG->dirroot . '/' . $config->destination, 'w'); + $result = $curl->$method($config->curl, $postdata, $options); + fwrite($file, $result); + fclose($file); + } else { + if (!$this->hassideeffect && !$isdryrun) { + // Simple call without download. + if ($config->method != 'get') { + $result = $curl->$method($config->curl, $postdata, $options); + $this->hassideeffect = true; + } else { + $result = $curl->get($config->curl, [], $options); + } + } + } + + $info = $curl->get_info(); + // Stores response to be reusable by other steps. + $response = $curl->getResponse(); + $httpcode = $info['http_code'] ?? null; + $connecttime = $info['connect_time'] ?? null; + $totaltime = $info['total_time'] ?? null; + $sizeupload = $info['size_upload'] ?? null; + $errno = $curl->get_errno(); + + if (($httpcode >= 400 || empty($response) || $errno == 28) && !$isdryrun) { + $codestatustext = null; + if (is_string($result)) { + $result = json_decode($result, true)['error'] ?? $result; + $codestatustext = !is_array($result) ? $httpcode . '/' . $result : $httpcode . '/' . reset($result); + } + throw new \moodle_exception($codestatustext); + } + + $this->enginestep->stepdef->set_var('result', $result); + return true; } } diff --git a/lang/en/tool_dataflows.php b/lang/en/tool_dataflows.php index 94a54730..88c12bae 100644 --- a/lang/en/tool_dataflows.php +++ b/lang/en/tool_dataflows.php @@ -232,3 +232,15 @@ $string['trigger_cron:crontab_desc'] = 'The schedule is edited as five values: minute, hour, day, month and day of month, in that order. The values are in crontab format.'; $string['trigger_cron:strftime_datetime'] = '%d %b %Y, %H:%M'; $string['trigger_cron:next_run_time'] = 'Next run time: {$a}'; + +// Connector cURL. +$string['connector_curl:curl'] = 'Client URL'; +$string['connector_curl:destination'] = 'File / Response destination'; +$string['connector_curl:destination_help'] = 'Should this field be left blank, +response will be stored in step definition to be reused subsequently by other steps.'; +$string['connector_curl:headers'] = 'Headers'; +$string['connector_curl:method'] = 'HTTP request method'; +$string['connector_curl:rawpostdata'] = 'Raw post data'; +$string['connector_curl:sideeffects'] = 'Does this request have side effects?'; +$string['connector_curl:timeout'] = 'Timeout'; +$string['connector_curl:timeout_help'] = 'Time after which curl request will abort, default is 60 seconds.'; diff --git a/version.php b/version.php index 9ab03504..7a854a1e 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022062900; -$plugin->release = 2022062900; +$plugin->version = 2022063000; +$plugin->release = 2022063000; $plugin->requires = 2017051500; // Our lowest supported Moodle (3.3.0).