<?php
/**
 *  Web services utility functions and classes
 *
 * @package    mahara
 * @subpackage auth-webservice
 * @author     Catalyst IT Limited <mahara@catalyst.net.nz>
 * @license    https://www.gnu.org/licenses/gpl-3.0.html GNU GPL version 3 or later
 * @copyright  For copyright information on Mahara, please see the README file distributed with this software.
 *
 */

defined('INTERNAL') || die();

require_once(get_config('docroot') . 'webservice/locallib.php');
require_once(get_config('docroot') . 'artefact/lib.php');
require_once(get_config('docroot') . 'webservice/libs/net.php');
require_once(get_config('docroot') . 'api/xmlrpc/lib.php');

/**
 * The directory within a component that contains the web service files
 */
define('WEBSERVICE_DIRECTORY', 'webservice');

/**
 * Security token used for allowing access
 * from external application such as web services.
 * Scripts do not use any session, performance is relatively
 * low because we need to load access info in each request.
 * Scripts are executed in parallel.
 */
define('EXTERNAL_TOKEN_PERMANENT', 0);

/**
 * Security token used for allowing access
 * of embedded applications, the code is executed in the
 * active user session. Token is invalidated after user logs out.
 * Scripts are executed serially - normal session locking is used.
 */
define('EXTERNAL_TOKEN_EMBEDDED', 1);

/**
 * OAuth Token type for registered applications oauth v1
 */
define('EXTERNAL_TOKEN_OAUTH1', 2);

/**
 * Security token self-generated by a normal user
 */
define('EXTERNAL_TOKEN_USER', 3);

/**
 * Personal User Tokens expiry time (12 weeks)
 */
define('EXTERNAL_TOKEN_USER_EXPIRES', (12 * 7 * 24 * 60 * 60));

/**
 * Username/password authentication (also called simple authentication)
 */
define('WEBSERVICE_AUTHMETHOD_USERNAME', 0);

/**
 * The most common token authentication (external app, mobile app...)
 */
define('WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN', 1);

/**
 * The token for embedded application (requires Moodle session)
 */
define('WEBSERVICE_AUTHMETHOD_SESSION_TOKEN', 2);

define('WEBSERVICE_AUTHMETHOD_OAUTH_TOKEN', 3);
define('WEBSERVICE_AUTHMETHOD_USER_TOKEN', 4);

// strictness check
define('IGNORE_MISSING', 1);
define('MUST_EXIST', 2);

/** Get remote addr constant */
define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');

/** Get remote addr constant */
define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');

/** Webservice language file */
define('WEBSERVICE_LANG', 'webservices');
/**
 * What debugging state is Web Services in
 *
 * @return boolean true on yes
 */
function ws_debugging() {
    // must be set in config.php
    if (get_config('enablewsdebugging')) {
        return true;
    }
    else {
        return false;
    }
}

/**
 * Check that a user is in the institution
 *
 * @param object $user The user being checked
 * @param string $institution The institution to check
 * @return boolean true if user is found in the institution
 */
function mahara_external_in_institution($user, $institution) {
    $institutions = array_keys(load_user_institutions($user->id));
    $auth_instance = get_record('auth_instance', 'id', $user->authinstance);
    $institutions[]= $auth_instance->institution;
    if (!in_array($institution, $institutions)) {
        return false;
    }
    return true;
}

/**
 * parameter definition for output of any Atom generator
 *
 * Returns description of method result value
 * @return external_description
 */
function mahara_external_atom_returns() {
    return new external_single_structure(
    array(
            'id'      => new external_value(PARAM_RAW, 'Atom document Id'),
            'title'   => new external_value(PARAM_RAW, 'Atom document Title'),
            'link'    => new external_value(PARAM_RAW, 'Atom document Link'),
            'email'   => new external_value(PARAM_RAW, 'Atom document Author Email'),
            'name'    => new external_value(PARAM_RAW, 'Atom document Author Name'),
            'updated' => new external_value(PARAM_RAW, 'Atom document Updated date'),
            'uri'     => new external_value(PARAM_RAW, 'Atom document URI'),
            'entries' => new external_multiple_structure(
                                new external_single_structure(
                                            array(
                                                    'id'        => new external_value(PARAM_RAW, 'Atom entry Id'),
                                                    'link'      => new external_value(PARAM_RAW, 'Atom entry Link'),
                                                    'email'     => new external_value(PARAM_RAW, 'Atom entry Author Link'),
                                                    'name'      => new external_value(PARAM_RAW, 'Atom entry Author Name'),
                                                    'updated'   => new external_value(PARAM_RAW, 'Atom entry updated date'),
                                                    'published' => new external_value(PARAM_RAW, 'Atom entry published date'),
                                                    'title'     => new external_value(PARAM_RAW, 'Atom entry Title'),
                                                    'summary'   => new external_value(PARAM_RAW, 'Atom entry Summary', VALUE_OPTIONAL),
                                                    'content'   => new external_value(PARAM_RAW, 'Atom entry Content', VALUE_OPTIONAL),
                                                    ), 'Atom entry', VALUE_OPTIONAL)
                    , 'Entries', VALUE_OPTIONAL),
                )
    );
}

/**
 * validate the user for webservices access
 * the account must use the webservice auth plugin
 * the account must have membership for the selected auth_instance
 *
 * @param object $dbuser
 * @return object|null $auth_instance or null if $dbuser is empty
 */
function webservice_validate_user($dbuser) {
    global $SESSION;
    if ($auth_instance = get_record_sql("SELECT * FROM {auth_instance}
                                            WHERE authname = 'webservice'
                                            AND active = 1
                                            AND institution = (
                                            SELECT institution FROM {auth_instance}
                                            WHERE id = ?
                                            AND active = 1
                                            )", array($dbuser->authinstance))) {
        // User belongs to an institution that contains the 'webservice' auth method
        $memberships = count_records('usr_institution', 'usr', $dbuser->id);
        if ($memberships == 0) {
            // auth instance should be a mahara one
            if ($auth_instance->institution == 'mahara') {
                return $auth_instance;
            }
        }
        else {
            $membership = get_record('usr_institution', 'usr', $dbuser->id, 'institution', $auth_instance->institution);
            if (!empty($membership)) {
                return $auth_instance;
            }
        }
    }
    return NULL;
}

/**
 * List all potential webservice locations
 * (i.e. plugins, local, and the "pseudo-module" /webservice).
 *
 * @return array of web service plugin directories
 */
function get_ws_subsystems() {
    static $plugindirs = false;

    if (!$plugindirs) {
        $plugindirs = [
            'webservice',
            'local'
        ];
        $activeplugins = plugin_all_installed();
        foreach ($activeplugins as $plugindata) {
            $plugindirs[] = "{$plugindata->plugintype}/{$plugindata->name}";
        }
    }

    return $plugindirs;
}

/**
 *  Generate a web services token
 * @param string $tokentype
 * @param integer $serviceorid
 * @param integer $userid
 * @param string $institution
 * @param integer $validuntil
 * @param string $iprestriction
 * @param string $clientname (Optional) Human-readable name of client program using this token
 * @param string $clientenv (Optional) Human-readable description of device/environment for client
 * @param string $clientguid (Optional) Unique identifier for the client program
 * @throws WebserviceException
 * @return string token
 */
function webservice_generate_token($tokentype, $serviceorid, $userid, $institution = 'mahara',  $validuntil = 0, $iprestriction = null, $clientname = null, $clientenv = null, $clientguid = null) {
    global $USER;
    // make sure the token doesn't exist (even if it should be almost impossible with the random generation)
    $numtries = 0;
    do {
        $numtries ++;
        $generatedtoken = md5(uniqid(rand(),1));
        if ($numtries > 5) {
            throw new WebserviceException('tokengenerationfailed');
        }
    } while (record_exists('external_tokens', 'token', $generatedtoken));
    $newtoken = new stdClass();
    $newtoken->token = $generatedtoken;
    if (!is_object($serviceorid)) {
        $service = get_record('external_services', 'id', $serviceorid);
    }
    else {
        $service = $serviceorid;
    }
    $newtoken->externalserviceid = $service->id;
    $newtoken->tokentype = $tokentype;
    $newtoken->userid = $userid;
    if ($tokentype == EXTERNAL_TOKEN_EMBEDDED) {
        $newtoken->sid = session_id();
    }

    $newtoken->institution = $institution;
    $newtoken->creatorid = $USER->get('id');
    $newtoken->ctime = db_format_timestamp(time());
    $newtoken->timecreated = time();
    $newtoken->publickeyexpires = time();
    $newtoken->wssigenc = 0;
    $newtoken->publickey = '';
    $newtoken->validuntil = $validuntil;
    $newtoken->clientname = $clientname;
    $newtoken->clientenv = $clientenv;
    $newtoken->clientguid = $clientguid;
    $newtoken->iprestriction = $iprestriction;
    insert_record('external_tokens', $newtoken);
    return $newtoken->token;
}

/**
 * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate
 * with the Moodle server through web services. The token is linked to the current session for the current page request.
 * It is expected this will be called in the script generating the html page that is embedding the client app and that the
 * returned token will be somehow passed into the client app being embedded in the page.
 * @param string $servicename name of the web service. Service name as defined in db/services.php
 * @param integer $userid
 * @param string $institution
 * @param integer $validuntil
 * @param string $iprestriction
 * @return int returns token id.
 */
function webservice_create_service_token($servicename, $userid, $institution = 'mahara',  $validuntil=0, $iprestriction='') {
    $service = get_record('external_services', 'name', $servicename, '*');
    return webservice_generate_token(EXTERNAL_TOKEN_EMBEDDED, $service, $userid, $institution,  $validuntil, $iprestriction);
}

/**
 * Calculate where the webservices directory should be, for a given value of
 * "component".
 *
 * There are three general types of "component" value we expect to see.
 * 1. "webservice": Indicates a the "core" webservices, which are in htdocs/webservice
 * 2. "local": Indicates custom webservices under htdocs/local/webservice
 * 3. "{plugintype}/{pluginname}": Indicates webservices for a plugin.
 *
 * @param string $component The component to look for
 * @param reference $plugintype If the component represents a plugin, the plugin's type will
 * be returned via this variable, passed by reference.
 * @param reference $pluginname If the component represents a plugin, the plugin's name will
 * be returned via this variable, passed by reference.
 * @return string Relative path to the component's webservices directory. If the component
 * is a plugin, this path will be relative the plugin's directory. Otherwise, it'll be
 * relative to $CFG->docroot.
 * @throws WebserviceCodingException
 */
function webservice_component_ws_directory($component, &$plugintype, &$pluginname) {
    if ($component == WEBSERVICE_DIRECTORY) {
        $plugintype = false;
        $pluginname = false;
        return WEBSERVICE_DIRECTORY;
    }

    if ($component == 'local') {
        $plugintype = false;
        $pluginname = false;
        return 'local/' . WEBSERVICE_DIRECTORY;
    }

    $bits = explode('/', $component);
    if (count($bits) == 2) {
        list($plugintype, $pluginname) = $bits;
        return WEBSERVICE_DIRECTORY;
    }

    throw new WebserviceCodingException("Invalid component name: '{$component}'");
}

/**
 * Returns detailed function information
 * @param string|object $function name of external function or record from external_function
 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
 *                        MUST_EXIST means throw exception if no record or multiple records found
 * @return object description or false if not found or exception thrown
 */
function webservice_function_info($function, $strictness=MUST_EXIST, $component = null) {
    $mustexist = ($strictness === MUST_EXIST);
    if (!is_object($function)) {
        if ($component) {
            $function = get_record('external_functions', 'name', $function, 'component', $component);
        }
        else {
            $function = get_record('external_functions', 'name', $function);
        }
        if (!$function) {
            return false;
        }
        $component = $function->component;
    }

    //first find and include the ext implementation class
    if (!class_exists($function->classname)) {

        $wsdir = webservice_component_ws_directory(
            $function->component,
            $plugintype,
            $pluginname
        );

        if ($plugintype && $pluginname) {
            // Standard plugin; can use safe_require
            $foundfile = safe_require_plugin(
                $plugintype,
                $pluginname,
                $wsdir . '/functions/' . $function->classname . '.php',
                'require_once',
                true
            );
            if (!$foundfile) {
                if ($mustexist) {
                    throw new WebserviceCodingException(get_string('cannotfindimplfile', 'auth.webservice'));
                }
                return false;
            }
        }
        else {
            // Not a plugin, must handle manually
            $filepath = get_config('docroot') . $wsdir . '/functions/' . $function->classname . '.php';
            if (!file_exists($filepath)) {
                if ($mustexist) {
                    throw new WebserviceCodingException(get_string('cannotfindimplfile', 'auth.webservice'));
                }
                return false;
            }
            require_once($filepath);
        }
    }

    $function->parameters_method = $function->methodname . '_parameters';
    $function->returns_method    = $function->methodname . '_returns';

    // make sure the implementation class is ok
    if (!method_exists($function->classname, $function->methodname)) {
        if ($mustexist) {
            throw new WebserviceCodingException(get_string('missingimplofmeth', 'auth.webservice', $function->classname . '::' . $function->methodname));
        }
        return false;
    }
    if (!method_exists($function->classname, $function->parameters_method)) {
        if ($mustexist) {
            throw new WebserviceCodingException(get_string('missingparamdesc', 'auth.webservice'));
        }
        return false;
    }
    if (!method_exists($function->classname, $function->returns_method)) {
        if ($mustexist) {
            throw new WebserviceCodingException(get_string('missingretvaldesc', 'auth.webservice'));
        }
        return false;
    }

    // fetch the parameters description
    $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
    if (!($function->parameters_desc instanceof external_function_parameters)) {
        if ($mustexist) {
            throw new WebserviceCodingException(get_string('invalidparamdesc', 'auth.webservice'));
        }
        return false;
    }

    // fetch the return values description
    $function->returns_desc = call_user_func(array($function->classname, $function->returns_method));
    // null means void result or result is ignored
    if (!is_null($function->returns_desc) and !($function->returns_desc instanceof external_description)) {
        if ($mustexist) {
            throw new WebserviceCodingException(get_string('invalidretdesc', 'auth.webservice'));
        }
        return false;
    }

    //now get the function description
    //TODO: use localised lang pack descriptions, it would be nice to have
    //      easy to understand descriptions in admin UI,
    //      on the other hand this is still a bit in a flux and we need to find some new naming
    //      conventions for these descriptions in lang packs
    $function->description = null;
    $result = webservice_load_services_file($function->component);
    $functionlist = $result['functions'];
    if (isset($functionlist[$function->name]['description'])) {
        $function->description = $functionlist[$function->name]['description'];
    }

    return $function;
}

/**
 * Returns a list of all of the webservice connection definitions declared
 * by all of the installed plugins.
 */
function webservice_connection_definitions() {

    $connections = array();

    $plugins = array();
    $plugins['blocktype'] = array();

    foreach (plugin_types()  as $plugin) {
        // this has to happen first because of broken artefact/blocktype ordering
        $plugins[$plugin] = array();
        $plugins[$plugin]['installed'] = array();
        $plugins[$plugin]['notinstalled'] = array();
    }
    foreach (array_keys($plugins) as $plugin) {
        if (db_table_exists($plugin . '_installed')) {
            if ($installed = plugins_installed($plugin, true)) {
                foreach ($installed as $i) {
                    $key = $i->name;
                    if ($plugin == 'blocktype') {
                        $key = blocktype_single_to_namespaced($i->name, $i->artefactplugin);
                    }
                    if (!safe_require_plugin($plugin, $key)) {
                        continue;
                    }
                    if ($i->active) {
                        $classname = generate_class_name($plugin, $key);
                        if (method_exists($classname, 'define_webservice_connections')) {
                            $conns = $classname::define_webservice_connections();
                            if (!empty($conns)) {
                                $connections[$classname] = array('connections' => $conns, 'type' => $plugin, 'key' => $key);
                            }
                        }
                    }
                    if ($plugin == 'artefact') {
                        safe_require('artefact', $key);
                        $classname = generate_class_name('artefact', $i->name);
                        if ($types = $classname::get_artefact_types()) {
                            foreach ($types as $t) {
                                $classname = generate_artefact_class_name($t);
                                if (method_exists($classname, 'define_webservice_connections')) {
                                    $conns = $classname::define_webservice_connections();
                                    if (!empty($conns)) {
                                        $connections[$classname] = array('connections' => $conns, 'type' => $plugin, 'key' => $key);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    return $connections;
}


/**
 * General web service library
 */
class webservice {

    /**
     * Get the list of all functions for given service ids
     * @param array $serviceids
     * @return array functions
     */
    public function get_external_functions($serviceids) {
        global $WS_FUNCTIONS;

        if (!empty($serviceids)) {
            $where = (count($serviceids) == 1 ? ' = '.array_shift($serviceids) : ' IN (' . implode(',', $serviceids) . ')');
            $sql = "SELECT f.*
                      FROM {external_functions} f
                     WHERE f.name IN (SELECT sf.functionname
                                        FROM {external_services_functions} sf
                                       WHERE sf.externalserviceid $where)";
            $functions = get_records_sql_array($sql, array());
        }
        else {
            $functions = array();
        }

        // stash functions for intro spective RPC calls later
        $WS_FUNCTIONS = array();
        foreach ($functions as $function) {
            $WS_FUNCTIONS[$function->name] = array('id' => $function->id);
        }

        return $functions;
    }
}

/**
 * Base class for external api methods.
 */
class external_api {
    private static $contextrestriction;

    /**
     * Set context restriction for all following subsequent function calls.
     * @param stdClass $contex
     * @return void
     */
    public static function set_context_restriction($context) {
        self::$contextrestriction = $context;
    }

    /**
     * This method has to be called before every operation
     * that takes a longer time to finish!
     *
     * @param int $seconds max expected time the next operation needs
     * @return void
     */
    public static function set_timeout($seconds=360) {
        $seconds = ($seconds < 300) ? 300 : $seconds;
        set_time_limit($seconds);
    }

    /**
     * Validates submitted function parameters, if anything is incorrect
     * WebserviceInvalidParameterException is thrown.
     * This is a simple recursive method which is intended to be called from
     * each implementation method of external API.
     * @param external_description $description description of parameters
     * @param mixed $params the actual parameters
     * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found
     */
    public static function validate_parameters(external_description $description, $params) {
        // we need to turn the social profile information into a single string to pass external_value test
        // because we can either pass in the information as a string 'profiletype|profileurl' or as an array
        if (isset($params['socialprofile']) && is_array($params['socialprofile'])) {
            $params['socialprofile'] = (!empty($params['socialprofile']['profiletype']) ? $params['socialprofile']['profiletype'] : '') . '|' . (!empty($params['socialprofile']['profileurl']) ? $params['socialprofile']['profileurl'] : '');
        }
        if ($description instanceof external_value) {
            if (is_array($params) or is_object($params)) {
                throw new WebserviceInvalidParameterException(get_string('errorscalartype', 'auth.webservice'));
            }

            if ($description->type == PARAM_BOOL) {
                // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
                if (is_bool($params) or $params === 0 or $params === 1 or $params === '0' or $params === '1') {
                    return (bool)$params;
                }
            }
            return validate_param($params, $description->type, $description->allownull, get_string('errorinvalidparamsapi', 'auth.webservice'));

        }
        else if ($description instanceof external_single_structure) {
            if (!is_array($params)) {
                throw new WebserviceInvalidParameterException(get_string('erroronlyarray', 'auth.webservice'));
            }
            $result = array();
            foreach ($description->keys as $key=>$subdesc) {
                if (!array_key_exists($key, $params)) {
                    if ($subdesc->required == VALUE_REQUIRED) {
                        throw new WebserviceInvalidParameterException(get_string('errormissingkey', 'auth.webservice', $key));
                    }
                    if ($subdesc->required == VALUE_DEFAULT) {
                        $result[$key] = $subdesc->default;
                    }

                    if ($subdesc->required == VALUE_OPTIONAL) {
                        $result[$key] = null;
                    }

                }
                else {
                    try {
                        $result[$key] = self::validate_parameters($subdesc, $params[$key]);
                    } catch (WebserviceInvalidParameterException $e) {
                        //it's ok to display debug info as here the information is useful for ws client/dev
                        throw new WebserviceParameterException(get_string('invalidextparam', 'auth.webservice', "key: $key - " . $e->getMessage() . (isset($e->debuginfo) ? " (debuginfo: " . $e->debuginfo . ") " : "")));
                    }
                }
                unset($params[$key]);
            }
            if (!empty($params)) {
                //list all unexpected keys
                $keys = '';
                $customkeys = '';
                foreach ($params as $key => $value) {
                    if (substr($key, 0, 7) === "custom_") {
                        $customkeys .= $key . ',';
                    }
                    else {
                        $keys .= $key . ',';
                    }
                }
                if (!empty($customkeys) && !get_config('productionmode')) {
                    log_info(get_string('errorunexpectedcustomkey', 'auth.webservice', $customkeys));
                }
                if (!empty($keys) && !get_config('productionmode')) {
                    // We will stop throwing error on unexpected param keys and instead just show them in error log
                    // when the site is not in production mode
                    log_info(get_string('errorunexpectedkey', 'auth.webservice', $keys));
                }
            }
            return $result;

        }
        else if ($description instanceof external_multiple_structure) {
            if (!is_array($params)) {
                throw new WebserviceInvalidParameterException(get_string('erroronlyarray', 'auth.webservice'));
            }
            $result = array();
            foreach ($params as $param) {
                $result[] = self::validate_parameters($description->content, $param);
            }
            return $result;

        }
        else {
            throw new WebserviceInvalidParameterException(get_string('errorinvalidparamsdesc', 'auth.webservice'));
        }
    }

    /**
     * Clean response
     * If a response attribute is unknown from the description, we just ignore the attribute.
     * If a response attribute is incorrect, WebserviceInvalidResponseException is thrown.
     * Note: this function is similar to validate parameters, however it is distinct because
     * parameters validation must be distinct from cleaning return values.
     * @param external_description $description description of the return values
     * @param mixed $response the actual response
     * @return mixed response with added defaults for optional items, WebserviceInvalidResponseException thrown if any problem found
     */
    public static function clean_returnvalue(external_description $description, $response) {
        if ($description instanceof external_value) {
            if (is_array($response) or is_object($response)) {
                throw new WebserviceInvalidResponseException(get_string('errorscalartype', 'auth.webservice'));
            }

            if ($description->type == PARAM_BOOL) {
                // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
                if (is_bool($response) or $response === 0 or $response === 1 or $response === '0' or $response === '1') {
                    return (bool)$response;
                }
            }
            return validate_param($response, $description->type, $description->allownull, get_string('errorinvalidresponseapi', 'auth.webservice'));

        }
        else if ($description instanceof external_single_structure) {
            if ($response === null) {
                if ($description->required == VALUE_REQUIRED) {
                    throw new WebserviceInvalidParameterException(get_string('errormissingkey', 'auth.webservice', $description->type));
                }
                else if ($description->required == VALUE_DEFAULT) {
                    return $description->default;
                }
                else {
                    return null;
                }
            }
            if (!is_array($response)) {
                throw new WebserviceInvalidResponseException(get_string('erroronlyarray', 'auth.webservice'));
            }
            $result = array();
            foreach ($description->keys as $key=>$subdesc) {
                if (!array_key_exists($key, $response)) {
                    if ($subdesc->required == VALUE_REQUIRED) {
                        throw new WebserviceParameterException('errorresponsemissingkey', $key);
                    }
                    else if ($subdesc->required == VALUE_DEFAULT) {
                        try {
                            $result[$key] = self::clean_returnvalue($subdesc, $subdesc->default);
                        }
                        catch (Exception $e) {
                            throw new WebserviceParameterException('invalidextresponse',$key . " (" . $e->getMessage() . ")");
                        }
                    }
                }
                else {
                    try {
                        $result[$key] = self::clean_returnvalue($subdesc, $response[$key]);
                    } catch (Exception $e) {
                        //it's ok to display debug info as here the information is useful for ws client/dev
                        throw new WebserviceParameterException('invalidextresponse', $key . " (" . $e->getMessage() . ")");
                    }
                }
                unset($response[$key]);
            }

            return $result;

        }
        else if ($description instanceof external_multiple_structure) {
            if ($response === null) {
                if ($description->required == VALUE_REQUIRED) {
                    throw new WebserviceInvalidParameterException(get_string('errormissingkey', 'auth.webservice', $description->type));
                }
                else if ($description->required == VALUE_DEFAULT) {
                    return $description->default;
                }
                else {
                    return null;
                }
            }
            if (!is_array($response)) {
                throw new WebserviceInvalidResponseException(get_string('erroronlyarray', 'auth.webservice'));
            }
            $result = array();
            foreach ($response as $param) {
                $result[] = self::clean_returnvalue($description->content, $param);
            }
            return $result;

        }
        else {
            throw new WebserviceInvalidResponseException(get_string('errorinvalidresponsedesc', 'auth.webservice'));
        }
    }

    /**
     * Returns detailed function information
     *
     * @param string|object $function name of external function or record from external_function
     * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
     *                        MUST_EXIST means throw exception if no record or multiple records found
     * @return stdClass description or false if not found or exception thrown
     */
    public static function external_function_info($function, $strictness=MUST_EXIST) {

        if (!is_object($function)) {
            if (!$function = get_record('external_functions', 'name', $function)) {
                return false;
            }
        }

        // First try class autoloading.
        if (!class_exists($function->classname)) {
            if ($function->classpath == 'webservice') {
                $function->classpath = get_config('docroot') . $function->classpath . '/functions/' . $function->classname . '.php';
            }
            else {
                $function->classpath = get_config('docroot') . $function->classpath;
                if (!preg_match('/\.php$/', $function->classpath)) {
                    $function->classpath .= '/functions/' . $function->classname . '.php';
                }
            }
            if (!file_exists($function->classpath)) {
                throw new MaharaException('Cannot find file with external function implementation');
            }
            require_once($function->classpath);
            if (!class_exists($function->classname)) {
                throw new MaharaException('Cannot find external class');
            }
        }

        $function->ajax_method = $function->methodname . '_is_allowed_from_ajax';
        $function->parameters_method = $function->methodname . '_parameters';
        $function->returns_method    = $function->methodname . '_returns';
        $function->deprecated_method = $function->methodname . '_is_deprecated';

        // Make sure the implementation class is ok.
        if (!method_exists($function->classname, $function->methodname)) {
            throw new MaharaException('Missing implementation method of ' . $function->classname . '::' . $function->methodname);
        }
        if (!method_exists($function->classname, $function->parameters_method)) {
            throw new MaharaException('Missing parameters description');
        }
        if (!method_exists($function->classname, $function->returns_method)) {
            throw new MaharaException('Missing returned values description');
        }
        if (method_exists($function->classname, $function->deprecated_method)) {
            if (call_user_func(array($function->classname, $function->deprecated_method)) === true) {
                $function->deprecated = true;
            }
        }
        $function->allowed_from_ajax = false;

        // Fetch the parameters description.
        $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
        if (!($function->parameters_desc instanceof external_function_parameters)) {
            throw new MaharaException('Invalid parameters description');
        }

        // Fetch the return values description.
        $function->returns_desc = call_user_func(array($function->classname, $function->returns_method));
        // Null means void result or result is ignored.
        if (!is_null($function->returns_desc) and !($function->returns_desc instanceof external_description)) {
            throw new MaharaException('Invalid return description');
        }

        return $function;
    }

       /**
     * Internal function to upload a file using the same logic whether
     * it's a standalone file or an attachment to a blog post.
     *
     * This function can deal with files that are in an array param,
     * but it will only do one of them at a time.
     *
     * @param string $inputname Name of the parameter the file is in
     * @param int $inputindex NULL if there's just one file; index of particular file if it's an array
     * @param string $foldername Folder to put the files in (or create if it doesn't exist yet.)
     * @param string $title
     * @param string $description
     * @param array $tags
     * @param int $recipient ID, if null, upload to self
     * @return ID of newly created file
     * @throws WebserviceInvalidParameterException
     */
    protected static function handle_file_upload($inputname, $inputindex = null, $foldername = null, $title = null, $description = null, $tags = array(), $recipient = null) {
        global $USER;
        if (!$_FILES[$inputname]) {
            throw new WebserviceInvalidParameterException('No uploaded files found in request');
        }
        safe_require('artefact', 'file');

        $data = new stdClass();
        $data->owner = $recipient ? $recipient : $USER->get('id'); // id of owner

        // See if a folder by this name already exists.
        // Create a folder by this name if it doesn't exist yet.
        $artefact = ArtefactTypeFolder::get_folder_by_name($foldername, null, $data->owner);
        if ($artefact) {
            $data->parent = $artefact->id;
            if ($data->parent == 0) {
                $data->parent = null;
            }
        }
        else {
            $fd = (object) array(
                'owner' => $data->owner,
                'title' => $foldername,
                'parent' => null,
            );
            $f = new ArtefactTypeFolder(0, $fd);
            $f->commit();
            $data->parent = $f->get('id');
        }

        if (!$title) {
            if ($inputindex) {
                $rawname = $_FILES[$inputname]['name'][$inputindex];
            }
            else {
                $rawname = $_FILES[$inputname]['name'];
            }
            $title = basename($rawname);
        }
        $data->title = ArtefactTypeFileBase::get_new_file_title($title, $data->parent, $data->owner);
        if ($description) {
            $data->description = $description;
        }
        if ($tags) {
            $data->tags = $tags;
        }

        // This will throw a QuotaExceededException or UploadExceptoin if there's
        // a problem.
        $artefact_id = ArtefactTypeFile::save_uploaded_file($inputname, $data, $inputindex);

        return $artefact_id;
    }
}

/**
 * Common ancestor of all parameter description classes
 */
abstract class external_description {
    /** @property string $description description of element */
    public $desc;
    /** @property bool $required element value required, null not allowed */
    public $required;
    /** @property mixed $default default value */
    public $default;

    /**
     * Constructor
     * @param string $desc
     * @param bool $required
     * @param mixed $default
     */
    public function __construct($desc, $required, $default) {
        $this->desc = $desc;
        $this->required = $required;
        $this->default = $default;
    }
}

/**
 * Scalar value description class
 */
class external_value extends external_description {
    /** @property mixed $type value type PARAM_XX */
    public $type;
    /** @property bool $allownull allow null values */
    public $allownull;
    /** @property string|false $oneof A string to identify a oneof grouping */
    public $oneof;

    /**
     * Constructor
     * @param mixed $type
     * @param string $desc
     * @param bool $required
     * @param mixed $default
     * @param bool $allownull
     * @param bool $oneof
     */
    public function __construct($type, $desc='', $required=VALUE_REQUIRED,
        $default=null, $allownull=NULL_ALLOWED, $oneof=false) {
        parent::__construct($desc, $required, $default);
        $this->type      = $type;
        $this->allownull = $allownull;
        $this->oneof     = $oneof;
    }
}

/**
 * Associative array description class
 */
class external_single_structure extends external_description {
    /** @property array $keys description of array keys key=>external_description */
    public $keys;

    /**
     * Constructor
     * @param array $keys
     * @param string $desc
     * @param bool $required
     * @param array $default
     */
    public function __construct(array $keys, $desc='',
    $required=VALUE_REQUIRED, $default=null) {
        parent::__construct($desc, $required, $default);
        $this->keys = $keys;
    }
}

/**
 * Bulk array description class.
 */
class external_multiple_structure extends external_description {
    /** @property external_description $content */
    public $content;

    /**
     * Constructor
     * @param external_description $content
     * @param string $desc
     * @param bool $required
     * @param array $default
     */
    public function __construct(external_description $content, $desc='',
    $required=VALUE_REQUIRED, $default=null) {
        parent::__construct($desc, $required, $default);
        $this->content = $content;
    }
}

/**
 * Description of top level - PHP function parameters.
 * @author skodak
 *
 */
class external_function_parameters extends external_single_structure {
}

/**
 * Is protocol enabled?
 * @param string $protocol name of WS protocol
 * @return bool
 */
function webservice_protocol_is_enabled($protocol) {
    if (!get_config('webservice_provider_enabled')) {
        return false;
    }
    return get_config('webservice_provider_'.$protocol.'_enabled');
}

//=== WS classes ===

/**
 * Mandatory interface for all test client classes.
 * @author Petr Skoda (skodak)
 */
interface webservice_test_client_interface {
    /**
     * Execute test client WS request
     * @param string $serverurl
     * @param string $function
     * @param array $params
     * @return mixed
     */
    public function simpletest($serverurl, $function, $params);
}

/**
 * Mandatory interface for all web service protocol classes
 * @author Petr Skoda (skodak)
 */
interface WebserviceServerInterface {
    /**
     * Process request from client.
     * @return void
     */
    public function run();
}

/**
 * Abstract web service base class.
 * @author Petr Skoda (skodak)
 */
abstract class WebserviceServer implements WebserviceServerInterface {

    /** @var string $wsname name of the web server plugin */
    protected $wsname = null;

    /** @var string $username name of local user */
    protected $username = null;

    /** @var string $password password of the local user */
    protected $password = null;

    /** @var string $service service for wsdl look up */
    protected $service = null;

    /** @var int $userid the local user */
    protected $userid = null;

    /** @var integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_* */
    protected $authmethod;

    /** @var integer $auth method string */
    protected $auth;

    /** @var string $token authentication token*/
    protected $token = null;

    /** @var int restrict call to one service id*/
    protected $restricted_serviceid = null;

    /** @var string info to add to logging*/
    protected $info = null;

    protected $oauth_token_details = null;
    /**
     * Constructor
     * @param integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_*
     */
    public function __construct($authmethod) {
        $this->authmethod = $authmethod;
    }

    /**
     * Authenticate user using username+password or token.
     * This function sets up $USER global.
     * It is safe to use has_capability() after this.
     * This method also verifies user is allowed to use this
     * server.
     * @return void
     */
    protected function authenticate_user() {
        global $USER, $SESSION, $WEBSERVICE_INSTITUTION, $WEBSERVICE_OAUTH_USER, $WEBSERVICE_OAUTH_SERVERID;
        if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
            $this->auth = 'USER';
            //we check that authentication plugin is enabled
            //it is only required by simple authentication
            $plugin = get_record('auth_installed', 'name', 'webservice');
            if (empty($plugin) || $plugin->active != 1) {
                throw new WebserviceAccessException(get_string('wsauthnotenabled', 'auth.webservice'));
            }

            if (!$this->username) {
                throw new WebserviceAccessException(get_string('missingusername', 'auth.webservice'));
            }

            if (!$this->password) {
                throw new WebserviceAccessException(get_string('missingpassword', 'auth.webservice'));
            }

            // special web service login
            safe_require('auth', 'webservice');

            // get the user
            $user = get_record('usr', 'username', $this->username);
            if (empty($user)) {
                throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice'));
            }

            // user account is nolonger validly configured
            if (!$auth_instance = webservice_validate_user($user)) {
                throw new WebserviceAccessException(get_string('invalidaccount', 'auth.webservice'));
            }
            // set the global for the web service users defined institution
            $WEBSERVICE_INSTITUTION = $auth_instance->institution;

            // get the institution from the external user
            $ext_user = get_record('external_services_users', 'userid', $user->id);
            if (empty($ext_user)) {
                throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice'));
            }
            // determine the internal auth instance
            $auth_instance = get_record('auth_instance', 'institution', $ext_user->institution, 'authname', 'webservice', 'active', 1);
            if (empty($auth_instance)) {
                throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice'));
            }

            // authenticate the user
            $auth = new AuthWebservice($auth_instance->id);
            if (!$auth->authenticate_user_account($user, $this->password, 'webservice')) {
                // log failed login attempts
                throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice'));
            }

        }
        else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN) {
            $this->auth = 'TOKEN';
            $user = $this->authenticate_by_token(EXTERNAL_TOKEN_PERMANENT);
        }
        else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_OAUTH_TOKEN) {
            //OAuth
            $this->auth = 'OAUTH';
            // special web service login
            safe_require('auth', 'webservice');

            // get the user - the user that authorised the token
            $user = $this->authenticate_by_token(EXTERNAL_TOKEN_OAUTH1);

            $is_site_admin = false;

            foreach (get_site_admins() as $site_admin) {
                if ($site_admin->id == $user->id) {
                    $is_site_admin = true;
                    break;
                }
            }

            if (!$is_site_admin) {

                // check user is member of configured OAuth institution
                $institutions = array_keys(load_user_institutions($this->oauth_token_details['user_id']));
                $auth_instance = get_record('auth_instance', 'id', $user->authinstance, 'active', 1);
                $institutions[]= $auth_instance->institution;
                if (!in_array($this->oauth_token_details['institution'], $institutions)) {
                    throw new WebserviceAccessException(get_string('institutiondenied', 'auth.webservice'));
                }
            }

            // set the global for the web service users defined institution
            $WEBSERVICE_INSTITUTION = $this->oauth_token_details['institution'];
            // set the note of the OAuth service owner
            $WEBSERVICE_OAUTH_USER = $this->oauth_token_details['service_user'];
            // set the OAuth server id
            $WEBSERVICE_OAUTH_SERVERID = $this->oauth_token_details['id'];
        }
        else {
            $this->auth = 'OTHER';
            $user = $this->authenticate_by_token(EXTERNAL_TOKEN_USER);
        }

        // now fake user login, the session is completely empty too
        $USER->reanimate($user->id, $user->authinstance);
    }

    /**
     * Authenticate by token type
     *
     * @param $tokentype string tokentype constant
     * @return $user object
     */
    public function authenticate_by_token($tokentype) {
        global $WEBSERVICE_INSTITUTION, $WEBSERVICE_AUTH_METHOD;

        if ($tokentype == EXTERNAL_TOKEN_OAUTH1) {
            $user = get_record('usr', 'id', $this->oauth_token_details['user_id']);
            if (empty($user)) {
                throw new WebserviceAccessException(get_string('wrongusernamepassword', 'auth.webservice'));
            }
            return $user;
        }

        if (empty($this->token)) {
            // log failed login attempts
            throw new WebserviceAccessException(get_string('invalidtokennotsupplied', 'auth.webservice'));
        }

        $token = get_record('external_tokens', 'token', $this->token);
        if (!$token) {
            // log failed login attempts
            throw new WebserviceAccessException(get_string('invalidtoken', 'auth.webservice'));
        }

        // tidy up the auth method - this could be user token or session token
        if ($token->tokentype != EXTERNAL_TOKEN_PERMANENT) {
            if ($token->tokentype === EXTERNAL_TOKEN_USER) {
                // TODO: These should probably be constants, not strings...
                $this->auth = 'TOKEN_USER';
            }
            else {
                $this->auth = 'OTHER';
            }
        }

        /**
         * check the valid until date
         */
        if ($token->validuntil and $token->validuntil < time()) {
            delete_records('external_tokens', 'token', $this->token, 'tokentype', $tokentype);
            throw new WebserviceAccessException(get_string('invalidtimedtoken', 'auth.webservice'));
        }

        if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
            throw new WebserviceAccessException(get_string('invalidiptoken', 'auth.webservice'));
        }

        $this->restricted_serviceid = $token->externalserviceid;

        $user = get_record('usr', 'id', $token->userid, 'deleted', 0);

        // log token access
        set_field('external_tokens', 'mtime', db_format_timestamp(time()), 'id', $token->id);

        // set the global for the web service users defined institution
        $WEBSERVICE_INSTITUTION = $token->institution;
        $WEBSERVICE_AUTH_METHOD = $token->authinstance;

        return $user;
    }

    /**
     * Intercept some maharawssettingXXX $_GET and $_POST parameter
     * that are related to the web service call and are not the function parameters
     */
    protected function set_web_service_call_settings() {
        global $CFG;

        // Default web service settings.
        // Must be the same XXX key name as the external_settings::set_XXX function.
        // Must be the same XXX ws parameter name as 'maharawssettingXXX'.
        $externalsettings = array(
            'raw' => false,
            'fileurl' => true,
            'filter' =>  false);

        // Load the external settings with the web service settings.
        $settings = external_settings::get_instance();
        foreach ($externalsettings as $name => $default) {

            $wsparamname = 'maharawssetting' . $name;

            // Retrieve and remove the setting parameter from the request.
            $value = param_variable($wsparamname, $default);
            unset($_GET[$wsparamname]);
            unset($_POST[$wsparamname]);

            $functioname = 'set_' . $name;
            $settings->$functioname($value);
        }

    }

    /**
     * Gets information about services the authenticated user is allowed
     * to access.
     * @param string $serviceid (Optional) Only look at this service
     * @param string $functionname (Optional) Services must contain this function
     * @throws WebserviceInvalidParameterException
     */
    protected function get_allowed_services($serviceid = false, $functionname = false) {
        global $USER;

        if ($functionname) {
            $fncond1 = 'AND sf.functionname = ?';
            $fncond2 = 'AND sf.functionname = ?';
        }
        else {
            $fncond1 = '';
            $fncond2 = '';
        }

        if ($serviceid) {
            $wscond1 = 'AND s.id = ? ';
            $wscond2 = 'AND s.id = ? ';
        }
        else {
            $wscond1 = '';
            $wscond2 = '';
        }

        if ($this->auth === 'TOKEN_USER') {
            $tokencond = 'AND s.tokenusers = 1';
        }
        else {
            $tokencond = '';
        }

        // now let's verify access control
        // Allow access only if:
        // - restrictedusers = 0
        // - OR
        //   - restrictedusers = 1
        //   - AND user is on the list for the service
        //   - AND user's listing hasn't expired
        //   - AND user's IP matches any restrictions for their listing
        $sql = "
            SELECT s.*, NULL AS iprestriction
            FROM
                {external_services} s
                INNER JOIN {external_services_functions} sf ON
                    sf.externalserviceid = s.id
                    AND s.restrictedusers = 0
                    $fncond1
            WHERE
                s.enabled = 1
                $tokencond
                $wscond1
        UNION
            SELECT s.*, su.iprestriction
            FROM
                {external_services} s
                INNER JOIN {external_services_functions} sf ON
                    sf.externalserviceid = s.id
                    AND s.restrictedusers = 1
                    $fncond2
                INNER JOIN {external_services_users} su ON
                    su.externalserviceid = s.id
                    AND su.userid = ?
            WHERE
                s.enabled = 1
                AND (su.validuntil IS NULL OR su.validuntil < ?)
                $tokencond
                $wscond2
";
        $params = array();
        $fncond1 && $params[] = $functionname;
        $wscond1 && $params[]= $serviceid;
        $fncond2 && $params[]= $functionname;
        $params[]= $USER->get('id');
        $params[]= time();
        $wscond2 && $params[]= $serviceid;
        $rs = get_records_sql_array($sql, $params);

        $remoteaddr = getremoteaddr();
        $serviceids = array();
        foreach ($rs as $service) {
            if ($service->iprestriction && !address_in_subnet($remoteaddr, $service->iprestriction)) {
                // wrong request source ip, sorry
                continue;
            }
            $serviceids[] = $service->id;
        }
        return $serviceids;
    }
}

/**
 * Web Service server base class, this class handles both
 * simple and token authentication.
 * @author Petr Skoda (skodak)
 */
abstract class WebserviceBaseServer extends WebserviceServer {

    /** @property array $parameters the function parameters - the real values submitted in the request */
    protected $parameters = null;

    /** @property string $functionname the name of the function that is executed */
    protected $functionname = null;

    /** @property object $function full function description */
    protected $function = null;

    /** @property mixed $returns function return value */
    protected $returns = null;

    /**
     * This method parses the request input, it needs to get:
     *  1/ user authentication - username+password or token
     *  2/ function name
     *  3/ function parameters
     *
     * @return void
     */
    abstract protected function parse_request();

    /**
     * Send the result of function call to the WS client.
     * @return void
     */
    abstract protected function send_response();

    /**
     * Send the error information to the WS client.
     * @param exception $ex
     * @return void
     */
    abstract protected function send_error($ex=null);

    /**
     * Process request from client.
     * @return void
     */
    public function run() {
        global $WEBSERVICE_FUNCTION_RUN, $USER, $WEBSERVICE_INSTITUTION, $WEBSERVICE_START;

        $WEBSERVICE_START = microtime(true);

        // we will probably need a lot of memory in some functions
        raise_memory_limit('128M');

        // set some longer timeout, this script is not sending any output,
        // this means we need to manually extend the timeout operations
        // that need longer time to finish
        external_api::set_timeout();

        // set up exception handler first, we want to sent them back in correct format that
        // the other system understands
        // we do not need to call the original default handler because this ws handler does everything
        set_exception_handler(array($this, 'exception_handler'));

        // Set the auth method as UNKNOWN at this point - it should be changed to correct value
        // in authenticate_user() function
        $this->auth = 'UNKNOWN';

        // init all properties from the request data
        $this->parse_request();

        // authenticate user, this has to be done after the request parsing
        // this also sets up $USER and $SESSION
        $this->authenticate_user();

        // find all needed function info and make sure user may actually execute the function
        $this->load_function_info();

        // finally, execute the function - any errors are caught by the default exception handler
        $this->execute();

        $time_end = microtime(true);
        $time_taken = $time_end - $WEBSERVICE_START;

        //log the web service request
        $log = (object)  array('timelogged' => time(),
                               'userid' => $USER->get('id'),
                               'externalserviceid' => $this->restricted_serviceid,
                               'institution' => $WEBSERVICE_INSTITUTION,
                               'protocol' => 'REST',
                               'auth' => $this->auth,
                               'functionname' => $this->functionname,
                               'timetaken' => number_format($time_taken, 5, '.', ''),
                               'uri' => $_SERVER['REQUEST_URI'],
                               'info' => '',
                               'ip' => getremoteaddr());
        self::log_webservice_call($log);

        // send the results back in correct format
        $this->send_response();

        // session cleanup
        $this->session_cleanup();

        die;
    }

    /**
     * Specialised exception handler, we can not use the standard one because
     * it can not just print html to output.
     *
     * @param exception $ex
     * @return void does not return
     */
    public function exception_handler($ex) {
        global $WEBSERVICE_FUNCTION_RUN, $USER, $WEBSERVICE_INSTITUTION, $WEBSERVICE_START;

        // detect active db transactions, rollback and log as error
        db_rollback();

        $time_end = microtime(true);
        $time_taken = $time_end - $WEBSERVICE_START;

        //log the error on the web service request
        $log = (object)  array('timelogged' => time(),
                               'userid' => $USER->get('id'),
                               'externalserviceid' => $this->restricted_serviceid,
                               'institution' => $WEBSERVICE_INSTITUTION,
                               'protocol' => 'REST',
                               'auth' => $this->auth,
                               'functionname' => ($WEBSERVICE_FUNCTION_RUN ? $WEBSERVICE_FUNCTION_RUN : $this->functionname),
                               'timetaken' => number_format($time_taken, 5, '.', ''),
                               'uri' => $_SERVER['REQUEST_URI'],
                               'info' => 'exception: ' . get_class($ex) . ' message: ' . $ex->getMessage() . ' debuginfo: ' . (isset($ex->debuginfo) ? $ex->debuginfo : ''),
                               'ip' => getremoteaddr());
        self::log_webservice_call($log);

        // some hacks might need a cleanup hook
        $this->session_cleanup($ex);

        // now let the plugin send the exception to client
        $this->send_error($ex);

        // not much else we can do now, add some logging later
        exit(1);
    }

    /**
     * Record an external_services_logs entry for a web service function call.
     *
     * @param object $log The log object to insert
     * @return int The id of the new log entry.
     */
    public static function log_webservice_call($log) {
        return insert_record('external_services_logs', $log, 'id', true);
    }

    /**
     * Future hook needed for emulated sessions.
     * @param exception $exception null means normal termination, $exception received when WS call failed
     * @return void
     */
    protected function session_cleanup($exception=null) {
        global $USER;

        $USER->logout();
    }

    /**
     * Fetches the function description from database,
     * verifies user is allowed to use this function and
     * loads all parameters and return descriptions.
     * @return void
     */
    protected function load_function_info() {
        global $USER;

        if (empty($this->functionname)) {
            throw new WebserviceInvalidParameterException(get_string('missingfuncname', 'auth.webservice'));
        }

        // function must exist
        $function = webservice_function_info($this->functionname);
        if (!$function) {
            throw new WebserviceInvalidParameterException(get_string('accessextfunctionnotconf', 'auth.webservice'));
        }

        // Check that the function is in a service this user is allowed
        // to access.
        $serviceids = $this->get_allowed_services($this->restricted_serviceid, $this->functionname);
        if (!count($serviceids)) {
            throw new WebserviceAccessException(get_string('accesstofunctionnotallowed', 'auth.webservice', $this->functionname));
        }

        // now get the list of all functions - this triggers the stashing of
        // functions in the context
        $wsmanager = new webservice();
        $functions = $wsmanager->get_external_functions($serviceids);

        // we have all we need now
        $this->function = $function;
    }

    /**
     * Execute previously loaded function using parameters parsed from the request data.
     * @return void
     */
    protected function execute() {
        // validate params, this also sorts the params properly, we need the correct order in the next part
        ksort($this->parameters);
        $params = call_user_func(array($this->function->classname, 'validate_parameters'), $this->function->parameters_desc, $this->parameters);

        // execute - yay!
        log_debug('executing: ' . $this->function->classname . "/" . $this->function->methodname);
        $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), array_values($params));
    }
}

/**
 * Delete all service and external functions information defined in the specified component.
 * @param string $component name of component (mahara, local, etc.)
 * @return void
 */
function external_delete_descriptions($component) {

    $params = array($component);

    delete_records_select('external_services_users', "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
    delete_records_select('external_tokens', "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
    delete_records_select('oauth_server_token', "osr_id_ref IN (SELECT id FROM {oauth_server_registry} WHERE externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?))", $params);
    delete_records_select('oauth_server_config', "oauthserverregistryid IN (SELECT id FROM {oauth_server_registry} WHERE externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?))", $params);
    delete_records_select('oauth_server_registry', "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
    delete_records_select(
        'external_services_functions',
        "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)"
            . " OR functionname IN (SELECT name FROM {external_functions} WHERE component = ?)",
        array($component, $component)
    );
    delete_records('external_services', 'component', $component);
    delete_records('external_functions', 'component', $component);
}

/**
 * The web services cron callback
 * clean out the old records that are N seconds old
 */
function webservice_clean_webservice_logs() {
    $LOG_AGE = 8 * 24 * 60 * 60; // 8 days
    delete_records_select('external_services_logs', 'timelogged < ?', array(time() - $LOG_AGE));
}

/**
 * Reload the webservice descriptions for all plugins
 *
 * @return bool true = success
 */

function external_reload_webservices() {

    // first - prune all components that are nolonger available/installed
    $dead_components = get_records_sql_array(
        'SELECT DISTINCT component AS component
        FROM {external_functions}
        WHERE
            component != \'\'
            AND component NOT IN ('.
                implode(', ', array_fill(1, count(get_ws_subsystems()), '?'))
        .')',
        get_ws_subsystems()
    );
    if ($dead_components) {
        foreach ($dead_components as $component) {
            external_delete_descriptions($component->component);
        }
    }
    foreach (get_ws_subsystems() as $component) {
        external_reload_component($component);
    }

    return true;
}

/**
 * Utility function to load up the $services and $functions arrays
 * from the services.php file for the specified component. (Calling it
 * from its own function in order to avoid polluting the namespace.)
 *
 * @param string $component
 * @return array [$services, $functions]
 */
function webservice_load_services_file($component) {

    $wsdir = webservice_component_ws_directory(
        $component,
        $plugintype,
        $pluginname
    );

    if ($plugintype && $pluginname) {
        // Standard plugin; can use safe_require
        $file = safe_require(
            $plugintype,
            $pluginname,
            WEBSERVICE_DIRECTORY . '/services.php',
            'include',
            true,
            array('services', 'functions')
        );
        $services = $functions = null;
        if ($file) {
            $services = $file['services'];
            $functions = $file['functions'];
        }
    }
    else {
        // Not a plugin, must handle manually
        $filepath = get_config('docroot') . $wsdir . '/services.php';
        if (file_exists($filepath)) {
            include($filepath);
        }
    }

    if (empty($services)) {
        $services = array();
    }
    if (empty($functions)) {
        $functions = array();
    }
    return array(
        'services' => $services,
        'functions' => $functions
    );
}

/**
 * Reload the webservice descriptions for a single plugins
 *
 * @param string $component ("webservice", "local", or a plugin e.g.
 * "artefact/internal".
 * @return bool Whether or not we found webservices for this component
 */
function external_reload_component($component) {

    // Load arrays $services and $functions from the plugin or component's
    // {path_to_plugin}/webservice/services.php file.
    $result = webservice_load_services_file($component);
    $services = $result['services'];
    $functions = $result['functions'];

    // Does the component have a valid services.php file?
    if (!$services && !$functions) {
        external_delete_descriptions($component);
        return false;
    }

    // update all function first
    $dbfunctions = get_records_array('external_functions', 'component', $component);
    if (!empty($dbfunctions)) {
        foreach ($dbfunctions as $dbfunction) {
            if (empty($functions[$dbfunction->name])) {
                // the functions is nolonger available for use
                delete_records('external_services_functions', 'functionname', $dbfunction->name);
                delete_records('external_functions', 'id', $dbfunction->id);
                continue;
            }
            $function = $functions[$dbfunction->name];
            unset($functions[$dbfunction->name]);
            // Fill in default classpath
            if (empty($function['classpath'])) {
                if ($component === WEBSERVICE_DIRECTORY) {
                    $function['classpath'] = WEBSERVICE_DIRECTORY;
                }
                else {
                    $function['classpath'] = $component . '/' . WEBSERVICE_DIRECTORY;
                }
            }

            $update = false;
            if ($dbfunction->classname != $function['classname']) {
                $dbfunction->classname = $function['classname'];
                $update = true;
            }
            if ($dbfunction->methodname != $function['methodname']) {
                $dbfunction->methodname = $function['methodname'];
                $update = true;
            }
            if ($dbfunction->classpath != $function['classpath']) {
                $dbfunction->classpath = $function['classpath'];
                $update = true;
            }
            if (isset($function['hasconfig']) && $dbfunction->hasconfig != $function['hasconfig']) {
                $dbfunction->hasconfig = $function['hasconfig'];
                $update = true;
            }
            else if (!isset($function['hasconfig']) && $dbfunction->hasconfig !== 0) {
                $dbfunction->hasconfig = 0;
                $update = true;
            }
            if ($update) {
                update_record('external_functions', $dbfunction);
            }
        }
    }

    foreach ($functions as $fname => $function) {
        $dbfunction = new stdClass();
        $dbfunction->name       = $fname;
        $dbfunction->classname  = $function['classname'];
        $dbfunction->methodname = $function['methodname'];
        $dbfunction->classpath  = empty($function['classpath']) ? null : $function['classpath'];
        $dbfunction->component  = $component;
        $dbfunction->hasconfig  = isset($function['hasconfig']) ? $function['hasconfig'] : 0;
        $dbfunction->id = insert_record('external_functions', $dbfunction);
    }
    unset($functions);

    // now deal with services
    $dbservices = get_records_array('external_services', 'component', $component);

    if (!empty($dbservices)) {
        foreach ($dbservices as $dbservice) {
            if (empty($services[$dbservice->name])) {
                delete_records('external_services_functions', 'externalserviceid', $dbservice->id);
                delete_records('external_services_users', 'externalserviceid', $dbservice->id);
                delete_records('external_tokens', 'externalserviceid', $dbservice->id);
                delete_records_select('oauth_server_token', "osr_id_ref IN (SELECT id FROM {oauth_server_registry} WHERE externalserviceid = ?)", array($dbservice->id));
                delete_records_select('oauth_server_registry', "externalserviceid = ?", array($dbservice->id));
                delete_records('external_services', 'id', $dbservice->id);
                continue;
            }
            $service = $services[$dbservice->name];
            unset($services[$dbservice->name]);
            $service['enabled'] = empty($service['enabled']) ? 0 : $service['enabled'];
            $service['restrictedusers'] = ((isset($service['restrictedusers']) && $service['restrictedusers'] == 1) ? 1 : 0);
            $service['tokenusers'] = ((isset($service['tokenusers']) && $service['tokenusers'] == 1) ? 1 : 0);
            $service['shortname'] = (isset($service['shortname']) ? $service['shortname'] : '');

            $update = false;
            if ($dbservice->enabled != $service['enabled']) {
                $dbservice->enabled = $service['enabled'];
                $update = true;
            }
            if ($dbservice->restrictedusers != $service['restrictedusers']) {
                $dbservice->restrictedusers = $service['restrictedusers'];
                $update = true;
            }
            if ($dbservice->tokenusers != $service['tokenusers']) {
                $dbservice->tokenusers = $service['tokenusers'];
                $update = true;
            }
            if ($dbservice->shortname !== $service['shortname']) {
                $dbservice->shortname = $service['shortname'];
                $update = true;
            }
            // Optional "apiversion" field, to let webservice clients adapt gracefully to changes
            // in a service over time.
            $libApiVersion = (int)(isset($service['apiversion']) ? $service['apiversion'] : false);
            if ($dbservice->apiversion !== $libApiVersion) {
                $dbservice->apiversion = $libApiVersion;
                $update = true;
            }
            if ($update) {
                $dbservice->mtime = db_format_timestamp(time());
                update_record('external_services', $dbservice);
            }

            $functions = get_records_array('external_services_functions', 'externalserviceid', $dbservice->id);
            if (!empty($functions)) {
                foreach ($functions as $function) {
                    $key = array_search($function->functionname, $service['functions']);
                    if ($key === false) {
                        delete_records('external_services_functions', 'id', $function->id);
                    }
                    else {
                        unset($service['functions'][$key]);
                    }
                }
            }
            foreach ($service['functions'] as $fname) {
                $newf = new stdClass();
                $newf->externalserviceid = $dbservice->id;
                $newf->functionname      = $fname;
                insert_record('external_services_functions', $newf);
            }
            unset($functions);
        }
    }
    foreach ($services as $name => $service) {
        $dbservice = new stdClass();
        $dbservice->name               = $name;
        $dbservice->shortname          = (isset($service['shortname']) ? $service['shortname'] : '');
        $dbservice->enabled            = empty($service['enabled']) ? 0 : $service['enabled'];
        $dbservice->restrictedusers    = ((isset($service['restrictedusers']) && $service['restrictedusers'] == 1) ? 1 : 0);
        $dbservice->tokenusers         = ((isset($service['tokenusers']) && $service['tokenusers'] == 1) ? 1 : 0);
        $dbservice->component          = $component;
        $dbservice->apiversion         = (isset($service['apiversion']) ? $service['apiversion'] : null);
        $dbservice->ctime              = db_format_timestamp(time());
        $dbservice->mtime = $dbservice->ctime;
        $dbservice->id = insert_record('external_services', $dbservice, 'id', true);
        foreach ($service['functions'] as $fname) {
            $newf = new stdClass();
            $newf->externalserviceid = $dbservice->id;
            $newf->functionname      = $fname;
            insert_record('external_services_functions', $newf);
        }
    }

    return true;
}

/**
 * General System type Exception class for errors thrown inside the core
 * web service handling code
 */
class WebserviceException extends MaharaException {

    public $errorcode = null;

    /**
     * Constructor
     *
     * @param string $errorcode The name of the string to print
     * @param string $debuginfo optional debugging information
     * @param integer $errornumber A numerical identifier for the error (optional)
     */
    function __construct($errorcode = null, $debuginfo = '', $errornumber = null) {
        $this->errorcode = rtrim($errorcode, '0123456789');

        if (string_exists($errorcode, 'auth.webservice')) {
            $count = count_string_args($errorcode, 'auth.webservice');
            if ($count) {
                $message = get_string($errorcode, 'auth.webservice', $debuginfo);
            }
            else {
                $message = get_string($errorcode, 'auth.webservice');
            }
        }
        else {
            $count = 0;
            $message = $errorcode;
        }

        if ($debuginfo && !$count) {
            $message .= ' : ' . $debuginfo;
        }

        // In 15.04-16.04, the third parameter to this constructor was
        // documented as an object. Nothing was done with this object,
        // so it's unlikely that changing it broke anything. But just
        // in case, make sure that this param, if provided, is cast
        // to an integer.
        if ($errornumber !== null) {
            $errornumber = (int) $errornumber;
        }

        parent::__construct($message, $errornumber);
    }

    public function get_error_name() {
        // Return the error lang string identifier. Trim off any integers
        // from the end of it, in case we've added one in to notify
        // translators of a change in the translated string
        return $this->errorcode;
    }
 }

/**
 * Web service parameter exception class
 *
 * This exception must be thrown to the web service client when a web service parameter is invalid
 * The error string is gotten from webservice.php
 */
class WebserviceParameterException extends WebserviceException {}

/**
 * Exception indicating programming error, must be fixed by a programer. For example
 * a core API might throw this type of exception if a plugin calls it incorrectly.
 */
class WebserviceCodingException extends WebserviceException {
    /**
     * Constructor
     *
     * @param string $debuginfo optional debugging information
     */
    function __construct($debuginfo='') {
        parent::__construct('codingerror', $debuginfo);
    }
}

/**
 * Exception indicating malformed parameter problem.
 * This exception is not supposed to be thrown when processing
 * user submitted data in forms. It is more suitable
 * for WS and other low level stuff.
 */
class WebserviceInvalidParameterException extends WebserviceException {
    /**
     * Constructor
     *
     * @param string $debuginfo some detailed information
     */
    function __construct($debuginfo='') {
        parent::__construct('invalidparameter', $debuginfo);
    }
}

/**
 * Exception indicating malformed response problem.
 * This exception is not supposed to be thrown when processing
 * user submitted data in forms. It is more suitable
 * for WS and other low level stuff.
 */
class WebserviceInvalidResponseException extends WebserviceException {
    /**
     * Constructor
     *
     * @param string $debuginfo some detailed information
     */
    function __construct($debuginfo='') {
        parent::__construct('invalidresponse', $debuginfo);
    }
}

/**
 * Exception indicating access control problem in web service call
 */
class WebserviceAccessException extends WebserviceException {
    /**
     * Constructor
     *
     * @param string $debuginfo some detailed information
     */
    function __construct($debuginfo='') {
        parent::__construct('accessexception', $debuginfo);
    }
}

/**
 * Exception indicating missing file problem in web service call
 */
class WebserviceFileNotFoundException extends WebserviceException {
    /**
     * Constructor
     *
     * @param string $debuginfo some detailed information
     */
    function __construct($debuginfo='') {
        parent::__construct('filenotfoundexception', $debuginfo);
    }
}

/**
 * Process the logged-in user's REST-based request for a webservices token.
 * Checks whether the user has permission to self-generate a token for the
 * requested service group. Then it issues a new token, or retrieves an
 * existing one if the user already has an applicable token.
 *
 * @param string $serviceshortname Shortname of the desired service group
 * @param string $servicecomponent The service group's component
 * @param string $clientname (Optional) Human-readable name of client program using this token
 * @param string $clientenv (Optional) Human-readable description of device/environment for client
 * @param string $clientguid (Optional) Unique identifier for the client program
 * @throws WebserviceException
 * @return string The token generated
 */
function webservice_user_token_selfservice($serviceshortname, $servicecomponent, $clientname, $clientenv, $clientguid) {
    global $USER;
    // TODO: more granular access controls: Is this user allowed to access webservices at all?

    // From here, we know that the user has at least logged in, so we can
    // expose a little bit more information in the error responses.
    $service = get_record('external_services', 'shortname', $serviceshortname, 'component', $servicecomponent);
    if (empty($service)) {
        // will throw exception if no token found
        throw new WebserviceException(
            'servicenotfound',
            "No service group found with name $servicecomponent/$serviceshortname",
            400
        );
    }
    else if (!$service->enabled) {
        throw new WebserviceException(
            'servicenotenabled',
            'Requested service group is disabled.',
            501
        );
    }

    // TODO: more granular access controls: Is this user allowed to access this particular service group?

    //specific checks related to user restricted service
    if ($service->restrictedusers) {
        $authoriseduser = get_record(
            'external_services_users',
            'externalserviceid', $service->id,
            'userid', $USER->get('id')
        );

        if (empty($authoriseduser)) {
            throw new WebserviceException(
                'usernotauthorised',
                'This service is restricted to authorized users only.',
                403
            );
        }

        if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) {
            throw new WebserviceException(
                'userauthorisationexpired',
                'Your access rights to this service have expired.',
                403
            );
        }

        require_once(get_config('docroot') . 'webservice/libs/net.php');
        if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) {
            throw new WebserviceException(
                'ipnotauthorised',
                'This service is restricted to authorized IP ranges only.',
                403
            );
        }
    }

    // Check if a token has already been created for this user and this service
    $tokensql = "SELECT t.id, t.sid, t.token, t.validuntil, t.iprestriction
          FROM {external_tokens} t
         WHERE t.userid = ? AND t.externalserviceid = ? AND t.tokentype = ?";

    $tokenparams = array(
        $USER->get('id'),
        $service->id,
        EXTERNAL_TOKEN_USER
    );

    // Client specified a GUID; so only re-use that same token.
    if ($clientname || $clientguid) {
        $tokensql .= ' AND clientname = ? AND clientguid = ? ';
        $tokenparams[] = $clientname;
        $tokenparams[] = $clientguid;
    }

    $tokensql .= ' ORDER BY t.ctime ASC';
    $tokens = get_records_sql_array($tokensql, $tokenparams);
    if (!$tokens) {
        $tokens = array();
    }
    //A bit of sanity checks
    foreach ($tokens as $key=>$token) {

        /// Checks related to a specific token. (script execution continue)
        $unsettoken = false;

        // Take this opportunity to delete expired tokens
        // (similar logic to the web service servers
        //    /webservice/lib.php/webservice_server::authenticate_by_token())
        if (!empty($token->validuntil) and $token->validuntil < time()) {
            delete_records('external_tokens', 'id', $token->id);
            $unsettoken = true;
        }

        // remove token if its ip not in whitelist
        if (isset($token->iprestriction) and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
            $unsettoken = true;
        }

        if ($unsettoken) {
            unset($tokens[$key]);
        }
    }

    // if some valid tokens exist then use the most recent
    if (count($tokens) > 0) {
        // Retrieve an existing token
        $token = array_pop($tokens);
        // log token access
        set_field(
            'external_tokens',
            'mtime',
            db_format_timestamp(time()),
            'id',
            $token->id
        );
        $token = $token->token;
    }
    else {
        // Generate a new token
        // If you wanted to separately restrict the ability to *generate*
        // a token, (as opposed to just retrieving one), this would be the
        // place to do it.
        $token = webservice_generate_token(
            EXTERNAL_TOKEN_USER,
            $service,
            $USER->get('id'), // token user
            null, // institution
            (time() + EXTERNAL_TOKEN_USER_EXPIRES), //expiration
            null, // iprestriction
            $clientname,
            $clientenv,
            $clientguid
        );
    }

    return $token;
}
