���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/auth.tar
���ѧ٧ѧ�
shibboleth/db/upgrade.php 0000644 00000005376 15152311434 0011426 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Shibboleth authentication plugin upgrade code * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Function to upgrade auth_shibboleth. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_shibboleth_upgrade($oldversion) { global $CFG, $DB, $OUTPUT; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2021052501) { // The 'Data modification API' setting in the Shibboleth authentication plugin can no longer be configured // to use files located within the site data directory, as it exposes the site to security risks. Therefore, // we need to find every existing case and reset the 'Data modification API' setting to its default value. $convertdataconfig = get_config('auth_shibboleth', 'convert_data'); if (preg_match('/' . preg_quote($CFG->dataroot, '/') . '/', realpath($convertdataconfig))) { set_config('convert_data', '', 'auth_shibboleth'); $warn = 'Your \'Data modification API\' setting in the Shibboleth authentication plugin is currently configured to use a file located within the current site data directory ($CFG->dataroot). You are no longer able to use files from within this directory for this purpose as it exposes your site to security risks. This setting has been reset to its default value. Please reconfigure it by providing a path to a file which is not located within the site data directory.'; echo $OUTPUT->notification($warn, 'notifyproblem'); } upgrade_plugin_savepoint(true, 2021052501, 'auth', 'shibboleth'); } // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } shibboleth/db/install.php 0000644 00000000113 15152311434 0011425 0 ustar 00 <?php function xmldb_auth_shibboleth_install() { global $CFG, $DB; } shibboleth/logout.php 0000644 00000011223 15152311434 0010707 0 ustar 00 <?php // Implements logout for Shibboleth authenticated users according to: // - https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPLogoutInitiator // - https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPNotify require_once("../../config.php"); require_once($CFG->dirroot."/auth/shibboleth/auth.php"); $action = optional_param('action', '', PARAM_ALPHA); $redirect = optional_param('return', '', PARAM_URL); // Find out whether host supports https $protocol = 'http://'; if (is_https()) { $protocol = 'https://'; } // If the shibboleth plugin is not enable, throw an exception. if (!is_enabled_auth('shibboleth')) { throw new moodle_exception(get_string('pluginnotenabled', 'auth', 'shibboleth')); } // Front channel logout. $inputstream = file_get_contents("php://input"); if ($action == 'logout' && !empty($redirect)) { if (isloggedin($USER) && $USER->auth == 'shibboleth') { // Logout user from application. require_logout(); } // Finally, send user to the return URL. redirect($redirect); } else if (!empty($inputstream)) { // Back channel logout. // Set SOAP header. $server = new SoapServer($protocol.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].'/LogoutNotification.wsdl'); $server->addFunction("LogoutNotification"); $server->handle(); } else { // Return WSDL. header('Content-Type: text/xml'); echo <<<WSDL <?xml version ="1.0" encoding ="UTF-8" ?> <definitions name="LogoutNotification" targetNamespace="urn:mace:shibboleth:2.0:sp:notify" xmlns:notify="urn:mace:shibboleth:2.0:sp:notify" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns="http://schemas.xmlsoap.org/wsdl/"> <!-- This page either has to be called with the GET arguments 'action' and 'return' via a redirect from the Shibboleth Service Provider logout handler (front-channel logout) or via a SOAP request by a Shibboleth Service Provider (back-channel logout). Because neither of these two variants seems to be the case, the WSDL file for the web service is returned. For more information see: - https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPLogoutInitiator - https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPNotify --> <types> <schema targetNamespace="urn:mace:shibboleth:2.0:sp:notify" xmlns="http://www.w3.org/2000/10/XMLSchema" xmlns:notify="urn:mace:shibboleth:2.0:sp:notify"> <simpleType name="string"> <restriction base="string"> <minLength value="1"/> </restriction> </simpleType> <element name="OK" type="notify:OKType"/> <complexType name="OKType"> <sequence/> </complexType> </schema> </types> <message name="getLogoutNotificationRequest"> <part name="SessionID" type="notify:string" /> </message> <message name="getLogoutNotificationResponse" > <part name="OK"/> </message> <portType name="LogoutNotificationPortType"> <operation name="LogoutNotification"> <input message="getLogoutNotificationRequest"/> <output message="getLogoutNotificationResponse"/> </operation> </portType> <binding name="LogoutNotificationBinding" type="notify:LogoutNotificationPortType"> <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/> <operation name="LogoutNotification"> <soap:operation soapAction="urn:xmethods-logout-notification#LogoutNotification"/> </operation> </binding> <service name="LogoutNotificationService"> <port name="LogoutNotificationPort" binding="notify:LogoutNotificationBinding"> <soap:address location="{$protocol}{$_SERVER['HTTP_HOST']}{$_SERVER['PHP_SELF']}"/> </port> </service> </definitions> WSDL; exit; } /******************************************************************************/ /** * Handles SOAP Back-channel logout notification * * @param string $spsessionid SP-provided Shibboleth Session ID * @return SoapFault or void if everything was fine */ function LogoutNotification($spsessionid) { $sessionclass = \core\session\manager::get_handler_class(); switch ($sessionclass) { case '\core\session\file': return \auth_shibboleth\helper::logout_file_session($spsessionid); case '\core\session\database': return \auth_shibboleth\helper::logout_db_session($spsessionid); default: throw new moodle_exception("Shibboleth logout not implemented for '$sessionclass'"); } // If no SoapFault was thrown, the function will return OK as the SP assumes. } shibboleth/templates/login_form.mustache 0000644 00000012663 15152311434 0014562 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template auth_shibboleth/login_form Template for the Shibboleth authentication plugin's login form. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * adminemail String The Administrator's email address. * cansignup Boolean Whether a new user can sign up for an account. * guestlogin Boolean Whether to show the guest login section. * guestloginurl String The URL for guest login. * idps Array The list of identity providers for the Shibboleth authentication plugin in value-name pairs per IDP. * instructions String Signup instructions. * isvalid Boolean Whether form validation passes. * loginname String The custom login name. * logintoken String The login token. * loginurl String The login URL. * showinstructions Boolean Whether to show additional login instructions. * signupurl String The signup URL. Example context (json): { "loginurl": "#", "guestloginurl": "#", "guestlogin": true, "idps": [ { "value": 1, "name": "IDP 1" }, { "value": 2, "name": "IDP 2", "selected": true }, { "value": 3, "name": "IDP 3" } ], "showinstructions": true, "logintoken": "abcde", "adminemail": "admin@example.com", "loginname": "Shib auth", "cansignup": true, "signupurl": "#", "instructions": "Sign up here", "isvalid": false } }} <div class="my-1 my-sm-5"></div> <div class="container"> <div class="card"> <h2 class="card-header"> {{#loginname}}{{.}}{{/loginname}} {{^loginname}}{{#str}}auth_shibboleth_login_long, auth_shibboleth{{/str}}{{/loginname}} </h2> <div class="card-body"> <div class="row justify-content-center ml-1 mr-1 mb-1"> <div class="col-md-5"> <form action="{{loginurl}}" method="post" id="login"> <div class="form-group"> <label for="idp">{{#str}}auth_shibboleth_select_organization, auth_shibboleth{{/str}}</label> <select id="idp" name="idp" class="form-control input-block-level {{^isvalid}}is-invalid{{/isvalid}}"> <option value="-">{{#str}}auth_shibboleth_select_member, auth_shibboleth{{/str}}</option> {{#idps}} <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option> {{/idps}} </select> <div class="invalid-feedback text-danger mb-1" {{#isvalid}}hidden{{/isvalid}}> {{#str}}auth_shibboleth_errormsg, auth_shibboleth{{/str}} </div> </div> <button type="submit" class="btn btn-primary btn-block mb-1" accesskey="s"> {{#str}}select, moodle{{/str}} </button> <p class="form-text text-muted mt-1 mb-1"> {{#str}}auth_shib_contact_administrator, auth_shibboleth, {{adminemail}}{{/str}} </p> </form> </div> {{#guestlogin}} <div class="col-md-5"> <p> {{#str}}someallowguest, moodle{{/str}} </p> <form action="{{guestloginurl}}" method="post" id="guestlogin"> <div class="guestform"> <input type="hidden" name="logintoken" value="{{logintoken}}"> <input type="hidden" name="username" value="guest"> <input type="hidden" name="password" value="guest"> <button type="submit" class="btn btn-secondary btn-block"> {{#str}}loginguest, moodle{{/str}} </button> </div> </form> </div> {{/guestlogin}} </div> </div> </div> {{#showinstructions}} <div class="card mt-1"> <div class="card-body ml-1 mr-1 mb-1"> <h2 class="card-title">{{#str}}firsttime, moodle{{/str}}</h2> <p> {{{instructions}}} </p> {{#cansignup}} <form action="{{signupurl}}" method="get" id="signup"> <button type="submit" class="btn btn-secondary">{{#str}}startsignup, moodle{{/str}}</button> </form> {{/cansignup}} </div> </div> {{/showinstructions}} </div> shibboleth/README.txt 0000644 00000040766 15152311434 0010401 0 ustar 00 Shibboleth Authentication for Moodle ------------------------------------------------------------------------------- Requirements: - Shibboleth Service Provider 1.3 or newer. Recommended is 2.1 or newer. See documentation for your Shibboleth federation on how to set up Shibboleth. Changes: - 11. 2004: Created by Markus Hagman - 05. 2005: Modifications to login process by Martin Dougiamas - 05. 2005: Various extensions and fixes by Lukas Haemmerle - 06. 2005: Adaptions to new field locks and plugin config structures by Martin Langhoff and Lukas Haemmerle - 10. 2005: Added better error messages and moved text to language directories - 02. 2006: Simplified authentication so that authorization works properly Added instructions for IIS - 11. 2006: User capabilities are now loaded properly as of Moodle 1.7+ - 03. 2007: Adapted authentication method to Moodle 1.8 - 07. 2007: Fixed a but that caused problems with uppercase usernames - 10. 2007: Removed the requirement for email address, surname and given name attributes on request of Markus Hagman - 11. 2007: Integrated WAYF Service in Moodle - 12. 2008: Shibboleth 2.x and Single Logout support added - 1. 2008: Added logout hook and moved Shibboleth config strings to utf8 auth language files. - 3. 2009: Added various improvements and bug fixes reported by Ina M�ller from university Tuebingen and Peter Ellis of University of Washington - 4. 2009: Added another requirement for logout regarding the call back script - 6. 2009: Changed handler URL when integrated Discovery Service is used - 10. 2009: Fixed HTML entity preservation in Shibboleth settings Moodle Configuration with Dual login ------------------------------------------------------------------------------- 1. Protect the directory moodle/auth/shibboleth/index.php with Shibboleth. The page index.php in that directory actually logs in a Shibboleth user. For Apache you have to define a rule like the following in the Apache config: -- <Directory /path/to/moodle/auth/shibboleth/index.php> AuthType shibboleth ShibRequireSession On require valid-user </Directory> -- To restrict access to Moodle, replace the access rule 'require valid-user' with something that fits your needs, e.g. 'require affiliation student'. For IIS you have protect the auth/shibboleth directory directly in the RequestMap of the Shibboleth configuration file (shibboleth.xml or shibboleth2.xml). -- <Path name="moodle" requireSession="false" > <Path name="auth/shibboleth/index.php" requireSession="true" > <AccessControl> ... </AccessControl> </Path> </Path> -- Also see: https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPRequestMapper and https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPAccessControl 2. As Moodle admin, go to the 'Administrations >> Users >> Authentication' and click on the the 'Shibboleth' settings. 3. Fill in the fields of the form. The fields 'Username', 'First name', 'Surname', etc. should contain the name of the environment variables of the Shibboleth attributes that you want to map onto the corresponding Moodle variable (e.g. 'Shib-Person-surname' for the person's last name, refer the Shibboleth documentation or the documentation of your Shibboleth federation for information on which attributes are available). Especially the 'Username' field is of great importance because this attribute is used for the Moodle authentication of Shibboleth users. ############################################################################# Shibboleth Attributes needed by Moodle: For Moodle to work properly Shibboleth should at least provide the attribute that is used as username in Moodle. It has to be unique for all Shibboleth Be aware that Moodle converts the username to lowercase. So, the overall behaviour of the username will be case-insensitive. All attributes used for moodle must obey a certain length, otherwise Moodle cuts off the ends. Consult the Moodle documentation for further information on the maximum lengths for each field in the user profile. ############################################################################# 4.a If you want Shibboleth as your only authentication method with an external Where Are You From (WAYF) Service , set the 'Alternate Login URL' in the 'Common settings' in 'Administrations >> Users >> Authentication Options' to the the URL of the file 'moodle/auth/shibboleth/index.php'. This will enforce Shibboleth login. 4.b If you want to use the Moodle integrated WAYF service, you have to activate it in the Moodle Shibboleth authentication settings by checking the 'Moodle WAYF Service' checkbox and providing a list of entity IDs in the 'Identity Providers' textarea together with a name and an optional SessionInitiator URL, which usually is an absolute or relative URL pointing to the same host. If no SessionInitiator URL is given, the default one '/Shibboleth.sso' (only works for Shibboleth 1.3.x) will be used. For Shibboleth 2.x you have to add '/Shibboleth.sso/DS' as a SessionInitiator. Also see https://wiki.shibboleth.net/confluence/display/SHIB/SessionInitiator and https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPSessionInitiator Important Note: If you upgraded from a previous version of Moodle and now want to use the integrated WAYF, you have to make sure that in step 1 only the index.php script in moodle/auth/shibboleth/ is protected but *not* the other scripts and especially not the login.php script. If you were using the integrated WAYF alread with Shibboleth 1.3, it could be that the integrated WAYF is not working anymore after you updated Moodle. The reason is that the implicitly set default SessionInitiator changed in Moodle as well as in Shibboleth. For Shibboleth 1.3 one therefore has to add /Shibboleth.sso as third parameter whereas this is /Shibboleth.sso/DS for Shibboleth 2.x. 5. Save the changes for the 'Shibboleth settings'. Important Note: If you went for 4.b (integrated WAYF service), saving the settings will overwrite the Moodle Alternate Login URL using the Moodle web root URL. 6. If you want to use Shibboleth in addition to another authentication method not using the integrated WAYF service from 4.b, change the 'Instructions' in 'Administrations >> Users >> Manage authentication' to contain a link to the moodle/auth/shibboleth/index.php file which is protected by Shibboleth (see step 1.) and causes the Shibboleth login procedure to start. You can also use HTML code in that field, e.g. to include an image as a Shibboleth login button. Note: As of now you cannot use dual login together with the integrated WAYF service provided by Moodle (4.b). 7. Save the authentication changes. How the Shibboleth authentication works -------------------------------------------------------------------------------- To get Shibboleth authenticated in Moodle a user basically must access the Shibboleth-protected page /auth/shibboleth/index.php. If Shibboleth is the only authentication method (see 4.a), this happens automatically when a user selects his home organization in the Moodle WAYF service or if the alternate login URL is configured to be the protected /auth/shibboleth/index.php Otherwise, the user has to click on the link on the dual login page you provided in step 5.b. Moodle basically checks whether the Shibboleth attribute that you mapped as the username is present. This attribute should only be present if a user is Shibboleth authenticated. If the user's Moodle account has not existed yet, it gets automatically created. To prevent that every Shibboleth user can access your Moodle site you have to adapt the 'require valid-user' line in your webserver's config (see step 1) to allow only specific users. If you defined some authorization rules in step 1, these are checked by Shibboleth itself. Only users who met these rules actually can access /auth/shibboleth/index.php and get logged in. You can use Shibboleth AND another authentication method (it was tested with manual login). So, if there are a few users that don't have a Shibboleth login, you could create manual accounts for them and they could use the manual login. For other authentication methods you first have to configure them and then set Shibboleth as your authentication method. Users can log in only via one authentication method unless they have two accounts in Moodle. Shibboleth dual login with custom login page -------------------------------------------------------------------------------- You can create a dual login page that better fits your needs. For this to work, you have to set up the two authentication methods (e.g. 'Manual Accounts' and 'Shibboleth') and specify an alternate login link to your own dual login page. On that page you basically need a link to the Shibboleth-protected page ('/auth/shibboleth/index.php') for the Shibboleth login and a form that sends 'username' and 'password' to moodle/login/index.php. Set this web page then als alternate login page. Consult the Moodle documentation for further instructions and requirements. How to customize the way the Shibboleth user data is used in Moodle -------------------------------------------------------------------------------- Among the Shibboleth settings in Moodle there is a field that should contain a path to a php file that can be used as data manipulation hook. You can use this if you want to further process the way your Shibboleth attributes are used in Moodle. Due to security reasons this file cannot be located within the current site data directory ($CFG->dataroot). Example 1: Your Shibboleth federation uses an attribute that specifies the user's preferred language, but the content of this attribute is not compatible with the Moodle data representation, e.g. the Shibboleth attribute contains 'German' but Moodle needs a two letter value like 'de'. Example 2: The country, city and street are provided in one Shibboleth attribute and you want these values to be used in the Moodle user profile. So You have to parse the corresponding attribute to fill the user fields. If you want to use this hook you have to be a skilled PHP programmer. It is strongly recommended that you take a look at the file moodle/auth/shibboleth/auth.php, especially the function 'get_userinfo' where this file is included. The context of the file is the same as within this login function. So you can directly edit the object $result. Example file: -- <?php // Set the zip code and the adress if ($_SERVER[$this->config->field_map_address] != '') { // $address contains something like 'SWITCH$Limmatquai 138$CH-8021 Zurich' // We want to split this up to get: // institution, street, zipcode, city and country $address = $_SERVER[$this->config->field_map_address]; list($institution, $street, $zip_city) = explode('$', $address); preg_match('/ (.+)/', $zip_city, $regs); $city = $regs[1]; preg_match('/(.+)-/',$zip_city, $regs); $country = $regs[1]; $result["address"] = $street; $result["city"] = $city; $result["country"] = $country; $result["department"] = $institution; $result["description"] = "I am a Shibboleth user"; } ?> -- How to upgrade your Service Provider to 2.x ------------------------------------------------------------------------------- In case your upgrade your Service Provider 1.3.x to 2.x, be aware of the fact that in version 2.0 the default behaviour regarding attribute propagation changed. While the Service Provider 1.3.x published the Shibboleth attributes to the web server environment as HTTP Request headers, the Service Provider 2.x publishes attributes as environment variables, which increases the security for some platforms. However, this change has the effect that the attribute names change. E.g. while the surname attribute was published as 'HTTP_SHIB_PERSON_SURNAME' with 1.3.x, this attribute will be available in $_SERVER['Shib-Person-surname'] or depending on your /etc/shibboleth/attribute-map.xml file just as $_SERVER['sn']. Because Moodle needs to know what Shibboleth attributes it shall map onto which Moodle user profile field, one has to make sure the mapping is updated as well after the Service Provider upgrade. ******************************************************************************** Because you risk locking yourself out of Moodle it is strongly recommended to use the following approach when upgrading the Service Provider: 1. Enable manual authentication before the upgrade. 2. Make sure that you have at least one manual account with administration privileges working before upgrading your Service Provider to 2.x. 3. After the SP upgrade, use this account to log into Moodle and adapt the attribute mapping in 'Site Administration -> Users -> Shibboleth' to reflect the changed attribute names. You find the attribute names in the file /etc/shibboleth/attribute-map.xml listed as the 'id' value of an attribute definition. 4. If you are using the integrated WAYF, you may have to set the third parameter of each entry to '/Shibboleth.sso/DS' 5. Test the login with a Shibboleth account 6. If all is working, disable manual authentication again ******************************************************************************** How to add logout support -------------------------------------------------------------------------------- In order make Moodle support Shibboleth logout, one has to make the Shibboleth Service Provider (SP) aware of the Moodle logout capability. Only then the SP can trigger Moodle's front or back channel logout handler. To make the SP aware of the Moodle logout, you have to add the following to the Shibboleth main configuration file shibboleth2.xml (usually in /etc/shibboleth/) just before the <MetadataProvider> element. -- <Notify Channel="back" Location="https://#YOUR_MOODLE_HOSTNAME#/moodle/auth/shibboleth/logout.php" /> -- Then restart the Shibboleth daemon and check the log file for errors. If there were no errors, you can test the logout feature by accessing Moodle, authenticating via Shibboleth and the access the URL: #YOUR_MOODLE_HOSTNAME#/Shibboleth.sso/Logout (assuming you have a standard Shibboleth installation). If everything worked well, you should see a Shibboleth page saying that you were successfully logged out and if you go back to Moodle you also should be logged out from Moodle. Requirements: - PHP needs the Soap Extension, which maybe must installed manually: More information is available here http://ch.php.net/soap - Logout only works with Shibboleth Service Provider 2.1 or higher - /moodle/auth/shibboleth/logout.php *must not* be protected by Shibboleth! In case all of Moodle is protected with Shibboleth, you have to add something like this to your Apache configuration after all the other require rules -- <Directory /path/to/moodle/auth/shibboleth/logout.php> AuthType shibboleth ShibRequireSession Off require shibboleth </Directory> -- When using IIS, the same can be achieved by something like: -- <Path name="auth/shibboleth/logout.php" requireSession="false" > -- in the shibboleth2.xml RequestMap. Limitations: Single Logout is only supported when SAML2 is used at the SP and the IdP. As of October 2009, the Shibboleth Identity Provider 2.1.4 does not yet support Single Logout (SLO). Therefore, the single logout feature cannot be used yet in a Shibboleth only setup but there may be other SAML2 products that could be used as Identity Provider, e.g. SimpleSAML PHP. One of the reasons why SLO isn't supported yet is because there aren't many applications yet that were adapted to support front and back channel logout. Hopefully, the Moodle logout helps to motivate the developers to implement SLO. On the other hand, the easiest and safest way to log out still is to tell users to quit their web browsers :) Also see https://wiki.shibboleth.net/confluence/display/SHIB2/SLOIssues and https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPLogoutInitiator for some background information on this topic. -------------------------------------------------------------------------------- In case of problems and questions with Shibboleth authentication, contact Lukas Haemmerle <lukas.haemmerle@switch.ch> or Markus Hagman <hagman@hytti.uku.fi> shibboleth/auth.php 0000644 00000032451 15152311434 0010345 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: Shibboleth Authentication * Authentication using Shibboleth. * * Distributed under GPL (c)Markus Hagman 2004-2006 * * @package auth_shibboleth * @author Martin Dougiamas * @author Lukas Haemmerle * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Shibboleth authentication plugin. */ class auth_plugin_shibboleth extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'shibboleth'; $this->config = get_config('auth_shibboleth'); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_shibboleth() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username (with system magic quotes) * @param string $password The password (with system magic quotes) * @return bool Authentication success or failure. */ function user_login($username, $password) { global $SESSION; // If we are in the shibboleth directory then we trust the server var if (!empty($_SERVER[$this->config->user_attribute])) { // Associate Shibboleth session with user for SLO preparation $sessionkey = ''; if (isset($_SERVER['Shib-Session-ID'])){ // This is only available for Shibboleth 2.x SPs $sessionkey = $_SERVER['Shib-Session-ID']; } else { // Try to find out using the user's cookie foreach ($_COOKIE as $name => $value){ if (preg_match('/_shibsession_/i', $name)){ $sessionkey = $value; } } } // Set shibboleth session ID for logout $SESSION->shibboleth_session_id = $sessionkey; return (strtolower($_SERVER[$this->config->user_attribute]) == strtolower($username)); } else { // If we are not, the user has used the manual login and the login name is // unknown, so we return false. return false; } } /** * Returns the user information for 'external' users. In this case the * attributes provided by Shibboleth * * @return array $result Associative array of user data */ function get_userinfo($username) { // reads user information from shibboleth attributes and return it in array() global $CFG; // Check whether we have got all the essential attributes if ( empty($_SERVER[$this->config->user_attribute]) ) { throw new \moodle_exception( 'shib_not_all_attributes_error', 'auth_shibboleth' , '', "'".$this->config->user_attribute."' ('".$_SERVER[$this->config->user_attribute]."'), '". $this->config->field_map_firstname."' ('".$_SERVER[$this->config->field_map_firstname]."'), '". $this->config->field_map_lastname."' ('".$_SERVER[$this->config->field_map_lastname]."') and '". $this->config->field_map_email."' ('".$_SERVER[$this->config->field_map_email]."')"); } $attrmap = $this->get_attributes(); $result = array(); $search_attribs = array(); foreach ($attrmap as $key=>$value) { // Check if attribute is present if (!isset($_SERVER[$value])){ $result[$key] = ''; continue; } // Make usename lowercase if ($key == 'username'){ $result[$key] = strtolower($this->get_first_string($_SERVER[$value])); } else { $result[$key] = $this->get_first_string($_SERVER[$value]); } } // Provide an API to modify the information to fit the Moodle internal // data representation if ( $this->config->convert_data && $this->config->convert_data != '' && is_readable($this->config->convert_data) ) { // Include a custom file outside the Moodle dir to // modify the variable $moodleattributes include($this->config->convert_data); } return $result; } /** * Returns array containg attribute mappings between Moodle and Shibboleth. * * @return array */ function get_attributes() { $configarray = (array) $this->config; $moodleattributes = array(); $userfields = array_merge($this->userfields, $this->get_custom_user_profile_fields()); foreach ($userfields as $field) { if (isset($configarray["field_map_$field"])) { $moodleattributes[$field] = $configarray["field_map_$field"]; } } $moodleattributes['username'] = $configarray["user_attribute"]; return $moodleattributes; } function prevent_local_passwords() { return true; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return false; } /** * Whether shibboleth users can change their password or not. * * Shibboleth auth requires password to be changed on shibboleth server directly. * So it is required to have password change url set. * * @return bool true if there's a password url or false otherwise. */ function can_change_password() { if (!empty($this->config->changepasswordurl)) { return true; } else { return false; } } /** * Get password change url. * * @return moodle_url|null Returns URL to change password or null otherwise. */ function change_password_url() { if (!empty($this->config->changepasswordurl)) { return new moodle_url($this->config->changepasswordurl); } else { return null; } } /** * Hook for login page * */ function loginpage_hook() { global $SESSION, $CFG; // Prevent username from being shown on login page after logout $CFG->nolastloggedin = true; return; } /** * Hook for logout page * */ function logoutpage_hook() { global $SESSION, $redirect; // Only do this if logout handler is defined, and if the user is actually logged in via Shibboleth $logouthandlervalid = isset($this->config->logout_handler) && !empty($this->config->logout_handler); if (isset($SESSION->shibboleth_session_id) && $logouthandlervalid ) { // Check if there is an alternative logout return url defined if (isset($this->config->logout_return_url) && !empty($this->config->logout_return_url)) { // Set temp_redirect to alternative return url $temp_redirect = $this->config->logout_return_url; } else { // Backup old redirect url $temp_redirect = $redirect; } // Overwrite redirect in order to send user to Shibboleth logout page and let him return back $redirecturl = new moodle_url($this->config->logout_handler, array('return' => $temp_redirect)); $redirect = $redirecturl->out(); } } /** * Cleans and returns first of potential many values (multi-valued attributes) * * @param string $string Possibly multi-valued attribute from Shibboleth */ function get_first_string($string) { $list = explode( ';', $string); $clean_string = rtrim($list[0]); return $clean_string; } /** * Test if settings are correct, print info to output. */ public function test_settings() { global $OUTPUT; if (!isset($this->config->user_attribute) || empty($this->config->user_attribute)) { echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth", (new moodle_url('/auth/shibboleth/README.txt'))->out()), 'notifyproblem'); return; } if ($this->config->convert_data and $this->config->convert_data != '' and !is_readable($this->config->convert_data)) { echo $OUTPUT->notification(get_string("auth_shib_convert_data_warning", "auth_shibboleth"), 'notifyproblem'); return; } if (isset($this->config->organization_selection) && empty($this->config->organization_selection) && isset($this->config->alt_login) && $this->config->alt_login == 'on') { echo $OUTPUT->notification(get_string("auth_shib_no_organizations_warning", "auth_shibboleth"), 'notifyproblem'); return; } } /** * Return a list of identity providers to display on the login page. * * @param string $wantsurl The requested URL. * @return array List of arrays with keys url, iconurl and name. */ public function loginpage_idp_list($wantsurl) { $config = get_config('auth_shibboleth'); $result = []; // Before displaying the button check that Shibboleth is set-up correctly. if (empty($config->user_attribute)) { return $result; } $url = new moodle_url('/auth/shibboleth/index.php'); if ($config->auth_logo) { $iconurl = moodle_url::make_pluginfile_url( context_system::instance()->id, 'auth_shibboleth', 'logo', null, null, $config->auth_logo); } else { $iconurl = null; } $result[] = ['url' => $url, 'iconurl' => $iconurl, 'name' => $config->login_name]; return $result; } } /** * Sets the standard SAML domain cookie that is also used to preselect * the right entry on the local way * * @param string $selectedIDP IDP identifier */ function set_saml_cookie($selectedIDP) { if (isset($_COOKIE['_saml_idp'])) { $IDPArray = generate_cookie_array($_COOKIE['_saml_idp']); } else { $IDPArray = array(); } $IDPArray = appendCookieValue($selectedIDP, $IDPArray); setcookie ('_saml_idp', generate_cookie_value($IDPArray), time() + (100*24*3600)); } /** * Generate array of IdPs from Moodle Shibboleth settings * * @param string Text containing tuble/triple of IdP entityId, name and (optionally) session initiator * @return array Identifier of IdPs and their name/session initiator */ function get_idp_list($organization_selection) { $idp_list = array(); $idp_raw_list = explode("\n", $organization_selection); foreach ($idp_raw_list as $idp_line){ $idp_data = explode(',', $idp_line); if (isset($idp_data[2])) { $idp_list[trim($idp_data[0])] = array(trim($idp_data[1]),trim($idp_data[2])); } elseif(isset($idp_data[1])) { $idp_list[trim($idp_data[0])] = array(trim($idp_data[1])); } } return $idp_list; } /** * Generates an array of IDPs using the cookie value * * @param string Value of SAML domain cookie * @return array Identifiers of IdPs */ function generate_cookie_array($value) { // Decodes and splits cookie value $CookieArray = explode(' ', $value); $CookieArray = array_map('base64_decode', $CookieArray); return $CookieArray; } /** * Generate the value that is stored in the cookie using the list of IDPs * * @param array IdP identifiers * @return string SAML domain cookie value */ function generate_cookie_value($CookieArray) { // Merges cookie content and encodes it $CookieArray = array_map('base64_encode', $CookieArray); $value = implode(' ', $CookieArray); return $value; } /** * Append a value to the array of IDPs * * @param string IdP identifier * @param array IdP identifiers * @return array IdP identifiers with appended IdP */ function appendCookieValue($value, $CookieArray) { array_push($CookieArray, $value); $CookieArray = array_reverse($CookieArray); $CookieArray = array_unique($CookieArray); $CookieArray = array_reverse($CookieArray); return $CookieArray; } shibboleth/login.php 0000644 00000010706 15152311434 0010513 0 ustar 00 <?php require_once("../../config.php"); require_once($CFG->dirroot."/auth/shibboleth/auth.php"); $idp = optional_param('idp', null, PARAM_RAW); // Check for timed out sessions. if (!empty($SESSION->has_timed_out)) { $session_has_timed_out = true; $SESSION->has_timed_out = false; } else { $session_has_timed_out = false; } // Define variables used in page. $isvalid = true; $site = get_site(); $loginsite = get_string("loginsite"); $loginurl = (!empty($CFG->alternateloginurl)) ? $CFG->alternateloginurl : ''; $config = get_config('auth_shibboleth'); if (!empty($CFG->registerauth) or is_enabled_auth('none') or !empty($config->auth_instructions)) { $showinstructions = true; } else { $showinstructions = false; } $idplist = get_idp_list($config->organization_selection); if (isset($idp)) { if (isset($idplist[$idp])) { set_saml_cookie($idp); $targeturl = new moodle_url('/auth/shibboleth/index.php'); $idpinfo = $idplist[$idp]; // Redirect to SessionInitiator with entityID as argument. if (isset($idpinfo[1]) && !empty($idpinfo[1])) { $sso = $idpinfo[1]; } else { $sso = '/Shibboleth.sso'; } // For Shibboleth 1.x Service Providers. header('Location: ' . $sso . '?providerId=' . urlencode($idp) . '&target=' . urlencode($targeturl->out())); } else { $isvalid = false; } } $loginsite = get_string("loginsite"); $PAGE->set_url('/auth/shibboleth/login.php'); $PAGE->set_context(context_system::instance()); $PAGE->navbar->add($loginsite); $PAGE->set_title("$site->fullname: $loginsite"); $PAGE->set_heading($site->fullname); $PAGE->set_pagelayout('login'); echo $OUTPUT->header(); if (isloggedin() and !isguestuser()) { // Prevent logging when already logged in, we do not want them to relogin by accident because sesskey would be changed. echo $OUTPUT->box_start(); $params = array('sesskey' => sesskey(), 'loginpage' => 1); $logout = new single_button(new moodle_url('/login/logout.php', $params), get_string('logout'), 'post'); $continue = new single_button(new moodle_url('/'), get_string('cancel'), 'get'); echo $OUTPUT->confirm(get_string('alreadyloggedin', 'error', fullname($USER)), $logout, $continue); echo $OUTPUT->box_end(); } else { // Print login page. $selectedidp = '-'; if (isset($_COOKIE['_saml_idp'])) { $idpcookie = generate_cookie_array($_COOKIE['_saml_idp']); do { $selectedidp = array_pop($idpcookie); } while (!isset($idplist[$selectedidp]) && count($idpcookie) > 0); } $idps = []; foreach ($idplist as $value => $data) { $name = reset($data); $selected = $value === $selectedidp; $idps[] = (object)[ 'name' => $name, 'value' => $value, 'selected' => $selected ]; } // Whether the user can sign up. $cansignup = !empty($CFG->registerauth); // Default instructions. $instructions = format_text($config->auth_instructions); if (is_enabled_auth('none')) { $instructions = get_string('loginstepsnone'); } else if ($cansignup) { if ($CFG->registerauth === 'email' && empty($instructions)) { $instructions = get_string('loginsteps'); } } // Build the template context data. $templatedata = (object)[ 'adminemail' => get_admin()->email, 'cansignup' => $cansignup, 'guestlogin' => $CFG->guestloginbutton, 'guestloginurl' => new moodle_url('/login/index.php'), 'idps' => $idps, 'instructions' => $instructions, 'loginname' => $config->login_name ?? null, 'logintoken' => \core\session\manager::get_login_token(), 'loginurl' => new moodle_url('/auth/shibboleth/login.php'), 'showinstructions' => $showinstructions, 'signupurl' => new moodle_url('/login/signup.php'), 'isvalid' => $isvalid ]; // Render the login form. echo $OUTPUT->render_from_template('auth_shibboleth/login_form', $templatedata); } echo $OUTPUT->footer(); shibboleth/settings.php 0000644 00000010651 15152311434 0011242 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin settings and defaults. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // We use a couple of custom admin settings since we need to massage the data before it is inserted into the DB. require_once($CFG->dirroot.'/auth/shibboleth/classes/admin_setting_special_wayf_select.php'); require_once($CFG->dirroot.'/auth/shibboleth/classes/admin_setting_special_idp_configtextarea.php'); require_once($CFG->dirroot.'/auth/shibboleth/classes/admin_setting_special_convert_data_configfile.php'); // Introductory explanation. $readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out(); $settings->add(new admin_setting_heading('auth_shibboleth/pluginname', '', new lang_string('auth_shibbolethdescription', 'auth_shibboleth', $readmeurl))); // Username. $settings->add(new admin_setting_configtext('auth_shibboleth/user_attribute', get_string('username'), get_string('auth_shib_username_description', 'auth_shibboleth'), '', PARAM_RAW)); // Convert Data configuration file. $settings->add(new auth_shibboleth_admin_setting_convert_data('auth_shibboleth/convert_data', get_string('auth_shib_convert_data', 'auth_shibboleth'), get_string('auth_shib_convert_data_description', 'auth_shibboleth', $readmeurl), '')); // WAYF. $settings->add(new auth_shibboleth_admin_setting_special_wayf_select()); // Organization_selection. $settings->add(new auth_shibboleth_admin_setting_special_idp_configtextarea()); // Logout handler. $settings->add(new admin_setting_configtext('auth_shibboleth/logout_handler', get_string('auth_shib_logout_url', 'auth_shibboleth'), get_string('auth_shib_logout_url_description', 'auth_shibboleth'), '', PARAM_URL)); // Logout return URL. $settings->add(new admin_setting_configtext('auth_shibboleth/logout_return_url', get_string('auth_shib_logout_return_url', 'auth_shibboleth'), get_string('auth_shib_logout_return_url_description', 'auth_shibboleth'), '', PARAM_URL)); // Authentication method name. $settings->add(new admin_setting_configtext('auth_shibboleth/login_name', get_string('auth_shib_auth_method', 'auth_shibboleth'), get_string('auth_shib_auth_method_description', 'auth_shibboleth'), 'Shibboleth Login', PARAM_RAW_TRIMMED)); // Authentication method logo. $settings->add(new admin_setting_configstoredfile('auth_shibboleth/auth_logo', get_string('auth_shib_auth_logo', 'auth_shibboleth'), get_string('auth_shib_auth_logo_description', 'auth_shibboleth'), 'logo', 0, ['accepted_types' => ['image']])); // Login directions. $settings->add(new admin_setting_configtextarea('auth_shibboleth/auth_instructions', get_string('auth_shib_instructions_key', 'auth_shibboleth'), get_string('auth_shib_instructions_help', 'auth_shibboleth', $CFG->wwwroot.'/auth/shibboleth/index.php'), get_string('auth_shib_instructions', 'auth_shibboleth', $CFG->wwwroot.'/auth/shibboleth/index.php'), PARAM_RAW_TRIMMED)); // Password change URL. $settings->add(new admin_setting_configtext('auth_shibboleth/changepasswordurl', get_string('auth_shib_changepasswordurl', 'auth_shibboleth'), get_string('changepasswordhelp', 'auth'), '', PARAM_URL)); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('shibboleth'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, '', true, false, $authplugin->get_custom_user_profile_fields()); } shibboleth/upgrade.txt 0000644 00000001376 15152311434 0011065 0 ustar 00 This files describes API changes in /auth/shibboleth/*, information provided here is intended especially for developers. === 3.11 === * The 'Data modification API' (convert_data) setting can no longer be configured to use files located within the current site data directory ($CFG->dataroot), as it exposes the site to security risks. === 3.5.2 === * Moved the public function unserializesession in auth/shibboleth/logout.php to auth/shibboleth/classes/helper.php and made it private. This function should not have been used outside of this file. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/shibboleth' to 'auth_shibboleth'. shibboleth/index.php 0000644 00000011345 15152311434 0010512 0 ustar 00 <?php // Designed to be redirected from moodle/login/index.php require('../../config.php'); $context = context_system::instance(); $PAGE->set_url('/auth/shibboleth/index.php'); $PAGE->set_context($context); // Support for WAYFless URLs. $target = optional_param('target', '', PARAM_LOCALURL); if (!empty($target) && empty($SESSION->wantsurl)) { $SESSION->wantsurl = $target; } if (isloggedin() && !isguestuser()) { // Nothing to do if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { $urltogo = $SESSION->wantsurl; /// Because it's an address in this site unset($SESSION->wantsurl); } else { $urltogo = $CFG->wwwroot.'/'; /// Go to the standard home page unset($SESSION->wantsurl); /// Just in case } redirect($urltogo); } $pluginconfig = get_config('auth_shibboleth'); $shibbolethauth = get_auth_plugin('shibboleth'); // Check whether Shibboleth is configured properly $readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out(); if (empty($pluginconfig->user_attribute)) { throw new \moodle_exception('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl); } /// If we can find the Shibboleth attribute, save it in session and return to main login page if (!empty($_SERVER[$pluginconfig->user_attribute])) { // Shibboleth auto-login $frm = new stdClass(); $frm->username = strtolower($_SERVER[$pluginconfig->user_attribute]); // The password is never actually used, but needs to be passed to the functions 'user_login' and // 'authenticate_user_login'. Shibboleth returns true for the function 'prevent_local_password', which is // used when setting the password in 'update_internal_user_password'. When 'prevent_local_password' // returns true, the password is set to 'not cached' (AUTH_PASSWORD_NOT_CACHED) in the Moodle DB. However, // rather than setting the password to a hard-coded value, we will generate one each time, in case there are // changes to the Shibboleth plugin and it is actually used. $frm->password = generate_password(8); /// Check if the user has actually submitted login data to us $reason = null; if ($shibbolethauth->user_login($frm->username, $frm->password) && $user = authenticate_user_login($frm->username, $frm->password, false, $reason, false)) { complete_user_login($user); if (user_not_fully_set_up($USER, true)) { $urltogo = $CFG->wwwroot.'/user/edit.php?id='.$USER->id.'&course='.SITEID; // We don't delete $SESSION->wantsurl yet, so we get there later } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { $urltogo = $SESSION->wantsurl; /// Because it's an address in this site unset($SESSION->wantsurl); } else { $urltogo = $CFG->wwwroot.'/'; /// Go to the standard home page unset($SESSION->wantsurl); /// Just in case } /// Go to my-moodle page instead of homepage if defaulthomepage enabled if (!has_capability('moodle/site:config', context_system::instance()) and !empty($CFG->defaulthomepage) and !isguestuser()) { if ($urltogo == $CFG->wwwroot or $urltogo == $CFG->wwwroot.'/' or $urltogo == $CFG->wwwroot.'/index.php') { if ($CFG->defaulthomepage == HOMEPAGE_MY && !empty($CFG->enabledashboard)) { $urltogo = $CFG->wwwroot.'/my/'; } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) { $urltogo = $CFG->wwwroot.'/my/courses.php'; } } } redirect($urltogo); exit; } else { // The Shibboleth user couldn't be mapped to a valid Moodle user throw new \moodle_exception('shib_invalid_account_error', 'auth_shibboleth'); } } // If we can find any (user independent) Shibboleth attributes but no user // attributes we probably didn't receive any user attributes elseif (!empty($_SERVER['HTTP_SHIB_APPLICATION_ID']) || !empty($_SERVER['Shib-Application-ID'])) { throw new \moodle_exception('shib_no_attributes_error', 'auth_shibboleth' , '', '\''.$pluginconfig->user_attribute.'\', \''.$pluginconfig->field_map_firstname.'\', \''. $pluginconfig->field_map_lastname.'\' and \''.$pluginconfig->field_map_email.'\''); } else { throw new \moodle_exception('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl); } shibboleth/lib.php 0000644 00000004041 15152311434 0010144 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the hooks for the Shibboleth authentication module. * * @package auth_shibboleth * @copyright 2018 Fabrice Ménard * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Serves the logo file settings. * * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool false if file not found, does not return if found - justsend the file */ function auth_shibboleth_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { if ($context->contextlevel != CONTEXT_SYSTEM) { return false; } if ($filearea !== 'logo' ) { return false; } $itemid = 0; $filename = array_pop($args); if (!$args) { $filepath = '/'; } else { $filepath = '/'.implode('/', $args).'/'; } $fs = get_file_storage(); $file = $fs->get_file($context->id, 'auth_shibboleth', $filearea, $itemid, $filepath, $filename); if (!$file) { return false; } send_stored_file($file, null, 0, $forcedownload, $options); } shibboleth/version.php 0000644 00000002236 15152311434 0011067 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version information * * @package auth_shibboleth * @author Martin Dougiamas * @author Lukas Haemmerle * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_shibboleth'; // Full name of the plugin (used for diagnostics) shibboleth/classes/privacy/provider.php 0000644 00000002775 15152311434 0014356 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_shibboleth. * * @package auth_shibboleth * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_shibboleth\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_shibboleth implementing null_provider. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } shibboleth/classes/helper.php 0000644 00000011677 15152311434 0012327 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains a helper class for the Shibboleth authentication plugin. * * @package auth_shibboleth * @copyright 2018 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_shibboleth; defined('MOODLE_INTERNAL') || die(); /** * The helper class for the Shibboleth authentication plugin. * * @package auth_shibboleth * @copyright 2018 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Delete session of user using file sessions. * * @param string $spsessionid SP-provided Shibboleth Session ID * @return \SoapFault or void if everything was fine */ public static function logout_file_session($spsessionid) { global $CFG; if (!empty($CFG->session_file_save_path)) { $dir = $CFG->session_file_save_path; } else { $dir = $CFG->dataroot . '/sessions'; } if (is_dir($dir)) { if ($dh = opendir($dir)) { // Read all session files. while (($file = readdir($dh)) !== false) { // Check if it is a file. if (is_file($dir.'/'.$file)) { // Read session file data. $data = file($dir.'/'.$file); if (isset($data[0])) { $usersession = self::unserializesession($data[0]); // Check if we have found session that shall be deleted. if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) { // If there is a match, delete file. if ($usersession['SESSION']->shibboleth_session_id == $spsessionid) { // Delete session file. if (!unlink($dir.'/'.$file)) { return new SoapFault('LogoutError', 'Could not delete Moodle session file.'); } } } } } } closedir($dh); } } } /** * Delete session of user using DB sessions. * * @param string $spsessionid SP-provided Shibboleth Session ID */ public static function logout_db_session($spsessionid) { global $CFG, $DB; $sessions = $DB->get_records_sql( 'SELECT userid, sessdata FROM {sessions} WHERE timemodified > ?', array(time() - $CFG->sessiontimeout) ); foreach ($sessions as $session) { // Get user session from DB. $usersession = self::unserializesession(base64_decode($session->sessdata)); if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) { // If there is a match, kill the session. if ($usersession['SESSION']->shibboleth_session_id == trim($spsessionid)) { // Delete this user's sessions. \core\session\manager::kill_user_sessions($session->userid); } } } } /** * Unserialize a session string. * * @param string $serializedstring * @return array */ private static function unserializesession($serializedstring) { $variables = array(); $index = 0; // Find next delimiter after current index. It's key being the characters between those points. while ($delimiterpos = strpos($serializedstring, '|', $index)) { $key = substr($serializedstring, $index, $delimiterpos - $index); // Start unserializing immediately after the delimiter. PHP will read as much valid data as possible. $value = unserialize(substr($serializedstring, $delimiterpos + 1), ['allowed_classes' => ['stdClass']]); $variables[$key] = $value; // Advance index beyond the length of the previously captured serialized value. $index = $delimiterpos + 1 + strlen(serialize($value)); } return $variables; } } shibboleth/classes/admin_setting_special_convert_data_configfile.php 0000644 00000005153 15152311434 0022303 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Special setting for auth_shibboleth convert_data. * * @package auth_shibboleth * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Admin settings class for the convert_data option. * * @package auth_shibboleth * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_shibboleth_admin_setting_convert_data extends admin_setting_configfile { /** * Constructor. * * @param string $name * @param string $visiblename * @param string $description * @param mixed $defaultdirectory */ public function __construct($name, $visiblename, $description, $defaultdirectory) { parent::__construct($name, $visiblename, $description, $defaultdirectory); } /** * Validate the file path (location). * * This method ensures that the file defined as a data modification API exists and is not located in the site * data directory ($CFG->dataroot). We should prohibit using files from the site data directory as this introduces * security vulnerabilities. * * @param string $filepath The path to the file. * @return mixed bool true for success or string:error on failure. */ public function validate($filepath) { global $CFG; if (empty($filepath)) { return true; } // Fail if the file does not exist or it is not readable by the webserver process. if (!is_readable($filepath)) { return get_string('auth_shib_convert_data_warning', 'auth_shibboleth'); } // Fail if the absolute file path matches the currently defined dataroot path. if (preg_match('/' . preg_quote($CFG->dataroot, '/') . '/', realpath($filepath))) { return get_string('auth_shib_convert_data_filepath_warning', 'auth_shibboleth'); } return true; } } shibboleth/classes/admin_setting_special_idp_configtextarea.php 0000644 00000005621 15152311434 0021304 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Special setting for auth_shibboleth WAYF. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special setting for auth_shibboleth WAYF. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_shibboleth_admin_setting_special_idp_configtextarea extends admin_setting_configtextarea { /** * Calls parent::__construct with specific arguments. */ public function __construct() { $default = $orgdefault = "urn:mace:organization1:providerID, Example Organization 1 https://another.idp-id.com/shibboleth, Other Example Organization, /Shibboleth.sso/DS/SWITCHaai urn:mace:organization2:providerID, Example Organization 2, /Shibboleth.sso/WAYF/SWITCHaai"; parent::__construct('auth_shibboleth/organization_selection', get_string('auth_shib_idp_list', 'auth_shibboleth'), get_string('auth_shib_idp_list_description', 'auth_shibboleth'), $default, PARAM_RAW, '60', '8'); } /** * We need to overwrite the global "alternate login url" setting if wayf is enabled. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { global $CFG; $login = get_config('auth_shibboleth', 'alt_login'); if (isset($data) && !empty($data) && isset($login) && $login == 'on') { // Need to use the get_idp_list() function here. require_once($CFG->dirroot.'/auth/shibboleth/auth.php'); $idplist = get_idp_list($data); if (count($idplist) < 1) { return false; } $data = ''; foreach ($idplist as $idp => $value) { $data .= $idp.', '.$value[0]; if (isset($value[1])) { // Value[1] is optional. $data .= ', '.$value[1] . "\n"; } else { $data .= "\n"; } } } return parent::write_setting($data); } } shibboleth/classes/admin_setting_special_wayf_select.php 0000644 00000005332 15152311434 0017751 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Special settings for auth_shibboleth WAYF. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special settings for auth_shibboleth WAYF. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_shibboleth_admin_setting_special_wayf_select extends admin_setting_configselect { /** * Calls parent::__construct with specific arguments. */ public function __construct() { $yesno = array(); $yesno['off'] = new lang_string('no'); $yesno['on'] = new lang_string('yes'); parent::__construct('auth_shibboleth/alt_login', new lang_string('auth_shib_integrated_wayf', 'auth_shibboleth'), new lang_string('auth_shib_integrated_wayf_description', 'auth_shibboleth'), 'off', $yesno); } /** * We need to overwrite the global "alternate login url" setting if wayf is enabled. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { global $CFG; // Overwrite alternative login URL if integrated WAYF is used. if (isset($data) && $data == 'on') { set_config('alt_login', $data, 'auth_shibboleth'); set_config('alternateloginurl', $CFG->wwwroot.'/auth/shibboleth/login.php'); } else { // Check if integrated WAYF was enabled and is now turned off. // If it was and only then, reset the Moodle alternate URL. $oldsetting = get_config('auth_shibboleth', 'alt_login'); if (isset($oldsetting) and $oldsetting == 'on') { set_config('alt_login', 'off', 'auth_shibboleth'); set_config('alternateloginurl', ''); } $data = 'off'; } return parent::write_setting($data); } } shibboleth/lang/en/auth_shibboleth.php 0000644 00000015202 15152311434 0014066 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_shibboleth', language 'en'. * * @package auth_shibboleth * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_shib_auth_method'] = 'Authentication method name'; $string['auth_shib_auth_method_description'] = 'Provide a name for the Shibboleth authentication method that is familiar to your users. This could be the name of your Shibboleth federation, e.g. <tt>SWITCHaai Login</tt> or <tt>InCommon Login</tt> or similar.'; $string['auth_shib_auth_logo'] = 'Authentication method logo'; $string['auth_shib_auth_logo_description'] = 'Provide a logo for the Shibboleth authentication method that is familiar to your users. This could be the logo of your Shibboleth federation, e.g. <tt>SWITCHaai Login</tt> or <tt>InCommon Login</tt> or similar.'; $string['auth_shib_contact_administrator'] = 'In case you are not associated with the given organizations and you need access to a course on this server, please contact the <a href="mailto:{$a}">Moodle Administrator</a>.'; $string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using Shibboleth. For set-up details, see the <a href="{$a}">Shibboleth README</a>.'; $string['auth_shibboleth_errormsg'] = 'Please select the organization you are member of!'; $string['auth_shibboleth_login'] = 'Shibboleth login'; $string['auth_shibboleth_login_long'] = 'Login to Moodle via Shibboleth'; $string['auth_shibboleth_manual_login'] = 'Manual login'; $string['auth_shibboleth_select_member'] = 'I\'m a member of ...'; $string['auth_shibboleth_select_organization'] = 'For authentication via Shibboleth, please select your organisation from the drop-down menu:'; $string['auth_shib_convert_data'] = 'Data modification API'; $string['auth_shib_convert_data_description'] = 'You can use this API to further modify the data provided by Shibboleth. Read the <a href="{$a}">README</a> for further instructions.'; $string['auth_shib_convert_data_warning'] = 'The file does not exist or is not readable by the webserver process!'; $string['auth_shib_convert_data_filepath_warning'] = 'You cannot use a file that is located within the current site data directory ($CFG->dataroot) as the data modification API.'; $string['auth_shib_changepasswordurl'] = 'Password-change URL'; $string['auth_shib_idp_list'] = 'Identity providers'; $string['auth_shib_idp_list_description'] = 'Provide a list of Identity Provider entityIDs to let the user choose from on the login page.<br />On each line there must be a comma-separated tuple for entityID of the IdP (see the Shibboleth metadata file) and Name of IdP as it shall be displayed in the drop-down list.<br />As an optional third parameter you can add the location of a Shibboleth session initiator that shall be used in case your Moodle installation is part of a multi federation setup.'; $string['auth_shib_instructions'] = 'Use the <a href="{$a}">Shibboleth login</a> to get access via Shibboleth, if your institution supports it. Otherwise, use the normal login form shown here.'; $string['auth_shib_instructions_help'] = 'Here you should provide custom instructions for your users to explain Shibboleth. It will be shown on the login page in the instructions section. The instructions must include a link to "<b>{$a}</b>" that users click when they want to log in.'; $string['auth_shib_instructions_key'] = 'Login instructions'; $string['auth_shib_integrated_wayf'] = 'Moodle WAYF service'; $string['auth_shib_integrated_wayf_description'] = 'If you enable this, Moodle will use its own WAYF service instead of the one configured for Shibboleth. Moodle will display a drop-down list on this alternative login page where the user has to select his Identity Provider.'; $string['auth_shib_logout_return_url'] = 'Alternative logout return URL'; $string['auth_shib_logout_return_url_description'] = 'Provide the URL that Shibboleth users shall be redirected to after logging out.<br />If left empty, users will be redirected to the location that moodle will redirect users to'; $string['auth_shib_logout_url'] = 'Shibboleth Service Provider logout handler URL'; $string['auth_shib_logout_url_description'] = 'Provide the URL to the Shibboleth Service Provider logout handler. This typically is <tt>/Shibboleth.sso/Logout</tt>'; $string['auth_shib_no_organizations_warning'] = 'If you want to use the integrated WAYF service, you must provide a coma-separated list of Identity Provider entityIDs, their names and optionally a session initiator.'; $string['auth_shib_only'] = 'Shibboleth only'; $string['auth_shib_only_description'] = 'Check this option if a Shibboleth authentication shall be enforced'; $string['auth_shib_username_description'] = 'Name of the webserver Shibboleth environment variable that shall be used as Moodle username'; $string['shib_invalid_account_error'] = 'You seem to be Shibboleth authenticated but Moodle has no valid account for your username. Your account may not exist or it may have been suspended.'; $string['shib_no_attributes_error'] = 'You seem to be Shibboleth authenticated but Moodle didn\'t receive any user attributes. Please check that your Identity Provider releases the necessary attributes ({$a}) to the Service Provider Moodle is running on or inform the webmaster of this server.'; $string['shib_not_all_attributes_error'] = 'Moodle needs certain Shibboleth attributes which are not present in your case. The attributes are: {$a}<br />Please contact the webmaster of this server or your Identity Provider.'; $string['shib_not_set_up_error'] = 'Shibboleth authentication doesn\'t seem to be set up correctly because no Shibboleth environment variables are present for this page. Please consult the <a href="{$a}">README</a> for further instructions on how to set up Shibboleth authentication or contact the webmaster of this Moodle installation.'; $string['pluginname'] = 'Shibboleth'; $string['privacy:metadata'] = 'The Shibboleth authentication plugin does not store any personal data.'; db/db/upgrade.php 0000644 00000002663 15152311434 0007664 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * DB authentication plugin upgrade code * * @package auth_db * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Function to upgrade auth_db. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_db_upgrade($oldversion) { global $CFG, $DB; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } db/db/install.php 0000644 00000001657 15152311434 0007705 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * auth_db installer script. * * @package auth_db * @copyright 2009 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ function xmldb_auth_db_install() { global $CFG, $DB; } db/db/tasks.php 0000644 00000002304 15152311434 0007352 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Task definition for auth_db. * @author Guy Thomas <gthomas@moodlerooms.com> * @copyright Copyright (c) 2017 Blackboard Inc. * @package auth_db * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => '\auth_db\task\sync_users', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', 'day' => '*', 'month' => '*', 'dayofweek' => '*', 'disabled' => 1 ) ); db/cli/sync_users.php 0000644 00000007013 15152311434 0010606 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Extdb user sync script. * * This script is meant to be called from a system cronjob to * sync moodle user accounts with external database. * It is required when using internal passwords (== passwords not defined in external database). * * Sample cron entry: * # 5 minutes past 4am * 5 4 * * * sudo -u www-data /usr/bin/php /var/www/moodle/auth/db/cli/sync_users.php * * Notes: * - it is required to use the web server account when executing PHP CLI scripts * - you need to change the "www-data" to match the apache user account * - use "su" if "sudo" not available * - If you have a large number of users, you may want to raise the memory limits * by passing -d memory_limit=256M * - For debugging & better logging, you are encouraged to use in the command line: * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 * * Performance notes: * + The code is simpler, but not as optimized as its LDAP counterpart. * * @package auth_db * @copyright 2006 Martin Langhoff * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('CLI_SCRIPT', true); require(__DIR__.'/../../../config.php'); require_once("$CFG->libdir/clilib.php"); // Now get cli options. list($options, $unrecognized) = cli_get_params(array('noupdate'=>false, 'verbose'=>false, 'help'=>false), array('n'=>'noupdate', 'v'=>'verbose', 'h'=>'help')); if ($unrecognized) { $unrecognized = implode("\n ", $unrecognized); cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); } if ($options['help']) { $help = "Execute user account sync with external database. The auth_db plugin must be enabled and properly configured. Options: -n, --noupdate Skip update of existing users -v, --verbose Print verbose progress information -h, --help Print out this help Example: \$ sudo -u www-data /usr/bin/php auth/db/cli/sync_users.php Sample cron entry: # 5 minutes past 4am 5 4 * * * sudo -u www-data /usr/bin/php /var/www/moodle/auth/db/cli/sync_users.php "; echo $help; die; } if (!is_enabled_auth('db')) { cli_error('auth_db plugin is disabled, synchronisation stopped', 2); } cli_problem('[AUTH DB] The sync users cron has been deprecated. Please use the scheduled task instead.'); // Abort execution of the CLI script if the \auth_db\task\sync_users is enabled. $task = \core\task\manager::get_scheduled_task('auth_db\task\sync_users'); if (!$task->get_disabled()) { cli_error('[AUTH DB] The scheduled task sync_users is enabled, the cron execution has been aborted.'); } if (empty($options['verbose'])) { $trace = new null_progress_trace(); } else { $trace = new text_progress_trace(); } $update = empty($options['noupdate']); /** @var auth_plugin_db $dbauth */ $dbauth = get_auth_plugin('db'); $result = $dbauth->sync_users($trace, $update); exit($result); db/tests/db_test.php 0000644 00000056376 15152311434 0010450 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_db; /** * External database auth sync tests, this also tests adodb drivers * that are matching our four supported Moodle database drivers. * * @package auth_db * @category phpunit * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class db_test extends \advanced_testcase { /** @var string Original error log */ protected $oldlog; /** @var int The amount of users to create for the large user set deletion test */ protected $largedeletionsetsize = 128; public static function tearDownAfterClass(): void { global $DB; // Apply sqlsrv native driver error and logging default // settings while finishing the AdoDB tests. if ($DB->get_dbfamily() === 'mssql') { sqlsrv_configure("WarningsReturnAsErrors", false); sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_OFF); sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR); } } protected function init_auth_database() { global $DB, $CFG; require_once("$CFG->dirroot/auth/db/auth.php"); // Discard error logs from AdoDB. $this->oldlog = ini_get('error_log'); ini_set('error_log', "$CFG->dataroot/testlog.log"); $dbman = $DB->get_manager(); set_config('extencoding', 'utf-8', 'auth_db'); set_config('host', $CFG->dbhost, 'auth_db'); set_config('user', $CFG->dbuser, 'auth_db'); set_config('pass', $CFG->dbpass, 'auth_db'); set_config('name', $CFG->dbname, 'auth_db'); if (!empty($CFG->dboptions['dbport'])) { set_config('host', $CFG->dbhost.':'.$CFG->dboptions['dbport'], 'auth_db'); } switch ($DB->get_dbfamily()) { case 'mysql': set_config('type', 'mysqli', 'auth_db'); set_config('setupsql', "SET NAMES 'UTF-8'", 'auth_db'); set_config('sybasequoting', '0', 'auth_db'); if (!empty($CFG->dboptions['dbsocket'])) { $dbsocket = $CFG->dboptions['dbsocket']; if ((strpos($dbsocket, '/') === false and strpos($dbsocket, '\\') === false)) { $dbsocket = ini_get('mysqli.default_socket'); } set_config('type', 'mysqli://'.rawurlencode($CFG->dbuser).':'.rawurlencode($CFG->dbpass).'@'.rawurlencode($CFG->dbhost).'/'.rawurlencode($CFG->dbname).'?socket='.rawurlencode($dbsocket), 'auth_db'); } break; case 'oracle': set_config('type', 'oci8po', 'auth_db'); set_config('sybasequoting', '1', 'auth_db'); break; case 'postgres': set_config('type', 'postgres7', 'auth_db'); $setupsql = "SET NAMES 'UTF-8'"; if (!empty($CFG->dboptions['dbschema'])) { $setupsql .= "; SET search_path = '".$CFG->dboptions['dbschema']."'"; } set_config('setupsql', $setupsql, 'auth_db'); set_config('sybasequoting', '0', 'auth_db'); if (!empty($CFG->dboptions['dbsocket']) and ($CFG->dbhost === 'localhost' or $CFG->dbhost === '127.0.0.1')) { if (strpos($CFG->dboptions['dbsocket'], '/') !== false) { $socket = $CFG->dboptions['dbsocket']; if (!empty($CFG->dboptions['dbport'])) { $socket .= ':' . $CFG->dboptions['dbport']; } set_config('host', $socket, 'auth_db'); } else { set_config('host', '', 'auth_db'); } } break; case 'mssql': set_config('type', 'mssqlnative', 'auth_db'); set_config('sybasequoting', '1', 'auth_db'); // The native sqlsrv driver uses a comma as separator between host and port. $dbhost = $CFG->dbhost; if (!empty($dboptions['dbport'])) { $dbhost .= ',' . $dboptions['dbport']; } set_config('host', $dbhost, 'auth_db'); break; default: throw new exception('Unknown database family ' . $DB->get_dbfamily()); } $table = new \xmldb_table('auth_db_users'); $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('pass', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('email', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('firstname', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('lastname', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('animal', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); if ($dbman->table_exists($table)) { $dbman->drop_table($table); } $dbman->create_table($table); set_config('table', $CFG->prefix.'auth_db_users', 'auth_db'); set_config('fielduser', 'name', 'auth_db'); set_config('fieldpass', 'pass', 'auth_db'); set_config('field_map_lastname', 'lastname', 'auth_db'); set_config('field_updatelocal_lastname', 'oncreate', 'auth_db'); set_config('field_lock_lastname', 'unlocked', 'auth_db'); // Setu up field mappings. set_config('field_map_email', 'email', 'auth_db'); set_config('field_updatelocal_email', 'oncreate', 'auth_db'); set_config('field_updateremote_email', '0', 'auth_db'); set_config('field_lock_email', 'unlocked', 'auth_db'); // Create a user profile field and add mapping to it. $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'pet', 'name' => 'Pet', 'datatype' => 'text']); set_config('field_map_profile_field_pet', 'animal', 'auth_db'); set_config('field_updatelocal_profile_field_pet', 'oncreate', 'auth_db'); set_config('field_updateremote_profile_field_pet', '0', 'auth_db'); set_config('field_lock_profile_field_pet', 'unlocked', 'auth_db'); // Init the rest of settings. set_config('passtype', 'plaintext', 'auth_db'); set_config('changepasswordurl', '', 'auth_db'); set_config('debugauthdb', 0, 'auth_db'); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_db'); } protected function cleanup_auth_database() { global $DB; $dbman = $DB->get_manager(); $table = new \xmldb_table('auth_db_users'); $dbman->drop_table($table); ini_set('error_log', $this->oldlog); } public function test_plugin() { global $DB, $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); $this->resetAfterTest(true); // NOTE: It is strongly discouraged to create new tables in advanced_testcase classes, // but there is no other simple way to test ext database enrol sync, so let's // disable transactions are try to cleanup after the tests. $this->preventResetByRollback(); $this->init_auth_database(); /** @var auth_plugin_db $auth */ $auth = get_auth_plugin('db'); $authdb = $auth->db_init(); // Test adodb may access the table. $user1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com'); $user1->id = $DB->insert_record('auth_db_users', $user1); $sql = "SELECT * FROM {$auth->config->table}"; $rs = $authdb->Execute($sql); $this->assertInstanceOf('ADORecordSet', $rs); $this->assertFalse($rs->EOF); $fields = $rs->FetchRow(); $this->assertTrue(is_array($fields)); $this->assertTrue($rs->EOF); $rs->Close(); $authdb->Close(); // Test bulk user account creation. $user2 = (object)['name' => 'u2', 'pass' => 'heslo', 'email' => 'u2@example.com', 'animal' => 'cat']; $user2->id = $DB->insert_record('auth_db_users', $user2); $user3 = (object)array('name'=>'admin', 'pass'=>'heslo', 'email'=>'admin@example.com'); // Should be skipped. $user3->id = $DB->insert_record('auth_db_users', $user3); $this->assertCount(2, $DB->get_records('user')); $trace = new \null_progress_trace(); // Sync users and make sure that two events user_created werer triggered. $sink = $this->redirectEvents(); $auth->sync_users($trace, false); $events = $sink->get_events(); $sink->close(); $this->assertCount(2, $events); $this->assertTrue($events[0] instanceof \core\event\user_created); $this->assertTrue($events[1] instanceof \core\event\user_created); // Assert the two users were created. $this->assertEquals(4, $DB->count_records('user')); $u1 = $DB->get_record('user', array('username'=>$user1->name, 'auth'=>'db')); $this->assertSame($user1->email, $u1->email); $this->assertEmpty(profile_user_record($u1->id)->pet); $u2 = $DB->get_record('user', array('username'=>$user2->name, 'auth'=>'db')); $this->assertSame($user2->email, $u2->email); $this->assertSame($user2->animal, profile_user_record($u2->id)->pet); $admin = $DB->get_record('user', array('username'=>'admin', 'auth'=>'manual')); $this->assertNotEmpty($admin); // Test sync updates. $user2b = clone($user2); $user2b->email = 'u2b@example.com'; $user2b->animal = 'dog'; $DB->update_record('auth_db_users', $user2b); $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2->email, $u2->email); $this->assertSame($user2->animal, profile_user_record($u2->id)->pet); $auth->sync_users($trace, true); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2->email, $u2->email); set_config('field_updatelocal_email', 'onlogin', 'auth_db'); $auth->config->field_updatelocal_email = 'onlogin'; set_config('field_updatelocal_profile_field_pet', 'onlogin', 'auth_db'); $auth->config->field_updatelocal_profile_field_pet = 'onlogin'; $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2->email, $u2->email); $auth->sync_users($trace, true); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2b->email, $u2->email); $this->assertSame($user2b->animal, profile_user_record($u2->id)->pet); // Test sync deletes and suspends. $DB->delete_records('auth_db_users', array('id'=>$user2->id)); $this->assertCount(2, $DB->get_records('auth_db_users')); unset($user2); unset($user2b); $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_SUSPEND; $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(1, $DB->count_records('user', array('suspended'=>1))); $user2 = (object)array('name'=>'u2', 'pass'=>'heslo', 'email'=>'u2@example.com'); $user2->id = $DB->insert_record('auth_db_users', $user2); $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $DB->delete_records('auth_db_users', array('id'=>$user2->id)); set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_FULLDELETE; $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $user2 = (object)array('name'=>'u2', 'pass'=>'heslo', 'email'=>'u2@example.com'); $user2->id = $DB->insert_record('auth_db_users', $user2); $auth->sync_users($trace, false); $this->assertEquals(5, $DB->count_records('user')); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); // Test user_login(). $user3 = (object)array('name'=>'u3', 'pass'=>'heslo', 'email'=>'u3@example.com'); $user3->id = $DB->insert_record('auth_db_users', $user3); $this->assertFalse($auth->user_login('u4', 'heslo')); $this->assertTrue($auth->user_login('u1', 'heslo')); $this->assertFalse($DB->record_exists('user', array('username'=>'u3', 'auth'=>'db'))); $this->assertTrue($auth->user_login('u3', 'heslo')); $this->assertFalse($DB->record_exists('user', array('username'=>'u3', 'auth'=>'db'))); set_config('passtype', 'md5', 'auth_db'); $auth->config->passtype = 'md5'; $user3->pass = md5('heslo'); $DB->update_record('auth_db_users', $user3); $this->assertTrue($auth->user_login('u3', 'heslo')); // Test user created to see if the checking happens strictly. $usermd5 = (object)['name' => 'usermd5', 'pass' => '0e462097431906509019562988736854']; $usermd5->id = $DB->insert_record('auth_db_users', $usermd5); // md5('240610708') === '0e462097431906509019562988736854'. $this->assertTrue($auth->user_login('usermd5', '240610708')); $this->assertFalse($auth->user_login('usermd5', 'QNKCDZO')); set_config('passtype', 'sh1', 'auth_db'); $auth->config->passtype = 'sha1'; $user3->pass = sha1('heslo'); $DB->update_record('auth_db_users', $user3); $this->assertTrue($auth->user_login('u3', 'heslo')); // Test user created to see if the checking happens strictly. $usersha1 = (object)['name' => 'usersha1', 'pass' => '0e66507019969427134894567494305185566735']; $usersha1->id = $DB->insert_record('auth_db_users', $usersha1); // sha1('aaroZmOk') === '0e66507019969427134894567494305185566735'. $this->assertTrue($auth->user_login('usersha1', 'aaroZmOk')); $this->assertFalse($auth->user_login('usersha1', 'aaK1STfY')); set_config('passtype', 'saltedcrypt', 'auth_db'); $auth->config->passtype = 'saltedcrypt'; $user3->pass = password_hash('heslo', PASSWORD_BCRYPT); $DB->update_record('auth_db_users', $user3); $this->assertTrue($auth->user_login('u3', 'heslo')); set_config('passtype', 'internal', 'auth_db'); $auth->config->passtype = 'internal'; create_user_record('u3', 'heslo', 'db'); $this->assertTrue($auth->user_login('u3', 'heslo')); $DB->delete_records('auth_db_users', array('id'=>$user3->id)); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_KEEP; $this->assertTrue($auth->user_login('u3', 'heslo')); set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_SUSPEND; $this->assertFalse($auth->user_login('u3', 'heslo')); set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_FULLDELETE; $this->assertFalse($auth->user_login('u3', 'heslo')); set_config('passtype', 'sh1', 'auth_db'); $auth->config->passtype = 'sha1'; $this->assertFalse($auth->user_login('u3', 'heslo')); // Test login create and update. $user4 = (object)array('name'=>'u4', 'pass'=>'heslo', 'email'=>'u4@example.com'); $user4->id = $DB->insert_record('auth_db_users', $user4); set_config('passtype', 'plaintext', 'auth_db'); $auth->config->passtype = 'plaintext'; $iuser4 = create_user_record('u4', 'heslo', 'db'); $this->assertNotEmpty($iuser4); $this->assertSame($user4->name, $iuser4->username); $this->assertSame($user4->email, $iuser4->email); $this->assertSame('db', $iuser4->auth); $this->assertSame($CFG->mnet_localhost_id, $iuser4->mnethostid); $user4b = clone($user4); $user4b->email = 'u4b@example.com'; $DB->update_record('auth_db_users', $user4b); set_config('field_updatelocal_email', 'oncreate', 'auth_db'); $auth->config->field_updatelocal_email = 'oncreate'; update_user_record('u4'); $iuser4 = $DB->get_record('user', array('id'=>$iuser4->id)); $this->assertSame($user4->email, $iuser4->email); set_config('field_updatelocal_email', 'onlogin', 'auth_db'); $auth->config->field_updatelocal_email = 'onlogin'; update_user_record('u4'); $iuser4 = $DB->get_record('user', array('id'=>$iuser4->id)); $this->assertSame($user4b->email, $iuser4->email); // Test user_exists() $this->assertTrue($auth->user_exists('u1')); $this->assertTrue($auth->user_exists('admin')); $this->assertFalse($auth->user_exists('u3')); $this->assertTrue($auth->user_exists('u4')); $this->cleanup_auth_database(); } /** * Testing the function _colonscope() from ADOdb. */ public function test_adodb_colonscope() { global $CFG; require_once($CFG->libdir.'/adodb/adodb.inc.php'); require_once($CFG->libdir.'/adodb/drivers/adodb-odbc.inc.php'); require_once($CFG->libdir.'/adodb/drivers/adodb-db2ora.inc.php'); $this->resetAfterTest(false); $sql = "select * from table WHERE column=:1 AND anothercolumn > :0"; $arr = array('b', 1); list($sqlout, $arrout) = _colonscope($sql,$arr); $this->assertEquals("select * from table WHERE column=? AND anothercolumn > ?", $sqlout); $this->assertEquals(array(1, 'b'), $arrout); } /** * Testing the clean_data() method. */ public function test_clean_data() { global $DB; $this->resetAfterTest(false); $this->preventResetByRollback(); $this->init_auth_database(); $auth = get_auth_plugin('db'); $auth->db_init(); // Create users on external table. $extdbuser1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com'); $extdbuser1->id = $DB->insert_record('auth_db_users', $extdbuser1); // User with malicious data on the name (won't be imported). $extdbuser2 = (object)array('name'=>'user<script>alert(1);</script>xss', 'pass'=>'heslo', 'email'=>'xssuser@example.com'); $extdbuser2->id = $DB->insert_record('auth_db_users', $extdbuser2); $extdbuser3 = (object)array('name'=>'u3', 'pass'=>'heslo', 'email'=>'u3@example.com', 'lastname' => 'user<script>alert(1);</script>xss'); $extdbuser3->id = $DB->insert_record('auth_db_users', $extdbuser3); $trace = new \null_progress_trace(); // Let's test user sync make sure still works as expected.. $auth->sync_users($trace, true); $this->assertDebuggingCalled("The property 'lastname' has invalid data and has been cleaned."); // User with correct data, should be equal to external db. $user1 = $DB->get_record('user', array('email'=> $extdbuser1->email, 'auth'=>'db')); $this->assertEquals($extdbuser1->name, $user1->username); $this->assertEquals($extdbuser1->email, $user1->email); // Get the user on moodle user table. $user2 = $DB->get_record('user', array('email'=> $extdbuser2->email, 'auth'=>'db')); $user3 = $DB->get_record('user', array('email'=> $extdbuser3->email, 'auth'=>'db')); $this->assertEmpty($user2); $this->assertEquals($extdbuser3->name, $user3->username); $this->assertEquals('useralert(1);xss', $user3->lastname); $this->cleanup_auth_database(); } /** * Testing the deletion of a user when there are many users in the external DB. */ public function test_deleting_with_many_users() { global $DB; $this->resetAfterTest(true); $this->preventResetByRollback(); $this->init_auth_database(); $auth = get_auth_plugin('db'); $auth->db_init(); // Set to delete from moodle when missing from DB. set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_FULLDELETE; // Create users. $users = []; for ($i = 0; $i < $this->largedeletionsetsize; $i++) { $user = (object)array('username' => "u$i", 'name' => "u$i", 'pass' => 'heslo', 'email' => "u$i@example.com"); $user->id = $DB->insert_record('auth_db_users', $user); $users[] = $user; } // Sync to moodle. $trace = new \null_progress_trace(); $auth->sync_users($trace, true); // Check user is there. $user = array_shift($users); $moodleuser = $DB->get_record('user', array('email' => $user->email, 'auth' => 'db')); $this->assertNotNull($moodleuser); $this->assertEquals($user->username, $moodleuser->username); // Delete a user. $DB->delete_records('auth_db_users', array('id' => $user->id)); // Sync again. $auth->sync_users($trace, true); // Check user is no longer there. $moodleuser = $DB->get_record('user', array('id' => $moodleuser->id)); $this->assertFalse($auth->user_login($user->username, 'heslo')); $this->assertEquals(1, $moodleuser->deleted); // Make sure it was the only user deleted. $numberdeleted = $DB->count_records('user', array('deleted' => 1, 'auth' => 'db')); $this->assertEquals(1, $numberdeleted); $this->cleanup_auth_database(); } } db/auth.php 0000644 00000072314 15152311434 0006611 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: External Database Authentication * * Checks against an external database. * * @package auth_db * @author Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * External database authentication plugin. */ class auth_plugin_db extends auth_plugin_base { /** * Constructor. */ function __construct() { global $CFG; require_once($CFG->libdir.'/adodb/adodb.inc.php'); $this->authtype = 'db'; $this->config = get_config('auth_db'); $this->errorlogtag = '[AUTH DB] '; if (empty($this->config->extencoding)) { $this->config->extencoding = 'utf-8'; } } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ function user_login($username, $password) { global $CFG, $DB; if ($this->is_configured() === false) { debugging(get_string('auth_notconfigured', 'auth', $this->authtype)); return false; } $extusername = core_text::convert($username, 'utf-8', $this->config->extencoding); $extpassword = core_text::convert($password, 'utf-8', $this->config->extencoding); if ($this->is_internal()) { // Lookup username externally, but resolve // password locally -- to support backend that // don't track passwords. if (isset($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_KEEP) { // No need to connect to external database in this case because users are never removed and we verify password locally. if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype))) { return validate_internal_user_password($user, $password); } else { return false; } } $authdb = $this->db_init(); $rs = $authdb->Execute("SELECT * FROM {$this->config->table} WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'"); if (!$rs) { $authdb->Close(); debugging(get_string('auth_dbcantconnect','auth_db')); return false; } if (!$rs->EOF) { $rs->Close(); $authdb->Close(); // User exists externally - check username/password internally. if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype))) { return validate_internal_user_password($user, $password); } } else { $rs->Close(); $authdb->Close(); // User does not exist externally. return false; } } else { // Normal case: use external db for both usernames and passwords. $authdb = $this->db_init(); $rs = $authdb->Execute("SELECT {$this->config->fieldpass} FROM {$this->config->table} WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'"); if (!$rs) { $authdb->Close(); debugging(get_string('auth_dbcantconnect','auth_db')); return false; } if ($rs->EOF) { $authdb->Close(); return false; } $fields = array_change_key_case($rs->fields, CASE_LOWER); $fromdb = $fields[strtolower($this->config->fieldpass)]; $rs->Close(); $authdb->Close(); if ($this->config->passtype === 'plaintext') { return ($fromdb === $extpassword); } else if ($this->config->passtype === 'md5') { return (strtolower($fromdb) === md5($extpassword)); } else if ($this->config->passtype === 'sha1') { return (strtolower($fromdb) === sha1($extpassword)); } else if ($this->config->passtype === 'saltedcrypt') { return password_verify($extpassword, $fromdb); } else { return false; } } } /** * Connect to external database. * * @return ADOConnection * @throws moodle_exception */ function db_init() { if ($this->is_configured() === false) { throw new moodle_exception('auth_dbcantconnect', 'auth_db'); } // Connect to the external database (forcing new connection). $authdb = ADONewConnection($this->config->type); if (!empty($this->config->debugauthdb)) { $authdb->debug = true; ob_start(); //Start output buffer to allow later use of the page headers. } $authdb->Connect($this->config->host, $this->config->user, $this->config->pass, $this->config->name, true); $authdb->SetFetchMode(ADODB_FETCH_ASSOC); if (!empty($this->config->setupsql)) { $authdb->Execute($this->config->setupsql); } return $authdb; } /** * Returns user attribute mappings between moodle and the external database. * * @return array */ function db_attributes() { $moodleattributes = array(); // If we have custom fields then merge them with user fields. $customfields = $this->get_custom_user_profile_fields(); if (!empty($customfields) && !empty($this->userfields)) { $userfields = array_merge($this->userfields, $customfields); } else { $userfields = $this->userfields; } foreach ($userfields as $field) { if (!empty($this->config->{"field_map_$field"})) { $moodleattributes[$field] = $this->config->{"field_map_$field"}; } } $moodleattributes['username'] = $this->config->fielduser; return $moodleattributes; } /** * Reads any other information for a user from external database, * then returns it in an array. * * @param string $username * @return array */ function get_userinfo($username) { global $CFG; $extusername = core_text::convert($username, 'utf-8', $this->config->extencoding); $authdb = $this->db_init(); // Array to map local fieldnames we want, to external fieldnames. $selectfields = $this->db_attributes(); $result = array(); // If at least one field is mapped from external db, get that mapped data. if ($selectfields) { $select = array(); $fieldcount = 0; foreach ($selectfields as $localname=>$externalname) { // Without aliasing, multiple occurrences of the same external // name can coalesce in only occurrence in the result. $select[] = "$externalname AS F".$fieldcount; $fieldcount++; } $select = implode(', ', $select); $sql = "SELECT $select FROM {$this->config->table} WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'"; if ($rs = $authdb->Execute($sql)) { if (!$rs->EOF) { $fields = $rs->FetchRow(); // Convert the associative array to an array of its values so we don't have to worry about the case of its keys. $fields = array_values($fields); foreach (array_keys($selectfields) as $index => $localname) { $value = $fields[$index]; $result[$localname] = core_text::convert($value, $this->config->extencoding, 'utf-8'); } } $rs->Close(); } } $authdb->Close(); return $result; } /** * Change a user's password. * * @param stdClass $user User table object * @param string $newpassword Plaintext password * @return bool True on success */ function user_update_password($user, $newpassword) { global $DB; if ($this->is_internal()) { $puser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). if (update_internal_user_password($puser, $newpassword)) { $user->password = $puser->password; return true; } else { return false; } } else { // We should have never been called! return false; } } /** * Synchronizes user from external db to moodle user table. * * Sync should be done by using idnumber attribute, not username. * You need to pass firstsync parameter to function to fill in * idnumbers if they don't exists in moodle user table. * * Syncing users removes (disables) users that don't exists anymore in external db. * Creates new users and updates coursecreator status of users. * * This implementation is simpler but less scalable than the one found in the LDAP module. * * @param progress_trace $trace * @param bool $do_updates Optional: set to true to force an update of existing accounts * @return int 0 means success, 1 means failure */ function sync_users(progress_trace $trace, $do_updates=false) { global $CFG, $DB; require_once($CFG->dirroot . '/user/lib.php'); // List external users. $userlist = $this->get_userlist(); // Delete obsolete internal users. if (!empty($this->config->removeuser)) { $suspendselect = ""; if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $suspendselect = "AND u.suspended = 0"; } // Find obsolete users. if (count($userlist)) { $removeusers = array(); $params['authtype'] = $this->authtype; $sql = "SELECT u.id, u.username FROM {user} u WHERE u.auth=:authtype AND u.deleted=0 AND u.mnethostid=:mnethostid $suspendselect"; $params['mnethostid'] = $CFG->mnet_localhost_id; $internalusersrs = $DB->get_recordset_sql($sql, $params); $usernamelist = array_flip($userlist); foreach ($internalusersrs as $internaluser) { if (!array_key_exists($internaluser->username, $usernamelist)) { $removeusers[] = $internaluser; } } $internalusersrs->close(); } else { $sql = "SELECT u.id, u.username FROM {user} u WHERE u.auth=:authtype AND u.deleted=0 AND u.mnethostid=:mnethostid $suspendselect"; $params = array(); $params['authtype'] = $this->authtype; $params['mnethostid'] = $CFG->mnet_localhost_id; $removeusers = $DB->get_records_sql($sql, $params); } if (!empty($removeusers)) { $trace->output(get_string('auth_dbuserstoremove', 'auth_db', count($removeusers))); foreach ($removeusers as $user) { if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) { delete_user($user); $trace->output(get_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1); } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->suspended = 1; user_update_user($updateuser, false); $trace->output(get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1); } } } unset($removeusers); } if (!count($userlist)) { // Exit right here, nothing else to do. $trace->finished(); return 0; } // Update existing accounts. if ($do_updates) { // Narrow down what fields we need to update. $all_keys = array_keys(get_object_vars($this->config)); $updatekeys = array(); foreach ($all_keys as $key) { if (preg_match('/^field_updatelocal_(.+)$/',$key, $match)) { if ($this->config->{$key} === 'onlogin') { array_push($updatekeys, $match[1]); // The actual key name. } } } unset($all_keys); unset($key); // Only go ahead if we actually have fields to update locally. if (!empty($updatekeys)) { $update_users = array(); // All the drivers can cope with chunks of 10,000. See line 4491 of lib/dml/tests/dml_est.php $userlistchunks = array_chunk($userlist , 10000); foreach($userlistchunks as $userlistchunk) { list($in_sql, $params) = $DB->get_in_or_equal($userlistchunk, SQL_PARAMS_NAMED, 'u', true); $params['authtype'] = $this->authtype; $params['mnethostid'] = $CFG->mnet_localhost_id; $sql = "SELECT u.id, u.username, u.suspended FROM {user} u WHERE u.auth = :authtype AND u.deleted = 0 AND u.mnethostid = :mnethostid AND u.username {$in_sql}"; $update_users = $update_users + $DB->get_records_sql($sql, $params); } if ($update_users) { $trace->output("User entries to update: ".count($update_users)); foreach ($update_users as $user) { if ($this->update_user_record($user->username, $updatekeys, false, (bool) $user->suspended)) { $trace->output(get_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1); } else { $trace->output(get_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id))." - ".get_string('skipped'), 1); } } unset($update_users); } } } // Create missing accounts. // NOTE: this is very memory intensive and generally inefficient. $suspendselect = ""; if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $suspendselect = "AND u.suspended = 0"; } $sql = "SELECT u.id, u.username FROM {user} u WHERE u.auth=:authtype AND u.deleted='0' AND mnethostid=:mnethostid $suspendselect"; $users = $DB->get_records_sql($sql, array('authtype'=>$this->authtype, 'mnethostid'=>$CFG->mnet_localhost_id)); // Simplify down to usernames. $usernames = array(); if (!empty($users)) { foreach ($users as $user) { array_push($usernames, $user->username); } unset($users); } $add_users = array_diff($userlist, $usernames); unset($usernames); if (!empty($add_users)) { $trace->output(get_string('auth_dbuserstoadd','auth_db',count($add_users))); // Do not use transactions around this foreach, we want to skip problematic users, not revert everything. foreach($add_users as $user) { $username = $user; if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { if ($olduser = $DB->get_record('user', array('username' => $username, 'deleted' => 0, 'suspended' => 1, 'mnethostid' => $CFG->mnet_localhost_id, 'auth' => $this->authtype))) { $updateuser = new stdClass(); $updateuser->id = $olduser->id; $updateuser->suspended = 0; user_update_user($updateuser); $trace->output(get_string('auth_dbreviveduser', 'auth_db', array('name' => $username, 'id' => $olduser->id)), 1); continue; } } // Do not try to undelete users here, instead select suspending if you ever expect users will reappear. // Prep a few params. $user = $this->get_userinfo_asobj($user); $user->username = $username; $user->confirmed = 1; $user->auth = $this->authtype; $user->mnethostid = $CFG->mnet_localhost_id; if ($collision = $DB->get_record_select('user', "username = :username AND mnethostid = :mnethostid AND auth <> :auth", array('username'=>$user->username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype), 'id,username,auth')) { $trace->output(get_string('auth_dbinsertuserduplicate', 'auth_db', array('username'=>$user->username, 'auth'=>$collision->auth)), 1); continue; } try { $id = user_create_user($user, false, false); // It is truly a new user. $trace->output(get_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)), 1); } catch (moodle_exception $e) { $trace->output(get_string('auth_dbinsertusererror', 'auth_db', $user->username), 1); continue; } // If relevant, tag for password generation. if ($this->is_internal()) { set_user_preference('auth_forcepasswordchange', 1, $id); set_user_preference('create_password', 1, $id); } // Save custom profile fields here. require_once($CFG->dirroot . '/user/profile/lib.php'); $user->id = $id; profile_save_data($user); // Make sure user context is present. context_user::instance($id); \core\event\user_created::create_from_userid($id)->trigger(); } unset($add_users); } $trace->finished(); return 0; } function user_exists($username) { // Init result value. $result = false; $extusername = core_text::convert($username, 'utf-8', $this->config->extencoding); $authdb = $this->db_init(); $rs = $authdb->Execute("SELECT * FROM {$this->config->table} WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."' "); if (!$rs) { throw new \moodle_exception('auth_dbcantconnect', 'auth_db'); } else if (!$rs->EOF) { // User exists externally. $result = true; } $authdb->Close(); return $result; } function get_userlist() { // Init result value. $result = array(); $authdb = $this->db_init(); // Fetch userlist. $rs = $authdb->Execute("SELECT {$this->config->fielduser} FROM {$this->config->table} "); if (!$rs) { throw new \moodle_exception('auth_dbcantconnect', 'auth_db'); } else if (!$rs->EOF) { while ($rec = $rs->FetchRow()) { $rec = array_change_key_case((array)$rec, CASE_LOWER); array_push($result, $rec[strtolower($this->config->fielduser)]); } } $authdb->Close(); return $result; } /** * Reads user information from DB and return it in an object. * * @param string $username username * @return array */ function get_userinfo_asobj($username) { $user_array = truncate_userinfo($this->get_userinfo($username)); $user = new stdClass(); foreach($user_array as $key=>$value) { $user->{$key} = $value; } return $user; } /** * Called when the user record is updated. * Modifies user in external database. It takes olduser (before changes) and newuser (after changes) * compares information saved modified information to external db. * * @param stdClass $olduser Userobject before modifications * @param stdClass $newuser Userobject new modified userobject * @return boolean result * */ function user_update($olduser, $newuser) { if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) { error_log("ERROR:User renaming not allowed in ext db"); return false; } if (isset($olduser->auth) and $olduser->auth != $this->authtype) { return true; // Just change auth and skip update. } $curruser = $this->get_userinfo($olduser->username); if (empty($curruser)) { error_log("ERROR:User $olduser->username found in ext db"); return false; } $extusername = core_text::convert($olduser->username, 'utf-8', $this->config->extencoding); $authdb = $this->db_init(); $update = array(); foreach($curruser as $key=>$value) { if ($key == 'username') { continue; // Skip this. } if (empty($this->config->{"field_updateremote_$key"})) { continue; // Remote update not requested. } if (!isset($newuser->$key)) { continue; } $nuvalue = $newuser->$key; // Support for textarea fields. if (isset($nuvalue['text'])) { $nuvalue = $nuvalue['text']; } if ($nuvalue != $value) { $update[] = $this->config->{"field_map_$key"}."='".$this->ext_addslashes(core_text::convert($nuvalue, 'utf-8', $this->config->extencoding))."'"; } } if (!empty($update)) { $sql = "UPDATE {$this->config->table} SET ".implode(',', $update)." WHERE {$this->config->fielduser} = ?"; if (!$authdb->Execute($sql, array($this->ext_addslashes($extusername)))) { throw new \moodle_exception('auth_dbupdateerror', 'auth_db'); } } $authdb->Close(); return true; } function prevent_local_passwords() { return !$this->is_internal(); } /** * Returns true if this authentication plugin is "internal". * * Internal plugins use password hashes from Moodle user table for authentication. * * @return bool */ function is_internal() { if (!isset($this->config->passtype)) { return true; } return ($this->config->passtype === 'internal'); } /** * Returns false if this plugin is enabled but not configured. * * @return bool */ public function is_configured() { if (!empty($this->config->type)) { return true; } return false; } /** * Indicates if moodle should automatically update internal user * records with data from external sources using the information * from auth_plugin_base::get_userinfo(). * * @return bool true means automatically copy data from ext to user table */ function is_synchronised_with_external() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return ($this->is_internal() or !empty($this->config->changepasswordurl)); } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { if ($this->is_internal() || empty($this->config->changepasswordurl)) { // Standard form. return null; } else { // Use admin defined custom url. return new moodle_url($this->config->changepasswordurl); } } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return $this->is_internal(); } /** * Add slashes, we can not use placeholders or system functions. * * @param string $text * @return string */ function ext_addslashes($text) { if (empty($this->config->sybasequoting)) { $text = str_replace('\\', '\\\\', $text); $text = str_replace(array('\'', '"', "\0"), array('\\\'', '\\"', '\\0'), $text); } else { $text = str_replace("'", "''", $text); } return $text; } /** * Test if settings are ok, print info to output. * @private */ public function test_settings() { global $CFG, $OUTPUT; // NOTE: this is not localised intentionally, admins are supposed to understand English at least a bit... raise_memory_limit(MEMORY_HUGE); if (empty($this->config->table)) { echo $OUTPUT->notification(get_string('auth_dbnoexttable', 'auth_db'), 'notifyproblem'); return; } if (empty($this->config->fielduser)) { echo $OUTPUT->notification(get_string('auth_dbnouserfield', 'auth_db'), 'notifyproblem'); return; } $olddebug = $CFG->debug; $olddisplay = ini_get('display_errors'); ini_set('display_errors', '1'); $CFG->debug = DEBUG_DEVELOPER; $olddebugauthdb = $this->config->debugauthdb; $this->config->debugauthdb = 1; error_reporting($CFG->debug); $adodb = $this->db_init(); if (!$adodb or !$adodb->IsConnected()) { $this->config->debugauthdb = $olddebugauthdb; $CFG->debug = $olddebug; ini_set('display_errors', $olddisplay); error_reporting($CFG->debug); ob_end_flush(); echo $OUTPUT->notification(get_string('auth_dbcannotconnect', 'auth_db'), 'notifyproblem'); return; } $rs = $adodb->Execute("SELECT * FROM {$this->config->table} WHERE {$this->config->fielduser} <> 'random_unlikely_username'"); // Any unlikely name is ok here. if (!$rs) { echo $OUTPUT->notification(get_string('auth_dbcannotreadtable', 'auth_db'), 'notifyproblem'); } else if ($rs->EOF) { echo $OUTPUT->notification(get_string('auth_dbtableempty', 'auth_db'), 'notifyproblem'); $rs->close(); } else { $fields_obj = $rs->FetchObj(); $columns = array_keys((array)$fields_obj); echo $OUTPUT->notification(get_string('auth_dbcolumnlist', 'auth_db', implode(', ', $columns)), 'notifysuccess'); $rs->close(); } $adodb->Close(); $this->config->debugauthdb = $olddebugauthdb; $CFG->debug = $olddebug; ini_set('display_errors', $olddisplay); error_reporting($CFG->debug); ob_end_flush(); } /** * Clean the user data that comes from an external database. * @deprecated since 3.1, please use core_user::clean_data() instead. * @param array $user the user data to be validated against properties definition. * @return stdClass $user the cleaned user data. */ public function clean_data($user) { debugging('The method clean_data() has been deprecated, please use core_user::clean_data() instead.', DEBUG_DEVELOPER); return core_user::clean_data($user); } } db/settings.php 0000644 00000014724 15152311434 0007511 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin settings and defaults. * * @package auth_db * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // We use a couple of custom admin settings since we need to massage the data before it is inserted into the DB. require_once($CFG->dirroot.'/auth/db/classes/admin_setting_special_auth_configtext.php'); // Needed for constants. require_once($CFG->libdir.'/authlib.php'); // Introductory explanation. $settings->add(new admin_setting_heading('auth_db/pluginname', '', new lang_string('auth_dbdescription', 'auth_db'))); // Host. $settings->add(new admin_setting_configtext('auth_db/host', get_string('auth_dbhost_key', 'auth_db'), get_string('auth_dbhost', 'auth_db') . ' ' .get_string('auth_multiplehosts', 'auth'), '127.0.0.1', PARAM_RAW)); // Type. $dboptions = array(); $dbtypes = array("access", "ado_access", "ado", "ado_mssql", "borland_ibase", "csv", "db2", "fbsql", "firebird", "ibase", "informix72", "informix", "mssql", "mssql_n", "mssqlnative", "mysql", "mysqli", "mysqlt", "oci805", "oci8", "oci8po", "odbc", "odbc_mssql", "odbc_oracle", "oracle", "pdo", "postgres64", "postgres7", "postgres", "proxy", "sqlanywhere", "sybase", "vfp"); foreach ($dbtypes as $dbtype) { $dboptions[$dbtype] = $dbtype; } $settings->add(new admin_setting_configselect('auth_db/type', new lang_string('auth_dbtype_key', 'auth_db'), new lang_string('auth_dbtype', 'auth_db'), 'mysqli', $dboptions)); // Sybase quotes. $yesno = array( new lang_string('no'), new lang_string('yes'), ); $settings->add(new admin_setting_configselect('auth_db/sybasequoting', new lang_string('auth_dbsybasequoting', 'auth_db'), new lang_string('auth_dbsybasequotinghelp', 'auth_db'), 0, $yesno)); // DB Name. $settings->add(new admin_setting_configtext('auth_db/name', get_string('auth_dbname_key', 'auth_db'), get_string('auth_dbname', 'auth_db'), '', PARAM_RAW_TRIMMED)); // DB Username. $settings->add(new admin_setting_configtext('auth_db/user', get_string('auth_dbuser_key', 'auth_db'), get_string('auth_dbuser', 'auth_db'), '', PARAM_RAW_TRIMMED)); // Password. $settings->add(new admin_setting_configpasswordunmask('auth_db/pass', get_string('auth_dbpass_key', 'auth_db'), get_string('auth_dbpass', 'auth_db'), '')); // DB Table. $settings->add(new admin_setting_configtext('auth_db/table', get_string('auth_dbtable_key', 'auth_db'), get_string('auth_dbtable', 'auth_db'), '', PARAM_RAW_TRIMMED)); // DB User field. $settings->add(new admin_setting_configtext('auth_db/fielduser', get_string('auth_dbfielduser_key', 'auth_db'), get_string('auth_dbfielduser', 'auth_db'), '', PARAM_RAW_TRIMMED)); // DB User password. $settings->add(new admin_setting_configtext('auth_db/fieldpass', get_string('auth_dbfieldpass_key', 'auth_db'), get_string('auth_dbfieldpass', 'auth_db'), '', PARAM_RAW_TRIMMED)); // DB Password Type. $passtype = array(); $passtype["plaintext"] = get_string("plaintext", "auth"); $passtype["md5"] = get_string("md5", "auth"); $passtype["sha1"] = get_string("sha1", "auth"); $passtype["saltedcrypt"] = get_string("auth_dbsaltedcrypt", "auth_db"); $passtype["internal"] = get_string("internal", "auth"); $settings->add(new admin_setting_configselect('auth_db/passtype', new lang_string('auth_dbpasstype_key', 'auth_db'), new lang_string('auth_dbpasstype', 'auth_db'), 'plaintext', $passtype)); // Encoding. $settings->add(new admin_setting_configtext('auth_db/extencoding', get_string('auth_dbextencoding', 'auth_db'), get_string('auth_dbextencodinghelp', 'auth_db'), 'utf-8', PARAM_RAW_TRIMMED)); // DB SQL SETUP. $settings->add(new admin_setting_configtext('auth_db/setupsql', get_string('auth_dbsetupsql', 'auth_db'), get_string('auth_dbsetupsqlhelp', 'auth_db'), '', PARAM_RAW_TRIMMED)); // Debug ADOOB. $settings->add(new admin_setting_configselect('auth_db/debugauthdb', new lang_string('auth_dbdebugauthdb', 'auth_db'), new lang_string('auth_dbdebugauthdbhelp', 'auth_db'), 0, $yesno)); // Password change URL. $settings->add(new auth_db_admin_setting_special_auth_configtext('auth_db/changepasswordurl', get_string('auth_dbchangepasswordurl_key', 'auth_db'), get_string('changepasswordhelp', 'auth'), '', PARAM_URL)); // Label and Sync Options. $settings->add(new admin_setting_heading('auth_db/usersync', new lang_string('auth_sync_script', 'auth'), '')); // Sync Options. $deleteopt = array(); $deleteopt[AUTH_REMOVEUSER_KEEP] = get_string('auth_remove_keep', 'auth'); $deleteopt[AUTH_REMOVEUSER_SUSPEND] = get_string('auth_remove_suspend', 'auth'); $deleteopt[AUTH_REMOVEUSER_FULLDELETE] = get_string('auth_remove_delete', 'auth'); $settings->add(new admin_setting_configselect('auth_db/removeuser', new lang_string('auth_remove_user_key', 'auth'), new lang_string('auth_remove_user', 'auth'), AUTH_REMOVEUSER_KEEP, $deleteopt)); // Update users. $settings->add(new admin_setting_configselect('auth_db/updateusers', new lang_string('auth_dbupdateusers', 'auth_db'), new lang_string('auth_dbupdateusers_description', 'auth_db'), 0, $yesno)); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('db'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_dbextrafields', 'auth_db'), true, true, $authplugin->get_custom_user_profile_fields()); } db/upgrade.txt 0000644 00000001472 15152311434 0007324 0 ustar 00 This files describes API changes in /auth/db/*, information provided here is intended especially for developers. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/db' to 'auth_db'. === 3.1 === * The auth_plugin_db::clean_data() has been deprecated and will be removed in a future version. Please update to use core_user::clean_data() instead. === 2.9 === Some alterations have been made to the handling of case sensitity handling of passwords and password hashes which previously varied depending on database configuration: * Plain text password matching is now always case sensitive * sha1/md5 hash comparisons are now enforced case insensitive (as underlying they are hexidecimal values) db/version.php 0000644 00000002227 15152311434 0007331 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version details * * @package auth_db * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_db'; // Full name of the plugin (used for diagnostics) db/classes/privacy/provider.php 0000644 00000002735 15152311434 0012614 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_db. * * @package auth_db * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_db\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_db implementing null_provider. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } db/classes/admin_setting_special_auth_configtext.php 0000644 00000003163 15152311434 0017101 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Special settings for auth_db password_link. * * @package auth_db * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special settings for auth_db password_link. * * @package auth_db * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_db_admin_setting_special_auth_configtext extends admin_setting_configtext { /** * We need to overwrite the global "alternate login url" setting if wayf is enabled. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { if (get_config('auth_db', 'passtype') === 'internal') { // We need to clear the auth_db change password link. $data = ''; } return parent::write_setting($data); } } db/classes/task/sync_users.php 0000644 00000003546 15152311434 0012445 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Sync users task * @package auth_db * @author Guy Thomas <gthomas@moodlerooms.com> * @copyright Copyright (c) 2017 Blackboard Inc. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_db\task; defined('MOODLE_INTERNAL') || die(); /** * Sync users task class * @package auth_db * @author Guy Thomas <gthomas@moodlerooms.com> * @copyright Copyright (c) 2017 Blackboard Inc. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_users extends \core\task\scheduled_task { /** * Name for this task. * * @return string */ public function get_name() { return get_string('auth_dbsyncuserstask', 'auth_db'); } /** * Run task for synchronising users. */ public function execute() { if (!is_enabled_auth('db')) { mtrace('auth_db plugin is disabled, synchronisation stopped', 2); return; } $dbauth = get_auth_plugin('db'); $config = get_config('auth_db'); $trace = new \text_progress_trace(); $update = !empty($config->updateusers); $dbauth->sync_users($trace, $update); } } db/lang/en/auth_db.php 0000644 00000013545 15152311434 0010602 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_db', language 'en'. * * @package auth_db * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_dbcantconnect'] = 'Could not connect to the specified authentication database...'; $string['auth_dbdebugauthdb'] = 'Debug ADOdb'; $string['auth_dbdebugauthdbhelp'] = 'Debug ADOdb connection to external database - use when getting empty page during login. Not suitable for production sites.'; $string['auth_dbdeleteuser'] = 'Deleted user {$a->name} id {$a->id}'; $string['auth_dbdeleteusererror'] = 'Error deleting user {$a}'; $string['auth_dbdescription'] = 'This method uses an external database table to check whether a given username and password is valid. If the account is a new one, then information from other fields may also be copied across into Moodle.'; $string['auth_dbextencoding'] = 'External db encoding'; $string['auth_dbextencodinghelp'] = 'Encoding used in external database'; $string['auth_dbextrafields'] = 'These fields are optional. You can choose to pre-fill some Moodle user fields with information from the <b>external database fields</b> that you specify here. <p>If you leave these blank, then defaults will be used.</p><p>In either case, the user will be able to edit all of these fields after they log in.</p>'; $string['auth_dbfieldpass'] = 'Name of the field containing passwords'; $string['auth_dbfieldpass_key'] = 'Password field'; $string['auth_dbfielduser'] = 'Name of the field containing usernames. This field must be a varchar data type.'; $string['auth_dbfielduser_key'] = 'Username field'; $string['auth_dbhost'] = 'The computer hosting the database server. Use a system DSN entry if using ODBC. Use a PDO DSN entry if using PDO.'; $string['auth_dbhost_key'] = 'Host'; $string['auth_dbchangepasswordurl_key'] = 'Password-change URL'; $string['auth_dbinsertuser'] = 'Inserted user {$a->name} id {$a->id}'; $string['auth_dbinsertuserduplicate'] = 'Error inserting user {$a->username} - user with this username was already created through \'{$a->auth}\' plugin.'; $string['auth_dbinsertusererror'] = 'Error inserting user {$a}'; $string['auth_dbname'] = 'Name of the database itself. Leave empty if using an ODBC DSN. Leave empty if your PDO DSN already contains the database name.'; $string['auth_dbname_key'] = 'DB name'; $string['auth_dbpass'] = 'Password matching the above username'; $string['auth_dbpass_key'] = 'Password'; $string['auth_dbpasstype'] = '<p>Specify the format that the password field is using.</p> <p>Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you must provide a populated email address field in the external database, and you must enable the \auth_db\task\sync_users scheduled task. Moodle will send an email to new users with a temporary password.</p>'; $string['auth_dbpasstype_key'] = 'Password format'; $string['auth_dbreviveduser'] = 'Revived user {$a->name} id {$a->id}'; $string['auth_dbrevivedusererror'] = 'Error reviving user {$a}'; $string['auth_dbsaltedcrypt'] = 'Crypt one-way string hashing'; $string['auth_dbsetupsql'] = 'SQL setup command'; $string['auth_dbsetupsqlhelp'] = 'SQL command for special database setup, often used to setup communication encoding - example for MySQL and PostgreSQL: <em>SET NAMES \'utf8\'</em>'; $string['auth_dbsuspenduser'] = 'Suspended user {$a->name} id {$a->id}'; $string['auth_dbsuspendusererror'] = 'Error suspending user {$a}'; $string['auth_dbsybasequoting'] = 'Use sybase quotes'; $string['auth_dbsybasequotinghelp'] = 'Sybase style single quote escaping - needed for Oracle, MS SQL and some other databases. Do not use for MySQL!'; $string['auth_dbsyncuserstask'] = 'Synchronise users task'; $string['auth_dbtable'] = 'Name of the table in the database'; $string['auth_dbtable_key'] = 'Table'; $string['auth_dbtype'] = 'The database type (see the documentation <a href="http://adodb.org/dokuwiki/doku.php" target="_blank">ADOdb - Database Abstraction Layer for PHP</a> for details).'; $string['auth_dbtype_key'] = 'Database'; $string['auth_dbupdateusers'] = 'Update users'; $string['auth_dbupdateusers_description'] = 'As well as inserting new users, update existing users.'; $string['auth_dbupdatinguser'] = 'Updating user {$a->name} id {$a->id}'; $string['auth_dbuser'] = 'Username with read access to the database'; $string['auth_dbuser_key'] = 'DB user'; $string['auth_dbuserstoadd'] = 'User entries to add: {$a}'; $string['auth_dbuserstoremove'] = 'User entries to remove: {$a}'; $string['auth_dbnoexttable'] = 'External table not specified.'; $string['auth_dbnouserfield'] = 'External user field not specified.'; $string['auth_dbcannotconnect'] = 'Cannot connect to external database.'; $string['auth_dbcannotreadtable'] = 'Cannot read external table.'; $string['auth_dbtableempty'] = 'External table is empty.'; $string['auth_dbcolumnlist'] = 'External table contains the following columns:<br />{$a}'; $string['auth_dbupdateerror'] = 'Error updating external database.'; $string['pluginname'] = 'External database'; $string['privacy:metadata'] = 'The External database authentication plugin does not store any personal data.'; webservice/auth.php 0000644 00000010453 15152311434 0010356 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Web service auth plugin, reserves username, prevents normal login. * TODO: add IP restrictions and some other features - MDL-17135 * * @package auth_webservice * @copyright 2008 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Web service auth plugin. */ class auth_plugin_webservice extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'webservice'; $this->config = get_config('auth_webservice'); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_webservice() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username (with system magic quotes) * @param string $password The password (with system magic quotes) * * @return bool Authentication success or failure. */ function user_login($username, $password) { // normla logins not allowed! return false; } /** * Custom auth hook for web services. * @param string $username * @param string $password * @return bool success */ function user_login_webservice($username, $password) { global $CFG, $DB; // special web service login if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) { return validate_internal_user_password($user, $password); } return false; } /** * Updates the user's password. * * called when the user password is updated. * * @param object $user User table object (with system magic quotes) * @param string $newpassword Plaintext password (with system magic quotes) * @return boolean result * */ function user_update_password($user, $newpassword) { $user = get_complete_user_data('id', $user->id); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). return update_internal_user_password($user, $newpassword); } /** * Returns true if this authentication plugin is 'internal'. * * Webserice auth doesn't use password fields, it uses only tokens. * * @return bool */ function is_internal() { return false; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return false; } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { return null; } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return false; } /** * Confirm the new user as registered. This should normally not be used, * but it may be necessary if the user auth_method is changed to manual * before the user is confirmed. */ function user_confirm($username, $confirmsecret = null) { return AUTH_CONFIRM_ERROR; } } webservice/version.php 0000644 00000002222 15152311434 0011075 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version information * * @package auth_webservice * @copyright 2011 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_webservice'; // Full name of the plugin (used for diagnostics) webservice/classes/privacy/provider.php 0000644 00000002775 15152311434 0014371 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_webservice. * * @package auth_webservice * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_webservice\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_webservice implementing null_provider. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } webservice/lang/en/auth_webservice.php 0000644 00000002334 15152311434 0014116 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_webservice', language 'en'. * * @package auth_webservice * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_webservicedescription'] = 'This authentication method should be used for accounts that are exclusively for use by web service clients.'; $string['pluginname'] = 'Web services authentication'; $string['privacy:metadata'] = 'The Web services authentication plugin does not store any personal data.'; test_settings.php 0000644 00000004561 15152311434 0010161 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Test auth settings. * * @package core_auth * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../config.php'); require_once("$CFG->libdir/adminlib.php"); $auth = optional_param('auth', '', PARAM_RAW); if (!core_component::is_valid_plugin_name('auth', $auth)) { $auth = ''; } else if (!file_exists("$CFG->dirroot/auth/$auth/auth.php")) { $auth = ''; } navigation_node::override_active_url(new moodle_url('/admin/settings.php', array('section'=>'manageauths'))); admin_externalpage_setup('authtestsettings'); $returnurl = new moodle_url('/admin/settings.php', array('section'=>'manageauths')); echo $OUTPUT->header(); if (!$auth) { $options = array(); $plugins = core_component::get_plugin_list('auth'); foreach ($plugins as $name => $fulldir) { $plugin = get_auth_plugin($name); if (!$plugin or !method_exists($plugin, 'test_settings')) { continue; } $options[$name] = get_string('pluginname', 'auth_'.$name); } if (!$options) { redirect($returnurl); } echo $OUTPUT->heading(get_string('testsettings', 'core_auth')); $url = new moodle_url('/auth/test_settings.php', array('sesskey'=>sesskey())); echo $OUTPUT->single_select($url, 'auth', $options); echo $OUTPUT->footer(); } $plugin = get_auth_plugin($auth); if (!$plugin or !method_exists($plugin, 'test_settings')) { redirect($returnurl); } echo $OUTPUT->heading(get_string('testsettingsheading', 'core_auth', get_string('pluginname', 'auth_'.$auth))); $plugin->test_settings(); echo $OUTPUT->continue_button($returnurl); echo $OUTPUT->footer(); nologin/auth.php 0000644 00000006627 15152311434 0007675 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Nologin authentication login - prevents user login. * * @package auth_nologin * @author Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Plugin for no authentication - disabled user. */ class auth_plugin_nologin extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'nologin'; } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_nologin() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Do not allow any login. * */ function user_login($username, $password) { return false; } /** * No password updates. */ function user_update_password($user, $newpassword) { return false; } function prevent_local_passwords() { // just in case, we do not want to loose the passwords return false; } /** * No external data sync. * * @return bool */ function is_internal() { //we do not know if it was internal or external originally return true; } /** * No changing of password. * * @return bool */ function can_change_password() { return false; } /** * No password resetting. */ function can_reset_password() { return false; } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } /** * Returns information on how the specified user can change their password. * User accounts with authentication type set to nologin are disabled accounts. * They cannot change their password. * * @param stdClass $user A user object * @return string[] An array of strings with keys subject and message */ public function get_password_change_info(stdClass $user) : array { $site = get_site(); $data = new stdClass(); $data->firstname = $user->firstname; $data->lastname = $user->lastname; $data->username = $user->username; $data->sitename = format_string($site->fullname); $data->admin = generate_email_signoff(); $message = get_string('emailpasswordchangeinfodisabled', '', $data); $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname)); return [ 'subject' => $subject, 'message' => $message ]; } } nologin/version.php 0000644 00000002217 15152311434 0010410 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version information * * @package auth_nologin * @copyright 2011 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_nologin'; // Full name of the plugin (used for diagnostics) nologin/classes/privacy/provider.php 0000644 00000002761 15152311434 0013673 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_nologin. * * @package auth_nologin * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_nologin\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_nologin implementing null_provider. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } nologin/lang/en/auth_nologin.php 0000644 00000002346 15152311434 0012737 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_nologin', language 'en'. * * @package auth_nologin * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_nologindescription'] = 'Auxiliary plugin that prevents user to login into system and also discards any mail sent to the user. Can be used to <em>suspend</em> user accounts.'; $string['pluginname'] = 'No login'; $string['privacy:metadata'] = 'The No login authentication plugin does not store any personal data.'; index.html 0000644 00000000000 15152311434 0006526 0 ustar 00 none/db/upgrade.php 0000644 00000002671 15152311434 0010235 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * No authentication plugin upgrade code * * @package auth_none * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Function to upgrade auth_none. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_none_upgrade($oldversion) { global $CFG, $DB; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } none/auth.php 0000644 00000007145 15152311434 0007163 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Anobody can login with any password. * * @package auth_none * @author Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Plugin for no authentication. */ class auth_plugin_none extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'none'; $this->config = get_config('auth_none'); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_none() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work or don't exist and false * if the user exists and the password is wrong. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ function user_login ($username, $password) { global $CFG, $DB; if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) { return validate_internal_user_password($user, $password); } return true; } /** * Updates the user's password. * * called when the user password is updated. * * @param object $user User table object * @param string $newpassword Plaintext password * @return boolean result * */ function user_update_password($user, $newpassword) { $user = get_complete_user_data('id', $user->id); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). return update_internal_user_password($user, $newpassword); } function prevent_local_passwords() { return false; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return true; } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { return null; } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return true; } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } } none/settings.php 0000644 00000002505 15152311434 0010055 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin settings and defaults. * * @package auth_none * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // Introductory explanation. $settings->add(new admin_setting_heading('auth_none/pluginname', '', new lang_string('auth_nonedescription', 'auth_none'))); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('none'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); } none/upgrade.txt 0000644 00000000472 15152311434 0007675 0 ustar 00 This files describes API changes in /auth/none/*, information provided here is intended especially for developers. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/none' to 'auth_none'. none/lib.php 0000644 00000002167 15152311434 0006767 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Anybody can login with any password. * * @package auth_none * @category check * @copyright 2020 Brendan Heywood <brendan@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Add security check to make sure this isn't on in production. * * @return array check */ function auth_none_security_checks() { return [new auth_none\check\noauth()]; } none/version.php 0000644 00000002214 15152311434 0007677 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version information * * @package auth_none * @copyright 2011 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_none'; // Full name of the plugin (used for diagnostics) none/classes/privacy/provider.php 0000644 00000002745 15152311434 0013167 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_none. * * @package auth_none * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_none\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_none implementing null_provider. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } none/classes/check/noauth.php 0000644 00000004051 15152311434 0012223 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Verifies unsupported noauth setting * * @package auth_none * @copyright 2020 Brendan Heywood <brendan@catalyst-au.net> * @copyright 2008 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_none\check; defined('MOODLE_INTERNAL') || die(); use core\check\result; /** * Verifies unsupported noauth setting * * @copyright 2020 Brendan Heywood <brendan@catalyst-au.net> * @copyright 2008 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class noauth extends \core\check\check { /** * A link to a place to action this * * @return action_link */ public function get_action_link(): ?\action_link { return new \action_link( new \moodle_url('/admin/settings.php?section=manageauths'), get_string('authsettings', 'admin')); } /** * Return result * @return result */ public function get_result(): result { if (is_enabled_auth('none')) { $status = result::ERROR; $summary = get_string('checknoautherror', 'auth_none'); } else { $status = result::OK; $summary = get_string('checknoauthok', 'auth_none'); } $details = get_string('checknoauthdetails', 'auth_none'); return new result($status, $summary, $details); } } none/lang/en/auth_none.php 0000644 00000003301 15152311434 0011513 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_none', language 'en'. * * @package auth_none * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_nonedescription'] = 'Users can sign in and create valid accounts immediately, with no authentication against an external server and no confirmation via email. Be careful using this option - think of the security and administration problems this could cause.'; $string['pluginname'] = 'No authentication'; $string['privacy:metadata'] = 'The No authentication plugin does not store any personal data.'; $string['checknoauthdetails'] = '<p>The <em>No authentication</em> plugin is not intended for production sites. Please disable it unless this is a development test site.</p>'; $string['checknoautherror'] = 'The No authentication plugin cannot be used on production sites.'; $string['checknoauth'] = 'No authentication'; $string['checknoauthok'] = 'The no authentication plugin is disabled.'; tests/privacy/provider_test.php 0000644 00000010627 15152311434 0012772 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Data provider tests. * * @package core_auth * @category test * @copyright 2018 Frédéric Massart * @author Frédéric Massart <fred@branchup.tech> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; use core_privacy\tests\provider_testcase; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_auth\privacy\provider; /** * Data provider testcase class. * * @package core_auth * @category test * @copyright 2018 Frédéric Massart * @author Frédéric Massart <fred@branchup.tech> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { public function setUp(): void { $this->resetAfterTest(); } public function test_export_user_preferences() { $dg = $this->getDataGenerator(); $u1 = $dg->create_user(); $u2 = $dg->create_user(); $sysctx = \context_system::instance(); $now = time(); // Check nothing is there. writer::reset(); provider::export_user_preferences($u1->id); $prefs = writer::with_context($sysctx)->get_user_preferences('core_auth'); $this->assertEmpty((array) $prefs); // Set some preferences. set_user_preference('auth_forcepasswordchange', 1, $u1); set_user_preference('create_password', 1, $u1); set_user_preference('login_failed_count', 18, $u1); set_user_preference('login_failed_count_since_success', 7, $u1); set_user_preference('login_failed_last', $now - DAYSECS, $u1); set_user_preference('login_lockout', $now - HOURSECS, $u1); set_user_preference('login_lockout_ignored', 0, $u1); set_user_preference('login_lockout_secret', 'Hello world!', $u1); set_user_preference('auth_forcepasswordchange', 0, $u2); set_user_preference('create_password', 0, $u2); set_user_preference('login_lockout_ignored', 1, $u2); // Check user 1. writer::reset(); provider::export_user_preferences($u1->id); $prefs = writer::with_context($sysctx)->get_user_preferences('core_auth'); $this->assertEquals(transform::yesno(true), $prefs->auth_forcepasswordchange->value); $this->assertEquals(transform::yesno(true), $prefs->create_password->value); $this->assertEquals(18, $prefs->login_failed_count->value); $this->assertEquals(7, $prefs->login_failed_count_since_success->value); $this->assertEquals(transform::datetime($now - DAYSECS), $prefs->login_failed_last->value); $this->assertEquals(transform::datetime($now - HOURSECS), $prefs->login_lockout->value); $this->assertEquals(transform::yesno(false), $prefs->login_lockout_ignored->value); $this->assertEquals('Hello world!', $prefs->login_lockout_secret->value); // Check user 2. writer::reset(); provider::export_user_preferences($u2->id); $prefs = writer::with_context($sysctx)->get_user_preferences('core_auth'); $this->assertEquals(transform::yesno(false), $prefs->auth_forcepasswordchange->value); $this->assertEquals(transform::yesno(false), $prefs->create_password->value); $this->assertObjectNotHasAttribute('login_failed_count', $prefs); $this->assertObjectNotHasAttribute('login_failed_count_since_success', $prefs); $this->assertObjectNotHasAttribute('login_failed_last', $prefs); $this->assertObjectNotHasAttribute('login_lockout', $prefs); $this->assertEquals(transform::yesno(true), $prefs->login_lockout_ignored->value); $this->assertObjectNotHasAttribute('login_lockout_secret', $prefs); } } tests/digital_consent_test.php 0000644 00000014560 15152311434 0012631 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_auth; /** * Digital consent helper testcase. * * @package core_auth * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class digital_consent_test extends \advanced_testcase { public function test_is_age_digital_consent_verification_enabled() { global $CFG; $this->resetAfterTest(); // Age of digital consent verification is enabled. $CFG->agedigitalconsentverification = 0; $isenabled = \core_auth\digital_consent::is_age_digital_consent_verification_enabled(); $this->assertFalse($isenabled); } public function test_is_minor() { global $CFG; $this->resetAfterTest(); $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, 14', 'CZ, 13', 'DE, 14', 'DK, 13', ]); $CFG->agedigitalconsentmap = $agedigitalconsentmap; $usercountry1 = 'DK'; $usercountry2 = 'AU'; $userage1 = 12; $userage2 = 14; $userage3 = 16; // Test country exists in agedigitalconsentmap and user age is below the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage1, $usercountry1); $this->assertTrue($isminor); // Test country exists in agedigitalconsentmap and user age is above the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage2, $usercountry1); $this->assertFalse($isminor); // Test country does not exists in agedigitalconsentmap and user age is below the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage2, $usercountry2); $this->assertTrue($isminor); // Test country does not exists in agedigitalconsentmap and user age is above the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage3, $usercountry2); $this->assertFalse($isminor); } public function test_parse_age_digital_consent_map_valid_format() { // Value of agedigitalconsentmap has a valid format. $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, 14', 'BE, 13' ]); $ageconsentmapparsed = \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); $this->assertEquals([ '*' => 16, 'AT' => 14, 'BE' => 13 ], $ageconsentmapparsed ); } public function test_parse_age_digital_consent_map_invalid_format_missing_spaces() { // Value of agedigitalconsentmap has an invalid format (missing space separator between values). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT14', ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcomma', 'error', 'AT14')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_missing_default_value() { // Value of agedigitalconsentmap has an invalid format (missing default value). $agedigitalconsentmap = implode(PHP_EOL, [ 'BE, 16', 'AT, 14' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvaliddefault', 'error')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_invalid_country() { // Value of agedigitalconsentmap has an invalid format (invalid value for country). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'TEST, 14' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcountry', 'error', 'TEST')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_invalid_age_string() { // Value of agedigitalconsentmap has an invalid format (string value for age). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, ten' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidage', 'error', 'ten')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_missing_age() { // Value of agedigitalconsentmap has an invalid format (missing value for age). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, ' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidage', 'error', '')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_missing_country() { // Value of agedigitalconsentmap has an invalid format (missing value for country). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', ', 12' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcountry', 'error', '')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } } tests/behat/logout.php 0000644 00000002523 15152311434 0011014 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. // phpcs:disable moodle.Files.RequireLogin.Missing // phpcs:disable moodle.PHP.ForbiddenFunctions.Found /** * Login end point for Behat tests only. * * @package core_auth * @category test * @copyright Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../../config.php'); $behatrunning = defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING; if (!$behatrunning) { redirect(new moodle_url('/login/logout.php')); } require_logout(); $login = optional_param('loginpage', 0, PARAM_BOOL); if ($login) { redirect(get_login_url()); } else { redirect(new moodle_url('/')); } tests/behat/validateagedigitalconsentmap.feature 0000644 00000005025 15152311434 0016243 0 ustar 00 @core @verify_age_location Feature: Test validation of 'Age of digital consent' setting. In order to set the 'Age of digital consent' setting As an admin I need to provide valid data and valid format Background: Given I log in as "admin" And I navigate to "Users > Privacy and policies > Privacy settings" in site administration Scenario: Admin provides valid value for 'Age of digital consent'. Given I set the field "s__agedigitalconsentmap" to multiline: """ *, 16 AT, 14 BE, 14 """ When I press "Save changes" Then I should see "Changes saved" And I should not see "Some settings were not changed due to an error." And I should not see "The digital age of consent is not valid:" Scenario: Admin provides invalid format for 'Age of digital consent'. # Try to set a value with missing space separator Given I set the field "s__agedigitalconsentmap" to multiline: """ *16 AT, 14 BE, 14 """ When I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: \"*16\" has more or less than one comma separator." # Try to set a value with missing default age of consent When I set the field "s__agedigitalconsentmap" to multiline: """ AT, 14 BE, 14 """ And I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: Default (*) value is missing." Scenario: Admin provides invalid age of consent or country for 'Age of digital consent'. # Try to set a value containing invalid age of consent Given I set the field "s__agedigitalconsentmap" to multiline: """ *, 16 AT, age BE, 14 """ When I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: \"age\" is not a valid value for age." # Try to set a value containing invalid country When I set the field "s__agedigitalconsentmap" to multiline: """ *, 16 COUNTRY, 14 BE, 14 """ And I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: \"COUNTRY\" is not a valid value for country." tests/behat/loginform.feature 0000644 00000007452 15152311434 0012351 0 ustar 00 @auth @core_auth @javascript Feature: Test if the login form provides the correct feedback In order to check if the login form provides correct feedback As a user I need to go on login page and see feedback on incorrect username or password. Background: Given the following "users" exist: | username | | teacher1 | Scenario: Check invalid login message Given I follow "Log in" And I set the field "Username" to "teacher1" And I set the field "Password" to "incorrect" When I press "Log in" Then I should see "Invalid login, please try again" Scenario: Test login language selector Given remote langimport tests are enabled And the following "language packs" exist: | language | | nl | | es | And the following config values are set as admin: | langmenu | 1 | And I follow "Log in" And I open the action menu in "region-main" "region" # The line below contains the unicode character U+200E before and after the brackets, please be very careful editing this line. When I choose "Nederlands (nl)" in the open action menu Then I should see "Gebruikersnaam" @_file_upload Scenario: Set logo for loginpage Given I log in as "admin" And I navigate to "Appearance > Logos" in site administration And I upload "course/tests/fixtures/image.jpg" file to "Logo" filemanager And I press "Save changes" And I log out And I follow "Log in" Then "//img[@id='logoimage']" "xpath_element" should exist Scenario: Add a custom welcome message Given the following config values are set as admin: | auth_instructions | Lorem ipsum dolor sit amet | And I follow "Log in" Then I should see "Lorem ipsum dolor sit amet" Scenario: Show the maintenance mode message Given the following config values are set as admin: | maintenance_enabled | Disabled | | maintenance_message | Back online tomorrow | And I follow "Log in" Then I should see "Back online tomorrow" Scenario: User self registration Given the following config values are set as admin: | registerauth | Email-based self-registration | And I follow "Log in" Then I should see "Create new account" Scenario: Set OAuth providers Given I log in as "admin" And I navigate to "Plugins > Authentication > Manage authentication" in site administration And I click on "Enable" "link" in the "OAuth 2" "table_row" And I navigate to "Server > OAuth 2 services" in site administration And I press "Google" And I set the field "Client ID" to "1234" And I set the field "Client secret" to "1234" And I press "Save changes" And I press "Facebook" And I set the field "Client ID" to "1234" And I set the field "Client secret" to "1234" And I press "Save changes" And I press "Microsoft" And I set the field "Client ID" to "1234" And I set the field "Client secret" to "1234" And I press "Save changes" And I log out And I follow "Log in" Then I should see "Google" And I should see "Facebook" And I should see "Microsoft" Scenario: Test the login page auto focus feature Given the following config values are set as admin: | loginpageautofocus | Enabled | And I follow "Log in" Then the focused element is "Username" "field" And I set the field "Username" to "admin" And I set the field "Password" to "admin" And I press "Log in" And I log out And I follow "Log in" Then the focused element is "Password" "field" Scenario: Test the login page focus after error feature Given I follow "Log in" And I set the field "Username" to "admin" And I set the field "Password" to "wrongpassword" And I press "Log in" And I press the tab key Then the focused element is "Username" "field" tests/behat/login.feature 0000644 00000005444 15152311434 0011464 0 ustar 00 @core @core_auth Feature: Authentication In order to validate my credentials in the system As a user I need to log into the system Scenario: Log in with the predefined admin user with Javascript disabled Given I log in as "admin" Then I should see "You are logged in as Admin User" in the "page-footer" "region" @javascript Scenario: Log in with the predefined admin user with Javascript enabled Given I log in as "admin" Then I should see "You are logged in as Admin User" in the "page-footer" "region" Scenario: Log in as an existing admin user filling the form Given the following "users" exist: | username | password | firstname | lastname | email | | testuser | testuser | Test | User | moodle@example.com | And I am on site homepage When I follow "Log in" And I set the field "Username" to "testuser" And I set the field "Password" to "testuser" And I press "Log in" Then I should see "You are logged in as" in the "page-footer" "region" Scenario: Log in as an unexisting user filling the form Given the following "users" exist: | username | password | firstname | lastname | email | | testuser | testuser | Test | User | moodle@example.com | And I am on site homepage When I follow "Log in" And I set the field "Username" to "testuser" And I set the field "Password" to "unexisting" And I press "Log in" Then I should see "Invalid login, please try again" Scenario: Log out using the Log out link Given I log in as "admin" When I click on "Log out" "link" in the "#page-footer" "css_element" Then I should see "You are not logged in" in the "page-footer" "region" @javascript @accessibility Scenario: Login page must be accessible When I am on site homepage # The following tests are all provided to ensure that the accessibility tests themselves are tested. # In normal tests only one of the following is required. Then the page should meet accessibility standards And the page should meet "wcag131, wcag141, wcag412" accessibility standards And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests And I follow "Log in" And the page should meet accessibility standards And the page should meet "wcag131, wcag141, wcag412" accessibility standards And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests @javascript @accessibility Scenario: The login page must have sufficient colour contrast Given the following config values are set as admin: | custommenuitems | -This is a custom item\|/customurl/ | When I am on site homepage Then the page should meet "wcag143" accessibility standards And the page should meet accessibility standards with "wcag143" extra tests tests/behat/behat_auth.php 0000644 00000004777 15152311434 0011624 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Basic authentication steps definitions. * * @package core_auth * @category test * @copyright 2012 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); /** * Log in log out steps definitions. * * @package core_auth * @category test * @copyright 2012 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_auth extends behat_base { /** * Logs in the user. There should exist a user with the same value as username and password. * * @Given /^I log in as "(?P<username_string>(?:[^"]|\\")*)"$/ * @Given I am logged in as :username * @param string $username the user to log in as. * @param moodle_url|null $wantsurl optional, URL to go to after logging in. */ public function i_log_in_as(string $username, moodle_url $wantsurl = null) { // In the mobile app the required tasks are different (does not support $wantsurl). if ($this->is_in_app()) { $this->execute('behat_app::login', [$username]); return; } $loginurl = new moodle_url('/auth/tests/behat/login.php', [ 'username' => $username, ]); if ($wantsurl !== null) { $loginurl->param('wantsurl', $wantsurl->out_as_local_url()); } // Visit login page. $this->execute('behat_general::i_visit', [$loginurl]); } /** * Logs out of the system. * * @Given /^I log out$/ * @Given I am not logged in */ public function i_log_out() { $this->execute('behat_general::i_visit', [new moodle_url('/auth/tests/behat/logout.php')]); } } tests/behat/displayloginfailures.feature 0000644 00000005641 15152311434 0014604 0 ustar 00 @core @core_auth Feature: Test the 'showlogfailures' feature works. In order to see my recent login failures when logging in As a user I need to have at least one failed login attempt and then log in Background: Given the following "users" exist: | username | | teacher1 | And the following config values are set as admin: | displayloginfailures | 1 | # Given the user has at least one failed login attempt, when they login, then they should see both header and footer notices. Scenario: Check that 'displayloginfailures' works without javascript for teachers. # Simulate a log in failure for the teacher. Given I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "teacher1" And I set the field "Password" to "wrongpass" And I press "Log in" And I should see "Invalid login, please try again" # Now, log in with the correct credentials. When I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" # Confirm the notices are displayed. Then I should see "1 failed logins since your last login" in the ".navbar" "css_element" And I should see "1 failed logins since your last login" in the "page-footer" "region" # Confirm the notices disappear when navigating to another page. And I am on homepage And I should not see "1 failed logins since your last login" in the ".navbar" "css_element" And I should not see "1 failed logins since your last login" in the "page-footer" "region" # Given the user has at least one failed login attempt, when they login, then they should see both header and footer notices. Scenario: Check that 'displayloginfailures' works without javascript for admins. # Simulate a log in failure for the teacher. Given I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "admin" And I set the field "Password" to "wrongpass" And I press "Log in" And I should see "Invalid login, please try again" # Now, log in with the correct credentials. When I set the field "Username" to "admin" And I set the field "Password" to "admin" And I press "Log in" # Confirm the notices are displayed. Then I should see "1 failed logins since your last login" in the ".navbar" "css_element" And I should see "1 failed logins since your last login (Logs)" in the "page-footer" "region" # Confirm that the link works and that the notices disappear when navigating to another page. And I click on "Logs" "link" in the "page-footer" "region" And I should see "User login failed" in the "table.reportlog" "css_element" And I should not see "1 failed logins since your last login" in the ".navbar" "css_element" And I should not see "1 failed logins since your last login (Logs)" in the "page-footer" "region" tests/behat/login.php 0000644 00000005735 15152311434 0010623 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. // phpcs:disable moodle.Files.RequireLogin.Missing // phpcs:disable moodle.PHP.ForbiddenFunctions.Found /** * Login end point for Behat tests only. * * @package core_auth * @category test * @author Guy Thomas * @copyright 2021 Class Technologies Inc. {@link https://www.class.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../../config.php'); require_once("{$CFG->dirroot}/login/lib.php"); $behatrunning = defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING; if (!$behatrunning) { redirect(new moodle_url('/')); } $username = required_param('username', PARAM_ALPHANUMEXT); $wantsurl = optional_param('wantsurl', null, PARAM_URL); if (isloggedin()) { // If the user is already logged in, log them out and redirect them back to login again. require_logout(); redirect(new moodle_url('/auth/tests/behat/login.php', [ 'username' => $username, 'wantsurl' => (new moodle_url($wantsurl))->out(false), ])); } // Note - with behat, the password is always the same as the username. $password = $username; $failurereason = null; $user = authenticate_user_login($username, $password, true, $failurereason, false); if ($failurereason) { switch($failurereason) { case AUTH_LOGIN_NOUSER: $reason = get_string('invalidlogin'); break; case AUTH_LOGIN_SUSPENDED: $reason = 'User suspended'; break; case AUTH_LOGIN_FAILED: $reason = 'Login failed'; break; case AUTH_LOGIN_LOCKOUT: $reason = 'Account locked'; break; case AUTH_LOGIN_UNAUTHORISED: $reason = get_string('unauthorisedlogin', 'core', $username); break; default: $reason = "Unknown login failure: '{$failurereason}'"; break; } // Note: Do not throw an exception here as we sometimes test that login does not work. // Exceptions are automatic failures in Behat. \core\notification::add($reason, \core\notification::ERROR); redirect(new moodle_url('/')); } if (!complete_user_login($user)) { throw new Exception("Failed to login as behat step for $username"); } if (empty($wantsurl)) { $wantsurl = core_login_get_return_url(); } redirect(new moodle_url($wantsurl)); tests/behat/rememberusername.feature 0000644 00000004477 15152311434 0013717 0 ustar 00 @core @core_auth Feature: Test the 'remember username' feature works. In order for users to easily log in to the site As a user I need the site to remember my username when the feature is enabled Background: Given the following "users" exist: | username | | teacher1 | # Given the user has logged in and selected 'Remember username', when they log in again, then their username should be remembered. Scenario: Check that 'remember username' works without javascript for teachers. # Log in the first time with $CFG->rememberusername set to Yes. Given the following config values are set as admin: | rememberusername | 1 | And I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" And I log out # Log out and check that the username was remembered. When I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" Then the field "username" matches value "teacher1" # Given the user has logged in before and selected 'Remember username', when they log in again and unset 'Remember username', then # their username should be forgotten for future log in attempts. Scenario: Check that 'remember username' unsetting works without javascript for teachers. # Log in the first time with $CFG->rememberusername set to Optional. Given the following config values are set as admin: | rememberusername | 2 | And I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" And I log out # Log in again, the username should have been remembered. When I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" Then the field "username" matches value "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" And I log out And the following config values are set as admin: | rememberusername | 0 | # Check username has been forgotten. And I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" Then the field "username" matches value "" tests/behat/verifyageofconsent.feature 0000644 00000003775 15152311434 0014261 0 ustar 00 @core @verify_age_location Feature: Test the 'Digital age of consent verification' feature works. In order to self-register on the site As an user I need be to be over the age of digital consent Background: Given the following config values are set as admin: | registerauth | email | | agedigitalconsentverification | 1 | Scenario: User that is not considered a digital minor attempts to self-register on the site. # Try to access the sign up page. Given I am on homepage When I click on "Log in" "link" in the ".logininfo" "css_element" And I click on "Create new account" "link" Then I should see "Age and location verification" When I set the field "What is your age?" to "16" And I set the field "In which country do you live?" to "DZ" And I press "Proceed" Then I should see "New account" And I should see "Username" # Try to access the sign up page again. When I press "Cancel" And I click on "Create new account" "link" Then I should see "New account" And I should see "Username" Scenario: User that is considered a digital minor attempts to self-register on the site. # Try to access the sign up page. Given I am on homepage When I click on "Log in" "link" in the ".logininfo" "css_element" And I click on "Create new account" "link" Then I should see "Age and location verification" When I set the field "What is your age?" to "12" And I set the field "In which country do you live?" to "AT" And I press "Proceed" Then I should see "You are too young to create an account on this site." And I should see "Please ask your parent/guardian to contact:" # Try to access the sign up page again. When I click on "Back to the site home" "link" And I click on "Log in" "link" in the ".logininfo" "css_element" And I click on "Create new account" "link" Then I should see "You are too young to create an account on this site." And I should see "Please ask your parent/guardian to contact:" tests/external/external_test.php 0000644 00000021351 15152311434 0013123 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Auth external functions tests. * * @package core_auth * @category external * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ namespace core_auth\external; use auth_email_external; use core_auth_external; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External auth API tests. * * @package core_auth * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ class external_test extends externallib_advanced_testcase { /** @var string Original error log */ protected $oldlog; /** * Set up for every test */ public function setUp(): void { global $CFG; $this->resetAfterTest(true); $CFG->registerauth = 'email'; // Discard error logs. $this->oldlog = ini_get('error_log'); ini_set('error_log', "$CFG->dataroot/testlog.log"); } /** * Tear down to restore old logging.. */ protected function tearDown(): void { ini_set('error_log', $this->oldlog); parent::tearDown(); } /** * Test confirm_user */ public function test_confirm_user() { global $DB; $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = \external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $secret = $DB->get_field('user', 'secret', array('username' => $username)); // Confirm the user. $result = core_auth_external::confirm_user($username, $secret); $result = \external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $confirmed = $DB->get_field('user', 'confirmed', array('username' => $username)); $this->assertEquals(1, $confirmed); // Try to confirm the user again. $result = core_auth_external::confirm_user($username, $secret); $result = \external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result); $this->assertFalse($result['success']); $this->assertCount(1, $result['warnings']); $this->assertEquals('alreadyconfirmed', $result['warnings'][0]['warningcode']); // Try to use an invalid secret. $this->expectException('\moodle_exception'); $this->expectExceptionMessage(get_string('invalidconfirmdata', 'error')); $result = core_auth_external::confirm_user($username, 'zzZZzz'); } /** * Test age digital consent not enabled. */ public function test_age_digital_consent_verification_is_not_enabled() { global $CFG; $CFG->agedigitalconsentverification = 0; $result = core_auth_external::is_age_digital_consent_verification_enabled(); $result = \external_api::clean_returnvalue( core_auth_external::is_age_digital_consent_verification_enabled_returns(), $result); $this->assertFalse($result['status']); } /** * Test age digital consent is enabled. */ public function test_age_digital_consent_verification_is_enabled() { global $CFG; $CFG->agedigitalconsentverification = 1; $result = core_auth_external::is_age_digital_consent_verification_enabled(); $result = \external_api::clean_returnvalue( core_auth_external::is_age_digital_consent_verification_enabled_returns(), $result); $this->assertTrue($result['status']); } /** * Test resend_confirmation_email. */ public function test_resend_confirmation_email() { global $DB; $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = \external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $result = core_auth_external::resend_confirmation_email($username, $password); $result = \external_api::clean_returnvalue(core_auth_external::resend_confirmation_email_returns(), $result); $this->assertTrue($result['status']); $this->assertEmpty($result['warnings']); $confirmed = $DB->get_field('user', 'confirmed', array('username' => $username)); $this->assertEquals(0, $confirmed); } /** * Test resend_confirmation_email invalid username. */ public function test_resend_confirmation_email_invalid_username() { $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = \external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts. $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/invalidlogin'); $result = core_auth_external::resend_confirmation_email('abc', $password); } /** * Test resend_confirmation_email invalid password. */ public function test_resend_confirmation_email_invalid_password() { $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = \external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts. $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/invalidlogin'); $result = core_auth_external::resend_confirmation_email($username, 'abc'); } /** * Test resend_confirmation_email already confirmed user. */ public function test_resend_confirmation_email_already_confirmed_user() { global $DB; $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = \external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $secret = $DB->get_field('user', 'secret', array('username' => $username)); // Confirm the user. $result = core_auth_external::confirm_user($username, $secret); $result = \external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result); $this->assertTrue($result['success']); $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/alreadyconfirmed'); core_auth_external::resend_confirmation_email($username, $password); } } manual/db/upgrade.php 0000644 00000002715 15152311434 0010552 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Manual authentication plugin upgrade code * * @package auth_manual * @copyright 2011 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Function to upgrade auth_manual. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_manual_upgrade($oldversion) { global $CFG; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } manual/tests/privacy/provider_test.php 0000644 00000005031 15152311434 0014240 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Base class for unit tests for auth_manual. * * @package auth_manual * @category test * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_manual\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/auth/manual/auth.php'); use core_privacy\local\request\writer; use core_privacy\local\request\transform; use auth_manual\privacy\provider; /** * Unit tests for the auth_manual implementation of the privacy API. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends \core_privacy\tests\provider_testcase { /** @var \auth_plugin_manual Keeps the authentication plugin. */ protected $authplugin; /** * Basic setup for these tests. */ public function setUp(): void { $this->resetAfterTest(true); $this->authplugin = new \auth_plugin_manual(); } /** * Test to check export_user_preferences. * returns user preferences data. */ public function test_export_user_preferences() { $user = $this->getDataGenerator()->create_user(); $this->authplugin->user_update_password($user, 'MyPrivacytestPassword*'); provider::export_user_preferences($user->id); $writer = writer::with_context(\context_system::instance()); $prefs = $writer->get_user_preferences('auth_manual'); $time = transform::datetime(get_user_preferences('auth_manual_passwordupdatetime', 0, $user->id)); $this->assertEquals($time, $prefs->auth_manual_passwordupdatetime->value); $this->assertEquals(get_string('privacy:metadata:preference:passwordupdatetime', 'auth_manual'), $prefs->auth_manual_passwordupdatetime->description); } } manual/tests/manual_test.php 0000644 00000006426 15152311434 0012217 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_manual; use auth_plugin_manual; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/auth/manual/auth.php'); /** * Manual authentication tests class. * * @package auth_manual * @category test * @copyright 2014 Gilles-Philippe Leblanc <gilles-philippe.leblanc@umontreal.ca> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manual_test extends \advanced_testcase { /** @var auth_plugin_manual Keeps the authentication plugin. */ protected $authplugin; /** * Setup test data. */ protected function setUp(): void { $this->resetAfterTest(true); $this->authplugin = new auth_plugin_manual(); set_config('expiration', '1', 'auth_manual'); set_config('expiration_warning', '2', 'auth_manual'); set_config('expirationtime', '30', 'auth_manual'); $this->authplugin->config = get_config(auth_plugin_manual::COMPONENT_NAME); } /** * Test user_update_password method. */ public function test_user_update_password() { $user = $this->getDataGenerator()->create_user(); $expectedtime = time(); $passwordisupdated = $this->authplugin->user_update_password($user, 'MyNewPassword*'); // Assert that the actual time should be equal or a little greater than the expected time. $this->assertGreaterThanOrEqual($expectedtime, get_user_preferences('auth_manual_passwordupdatetime', 0, $user->id)); // Assert that the password was successfully updated. $this->assertTrue($passwordisupdated); } /** * Test test_password_expire method. */ public function test_password_expire() { $userrecord = array(); $expirationtime = 31 * DAYSECS; $userrecord['timecreated'] = time() - $expirationtime; $user1 = $this->getDataGenerator()->create_user($userrecord); $user2 = $this->getDataGenerator()->create_user(); // The user 1 was created 31 days ago and has not changed his password yet, so the password has expirated. $this->assertLessThanOrEqual(-1, $this->authplugin->password_expire($user1->username)); // The user 2 just came to be created and has not changed his password yet, so the password has not expirated. $this->assertEquals(30, $this->authplugin->password_expire($user2->username)); $this->authplugin->user_update_password($user1, 'MyNewPassword*'); // The user 1 just updated his password so the password has not expirated. $this->assertEquals(30, $this->authplugin->password_expire($user1->username)); } } manual/tests/behat/auth_manual.feature 0000644 00000001666 15152311434 0014131 0 ustar 00 @auth @auth_manual Feature: Test manual authentication works. In order to check manual authentication As a teacher I need to go on login page and enter username and password. Background: Given the following "users" exist: | username | | teacher1 | @javascript Scenario: Check login works with javascript. Given I am on homepage And I expand navigation bar And I click on "Log in" "link" in the ".logininfo" "css_element" When I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" When I press "Log in" Then I should see "You are logged in as" Scenario: Check login works without javascript. Given I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" When I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" When I press "Log in" Then I should see "You are logged in as" manual/auth.php 0000644 00000014703 15152311434 0007477 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: Manual Authentication * Just does a simple check against the moodle database. * * @package auth_manual * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Manual authentication plugin. * * @package auth * @subpackage manual * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_plugin_manual extends auth_plugin_base { /** * The name of the component. Used by the configuration. */ const COMPONENT_NAME = 'auth_manual'; const LEGACY_COMPONENT_NAME = 'auth/manual'; /** * Constructor. */ public function __construct() { $this->authtype = 'manual'; $config = get_config(self::COMPONENT_NAME); $legacyconfig = get_config(self::LEGACY_COMPONENT_NAME); $this->config = (object)array_merge((array)$legacyconfig, (array)$config); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_manual() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. (Non-mnet accounts only!) * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ function user_login($username, $password) { global $CFG, $DB, $USER; if (!$user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) { return false; } if (!validate_internal_user_password($user, $password)) { return false; } if ($password === 'changeme') { // force the change - this is deprecated and it makes sense only for manual auth, // because most other plugins can not change password easily or // passwords are always specified by users set_user_preference('auth_forcepasswordchange', true, $user->id); } return true; } /** * Updates the user's password. * * Called when the user password is updated. * * @param object $user User table object * @param string $newpassword Plaintext password * @return boolean result */ function user_update_password($user, $newpassword) { $user = get_complete_user_data('id', $user->id); set_user_preference('auth_manual_passwordupdatetime', time(), $user->id); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). return update_internal_user_password($user, $newpassword); } function prevent_local_passwords() { return false; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return true; } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { return null; } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return true; } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } /** * Return number of days to user password expires. * * If user password does not expire, it should return 0 or a positive value. * If user password is already expired, it should return negative value. * * @param mixed $username username (with system magic quotes) * @return integer */ public function password_expire($username) { $result = 0; if (!empty($this->config->expirationtime)) { $user = core_user::get_user_by_username($username, 'id,timecreated'); $lastpasswordupdatetime = get_user_preferences('auth_manual_passwordupdatetime', $user->timecreated, $user->id); $expiretime = $lastpasswordupdatetime + $this->config->expirationtime * DAYSECS; $now = time(); $result = ($expiretime - $now) / DAYSECS; if ($expiretime > $now) { $result = ceil($result); } else { $result = floor($result); } } return $result; } /** * Confirm the new user as registered. This should normally not be used, * but it may be necessary if the user auth_method is changed to manual * before the user is confirmed. * * @param string $username * @param string $confirmsecret */ function user_confirm($username, $confirmsecret = null) { global $DB; $user = get_complete_user_data('username', $username); if (!empty($user)) { if ($user->confirmed) { return AUTH_CONFIRM_ALREADY; } else { $DB->set_field("user", "confirmed", 1, array("id"=>$user->id)); return AUTH_CONFIRM_OK; } } else { return AUTH_CONFIRM_ERROR; } } } manual/settings.php 0000644 00000006161 15152311434 0010375 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin settings and defaults * * @package auth_manual * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // Introductory explanation. $settings->add(new admin_setting_heading('auth_manual/pluginname', new lang_string('passwdexpire_settings', 'auth_manual'), new lang_string('auth_manualdescription', 'auth_manual'))); $expirationoptions = array( new lang_string('no'), new lang_string('yes'), ); $settings->add(new admin_setting_configselect('auth_manual/expiration', new lang_string('expiration', 'auth_manual'), new lang_string('expiration_desc', 'auth_manual'), 0, $expirationoptions)); $expirationtimeoptions = array( '30' => new lang_string('numdays', '', 30), '60' => new lang_string('numdays', '', 60), '90' => new lang_string('numdays', '', 90), '120' => new lang_string('numdays', '', 120), '150' => new lang_string('numdays', '', 150), '180' => new lang_string('numdays', '', 180), '365' => new lang_string('numdays', '', 365), ); $settings->add(new admin_setting_configselect('auth_manual/expirationtime', new lang_string('passwdexpiretime', 'auth_manual'), new lang_string('passwdexpiretime_desc', 'auth_manual'), 30, $expirationtimeoptions)); $expirationwarningoptions = array( '0' => new lang_string('never'), '1' => new lang_string('numdays', '', 1), '2' => new lang_string('numdays', '', 2), '3' => new lang_string('numdays', '', 3), '4' => new lang_string('numdays', '', 4), '5' => new lang_string('numdays', '', 5), '6' => new lang_string('numdays', '', 6), '7' => new lang_string('numdays', '', 7), '10' => new lang_string('numdays', '', 10), '14' => new lang_string('numdays', '', 14), ); $settings->add(new admin_setting_configselect('auth_manual/expiration_warning', new lang_string('expiration_warning', 'auth_manual'), new lang_string('expiration_warning_desc', 'auth_manual'), 0, $expirationwarningoptions)); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('manual'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); } manual/upgrade.txt 0000644 00000000500 15152311434 0010203 0 ustar 00 This files describes API changes in /auth/manual/*, information provided here is intended especially for developers. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/manual' to 'auth_manual'. manual/version.php 0000644 00000002253 15152311434 0010220 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Manual authentication plugin version information * * @package auth_manual * @copyright 2011 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_manual'; // Full name of the plugin (used for diagnostics) manual/classes/privacy/provider.php 0000644 00000005154 15152311434 0013502 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_manual. * * @package auth_manual * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_manual\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\request\writer; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\transform; /** * Privacy provider for the authentication manual. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\user_preference_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised item collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { // There is a one user preference. $collection->add_user_preference('auth_manual_passwordupdatetime', 'privacy:metadata:preference:passwordupdatetime'); return $collection; } /** * Export all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { $lastpasswordupdatetime = get_user_preferences('auth_manual_passwordupdatetime', null, $userid); if ($lastpasswordupdatetime !== null) { $time = transform::datetime($lastpasswordupdatetime); writer::export_user_preference('auth_manual', 'auth_manual_passwordupdatetime', $time, get_string('privacy:metadata:preference:passwordupdatetime', 'auth_manual') ); } } } manual/lang/en/auth_manual.php 0000644 00000003303 15152311434 0012351 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_manual', language 'en'. * * @package auth_manual * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_manualdescription'] = 'This method removes any way for users to create their own accounts. All accounts must be manually created by the admin user.'; $string['expiration'] = 'Enable password expiry'; $string['expiration_desc'] = 'Allow passwords to expire after a specified time.'; $string['expiration_warning'] = 'Notification threshold'; $string['expiration_warning_desc'] = 'Number of days before password expiry that a notification is issued.'; $string['passwdexpiretime'] = 'Password duration'; $string['passwdexpiretime_desc'] = 'Length of time for which a password is valid.'; $string['pluginname'] = 'Manual accounts'; $string['passwdexpire_settings'] = 'Password expiry settings'; $string['privacy:metadata:preference:passwordupdatetime'] = 'The date of the last password change.'; oauth2/db/upgrade.php 0000644 00000002655 15152311434 0010502 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * OAuth2 authentication plugin upgrade code * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Upgrade function * * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_oauth2_upgrade($oldversion) { global $DB; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } oauth2/db/events.php 0000644 00000002035 15152311434 0010347 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file definies observers needed by the plugin. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // List of observers. $observers = [ [ 'eventname' => '\core\event\user_deleted', 'callback' => '\auth_oauth2\api::user_deleted', ], ]; oauth2/db/access.php 0000644 00000002154 15152311434 0010306 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Capability definitions for this plugin. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'auth/oauth2:managelinkedlogins' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_USER, 'archetypes' => array( 'user' => CAP_ALLOW ) ), ]; oauth2/db/install.xml 0000644 00000004200 15152311434 0010516 0 ustar 00 <?xml version="1.0" encoding="UTF-8" ?> <XMLDB PATH="auth/oauth2/db" VERSION="20170323" COMMENT="XMLDB file for Moodle auth/oauth2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" > <TABLES> <TABLE NAME="auth_oauth2_linked_login" COMMENT="Accounts linked to a users Moodle account."> <FIELDS> <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The user account this oauth login is linked to."/> <FIELD NAME="issuerid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="username" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The external username to map to this moodle account"/> <FIELD NAME="email" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The external email to map to this moodle account"/> <FIELD NAME="confirmtoken" TYPE="char" LENGTH="64" NOTNULL="true" SEQUENCE="false" COMMENT="If this is not empty - the user has not confirmed their email to create the link."/> <FIELD NAME="confirmtokenexpires" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> <KEY NAME="usermodified_key" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/> <KEY NAME="userid_key" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/> <KEY NAME="issuerid_key" TYPE="foreign" FIELDS="issuerid" REFTABLE="oauth2_issuer" REFFIELDS="id"/> <KEY NAME="uniq_key" TYPE="unique" FIELDS="userid, issuerid, username"/> </KEYS> <INDEXES> <INDEX NAME="search_index" UNIQUE="false" FIELDS="issuerid, username"/> </INDEXES> </TABLE> </TABLES> </XMLDB> oauth2/confirm-linkedlogin.php 0000644 00000005510 15152311434 0012411 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Confirm self oauth2 user. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once($CFG->libdir . '/authlib.php'); $token = required_param('token', PARAM_RAW); $username = required_param('username', PARAM_USERNAME); $userid = required_param('userid', PARAM_INT); $issuerid = required_param('issuerid', PARAM_INT); $redirect = optional_param('redirect', '', PARAM_LOCALURL); // Where to redirect the browser once the user has been confirmed. $PAGE->set_url('/auth/oauth2/confirm-linkedlogin.php'); $PAGE->set_context(context_system::instance()); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $confirmed = \auth_oauth2\api::confirm_link_login($userid, $username, $issuerid, $token); if ($confirmed) { // The user has confirmed successfully, let's log them in. if (!$user = get_complete_user_data('id', $userid)) { throw new \moodle_exception('cannotfinduser', '', '', $userid); } if (!$user->suspended) { complete_user_login($user); \core\session\manager::apply_concurrent_login_limit($user->id, session_id()); // Check where to go, $redirect has a higher preference. if (empty($redirect) and !empty($SESSION->wantsurl) ) { $redirect = $SESSION->wantsurl; unset($SESSION->wantsurl); } if (!empty($redirect)) { redirect($redirect); } } $PAGE->navbar->add(get_string("confirmed")); $PAGE->set_title(get_string("confirmed")); $PAGE->set_heading($COURSE->fullname); echo $OUTPUT->header(); echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); echo "<h3>".get_string("thanks").", ". fullname($USER) . "</h3>\n"; echo "<p>".get_string("confirmed")."</p>\n"; echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; } else { \core\notification::error(get_string('confirmationinvalid', 'auth_oauth2')); } redirect("$CFG->wwwroot/"); oauth2/tests/auth_test.php 0000644 00000006711 15152311434 0011625 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_oauth2; /** * Auth oauth2 auth functions tests. * * @package auth_oauth2 * @category test * @copyright 2019 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \auth_oauth2\auth */ class auth_test extends \advanced_testcase { public function test_get_password_change_info() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(['auth' => 'oauth2']); $auth = get_auth_plugin($user->auth); $info = $auth->get_password_change_info($user); $this->assertEqualsCanonicalizing(['subject', 'message'], array_keys($info)); $this->assertStringContainsString( 'your password cannot be reset because you are using your account on another site to log in', $info['message']); } /** * Test complete_login for oauth2. * @covers ::complete_login */ public function test_oauth2_complete_login(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $wantsurl = new \moodle_url('/'); $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; $info['firstname'] = 'Apple'; $info['lastname'] = 'Fruit'; $info['url'] = 'http://apple.com/'; $info['alternamename'] = 'Beatles'; $info['auth'] = 'oauth2'; $user = \auth_oauth2\api::create_new_confirmed_account($info, $issuer); $auth = get_auth_plugin($user->auth); // Set up mock data. $client = $this->createMock(\core\oauth2\client::class); $client->expects($this->once())->method('get_raw_userinfo')->willReturn((object)$info); $client->expects($this->once())->method('get_userinfo')->willReturn($info); $client->expects($this->once())->method('get_issuer')->willReturn($issuer); $sink = $this->redirectEvents(); try { // Need @ as it will fail at \core\session\manager::login_user for session_regenerate_id. @$auth->complete_login($client, $wantsurl); } catch (\Exception $e) { // This happens as complete login is using 'redirect'. $this->assertInstanceOf(\moodle_exception::class, $e); } $events = $sink->get_events(); $sink->close(); // There are 2 events. First is core\event\user_updated and second is core\event\user_loggedin. $event = $events[1]; $this->assertInstanceOf('core\event\user_loggedin', $event); // Make sure the extra record is in the user_loggedin event. $extrauserinfo = $event->other['extrauserinfo']; $this->assertEquals($info, $extrauserinfo); } } oauth2/tests/privacy/provider_test.php 0000644 00000026477 15152311434 0014206 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy test for the authentication oauth2 * * @package auth_oauth2 * @category test * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2\privacy; defined('MOODLE_INTERNAL') || die(); use auth_oauth2\privacy\provider; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; use core_privacy\local\request\approved_userlist; /** * Privacy test for the authentication oauth2 * * @package auth_oauth2 * @category test * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Set up method. */ public function setUp(): void { $this->resetAfterTest(); $this->setAdminUser(); } /** * Check that a user context is returned if there is any user data for this user. */ public function test_get_contexts_for_userid() { $user = $this->getDataGenerator()->create_user(); $this->assertEmpty(provider::get_contexts_for_userid($user->id)); $issuer = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); $contextlist = provider::get_contexts_for_userid($user->id); // Check that we only get back one context. $this->assertCount(1, $contextlist); // Check that a context is returned is the expected. $usercontext = \context_user::instance($user->id); $this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]); } /** * Test that user data is exported correctly. */ public function test_export_user_data() { $user = $this->getDataGenerator()->create_user(); $issuer = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); $usercontext = \context_user::instance($user->id); $writer = writer::with_context($usercontext); $this->assertFalse($writer->has_any_data()); $approvedlist = new approved_contextlist($user, 'auth_oauth2', [$usercontext->id]); provider::export_user_data($approvedlist); $data = $writer->get_data([get_string('privacy:metadata:auth_oauth2', 'auth_oauth2'), $issuer->get('name')]); $this->assertEquals($info['username'], $data->username); $this->assertEquals($info['email'], $data->email); } /** * Test deleting all user data for a specific context. */ public function test_delete_data_for_all_users_in_context() { global $DB; $user1 = $this->getDataGenerator()->create_user(); $issuer1 = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer1, $user1->id, false); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $issuer2 = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'jerry'; $info['email'] = 'jerry@example.com'; \auth_oauth2\api::link_login($info, $issuer2, $user2->id, false); $user2context = \context_user::instance($user2->id); // Get all oauth2 accounts. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array()); // There should be two. $this->assertCount(2, $oauth2accounts); // Delete everything for the first user context. provider::delete_data_for_all_users_in_context($user1context); // Get all oauth2 accounts match with user1. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', ['userid' => $user1->id]); $this->assertCount(0, $oauth2accounts); // Get all oauth2 accounts. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array()); // There should be one. $this->assertCount(1, $oauth2accounts); } /** * This should work identical to the above test. */ public function test_delete_data_for_user() { global $DB; $user1 = $this->getDataGenerator()->create_user(); $issuer1 = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer1, $user1->id, false); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $issuer2 = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'jerry'; $info['email'] = 'jerry@example.com'; \auth_oauth2\api::link_login($info, $issuer2, $user2->id, false); $user2context = \context_user::instance($user2->id); // Get all oauth2 accounts. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array()); // There should be two. $this->assertCount(2, $oauth2accounts); // Delete everything for the first user. $approvedlist = new approved_contextlist($user1, 'auth_oauth2', [$user1context->id]); provider::delete_data_for_user($approvedlist); // Get all oauth2 accounts match with user1. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', ['userid' => $user1->id]); $this->assertCount(0, $oauth2accounts); // Get all oauth2 accounts. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array()); // There should be one user. $this->assertCount(1, $oauth2accounts); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context() { $this->resetAfterTest(); $component = 'auth_oauth2'; // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $issuer = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users() { $this->resetAfterTest(); $component = 'auth_oauth2'; // Create user1. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $issuer1 = \core\oauth2\api::create_standard_issuer('google'); $info1 = []; $info1['username'] = 'gina1'; $info1['email'] = 'gina@example1.com'; \auth_oauth2\api::link_login($info1, $issuer1, $user1->id, false); $issuer2 = \core\oauth2\api::create_standard_issuer('google'); $info2 = []; $info2['username'] = 'gina2'; $info2['email'] = 'gina@example2.com'; \auth_oauth2\api::link_login($info2, $issuer2, $user2->id, false); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); } } oauth2/tests/behat/settings_test.feature 0000644 00000002437 15152311434 0014454 0 ustar 00 @auth @auth_oauth2 @javascript Feature: OAuth2 settings test functionality In order to use them later for authentication As an administrator I need to be able to test configured OAuth2 login services. Background: Given I log in as "admin" And I change window size to "large" Scenario: Test oAuth2 authentication settings with no configured service. Given I navigate to "Plugins > Authentication > Manage authentication" in site administration And I click on "Test settings" "link" in the "OAuth 2" "table_row" Then I should see "There are no configured OAuth2 providers" Scenario: Test oAuth2 authentication settings for a configured service. Given I navigate to "Server > OAuth 2 services" in site administration And I press "Google" And I set the following fields to these values: | Name | Testing service | | Client ID | thisistheclientid | | Client secret | supersecret | And I press "Save changes" And I navigate to "Plugins > Authentication > Manage authentication" in site administration And I click on "Test settings" "link" in the "OAuth 2" "table_row" Then I should see "Testing service" oauth2/tests/api_test.php 0000644 00000017534 15152311434 0011442 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_oauth2; /** * External auth oauth2 API tests. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api_test extends \advanced_testcase { /** * Test the cleaning of orphaned linked logins for all issuers. */ public function test_clean_orphaned_linked_logins() { $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('google'); \core\oauth2\api::create_standard_issuer('microsoft'); $user = $this->getDataGenerator()->create_user(); $info = []; $info['username'] = 'banana'; $info['email'] = 'banana@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); \core\oauth2\api::delete_issuer($issuer->get('id')); $linkedlogins = \auth_oauth2\api::get_linked_logins($user->id, $issuer); $this->assertCount(1, $linkedlogins); \auth_oauth2\api::clean_orphaned_linked_logins(); $linkedlogins = \auth_oauth2\api::get_linked_logins($user->id, $issuer); $this->assertCount(0, $linkedlogins); $match = \auth_oauth2\api::match_username_to_user('banana', $issuer); $this->assertFalse($match); } /** * Test the cleaning of orphaned linked logins for a specific issuer. */ public function test_clean_orphaned_linked_logins_with_issuer_id() { $this->resetAfterTest(); $this->setAdminUser(); $issuer1 = \core\oauth2\api::create_standard_issuer('google'); $issuer2 = \core\oauth2\api::create_standard_issuer('microsoft'); $user1 = $this->getDataGenerator()->create_user(); $info = []; $info['username'] = 'banana'; $info['email'] = 'banana@example.com'; \auth_oauth2\api::link_login($info, $issuer1, $user1->id, false); $user2 = $this->getDataGenerator()->create_user(); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; \auth_oauth2\api::link_login($info, $issuer2, $user2->id, false); \core\oauth2\api::delete_issuer($issuer1->get('id')); \auth_oauth2\api::clean_orphaned_linked_logins($issuer1->get('id')); $linkedlogins = \auth_oauth2\api::get_linked_logins($user1->id, $issuer1); $this->assertCount(0, $linkedlogins); $linkedlogins = \auth_oauth2\api::get_linked_logins($user2->id, $issuer2); $this->assertCount(1, $linkedlogins); } /** * Test creating a new confirmed account. * Including testing that user profile fields are correctly set. * * @covers \auth_oauth2\api::create_new_confirmed_account */ public function test_create_new_confirmed_account() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; $info['firstname'] = 'Apple'; $info['lastname'] = 'Fruit'; $info['alternatename'] = 'Beatles'; $info['idnumber'] = '123456'; $info['city'] = 'Melbourne'; $info['country'] = 'AU'; $info['institution'] = 'ACME Inc'; $info['department'] = 'Misc Explosives'; $createduser = \auth_oauth2\api::create_new_confirmed_account($info, $issuer); // Get actual user record from DB to check. $userdata = $DB->get_record('user', ['id' => $createduser->id]); // Confirm each value supplied from issuers is saved into the user record. foreach ($info as $key => $value) { $this->assertEquals($value, $userdata->$key); } // Explicitly test the user is confirmed. $this->assertEquals(1, $userdata->confirmed); } /** * Test auto-confirming linked logins. */ public function test_linked_logins() { $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('google'); $user = $this->getDataGenerator()->create_user(); $info = []; $info['username'] = 'banana'; $info['email'] = 'banana@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); // Try and match a user with a linked login. $match = \auth_oauth2\api::match_username_to_user('banana', $issuer); $this->assertEquals($user->id, $match->get('userid')); $linkedlogins = \auth_oauth2\api::get_linked_logins($user->id, $issuer); \auth_oauth2\api::delete_linked_login($linkedlogins[0]->get('id')); $match = \auth_oauth2\api::match_username_to_user('banana', $issuer); $this->assertFalse($match); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; $info['firstname'] = 'Apple'; $info['lastname'] = 'Fruit'; $info['url'] = 'http://apple.com/'; $info['alternamename'] = 'Beatles'; $newuser = \auth_oauth2\api::create_new_confirmed_account($info, $issuer); $match = \auth_oauth2\api::match_username_to_user('apple', $issuer); $this->assertEquals($newuser->id, $match->get('userid')); } /** * Test that is_enabled correctly identifies when the plugin is enabled. */ public function test_is_enabled() { $this->resetAfterTest(); set_config('auth', 'manual,oauth2'); $this->assertTrue(\auth_oauth2\api::is_enabled()); } /** * Test that is_enabled correctly identifies when the plugin is disabled. */ public function test_is_enabled_disabled() { $this->resetAfterTest(); set_config('auth', 'manual'); $this->assertFalse(\auth_oauth2\api::is_enabled()); } /** * Test creating a user via the send confirm account email method. * Including testing that user profile fields are correctly set. * * @covers \auth_oauth2\api::send_confirm_account_email */ public function test_send_confirm_account_email() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; $info['firstname'] = 'Apple'; $info['lastname'] = 'Fruit'; $info['alternatename'] = 'Beatles'; $info['idnumber'] = '123456'; $info['city'] = 'Melbourne'; $info['country'] = 'AU'; $info['institution'] = 'ACME Inc'; $info['department'] = 'Misc Explosives'; $createduser = \auth_oauth2\api::send_confirm_account_email($info, $issuer); // Get actual user record from DB to check. $userdata = $DB->get_record('user', ['id' => $createduser->id]); // Confirm each value supplied from issuers is saved into the user record. foreach ($info as $key => $value) { $this->assertEquals($value, $userdata->$key); } // Explicitly test the user is not yet confirmed. $this->assertEquals(0, $userdata->confirmed); } } oauth2/templates/idpresponse.mustache 0000644 00000003505 15152311434 0014034 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template auth_oauth2/idpresponse Render response returned from the oAuth IdP for supplied test user. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * pairs - array of key value pairs return from the oAuth IdP * name - the name field from the IdP for the field * value - the value of the field for the supplied test user Example context (json): { "pairs": [{ "name": "firstname", "value": "Jane" }, { "name": "lastname", "value": "Awesome" }] } }} <div class="container mb-3"> <h3>{{#str}}userinfo, auth_oauth2{{/str}}</h3> <table class="table"> <thead> <tr> <th scope="col">{{#str}}key, auth_oauth2{{/str}}</th> <th scope="col">{{#str}}value, auth_oauth2{{/str}}</th> </tr> </thead> <tbody> {{#pairs}} <tr> <td>{{name}}</td> <td>{{{value}}}</td> </tr> {{/pairs}} </tbody> </table> </div> oauth2/templates/idps.mustache 0000644 00000003674 15152311434 0012447 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template auth_oauth2/idps Render available oauth2 idps for testing. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * idps - array of oAuth IdP objects * name - the display name of the IdP * url - the url of the oAuth test endpoint * iconurl - the icon of the oAuth IdP Example context (json): { "idps": [{ "name": "Microsoft", "url": "http:\/\/localhost\/auth\/oauth2\/test.php?id=2&sesskey=4js6afElZh", "iconurl": "https:\/\/www.microsoft.com\/favicon.ico" }] } }} <div class="container mb-3"> <div class="potentialidps-header row"> <h3>{{#str}}testidplogin, auth_oauth2{{/str}}</h3> </div> <div class="potentialidps row"> <div class="potentialidplist"> {{#idps}} <div class="potentialidp mb-2"> <a class="btn btn-secondary btn-block" href="{{{url}}}" title="{{name}}"> {{#iconurl}}<img src="{{{iconurl}}}" alt="{{name}}" width="24" height="24" class="mr-1"/>{{/iconurl}} {{name}} </a> </div> {{/idps}} </div> </div> </div> oauth2/linkedlogins.php 0000644 00000010560 15152311434 0011142 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * OAuth 2 Linked login configuration page. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese <damyon@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->libdir.'/tablelib.php'); $PAGE->set_url('/auth/oauth2/linkedlogins.php'); $PAGE->set_context(context_user::instance($USER->id)); $PAGE->set_pagelayout('admin'); $strheading = get_string('linkedlogins', 'auth_oauth2'); $PAGE->set_title($strheading); $PAGE->set_heading($strheading); require_login(); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $action = optional_param('action', '', PARAM_ALPHAEXT); if ($action == 'new') { require_sesskey(); $issuerid = required_param('issuerid', PARAM_INT); $issuer = \core\oauth2\api::get_issuer($issuerid); if (!$issuer->is_available_for_login()) { throw new \moodle_exception('issuernologin', 'auth_oauth2'); } // We do a login dance with this issuer. $addparams = ['action' => 'new', 'issuerid' => $issuerid, 'sesskey' => sesskey()]; $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams); $client = \core\oauth2\api::get_user_oauth_client($issuer, $addurl); if (optional_param('logout', false, PARAM_BOOL)) { $client->log_out(); } if (!$client->is_logged_in()) { redirect($client->get_login_url()); } $userinfo = $client->get_userinfo(); if (!empty($userinfo)) { try { \auth_oauth2\api::link_login($userinfo, $issuer); redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } catch (Exception $e) { redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR); } } else { redirect($PAGE->url, get_string('notloggedin', 'auth_oauth2'), null, \core\output\notification::NOTIFY_ERROR); } } else if ($action == 'delete') { require_sesskey(); $linkedloginid = required_param('linkedloginid', PARAM_INT); auth_oauth2\api::delete_linked_login($linkedloginid); redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } $renderer = $PAGE->get_renderer('auth_oauth2'); $linkedloginid = optional_param('id', '', PARAM_RAW); $linkedlogin = null; auth_oauth2\api::clean_orphaned_linked_logins(); $issuers = \core\oauth2\api::get_all_issuers(true); $anyshowinloginpage = false; $issuerbuttons = array(); foreach ($issuers as $issuer) { if (!$issuer->is_available_for_login()) { continue; } $anyshowinloginpage = true; $addparams = ['action' => 'new', 'issuerid' => $issuer->get('id'), 'sesskey' => sesskey(), 'logout' => true]; $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams); $issuerbuttons[$issuer->get('id')] = $renderer->single_button($addurl, get_string('createnewlinkedlogin', 'auth_oauth2', s($issuer->get_display_name()))); } if (!$anyshowinloginpage) { // Just a notification that we can't make it. $preferencesurl = new moodle_url('/user/preferences.php'); redirect($preferencesurl, get_string('noissuersavailable', 'auth_oauth2'), null, \core\output\notification::NOTIFY_WARNING); } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('linkedlogins', 'auth_oauth2')); echo $OUTPUT->doc_link('Linked_Logins', get_string('linkedloginshelp', 'auth_oauth2')); $linkedlogins = auth_oauth2\api::get_linked_logins(); echo $renderer->linked_logins_table($linkedlogins); foreach ($issuerbuttons as $issuerbutton) { echo $issuerbutton; } echo $OUTPUT->footer(); oauth2/auth.php 0000644 00000004067 15152311434 0007426 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Open ID authentication. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Plugin for oauth2 authentication. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ class auth_plugin_oauth2 extends \auth_oauth2\auth { /** * Test the various configured Oauth2 providers. */ public function test_settings() { global $OUTPUT; $authplugin = get_auth_plugin('oauth2'); $idps = $authplugin->loginpage_idp_list(''); $templateidps = []; if (empty($idps)) { echo $OUTPUT->notification(get_string('noconfiguredidps', 'auth_oauth2'), 'notifyproblem'); return; } else { foreach ($idps as $idp) { $idpid = $idp['url']->get_param('id'); $sesskey = $idp['url']->get_param('sesskey'); $testurl = new moodle_url('/auth/oauth2/test.php', ['id' => $idpid, 'sesskey' => $sesskey]); $templateidps[] = ['name' => $idp['name'], 'url' => $testurl->out(), 'iconurl' => $idp['iconurl']]; } echo $OUTPUT->render_from_template('auth_oauth2/idps', ['idps' => $templateidps]); } } } oauth2/login.php 0000644 00000003671 15152311434 0007575 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Open ID authentication. This file is a simple login entry point for OAuth identity providers. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ require_once('../../config.php'); $issuerid = required_param('id', PARAM_INT); $wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_URL)); $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/auth/oauth2/login.php', ['id' => $issuerid])); require_sesskey(); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $issuer = new \core\oauth2\issuer($issuerid); if (!$issuer->is_available_for_login()) { throw new \moodle_exception('issuernologin', 'auth_oauth2'); } $returnparams = ['wantsurl' => $wantsurl, 'sesskey' => sesskey(), 'id' => $issuerid]; $returnurl = new moodle_url('/auth/oauth2/login.php', $returnparams); $client = \core\oauth2\api::get_user_oauth_client($issuer, $returnurl); if ($client) { if (!$client->is_logged_in()) { redirect($client->get_login_url()); } $auth = new \auth_oauth2\auth(); $auth->complete_login($client, $wantsurl); } else { throw new moodle_exception('Could not get an OAuth client.'); } oauth2/settings.php 0000644 00000002507 15152311434 0010322 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin settings and defaults. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { $warning = $OUTPUT->notification(get_string('createaccountswarning', 'auth_oauth2'), 'warning'); $settings->add(new admin_setting_heading('auth_oauth2/pluginname', '', $warning)); $authplugin = get_auth_plugin('oauth2'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_fieldlocks_help', 'auth'), false, false, $authplugin->customfields); } oauth2/confirm-account.php 0000644 00000006515 15152311434 0011554 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Confirm self oauth2 user. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once($CFG->libdir . '/authlib.php'); $usersecret = required_param('token', PARAM_RAW); $username = required_param('username', PARAM_USERNAME); $redirect = optional_param('redirect', '', PARAM_LOCALURL); // Where to redirect the browser once the user has been confirmed. $PAGE->set_url('/auth/oauth2/confirm-account.php'); $PAGE->set_context(context_system::instance()); $auth = get_auth_plugin('oauth2'); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $confirmed = $auth->user_confirm($username, $usersecret); if ($confirmed == AUTH_CONFIRM_ALREADY) { $user = get_complete_user_data('username', $username); $PAGE->navbar->add(get_string("alreadyconfirmed")); $PAGE->set_title(get_string("alreadyconfirmed")); $PAGE->set_heading($COURSE->fullname); echo $OUTPUT->header(); echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); echo "<p>".get_string("alreadyconfirmed")."</p>\n"; echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; } else if ($confirmed == AUTH_CONFIRM_OK) { // The user has confirmed successfully, let's log them in. if (!$user = get_complete_user_data('username', $username)) { throw new \moodle_exception('cannotfinduser', '', '', s($username)); } if (!$user->suspended) { complete_user_login($user); \core\session\manager::apply_concurrent_login_limit($user->id, session_id()); // Check where to go, $redirect has a higher preference. if (empty($redirect) and !empty($SESSION->wantsurl) ) { $redirect = $SESSION->wantsurl; unset($SESSION->wantsurl); } if (!empty($redirect)) { redirect($redirect); } } $PAGE->navbar->add(get_string("confirmed")); $PAGE->set_title(get_string("confirmed")); $PAGE->set_heading($COURSE->fullname); echo $OUTPUT->header(); echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); echo "<h3>".get_string("thanks").", ". fullname($USER) . "</h3>\n"; echo "<p>".get_string("confirmed")."</p>\n"; echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; } else { \core\notification::error(get_string('confirmationinvalid', 'auth_oauth2')); } redirect("$CFG->wwwroot/"); oauth2/lib.php 0000644 00000004206 15152311434 0007226 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Callbacks for auth_oauth2 * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Navigation hook to add to preferences page. * * @param navigation_node $useraccount * @param stdClass $user * @param context_user $context * @param stdClass $course * @param context_course $coursecontext */ function auth_oauth2_extend_navigation_user_settings(navigation_node $useraccount, stdClass $user, context_user $context, stdClass $course, context_course $coursecontext) { global $USER; if (\auth_oauth2\api::is_enabled() && !\core\session\manager::is_loggedinas()) { if (has_capability('auth/oauth2:managelinkedlogins', $context) && $user->id == $USER->id) { $parent = $useraccount->parent->find('useraccount', navigation_node::TYPE_CONTAINER); $parent->add(get_string('linkedlogins', 'auth_oauth2'), new moodle_url('/auth/oauth2/linkedlogins.php')); } } } /** * Callback to remove linked logins for deleted users. * * @param stdClass $user */ function auth_oauth2_pre_user_delete($user) { global $DB; $DB->delete_records(auth_oauth2\linked_login::TABLE, ['userid' => $user->id]); } oauth2/version.php 0000644 00000002177 15152311434 0010152 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version information * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics). oauth2/classes/output/renderer.php 0000644 00000006046 15152311434 0013267 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Output rendering for the plugin. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2\output; use plugin_renderer_base; use html_table; use html_table_cell; use html_table_row; use html_writer; use auth_oauth2\linked_login; use moodle_url; defined('MOODLE_INTERNAL') || die(); /** * Implements the plugin renderer * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends plugin_renderer_base { /** * This function will render one beautiful table with all the linked_logins. * * @param linked_login[] $linkedlogins - list of all linked logins. * @return string HTML to output. */ public function linked_logins_table($linkedlogins) { global $CFG; $table = new html_table(); $table->head = [ get_string('issuer', 'auth_oauth2'), get_string('info', 'auth_oauth2'), get_string('edit'), ]; $table->attributes['class'] = 'admintable generaltable'; $data = []; $index = 0; foreach ($linkedlogins as $linkedlogin) { // Issuer. $issuerid = $linkedlogin->get('issuerid'); $issuer = \core\oauth2\api::get_issuer($issuerid); $issuercell = new html_table_cell(s($issuer->get('name'))); // Issuer. $username = $linkedlogin->get('username'); $email = $linkedlogin->get('email'); $usernamecell = new html_table_cell(s($email) . ', (' . s($username) . ')'); $links = ''; // Delete. $deleteparams = ['linkedloginid' => $linkedlogin->get('id'), 'action' => 'delete', 'sesskey' => sesskey()]; $deleteurl = new moodle_url('/auth/oauth2/linkedlogins.php', $deleteparams); $deletelink = html_writer::link($deleteurl, $this->pix_icon('t/delete', get_string('delete'))); $links .= ' ' . $deletelink; $editcell = new html_table_cell($links); $row = new html_table_row([ $issuercell, $usernamecell, $editcell, ]); $data[] = $row; $index++; } $table->data = $data; return html_writer::table($table); } } oauth2/classes/privacy/provider.php 0000644 00000020013 15152311434 0013416 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy class for requesting user data for auth_oauth2. * * @package auth_oauth2 * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\metadata\collection; use core_privacy\local\request\contextlist; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy provider for auth_oauth2 * * @package auth_oauth2 * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Get information about the user data stored by this plugin. * * @param collection $collection An object for storing metadata. * @return collection The metadata. */ public static function get_metadata(collection $collection) : collection { $authfields = [ 'timecreated' => 'privacy:metadata:auth_oauth2:timecreated', 'timemodified' => 'privacy:metadata:auth_oauth2:timemodified', 'usermodified' => 'privacy:metadata:auth_oauth2:usermodified', 'userid' => 'privacy:metadata:auth_oauth2:userid', 'issuerid' => 'privacy:metadata:auth_oauth2:issuerid', 'username' => 'privacy:metadata:auth_oauth2:username', 'email' => 'privacy:metadata:auth_oauth2:email', 'confirmtoken' => 'privacy:metadata:auth_oauth2:confirmtoken', 'confirmtokenexpires' => 'privacy:metadata:auth_oauth2:confirmtokenexpires' ]; $collection->add_database_table('auth_oauth2_linked_login', $authfields, 'privacy:metadata:auth_oauth2:tableexplanation'); // Regarding this block, we are unable to export or purge this data, as // it would damage the oauth2 data across the whole site. foreach ([ 'oauth2_endpoint', 'oauth2_user_field_mapping', 'oauth2_access_token', 'oauth2_system_account', ] as $tablename) { $collection->add_database_table($tablename, [ 'usermodified' => 'privacy:metadata:auth_oauth2:usermodified', ], 'privacy:metadata:auth_oauth2:tableexplanation'); } $collection->link_subsystem('core_auth', 'privacy:metadata:auth_oauth2:authsubsystem'); return $collection; } /** * Return all contexts for this userid. In this situation the user context. * * @param int $userid The user ID. * @return contextlist The list of context IDs. */ public static function get_contexts_for_userid(int $userid) : contextlist { $sql = "SELECT ctx.id FROM {auth_oauth2_linked_login} ao JOIN {context} ctx ON ctx.instanceid = ao.userid AND ctx.contextlevel = :contextlevel WHERE ao.userid = :userid"; $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT userid FROM {auth_oauth2_linked_login} WHERE userid = ?"; $params = [$context->instanceid]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all oauth2 information for the list of contexts and this user. * * @param approved_contextlist $contextlist The list of approved contexts for a user. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; // Export oauth2 linked accounts. $context = \context_user::instance($contextlist->get_user()->id); $sql = "SELECT ll.id, ll.username, ll.email, ll.timecreated, ll.timemodified, oi.name as issuername FROM {auth_oauth2_linked_login} ll JOIN {oauth2_issuer} oi ON oi.id = ll.issuerid WHERE ll.userid = :userid"; if ($oauth2accounts = $DB->get_records_sql($sql, ['userid' => $contextlist->get_user()->id])) { foreach ($oauth2accounts as $oauth2account) { $data = (object)[ 'timecreated' => transform::datetime($oauth2account->timecreated), 'timemodified' => transform::datetime($oauth2account->timemodified), 'issuerid' => $oauth2account->issuername, 'username' => $oauth2account->username, 'email' => $oauth2account->email ]; writer::with_context($context)->export_data([ get_string('privacy:metadata:auth_oauth2', 'auth_oauth2'), $oauth2account->issuername ], $data); } } } /** * Delete all user data for this context. * * @param \context $context The context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { if ($context->contextlevel != CONTEXT_USER) { return; } static::delete_user_data($context->instanceid); } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_user_data($context->instanceid); } } /** * Delete all user data for this user only. * * @param approved_contextlist $contextlist The list of approved contexts for a user. */ public static function delete_data_for_user(approved_contextlist $contextlist) { if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel != CONTEXT_USER) { continue; } if ($context->instanceid == $userid) { // Because we only use user contexts the instance ID is the user ID. static::delete_user_data($context->instanceid); } } } /** * This does the deletion of user data for the auth_oauth2. * * @param int $userid The user ID */ protected static function delete_user_data(int $userid) { global $DB; // Because we only use user contexts the instance ID is the user ID. $DB->delete_records('auth_oauth2_linked_login', ['userid' => $userid]); } } oauth2/classes/auth.php 0000644 00000060532 15152311434 0011062 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Anobody can login with any password. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ namespace auth_oauth2; defined('MOODLE_INTERNAL') || die(); use pix_icon; use moodle_url; use core_text; use context_system; use stdClass; use core\oauth2\issuer; use core\oauth2\client; require_once($CFG->libdir.'/authlib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->dirroot.'/user/profile/lib.php'); /** * Plugin for oauth2 authentication. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ class auth extends \auth_plugin_base { /** * @var stdClass $userinfo The set of user info returned from the oauth handshake */ private static $userinfo; /** * @var stdClass $userpicture The url to a picture. */ private static $userpicture; /** * Constructor. */ public function __construct() { $this->authtype = 'oauth2'; $this->config = get_config('auth_oauth2'); $this->customfields = $this->get_custom_user_profile_fields(); } /** * Returns true if the username and password work or don't exist and false * if the user exists and the password is wrong. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ public function user_login($username, $password) { $cached = $this->get_static_user_info(); if (empty($cached)) { // This means we were called as part of a normal login flow - without using oauth. return false; } $verifyusername = $cached['username']; if ($verifyusername == $username) { return true; } return false; } /** * We don't want to allow users setting an internal password. * * @return bool */ public function prevent_local_passwords() { return true; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ public function is_internal() { return false; } /** * Indicates if moodle should automatically update internal user * records with data from external sources using the information * from auth_plugin_base::get_userinfo(). * * @return bool true means automatically copy data from ext to user table */ public function is_synchronised_with_external() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ public function can_change_password() { return false; } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ public function change_password_url() { return null; } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ public function can_reset_password() { return false; } /** * Returns true if plugin can be manually set. * * @return bool */ public function can_be_manually_set() { return true; } /** * Return the userinfo from the oauth handshake. Will only be valid * for the logged in user. * @param string $username */ public function get_userinfo($username) { $cached = $this->get_static_user_info(); if (!empty($cached) && $cached['username'] == $username) { return $cached; } return false; } /** * Return a list of identity providers to display on the login page. * * @param string|moodle_url $wantsurl The requested URL. * @return array List of arrays with keys url, iconurl and name. */ public function loginpage_idp_list($wantsurl) { $providers = \core\oauth2\api::get_all_issuers(true); $result = []; if (empty($wantsurl)) { $wantsurl = '/'; } foreach ($providers as $idp) { if ($idp->is_available_for_login()) { $params = ['id' => $idp->get('id'), 'wantsurl' => $wantsurl, 'sesskey' => sesskey()]; $url = new moodle_url('/auth/oauth2/login.php', $params); $icon = $idp->get('image'); $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get_display_name()]; } } return $result; } /** * Statically cache the user info from the oauth handshake * @param stdClass $userinfo */ private function set_static_user_info($userinfo) { self::$userinfo = $userinfo; } /** * Get the static cached user info * @return stdClass */ private function get_static_user_info() { return self::$userinfo; } /** * Statically cache the user picture from the oauth handshake * @param string $userpicture */ private function set_static_user_picture($userpicture) { self::$userpicture = $userpicture; } /** * Get the static cached user picture * @return string */ private function get_static_user_picture() { return self::$userpicture; } /** * If this user has no picture - but we got one from oauth - set it. * @param stdClass $user * @return boolean True if the image was updated. */ private function update_picture($user) { global $CFG, $DB, $USER; require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/gdlib.php'); require_once($CFG->dirroot . '/user/lib.php'); $fs = get_file_storage(); $userid = $user->id; if (!empty($user->picture)) { return false; } if (!empty($CFG->enablegravatar)) { return false; } $picture = $this->get_static_user_picture(); if (empty($picture)) { return false; } $context = \context_user::instance($userid, MUST_EXIST); $fs->delete_area_files($context->id, 'user', 'newicon'); $filerecord = array( 'contextid' => $context->id, 'component' => 'user', 'filearea' => 'newicon', 'itemid' => 0, 'filepath' => '/', 'filename' => 'image' ); try { $fs->create_file_from_string($filerecord, $picture); } catch (\file_exception $e) { return get_string($e->errorcode, $e->module, $e->a); } $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false); // There should only be one. $iconfile = reset($iconfile); // Something went wrong while creating temp file - remove the uploaded file. if (!$iconfile = $iconfile->copy_content_to_temp()) { $fs->delete_area_files($context->id, 'user', 'newicon'); return false; } // Copy file to temporary location and the send it for processing icon. $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile); // Delete temporary file. @unlink($iconfile); // Remove uploaded file. $fs->delete_area_files($context->id, 'user', 'newicon'); // Set the user's picture. $updateuser = new stdClass(); $updateuser->id = $userid; $updateuser->picture = $newpicture; $USER->picture = $newpicture; user_update_user($updateuser); return true; } /** * Update user data according to data sent by authorization server. * * @param array $externaldata data from authorization server * @param stdClass $userdata Current data of the user to be updated * @return stdClass The updated user record, or the existing one if there's nothing to be updated. */ private function update_user(array $externaldata, $userdata) { $user = (object) [ 'id' => $userdata->id, ]; // We can only update if the default authentication type of the user is set to OAuth2 as well. Otherwise, we might mess // up the user data of other users that use different authentication mechanisms (e.g. linked logins). if ($userdata->auth !== $this->authtype) { return $userdata; } $allfields = array_merge($this->userfields, $this->customfields); // Go through each field from the external data. foreach ($externaldata as $fieldname => $value) { if (!in_array($fieldname, $allfields)) { // Skip if this field doesn't belong to the list of fields that can be synced with the OAuth2 issuer. continue; } $userhasfield = property_exists($userdata, $fieldname); // Find out if it is a profile field. $isprofilefield = strpos($fieldname, 'profile_field_') === 0; $profilefieldname = str_replace('profile_field_', '', $fieldname); $userhasprofilefield = $isprofilefield && array_key_exists($profilefieldname, $userdata->profile); // Just in case this field is on the list, but not part of the user data. This shouldn't happen though. if (!($userhasfield || $userhasprofilefield)) { continue; } // Get the old value. $oldvalue = $isprofilefield ? (string) $userdata->profile[$profilefieldname] : (string) $userdata->$fieldname; // Get the lock configuration of the field. if (!empty($this->config->{'field_lock_' . $fieldname})) { $lockvalue = $this->config->{'field_lock_' . $fieldname}; } else { $lockvalue = 'unlocked'; } // We should update fields that meet the following criteria: // - Lock value set to 'unlocked'; or 'unlockedifempty', given the current value is empty. // - The value has changed. if ($lockvalue === 'unlocked' || ($lockvalue === 'unlockedifempty' && empty($oldvalue))) { $value = (string)$value; if ($oldvalue !== $value) { $user->$fieldname = $value; } } } // Update the user data. user_update_user($user, false); // Save user profile data. profile_save_data($user); // Refresh user for $USER variable. return get_complete_user_data('id', $user->id); } /** * Confirm the new user as registered. * * @param string $username * @param string $confirmsecret */ public function user_confirm($username, $confirmsecret) { global $DB; $user = get_complete_user_data('username', $username); if (!empty($user)) { if ($user->auth != $this->authtype) { return AUTH_CONFIRM_ERROR; } else if ($user->secret === $confirmsecret && $user->confirmed) { return AUTH_CONFIRM_ALREADY; } else if ($user->secret === $confirmsecret) { // They have provided the secret key to get in. $DB->set_field("user", "confirmed", 1, array("id" => $user->id)); return AUTH_CONFIRM_OK; } } else { return AUTH_CONFIRM_ERROR; } } /** * Print a page showing that a confirm email was sent with instructions. * * @param string $title * @param string $message */ public function print_confirm_required($title, $message) { global $PAGE, $OUTPUT, $CFG; $PAGE->navbar->add($title); $PAGE->set_title($title); $PAGE->set_heading($PAGE->course->fullname); echo $OUTPUT->header(); notice($message, "$CFG->wwwroot/index.php"); } /** * Complete the login process after oauth handshake is complete. * @param \core\oauth2\client $client * @param string $redirecturl * @return void Either redirects or throws an exception */ public function complete_login(client $client, $redirecturl) { global $CFG, $SESSION, $PAGE; $rawuserinfo = $client->get_raw_userinfo(); $userinfo = $client->get_userinfo(); if (!$userinfo) { // Trigger login failed event. $failurereason = AUTH_LOGIN_NOUSER; $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown', 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('loginerror_nouserinfo', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (empty($userinfo['username']) || empty($userinfo['email'])) { // Trigger login failed event. $failurereason = AUTH_LOGIN_NOUSER; $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown', 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('loginerror_userincomplete', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); $oauthemail = $userinfo['email']; // Once we get here we have the user info from oauth. $userwasmapped = false; // Clean and remember the picture / lang. if (!empty($userinfo['picture'])) { $this->set_static_user_picture($userinfo['picture']); unset($userinfo['picture']); } if (!empty($userinfo['lang'])) { $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { unset($userinfo['lang']); } } $issuer = $client->get_issuer(); // First we try and find a defined mapping. $linkedlogin = api::match_username_to_user($userinfo['username'], $issuer); if (!empty($linkedlogin) && empty($linkedlogin->get('confirmtoken'))) { $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid')); if ($mappeduser && $mappeduser->suspended) { $failurereason = AUTH_LOGIN_SUSPENDED; $event = \core\event\user_login_failed::create([ 'userid' => $mappeduser->id, 'other' => [ 'username' => $userinfo['username'], 'reason' => $failurereason ] ]); $event->trigger(); $SESSION->loginerrormsg = get_string('invalidlogin'); $client->log_out(); redirect(new moodle_url('/login/index.php')); } else if ($mappeduser && ($mappeduser->confirmed || !$issuer->get('requireconfirmation'))) { // Update user fields. $userinfo = $this->update_user($userinfo, $mappeduser); $userwasmapped = true; } else { // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('confirmationpending', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } } else if (!empty($linkedlogin)) { // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('confirmationpending', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (!$issuer->is_valid_login_domain($oauthemail)) { // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('notloggedindebug', 'auth_oauth2', get_string('loginerror_invaliddomain', 'auth_oauth2')); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (!$userwasmapped) { // No defined mapping - we need to see if there is an existing account with the same email. $moodleuser = \core_user::get_user_by_email($userinfo['email']); if (!empty($moodleuser)) { if ($issuer->get('requireconfirmation')) { $PAGE->set_url('/auth/oauth2/confirm-link-login.php'); $PAGE->set_context(context_system::instance()); \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id); // Request to link to existing account. $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2'); $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email); $this->print_confirm_required($emailconfirm, $message); exit(); } else { \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true); // We dont have profile loaded on $moodleuser, so load it. require_once($CFG->dirroot.'/user/profile/lib.php'); profile_load_custom_fields($moodleuser); $userinfo = $this->update_user($userinfo, $moodleuser); // No redirect, we will complete this login. } } else { // This is a new account. $exists = \core_user::get_user_by_username($userinfo['username']); // Creating a new user? if ($exists) { // Trigger login failed event. $failurereason = AUTH_LOGIN_FAILED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); // The username exists but the emails don't match. Refuse to continue. $errormsg = get_string('accountexists', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (email_is_not_allowed($userinfo['email'])) { // Trigger login failed event. $failurereason = AUTH_LOGIN_FAILED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); // The username exists but the emails don't match. Refuse to continue. $reason = get_string('loginerror_invaliddomain', 'auth_oauth2'); $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (!empty($CFG->authpreventaccountcreation)) { // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); // The username does not exist and settings prevent creating new accounts. $reason = get_string('loginerror_cannotcreateaccounts', 'auth_oauth2'); $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if ($issuer->get('requireconfirmation')) { $PAGE->set_url('/auth/oauth2/confirm-account.php'); $PAGE->set_context(context_system::instance()); // Create a new (unconfirmed account) and send an email to confirm it. $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer); $this->update_picture($user); $emailconfirm = get_string('emailconfirm'); $message = get_string('emailconfirmsent', '', $userinfo['email']); $this->print_confirm_required($emailconfirm, $message); exit(); } else { // Create a new confirmed account. $newuser = \auth_oauth2\api::create_new_confirmed_account($userinfo, $issuer); $userinfo = get_complete_user_data('id', $newuser->id); // No redirect, we will complete this login. } } } // We used to call authenticate_user - but that won't work if the current user has a different default authentication // method. Since we now ALWAYS link a login - if we get to here we can directly allow the user in. $user = (object) $userinfo; // Add extra loggedin info. $this->set_extrauserinfo((array)$rawuserinfo); complete_user_login($user, $this->get_extrauserinfo()); $this->update_picture($user); redirect($redirecturl); } /** * Returns information on how the specified user can change their password. * The password of the oauth2 accounts is not stored in Moodle. * * @param stdClass $user A user object * @return string[] An array of strings with keys subject and message */ public function get_password_change_info(stdClass $user) : array { $site = get_site(); $data = new stdClass(); $data->firstname = $user->firstname; $data->lastname = $user->lastname; $data->username = $user->username; $data->sitename = format_string($site->fullname); $data->admin = generate_email_signoff(); $message = get_string('emailpasswordchangeinfo', 'auth_oauth2', $data); $subject = get_string('emailpasswordchangeinfosubject', 'auth_oauth2', format_string($site->fullname)); return [ 'subject' => $subject, 'message' => $message ]; } } oauth2/classes/linked_login.php 0000644 00000007020 15152311434 0012550 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing issuers from the DB. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2; defined('MOODLE_INTERNAL') || die(); use core\persistent; /** * Class for loading/storing issuer from the DB * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class linked_login extends persistent { const TABLE = 'auth_oauth2_linked_login'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'issuerid' => array( 'type' => PARAM_INT ), 'userid' => array( 'type' => PARAM_INT ), 'username' => array( 'type' => PARAM_RAW ), 'email' => array( 'type' => PARAM_RAW ), 'confirmtoken' => array( 'type' => PARAM_RAW ), 'confirmtokenexpires' => array( 'type' => PARAM_INT ) ); } /** * Check whether there are any valid linked accounts for this issuer * and username combination. * * @param \core\oauth2\issuer $issuer The issuer * @param string $username The username to check */ public static function has_existing_issuer_match(\core\oauth2\issuer $issuer, $username) { global $DB; $where = "issuerid = :issuerid AND username = :username AND (confirmtokenexpires = 0 OR confirmtokenexpires > :maxexpiry)"; $count = $DB->count_records_select(static::TABLE, $where, [ 'issuerid' => $issuer->get('id'), 'username' => $username, 'maxexpiry' => (new \DateTime('NOW'))->getTimestamp(), ]); return $count > 0; } /** * Remove all linked logins that are using issuers that have been deleted. * * @param int $issuerid The issuer id of the issuer to check, or false to check all (defaults to all) * @return boolean */ public static function delete_orphaned($issuerid = false) { global $DB; // Delete any linked_login entries with a issuerid // which does not exist in the issuer table. // In the left join, the issuer id will be null // where a match linked_login.issuerid is not found. $sql = "DELETE FROM {" . self::TABLE . "} WHERE issuerid NOT IN (SELECT id FROM {" . \core\oauth2\issuer::TABLE . "})"; $params = []; if (!empty($issuerid)) { $sql .= ' AND issuerid = ?'; $params['issuerid'] = $issuerid; } return $DB->execute($sql, $params); } } oauth2/classes/api.php 0000644 00000034246 15152311434 0010675 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing oauth2 linked logins from the DB. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2; use context_user; use stdClass; use moodle_exception; use moodle_url; defined('MOODLE_INTERNAL') || die(); /** * Static list of api methods for auth oauth2 configuration. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** * Remove all linked logins that are using issuers that have been deleted. * * @param int $issuerid The issuer id of the issuer to check, or false to check all (defaults to all) * @return boolean */ public static function clean_orphaned_linked_logins($issuerid = false) { return linked_login::delete_orphaned($issuerid); } /** * List linked logins * * Requires auth/oauth2:managelinkedlogins capability at the user context. * * @param int $userid (defaults to $USER->id) * @return boolean */ public static function get_linked_logins($userid = false) { global $USER; if ($userid === false) { $userid = $USER->id; } if (\core\session\manager::is_loggedinas()) { throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); } $context = context_user::instance($userid); require_capability('auth/oauth2:managelinkedlogins', $context); return linked_login::get_records(['userid' => $userid, 'confirmtoken' => '']); } /** * See if there is a match for this username and issuer in the linked_login table. * * @param string $username as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @return stdClass User record if found. */ public static function match_username_to_user($username, $issuer) { $params = [ 'issuerid' => $issuer->get('id'), 'username' => $username ]; $result = linked_login::get_record($params); if ($result) { $user = \core_user::get_user($result->get('userid')); if (!empty($user) && !$user->deleted) { return $result; } } return false; } /** * Link a login to this account. * * Requires auth/oauth2:managelinkedlogins capability at the user context. * * @param array $userinfo as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @param int $userid (defaults to $USER->id) * @param bool $skippermissions During signup we need to set this before the user is setup for capability checks. * @return bool */ public static function link_login($userinfo, $issuer, $userid = false, $skippermissions = false) { global $USER; if ($userid === false) { $userid = $USER->id; } if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { throw new moodle_exception('alreadylinked', 'auth_oauth2'); } if (\core\session\manager::is_loggedinas()) { throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); } $context = context_user::instance($userid); if (!$skippermissions) { require_capability('auth/oauth2:managelinkedlogins', $context); } $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; $record->userid = $userid; $existing = linked_login::get_record((array)$record); if ($existing) { $existing->set('confirmtoken', ''); $existing->update(); return $existing; } $record->email = $userinfo['email']; $record->confirmtoken = ''; $record->confirmtokenexpires = 0; $linkedlogin = new linked_login(0, $record); return $linkedlogin->create(); } /** * Send an email with a link to confirm linking this account. * * @param array $userinfo as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @param int $userid (defaults to $USER->id) * @return bool */ public static function send_confirm_link_login_email($userinfo, $issuer, $userid) { $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; $record->userid = $userid; if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { throw new moodle_exception('alreadylinked', 'auth_oauth2'); } $record->email = $userinfo['email']; $record->confirmtoken = random_string(32); $expires = new \DateTime('NOW'); $expires->add(new \DateInterval('PT30M')); $record->confirmtokenexpires = $expires->getTimestamp(); $linkedlogin = new linked_login(0, $record); $linkedlogin->create(); // Construct the email. $site = get_site(); $supportuser = \core_user::get_support_user(); $user = get_complete_user_data('id', $userid); $data = new stdClass(); $data->fullname = fullname($user); $data->sitename = format_string($site->fullname); $data->admin = generate_email_signoff(); $data->issuername = format_string($issuer->get('name')); $data->linkedemail = format_string($linkedlogin->get('email')); $subject = get_string('confirmlinkedloginemailsubject', 'auth_oauth2', format_string($site->fullname)); $params = [ 'token' => $linkedlogin->get('confirmtoken'), 'userid' => $userid, 'username' => $userinfo['username'], 'issuerid' => $issuer->get('id'), ]; $confirmationurl = new moodle_url('/auth/oauth2/confirm-linkedlogin.php', $params); $data->link = $confirmationurl->out(false); $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data); $data->link = $confirmationurl->out(); $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true); $user->mailformat = 1; // Always send HTML version as well. // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. return email_to_user($user, $supportuser, $subject, $message, $messagehtml); } /** * Look for a waiting confirmation token, and if we find a match - confirm it. * * @param int $userid * @param string $username * @param int $issuerid * @param string $token * @return boolean True if we linked. */ public static function confirm_link_login($userid, $username, $issuerid, $token) { if (empty($token) || empty($userid) || empty($issuerid) || empty($username)) { return false; } $params = [ 'userid' => $userid, 'username' => $username, 'issuerid' => $issuerid, 'confirmtoken' => $token, ]; $login = linked_login::get_record($params); if (empty($login)) { return false; } $expires = $login->get('confirmtokenexpires'); if (time() > $expires) { $login->delete(); return; } $login->set('confirmtokenexpires', 0); $login->set('confirmtoken', ''); $login->update(); return true; } /** * Create an account with a linked login that is already confirmed. * * @param array $userinfo as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @return bool */ public static function create_new_confirmed_account($userinfo, $issuer) { global $CFG, $DB; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); $user = new stdClass(); $user->auth = 'oauth2'; $user->mnethostid = $CFG->mnet_localhost_id; $user->secret = random_string(15); $user->password = ''; $user->confirmed = 1; // Set the user to confirmed. $user = self::save_user($userinfo, $user); // The linked account is pre-confirmed. $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; $record->userid = $user->id; $record->email = $userinfo['email']; $record->confirmtoken = ''; $record->confirmtokenexpires = 0; $linkedlogin = new linked_login(0, $record); $linkedlogin->create(); return $user; } /** * Send an email with a link to confirm creating this account. * * @param array $userinfo as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @param int $userid (defaults to $USER->id) * @return bool */ public static function send_confirm_account_email($userinfo, $issuer) { global $CFG, $DB; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { throw new moodle_exception('alreadylinked', 'auth_oauth2'); } $user = new stdClass(); $user->auth = 'oauth2'; $user->mnethostid = $CFG->mnet_localhost_id; $user->secret = random_string(15); $user->password = ''; $user->confirmed = 0; // The user is not yet confirmed. $user = self::save_user($userinfo, $user); // The linked account is pre-confirmed. $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; $record->userid = $user->id; $record->email = $userinfo['email']; $record->confirmtoken = ''; $record->confirmtokenexpires = 0; $linkedlogin = new linked_login(0, $record); $linkedlogin->create(); // Construct the email. $site = get_site(); $supportuser = \core_user::get_support_user(); $user = get_complete_user_data('id', $user->id); $data = new stdClass(); $data->fullname = fullname($user); $data->sitename = format_string($site->fullname); $data->admin = generate_email_signoff(); $subject = get_string('confirmaccountemailsubject', 'auth_oauth2', format_string($site->fullname)); $params = [ 'token' => $user->secret, 'username' => $userinfo['username'] ]; $confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params); $data->link = $confirmationurl->out(false); $message = get_string('confirmaccountemail', 'auth_oauth2', $data); $data->link = $confirmationurl->out(); $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true); $user->mailformat = 1; // Always send HTML version as well. // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. email_to_user($user, $supportuser, $subject, $message, $messagehtml); return $user; } /** * Delete linked login * * Requires auth/oauth2:managelinkedlogins capability at the user context. * * @param int $linkedloginid * @return boolean */ public static function delete_linked_login($linkedloginid) { $login = new linked_login($linkedloginid); $userid = $login->get('userid'); if (\core\session\manager::is_loggedinas()) { throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); } $context = context_user::instance($userid); require_capability('auth/oauth2:managelinkedlogins', $context); $login->delete(); } /** * Delete linked logins for a user. * * @param \core\event\user_deleted $event * @return boolean */ public static function user_deleted(\core\event\user_deleted $event) { global $DB; $userid = $event->objectid; return $DB->delete_records(linked_login::TABLE, ['userid' => $userid]); } /** * Is the plugin enabled. * * @return bool */ public static function is_enabled() { return is_enabled_auth('oauth2'); } /** * Create a new user & update the profile fields * * @param array $userinfo * @param object $user * @return object */ private static function save_user(array $userinfo, object $user): object { // Map supplied issuer user info to Moodle user fields. $userfieldmapping = new \core\oauth2\user_field_mapping(); $userfieldlist = $userfieldmapping->get_internalfields(); $hasprofilefield = false; foreach ($userfieldlist as $field) { if (isset($userinfo[$field]) && $userinfo[$field]) { $user->$field = $userinfo[$field]; // Check whether the profile fields exist or not. $hasprofilefield = $hasprofilefield || strpos($field, \core_user\fields::PROFILE_FIELD_PREFIX) === 0; } } // Create a new user. $user->id = user_create_user($user, false, true); // If profile fields exist then save custom profile fields data. if ($hasprofilefield) { profile_save_data($user); } return $user; } } oauth2/test.php 0000644 00000005442 15152311434 0007442 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file allows for testing of login via configured oauth2 IDP poviders. * * @package auth_oauth2 * @copyright 2021 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ // Require_login is not needed here. // phpcs:disable moodle.Files.RequireLogin.Missing require_once('../../config.php'); require_sesskey(); $issuerid = required_param('id', PARAM_INT); $url = new moodle_url('/auth/oauth2/test.php', ['id' => $issuerid, 'sesskey' => sesskey()]); $PAGE->set_context(context_system::instance()); $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $issuer = new \core\oauth2\issuer($issuerid); if (!$issuer->is_available_for_login()) { throw new \moodle_exception('issuernologin', 'auth_oauth2'); } $client = \core\oauth2\api::get_user_oauth_client($issuer, $url); if ($client) { // We have a valid client, now lets see if we can log into the IDP. if (!$client->is_logged_in()) { redirect($client->get_login_url()); } echo $OUTPUT->header(); // We were successful logging into the IDP. echo $OUTPUT->notification(get_string('loggedin', 'auth_oauth2'), 'notifysuccess'); // Try getting user info from the IDP. $endpointurl = $client->get_issuer()->get_endpoint_url('userinfo'); $response = $client->get($endpointurl); $userinfo = json_decode($response, true); $templateinfo = []; foreach ($userinfo as $key => $value) { // We are just displaying the data from the IdP for testing purposes, // so we are more interested in displaying it to the admin than // processing it. if (is_array($value)) { $value = json_encode($value); } $templateinfo[] = ['name' => $key, 'value' => $value]; } // Display user info. if (!empty($templateinfo)) { echo $OUTPUT->render_from_template('auth_oauth2/idpresponse', ['pairs' => $templateinfo]); } } else { throw new moodle_exception('Could not get an OAuth client.'); } echo $OUTPUT->footer(); oauth2/lang/en/auth_oauth2.php 0000644 00000016075 15152311434 0012235 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_oauth2', language 'en'. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['accountexists'] = 'A user already exists on this site with this username. If this is your account, log in by entering your username and password and add it as a linked login via your preferences page.'; $string['auth_oauth2description'] = 'OAuth 2 standards based authentication'; $string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; $string['confirmaccountemail'] = 'Hi {$a->fullname}, A new account has been requested at \'{$a->sitename}\' using your email address. To confirm your new account, please go to this web address: {$a->link} In most mail programs, this should appear as a blue link which you can just click on. If that doesn\'t work, then cut and paste the address into the address line at the top of your web browser window. If you need help, please contact the site administrator, {$a->admin} If you did not do this, someone else could be trying to compromise your account. Please contact the site administrator immediately.'; $string['confirmaccountemailsubject'] = '{$a}: account confirmation'; $string['confirmationinvalid'] = 'The confirmation link is either invalid, or has expired. Please start the login process again to generate a new confirmation email.'; $string['confirmationpending'] = 'This account is pending email confirmation.'; $string['confirmlinkedloginemail'] = 'Hi {$a->fullname}, A request has been made to link the {$a->issuername} login {$a->linkedemail} to your account at \'{$a->sitename}\' using your email address. To confirm this request and link these logins, please go to this web address: {$a->link} In most mail programs, this should appear as a blue link which you can just click on. If that doesn\'t work, then cut and paste the address into the address line at the top of your web browser window. If you need help, please contact the site administrator, {$a->admin} If you did not do this, someone else could be trying to compromise your account. Please contact the site administrator immediately.'; $string['confirmlinkedloginemailsubject'] = '{$a}: linked login confirmation'; $string['createaccountswarning'] = 'This authentication plugin allows users to create accounts on your site. You may want to enable the setting "authpreventaccountcreation" if you use this plugin.'; $string['createnewlinkedlogin'] = 'Link a new account ({$a})'; $string['emailconfirmlink'] = 'Link your accounts'; $string['emailconfirmlinksent'] = '<p>An existing account was found with this email address but it is not linked yet.</p> <p>The accounts must be linked before you can log in.</p> <p>An email should have been sent to your address at <b>{$a}</b>.</p> <p>It contains easy instructions to link your accounts.</p> <p>If you have any difficulty, contact the site administrator.</p>'; $string['emailpasswordchangeinfo'] = 'Hi {$a->firstname}, Someone (probably you) has requested a new password for your account on \'{$a->sitename}\'. However your password cannot be reset because you are using your account on another site to log in. Please log in as before, using the link on the login page. {$a->admin}'; $string['emailpasswordchangeinfosubject'] = '{$a}: Change password information'; $string['info'] = 'External account'; $string['issuer'] = 'OAuth 2 service'; $string['issuernologin'] = 'This issuer can not be used to login'; $string['key'] = 'Key'; $string['linkedlogins'] = 'Linked logins'; $string['linkedloginshelp'] = 'Help with linked logins'; $string['loggedin'] = 'User successfully authenticated with provider.'; $string['loginerror_userincomplete'] = 'The user information returned did not contain a username and email address. The OAuth 2 service may be configured incorrectly.'; $string['loginerror_nouserinfo'] = 'No user information was returned. The OAuth 2 service may be configured incorrectly.'; $string['loginerror_invaliddomain'] = 'The email address is not allowed at this site.'; $string['loginerror_authenticationfailed'] = 'The authentication process failed.'; $string['loginerror_cannotcreateaccounts'] = 'An account with your email address could not be found.'; $string['noconfiguredidps'] = 'There are no configured OAuth2 providers.'; $string['noissuersavailable'] = 'None of the configured OAuth 2 services allow you to link login accounts.'; $string['notloggedindebug'] = 'The login attempt failed. Reason: {$a}'; $string['notwhileloggedinas'] = 'Linked logins cannot be managed while logged in as another user.'; $string['oauth2:managelinkedlogins'] = 'Manage own linked login accounts'; $string['notenabled'] = 'Sorry, OAuth 2 authentication plugin is not enabled'; $string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; $string['pluginname'] = 'OAuth 2'; $string['alreadylinked'] = 'This external account is already linked to an account on this site'; $string['privacy:metadata:auth_oauth2'] = 'OAuth 2 authentication'; $string['privacy:metadata:auth_oauth2:authsubsystem'] = 'This plugin is connected to the authentication subsystem.'; $string['privacy:metadata:auth_oauth2:confirmtoken'] = 'The confirmation token.'; $string['privacy:metadata:auth_oauth2:confirmtokenexpires'] = 'The timestamp when the confirmation token expires.'; $string['privacy:metadata:auth_oauth2:email'] = 'The external email that maps to this account.'; $string['privacy:metadata:auth_oauth2:issuerid'] = 'The ID of the OAuth 2 issuer for this OAuth 2 login'; $string['privacy:metadata:auth_oauth2:tableexplanation'] = 'OAuth 2 accounts linked to a user\'s Moodle account.'; $string['privacy:metadata:auth_oauth2:timecreated'] = 'The timestamp when the user account was linked to the OAuth 2 login.'; $string['privacy:metadata:auth_oauth2:timemodified'] = 'The timestamp when this record was modified.'; $string['privacy:metadata:auth_oauth2:userid'] = 'The ID of the user account which the OAuth 2 login is linked to.'; $string['privacy:metadata:auth_oauth2:usermodified'] = 'The ID of the user who modified this account.'; $string['privacy:metadata:auth_oauth2:username'] = 'The external username that maps to this account.'; $string['testidplogin'] = 'Test login with:'; $string['userinfo'] = 'User data from provider:'; $string['value'] = 'Value'; upgrade.txt 0000644 00000011133 15152311434 0006732 0 ustar 00 This files describes API changes in /auth/* - plugins, information provided here is intended especially for developers. === 3.9 === * The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info: - upgrade_fix_config_auth_plugin_names() - upgrade_fix_config_auth_plugin_defaults() === 3.7 === * get_password_change_info() method is added to the base class and returns an array containing the subject and body of the message to the user that contains instructions on how to change their password. Authentication plugins can override this method if needed. === 3.6 === * Login forms generated from Moodle must include a login token to protect automated logins. See \core\session\manager::get_login_token(). === 3.5 === * The auth_db and auth_ldap plugins' implementations of update_user_record() have been removed and both now call the new implementation added in the base class. * Self registration plugins should use core_privacy\local\sitepolicy\manager instead of directly checking $CFG->sitepolicy , especially in custom signup forms. See https://docs.moodle.org/dev/Site_policy_handler === 3.3 === * Authentication plugins have been migrated to use the admin settings API. Plugins should use a settings.php file to manage configurations rather than using the old config.html files. See how the helper function upgrade_fix_config_auth_plugin_names() can be used to convert the legacy settings to the new ones. Another helper function upgrade_fix_config_auth_plugin_defaults() can be used to populate the settings with default values so that they are not falsely reported as newly added ones. * The function 'print_auth_lock_options' has been replaced by 'display_auth_lock_options' which uses the admin settings API. See auth_manual as an exmple of how it can be used. More information can be found in MDL-12689. * The list of supported identity providers (SSO IdP) returned by the 'loginpage_idp_list' method (used to render the login page and login block links) now supports a new key 'iconurl' which should be used instead of the legacy 'icon'. === 3.2 === * New auth hook - pre_user_login_hook() - available, triggered right after the user object is created. This can be used to modify the user object before any authentication errors are raised. * The block_login now displays the loginpage_idp_list() links as well as main login page. * The authentication plugin auth_radius has been moved to https://github.com/moodlehq/moodle-auth_radius * New auth_email::user_signup_with_confirmation() method has a new optional parameter $confirmationurl to provide a different confirmation URL. * New signup_is_enabled() function available in lib/authlib.php to safely check if sign-up is enabled in the site. === 3.0 === * login_signup_form::signup_captcha_enabled() now calls is_captcha_enabled() from the current auth plugin instead of from auth_email === 2.9 === * Do not update user->firstaccess from any auth plugin, the complete_user_login() does it automatically. * Add user_add_password_history() to user_signup() method. * New auth hook - pre_loginpage_hook() - available, triggered before redirecting to the login page. === 2.8 === * \core\session\manager::session_exists() now verifies the session is active instead of only checking the session data is present in low level session handler * MNet is no longer sending logs between the client and parent sites. auth_plugin_mnet::refresh_log() is now deprecated. There is no alternative. Please don't use this function. === 2.7 === * If you are returning a url in method change_password_url() from config, please make sure it is set before trying to use it. === 2.6 === * can_be_manually_set() - This function was introduced in the base class and returns false by default. If overriden by an authentication plugin to return true, the authentication plugin will be able to be manually set for users. For example, when bulk uploading users you will be able to select it as the authentication method they use. === 2.4 === required changes in code: * use role_get_name() or role_fix_names() if you need any role names, using role.name directly from database is not correct any more optional - no changes needed: * add support for custom user signup form - see auth_plugin_base::signup_form() function === 2.2 === required changes in code: * the correct sequence to set up global $USER is: $user = get_complete_user_data('username', $username); // or $user = authenticate_user_login() enrol_check_plugins($user); session_set_user($user); cas/languages.php 0000644 00000000743 15152311434 0007774 0 ustar 00 <?php $caslangconstprefix = 'PHPCAS_LANG_'; $caslangprefixlen = strlen('CAS_Languages_'); $CASLANGUAGES = array (); $consts = get_defined_constants(true); foreach ($consts['user'] as $key => $value) { if (preg_match("/^$caslangconstprefix/", $key)) { $CASLANGUAGES[$value] = substr($value, $caslangprefixlen); } } if (empty($CASLANGUAGES)) { $CASLANGUAGES = array (PHPCAS_LANG_ENGLISH => 'English', PHPCAS_LANG_FRENCH => 'French'); } cas/db/upgrade.php 0000644 00000003331 15152311434 0010036 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * CAS authentication plugin upgrade code * * @package auth_cas * @copyright 2013 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Function to upgrade auth_cas. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_cas_upgrade($oldversion) { global $CFG; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2021052501) { // Normalize the memberattribute_isdn plugin config. set_config('memberattribute_isdn', !empty(get_config('auth_cas', 'memberattribute_isdn')), 'auth_cas'); upgrade_plugin_savepoint(true, 2021052501, 'auth', 'cas'); } // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } cas/db/install.php 0000644 00000000104 15152311434 0010050 0 ustar 00 <?php function xmldb_auth_cas_install() { global $CFG, $DB; } cas/db/tasks.php 0000644 00000002261 15152311434 0007535 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Definition of auth_cas tasks. * * @package auth_cas * @category task * @copyright 2015 Vadim Dvorovenko <Vadimon@mail.ru> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => 'auth_cas\task\sync_task', 'blocking' => 0, 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'dayofweek' => '*', 'disabled' => 1 ) ); cas/cli/sync_users.php 0000644 00000005202 15152311434 0010765 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * CAS user sync script. * * This script is meant to be called from a cronjob to sync moodle with the CAS * backend in those setups where the CAS backend acts as 'master'. * * Notes: * - it is required to use the web server account when executing PHP CLI scripts * - you need to change the "www-data" to match the apache user account * - use "su" if "sudo" not available * - If you have a large number of users, you may want to raise the memory limits * by passing -d momory_limit=256M * - For debugging & better logging, you are encouraged to use in the command line: * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 * * Performance notes: * We have optimized it as best as we could for PostgreSQL and MySQL, with 27K students * we have seen this take 10 minutes. * * @package auth_cas * @copyright 2007 Jerome Gutierrez - based on code by Martin Langhoff * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 3.0 MDL-51824 - please do not use this CLI script any more, use scheduled task instead. * @todo MDL-50264 This will be deleted in Moodle 3.2. */ define('CLI_SCRIPT', true); require(__DIR__.'/../../../config.php'); require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->libdir.'/clilib.php'); // Ensure errors are well explained set_debugging(DEBUG_DEVELOPER, true); if (!is_enabled_auth('cas')) { error_log('[AUTH CAS] '.get_string('pluginnotenabled', 'auth_ldap')); die; } cli_problem('[AUTH CAS] The sync users cron has been deprecated. Please use the scheduled task instead.'); // Abort execution of the CLI script if the auth_cas\task\sync_task is enabled. $task = \core\task\manager::get_scheduled_task('auth_cas\task\sync_task'); if (!$task->get_disabled()) { cli_error('[AUTH CAS] The scheduled task sync_task is enabled, the cron execution has been aborted.'); } $casauth = get_auth_plugin('cas'); $casauth->sync_users(true); cas/README-CAS 0000644 00000000031 15152311434 0006567 0 ustar 00 CAS-module README cas/thirdpartylibs.xml 0000644 00000000715 15152311434 0011102 0 ustar 00 <?xml version="1.0"?> <libraries> <library> <location>CAS</location> <name>CAS</name> <description>phpCAS library to support CAS authentication plugin.</description> <version>1.6.0</version> <license>Apache</license> <licenseversion>2.0</licenseversion> <repository>https://github.com/apereo/phpCAS</repository> <copyrights> <copyright>2007-2020, Apereo Foundation</copyright> </copyrights> </library> </libraries> cas/auth.php 0000644 00000032747 15152311434 0007000 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: CAS Authentication * * Authentication using CAS (Central Authentication Server). * * @author Martin Dougiamas * @author Jerome GUTIERREZ * @author Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package auth_cas */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/auth/ldap/auth.php'); require_once($CFG->dirroot.'/auth/cas/CAS/vendor/autoload.php'); require_once($CFG->dirroot.'/auth/cas/CAS/vendor/apereo/phpcas/source/CAS.php'); /** * CAS authentication plugin. */ class auth_plugin_cas extends auth_plugin_ldap { /** * Constructor. */ public function __construct() { $this->authtype = 'cas'; $this->roleauth = 'auth_cas'; $this->errorlogtag = '[AUTH CAS] '; $this->init_plugin($this->authtype); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_cas() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } function prevent_local_passwords() { return true; } /** * Authenticates user against CAS * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username (with system magic quotes) * @param string $password The password (with system magic quotes) * @return bool Authentication success or failure. */ function user_login ($username, $password) { $this->connectCAS(); return phpCAS::isAuthenticated() && (trim(core_text::strtolower(phpCAS::getUser())) == $username); } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return false; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return false; } /** * Authentication choice (CAS or other) * Redirection to the CAS form or to login/index.php * for other authentication */ function loginpage_hook() { global $frm; global $CFG; global $SESSION, $OUTPUT, $PAGE; $site = get_site(); $CASform = get_string('CASform', 'auth_cas'); $username = optional_param('username', '', PARAM_RAW); $courseid = optional_param('courseid', 0, PARAM_INT); if (!empty($username)) { if (isset($SESSION->wantsurl) && (strstr($SESSION->wantsurl, 'ticket') || strstr($SESSION->wantsurl, 'NOCAS'))) { unset($SESSION->wantsurl); } return; } // Return if CAS enabled and settings not specified yet if (empty($this->config->hostname)) { return; } // If the multi-authentication setting is used, check for the param before connecting to CAS. if ($this->config->multiauth) { // If there is an authentication error, stay on the default authentication page. if (!empty($SESSION->loginerrormsg)) { return; } $authCAS = optional_param('authCAS', '', PARAM_RAW); if ($authCAS != 'CAS') { return; } } // Connection to CAS server $this->connectCAS(); if (phpCAS::checkAuthentication()) { $frm = new stdClass(); $frm->username = phpCAS::getUser(); $frm->password = 'passwdCas'; $frm->logintoken = \core\session\manager::get_login_token(); // Redirect to a course if multi-auth is activated, authCAS is set to CAS and the courseid is specified. if ($this->config->multiauth && !empty($courseid)) { redirect(new moodle_url('/course/view.php', array('id'=>$courseid))); } return; } if (isset($_GET['loginguest']) && ($_GET['loginguest'] == true)) { $frm = new stdClass(); $frm->username = 'guest'; $frm->password = 'guest'; $frm->logintoken = \core\session\manager::get_login_token(); return; } // Force CAS authentication (if needed). if (!phpCAS::isAuthenticated()) { phpCAS::setLang($this->config->language); phpCAS::forceAuthentication(); } } /** * Connect to the CAS (clientcas connection or proxycas connection) * */ function connectCAS() { global $CFG; static $connected = false; if (!$connected) { // Form the base URL of the server with just the protocol and hostname. $serverurl = new moodle_url("/"); $servicebaseurl = $serverurl->get_scheme() ? $serverurl->get_scheme() . "://" : ''; $servicebaseurl .= $serverurl->get_host(); // Add the port if set. $servicebaseurl .= $serverurl->get_port() ? ':' . $serverurl->get_port() : ''; // Make sure phpCAS doesn't try to start a new PHP session when connecting to the CAS server. if ($this->config->proxycas) { phpCAS::proxy($this->config->casversion, $this->config->hostname, (int) $this->config->port, $this->config->baseuri, $servicebaseurl, false); } else { phpCAS::client($this->config->casversion, $this->config->hostname, (int) $this->config->port, $this->config->baseuri, $servicebaseurl, false); } // Some CAS installs require SSLv3 that should be explicitly set. if (!empty($this->config->curl_ssl_version)) { phpCAS::setExtraCurlOption(CURLOPT_SSLVERSION, $this->config->curl_ssl_version); } $connected = true; } // If Moodle is configured to use a proxy, phpCAS needs some curl options set. if (!empty($CFG->proxyhost) && !is_proxybypass(phpCAS::getServerLoginURL())) { phpCAS::setExtraCurlOption(CURLOPT_PROXY, $CFG->proxyhost); if (!empty($CFG->proxyport)) { phpCAS::setExtraCurlOption(CURLOPT_PROXYPORT, $CFG->proxyport); } if (!empty($CFG->proxytype)) { // Only set CURLOPT_PROXYTYPE if it's something other than the curl-default http if ($CFG->proxytype == 'SOCKS5') { phpCAS::setExtraCurlOption(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); } } if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) { phpCAS::setExtraCurlOption(CURLOPT_PROXYUSERPWD, $CFG->proxyuser.':'.$CFG->proxypassword); if (defined('CURLOPT_PROXYAUTH')) { // any proxy authentication if PHP 5.1 phpCAS::setExtraCurlOption(CURLOPT_PROXYAUTH, CURLAUTH_BASIC | CURLAUTH_NTLM); } } } if ($this->config->certificate_check && $this->config->certificate_path){ phpCAS::setCasServerCACert($this->config->certificate_path); } else { // Don't try to validate the server SSL credentials phpCAS::setNoCasServerValidation(); } } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { return null; } /** * Returns true if user should be coursecreator. * * @param mixed $username username (without system magic quotes) * @return boolean result */ function iscreator($username) { if (empty($this->config->host_url) or (empty($this->config->attrcreators) && empty($this->config->groupecreators)) or empty($this->config->memberattribute)) { return false; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); // Test for group creator if (!empty($this->config->groupecreators)) { $ldapconnection = $this->ldap_connect(); if ($this->config->memberattribute_isdn) { if(!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { return false; } } else { $userid = $extusername; } $group_dns = explode(';', $this->config->groupecreators); if (ldap_isgroupmember($ldapconnection, $userid, $group_dns, $this->config->memberattribute)) { return true; } } // Build filter for attrcreator if (!empty($this->config->attrcreators)) { $attrs = explode(';', $this->config->attrcreators); $filter = '(& ('.$this->config->user_attribute."=$username)(|"; foreach ($attrs as $attr){ if(strpos($attr, '=')) { $filter .= "($attr)"; } else { $filter .= '('.$this->config->memberattribute."=$attr)"; } } $filter .= '))'; // Search $result = $this->ldap_get_userlist($filter); if (count($result) != 0) { return true; } } return false; } /** * Reads user information from LDAP and returns it as array() * * If no LDAP servers are configured, user information has to be * provided via other methods (CSV file, manually, etc.). Return * an empty array so existing user info is not lost. Otherwise, * calls parent class method to get user info. * * @param string $username username * @return mixed array with no magic quotes or false on error */ function get_userinfo($username) { if (empty($this->config->host_url)) { return array(); } return parent::get_userinfo($username); } /** * Syncronizes users from LDAP server to moodle user table. * * If no LDAP servers are configured, simply return. Otherwise, * call parent class method to do the work. * * @param bool $do_updates will do pull in data updates from LDAP if relevant * @return nothing */ function sync_users($do_updates=true) { if (empty($this->config->host_url)) { error_log('[AUTH CAS] '.get_string('noldapserver', 'auth_cas')); return; } parent::sync_users($do_updates); } /** * Hook for logout page */ function logoutpage_hook() { global $USER, $redirect; // Only do this if the user is actually logged in via CAS if ($USER->auth === $this->authtype) { // Check if there is an alternative logout return url defined if (isset($this->config->logout_return_url) && !empty($this->config->logout_return_url)) { // Set redirect to alternative return url $redirect = $this->config->logout_return_url; } } } /** * Post logout hook. * * Note: this method replace the prelogout_hook method to avoid redirect to CAS logout * before the event userlogout being triggered. * * @param stdClass $user clone of USER object object before the user session was terminated */ public function postlogout_hook($user) { global $CFG; // Only redirect to CAS logout if the user is logged as a CAS user. if (!empty($this->config->logoutcas) && $user->auth == $this->authtype) { $backurl = !empty($this->config->logout_return_url) ? $this->config->logout_return_url : $CFG->wwwroot; $this->connectCAS(); phpCAS::logoutWithRedirectService($backurl); } } /** * Return a list of identity providers to display on the login page. * * @param string|moodle_url $wantsurl The requested URL. * @return array List of arrays with keys url, iconurl and name. */ public function loginpage_idp_list($wantsurl) { if (empty($this->config->hostname)) { // CAS is not configured. return []; } if ($this->config->auth_logo) { $iconurl = moodle_url::make_pluginfile_url( context_system::instance()->id, 'auth_cas', 'logo', null, null, $this->config->auth_logo); } else { $iconurl = null; } return [ [ 'url' => new moodle_url(get_login_url(), [ 'authCAS' => 'CAS', ]), 'iconurl' => $iconurl, 'name' => format_string($this->config->auth_name), ], ]; } } cas/settings.php 0000644 00000034315 15152311434 0007670 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin settings and defaults. * * @package auth_cas * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { if (!function_exists('ldap_connect')) { $notify = new \core\output\notification(get_string('auth_casnotinstalled', 'auth_cas'), \core\output\notification::NOTIFY_WARNING); $settings->add(new admin_setting_heading('auth_casnotinstalled', '', $OUTPUT->render($notify))); } else { // We use a couple of custom admin settings since we need to massage the data before it is inserted into the DB. require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_lowercase_configtext.php'); require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_contexts_configtext.php'); // Include needed files. require_once($CFG->dirroot.'/auth/cas/auth.php'); require_once($CFG->dirroot.'/auth/cas/languages.php'); // Introductory explanation. $settings->add(new admin_setting_heading('auth_cas/pluginname', '', new lang_string('auth_casdescription', 'auth_cas'))); // CAS server configuration label. $settings->add(new admin_setting_heading('auth_cas/casserversettings', new lang_string('auth_cas_server_settings', 'auth_cas'), '')); // Authentication method name. $settings->add(new admin_setting_configtext('auth_cas/auth_name', get_string('auth_cas_auth_name', 'auth_cas'), get_string('auth_cas_auth_name_description', 'auth_cas'), get_string('auth_cas_auth_service', 'auth_cas'), PARAM_RAW_TRIMMED)); // Authentication method logo. $opts = array('accepted_types' => array('.png', '.jpg', '.gif', '.webp', '.tiff', '.svg')); $settings->add(new admin_setting_configstoredfile('auth_cas/auth_logo', get_string('auth_cas_auth_logo', 'auth_cas'), get_string('auth_cas_auth_logo_description', 'auth_cas'), 'logo', 0, $opts)); // Hostname. $settings->add(new admin_setting_configtext('auth_cas/hostname', get_string('auth_cas_hostname_key', 'auth_cas'), get_string('auth_cas_hostname', 'auth_cas'), '', PARAM_RAW_TRIMMED)); // Base URI. $settings->add(new admin_setting_configtext('auth_cas/baseuri', get_string('auth_cas_baseuri_key', 'auth_cas'), get_string('auth_cas_baseuri', 'auth_cas'), '', PARAM_RAW_TRIMMED)); // Port. $settings->add(new admin_setting_configtext('auth_cas/port', get_string('auth_cas_port_key', 'auth_cas'), get_string('auth_cas_port', 'auth_cas'), '', PARAM_INT)); // CAS Version. $casversions = array(); $casversions[CAS_VERSION_1_0] = 'CAS 1.0'; $casversions[CAS_VERSION_2_0] = 'CAS 2.0'; $settings->add(new admin_setting_configselect('auth_cas/casversion', new lang_string('auth_cas_casversion', 'auth_cas'), new lang_string('auth_cas_version', 'auth_cas'), CAS_VERSION_2_0, $casversions)); // Language. if (!isset($CASLANGUAGES) || empty($CASLANGUAGES)) { // Prevent warnings on other admin pages. // $CASLANGUAGES is defined in /auth/cas/languages.php. $CASLANGUAGES = array(); $CASLANGUAGES[PHPCAS_LANG_ENGLISH] = 'English'; $CASLANGUAGES[PHPCAS_LANG_FRENCH] = 'French'; } $settings->add(new admin_setting_configselect('auth_cas/language', new lang_string('auth_cas_language_key', 'auth_cas'), new lang_string('auth_cas_language', 'auth_cas'), PHPCAS_LANG_ENGLISH, $CASLANGUAGES)); // Proxy. $yesno = array( new lang_string('no'), new lang_string('yes'), ); $settings->add(new admin_setting_configselect('auth_cas/proxycas', new lang_string('auth_cas_proxycas_key', 'auth_cas'), new lang_string('auth_cas_proxycas', 'auth_cas'), 0 , $yesno)); // Logout option. $settings->add(new admin_setting_configselect('auth_cas/logoutcas', new lang_string('auth_cas_logoutcas_key', 'auth_cas'), new lang_string('auth_cas_logoutcas', 'auth_cas'), 0 , $yesno)); // Multi-auth. $settings->add(new admin_setting_configselect('auth_cas/multiauth', new lang_string('auth_cas_multiauth_key', 'auth_cas'), new lang_string('auth_cas_multiauth', 'auth_cas'), 0 , $yesno)); // Server validation. $settings->add(new admin_setting_configselect('auth_cas/certificate_check', new lang_string('auth_cas_certificate_check_key', 'auth_cas'), new lang_string('auth_cas_certificate_check', 'auth_cas'), 0 , $yesno)); // Certificate path. $settings->add(new admin_setting_configfile('auth_cas/certificate_path', get_string('auth_cas_certificate_path_key', 'auth_cas'), get_string('auth_cas_certificate_path', 'auth_cas'), '')); // CURL SSL version. $sslversions = array(); $sslversions[''] = get_string('auth_cas_curl_ssl_version_default', 'auth_cas'); if (defined('CURL_SSLVERSION_TLSv1')) { $sslversions[CURL_SSLVERSION_TLSv1] = get_string('auth_cas_curl_ssl_version_TLSv1x', 'auth_cas'); } if (defined('CURL_SSLVERSION_TLSv1_0')) { $sslversions[CURL_SSLVERSION_TLSv1_0] = get_string('auth_cas_curl_ssl_version_TLSv10', 'auth_cas'); } if (defined('CURL_SSLVERSION_TLSv1_1')) { $sslversions[CURL_SSLVERSION_TLSv1_1] = get_string('auth_cas_curl_ssl_version_TLSv11', 'auth_cas'); } if (defined('CURL_SSLVERSION_TLSv1_2')) { $sslversions[CURL_SSLVERSION_TLSv1_2] = get_string('auth_cas_curl_ssl_version_TLSv12', 'auth_cas'); } if (defined('CURL_SSLVERSION_SSLv2')) { $sslversions[CURL_SSLVERSION_SSLv2] = get_string('auth_cas_curl_ssl_version_SSLv2', 'auth_cas'); } if (defined('CURL_SSLVERSION_SSLv3')) { $sslversions[CURL_SSLVERSION_SSLv3] = get_string('auth_cas_curl_ssl_version_SSLv3', 'auth_cas'); } $settings->add(new admin_setting_configselect('auth_cas/curl_ssl_version', new lang_string('auth_cas_curl_ssl_version_key', 'auth_cas'), new lang_string('auth_cas_curl_ssl_version', 'auth_cas'), '' , $sslversions)); // Alt Logout URL. $settings->add(new admin_setting_configtext('auth_cas/logout_return_url', get_string('auth_cas_logout_return_url_key', 'auth_cas'), get_string('auth_cas_logout_return_url', 'auth_cas'), '', PARAM_URL)); // LDAP server settings. $settings->add(new admin_setting_heading('auth_cas/ldapserversettings', new lang_string('auth_ldap_server_settings', 'auth_ldap'), '')); // Host. $settings->add(new admin_setting_configtext('auth_cas/host_url', get_string('auth_ldap_host_url_key', 'auth_ldap'), get_string('auth_ldap_host_url', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Version. $versions = array(); $versions[2] = '2'; $versions[3] = '3'; $settings->add(new admin_setting_configselect('auth_cas/ldap_version', new lang_string('auth_ldap_version_key', 'auth_ldap'), new lang_string('auth_ldap_version', 'auth_ldap'), 3, $versions)); // Start TLS. $settings->add(new admin_setting_configselect('auth_cas/start_tls', new lang_string('start_tls_key', 'auth_ldap'), new lang_string('start_tls', 'auth_ldap'), 0 , $yesno)); // Encoding. $settings->add(new admin_setting_configtext('auth_cas/ldapencoding', get_string('auth_ldap_ldap_encoding_key', 'auth_ldap'), get_string('auth_ldap_ldap_encoding', 'auth_ldap'), 'utf-8', PARAM_RAW_TRIMMED)); // Page Size. (Hide if not available). $settings->add(new admin_setting_configtext('auth_cas/pagesize', get_string('pagesize_key', 'auth_ldap'), get_string('pagesize', 'auth_ldap'), '250', PARAM_INT)); // Bind settings. $settings->add(new admin_setting_heading('auth_cas/ldapbindsettings', new lang_string('auth_ldap_bind_settings', 'auth_ldap'), '')); // User ID. $settings->add(new admin_setting_configtext('auth_cas/bind_dn', get_string('auth_ldap_bind_dn_key', 'auth_ldap'), get_string('auth_ldap_bind_dn', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Password. $settings->add(new admin_setting_configpasswordunmask('auth_cas/bind_pw', get_string('auth_ldap_bind_pw_key', 'auth_ldap'), get_string('auth_ldap_bind_pw', 'auth_ldap'), '')); // User Lookup settings. $settings->add(new admin_setting_heading('auth_cas/ldapuserlookup', new lang_string('auth_ldap_user_settings', 'auth_ldap'), '')); // User Type. $settings->add(new admin_setting_configselect('auth_cas/user_type', new lang_string('auth_ldap_user_type_key', 'auth_ldap'), new lang_string('auth_ldap_user_type', 'auth_ldap'), 'default', ldap_supported_usertypes())); // Contexts. $settings->add(new auth_ldap_admin_setting_special_contexts_configtext('auth_cas/contexts', get_string('auth_ldap_contexts_key', 'auth_ldap'), get_string('auth_ldap_contexts', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Search subcontexts. $settings->add(new admin_setting_configselect('auth_cas/search_sub', new lang_string('auth_ldap_search_sub_key', 'auth_ldap'), new lang_string('auth_ldap_search_sub', 'auth_ldap'), 0 , $yesno)); // Dereference aliases. $optderef = array(); $optderef[LDAP_DEREF_NEVER] = get_string('no'); $optderef[LDAP_DEREF_ALWAYS] = get_string('yes'); $settings->add(new admin_setting_configselect('auth_cas/opt_deref', new lang_string('auth_ldap_opt_deref_key', 'auth_ldap'), new lang_string('auth_ldap_opt_deref', 'auth_ldap'), LDAP_DEREF_NEVER , $optderef)); // User attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_cas/user_attribute', get_string('auth_ldap_user_attribute_key', 'auth_ldap'), get_string('auth_ldap_user_attribute', 'auth_ldap'), '', PARAM_RAW)); // Member attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_cas/memberattribute', get_string('auth_ldap_memberattribute_key', 'auth_ldap'), get_string('auth_ldap_memberattribute', 'auth_ldap'), '', PARAM_RAW)); // Member attribute uses dn. $settings->add(new admin_setting_configselect('auth_cas/memberattribute_isdn', get_string('auth_ldap_memberattribute_isdn_key', 'auth_ldap'), get_string('auth_ldap_memberattribute_isdn', 'auth_ldap'), 0, $yesno)); // Object class. $settings->add(new admin_setting_configtext('auth_cas/objectclass', get_string('auth_ldap_objectclass_key', 'auth_ldap'), get_string('auth_ldap_objectclass', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Course Creators Header. $settings->add(new admin_setting_heading('auth_cas/coursecreators', new lang_string('coursecreators'), '')); // Course creators attribute field mapping. $settings->add(new admin_setting_configtext('auth_cas/attrcreators', get_string('auth_ldap_attrcreators_key', 'auth_ldap'), get_string('auth_ldap_attrcreators', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Course creator group field mapping. $settings->add(new admin_setting_configtext('auth_cas/groupecreators', get_string('auth_ldap_groupecreators_key', 'auth_ldap'), get_string('auth_ldap_groupecreators', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // User Account Sync. $settings->add(new admin_setting_heading('auth_cas/syncusers', new lang_string('auth_sync_script', 'auth'), '')); // Remove external user. $deleteopt = array(); $deleteopt[AUTH_REMOVEUSER_KEEP] = get_string('auth_remove_keep', 'auth'); $deleteopt[AUTH_REMOVEUSER_SUSPEND] = get_string('auth_remove_suspend', 'auth'); $deleteopt[AUTH_REMOVEUSER_FULLDELETE] = get_string('auth_remove_delete', 'auth'); $settings->add(new admin_setting_configselect('auth_cas/removeuser', new lang_string('auth_remove_user_key', 'auth'), new lang_string('auth_remove_user', 'auth'), AUTH_REMOVEUSER_KEEP, $deleteopt)); } // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('cas'); $help = get_string('auth_ldapextrafields', 'auth_ldap'); $help .= get_string('auth_updatelocal_expl', 'auth'); $help .= get_string('auth_fieldlock_expl', 'auth'); $help .= get_string('auth_updateremote_expl', 'auth'); $help .= '<hr />'; $help .= get_string('auth_updateremote_ldap', 'auth'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, $help, true, true, $authplugin->get_custom_user_profile_fields()); } cas/upgrade.txt 0000644 00000000467 15152311434 0007510 0 ustar 00 This files describes API changes in /auth/cas/*, information provided here is intended especially for developers. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/cas' to 'auth_cas'. cas/CAS/readme_moodle.txt 0000644 00000002163 15152311434 0011256 0 ustar 00 Description of phpCAS import into Moodle Last release can be found at https://github.com/apereo/phpCAS/releases NOTICE: * Before running composer command, make sure you have the composer version updated. * Composer version 2.2.4 2022-01-08 12:30:42 STEPS: * Make sure you're using the lowest supported PHP version for the given release (e.g. PHP 7.4 for Moodle 4.1) * Create a temporary folder outside your Moodle installation * Execute 'composer require apereo/phpcas:VERSION' * Check any new libraries that have been added and make sure they do not exist in Moodle already. * Remove the old 'vendor' directory in auth/cas/CAS/ * Copy contents of 'vendor' directory * Create a commit with only the library changes. - Note: Make sure to check the list of unversioned files and add any new files to the staging area. * Update auth/cas/thirdpartylibs.xml * Apply the modifications described in the CHANGES section * Create another commit with the previous two steps of changes CHANGES: * Remove all the hidden folders and files in vendor/apereo/phpcas/ (find . -name ".*"): - .codecov.yml - .gitattributes - .github cas/CAS/vendor/apereo/phpcas/NOTICE 0000644 00000007512 15152311434 0012656 0 ustar 00 Copyright 2007-2011, JA-SIG, Inc. This project includes software developed by Jasig. http://www.jasig.org/ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. =========================================================================== Copyright © 2003-2007, The ESUP-Portail consortium Requirements for sources originally licensed under the New BSD License: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of JA-SIG, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =========================================================================== Copyright (c) 2009, Regents of the University of Nebraska All rights reserved. Requirements for CAS_Autloader originally licensed under the New BSD License: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the University of Nebraska nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. cas/CAS/vendor/apereo/phpcas/LICENSE 0000644 00000026135 15152311434 0012761 0 ustar 00 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. cas/CAS/vendor/apereo/phpcas/CAS.php 0000644 00000002260 15152311434 0013064 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ require_once __DIR__.'/source/CAS.php'; trigger_error('Including CAS.php is deprecated. Install phpCAS using composer instead.', E_USER_DEPRECATED); cas/CAS/vendor/apereo/phpcas/README.md 0000644 00000002160 15152311434 0013223 0 ustar 00 phpCAS ======= phpCAS is an authentication library that allows PHP applications to easily authenticate users via a Central Authentication Service (CAS) server. Please see the wiki website for more information: https://apereo.github.io/phpCAS/ Api documentation can be found here: https://apereo.github.io/phpCAS/api/ [](https://github.com/apereo/phpCAS/actions/workflows/test.yml) LICENSE ------- Copyright 2007-2020, Apereo Foundation. This project includes software developed by Apereo Foundation. http://www.apereo.org/ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. cas/CAS/vendor/apereo/phpcas/phpunit.xml.dist 0000644 00000001047 15152311434 0015122 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="source/CAS.php" convertNoticesToExceptions="false" colors="true" stderr="true" backupGlobals="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> <coverage includeUncoveredFiles="false"> <include> <directory>source/</directory> </include> </coverage> <testsuites> <testsuite name="phpCAS Tests"> <directory>test/CAS/Tests/</directory> </testsuite> </testsuites> </phpunit> cas/CAS/vendor/apereo/phpcas/composer.json 0000644 00000002131 15152311434 0014464 0 ustar 00 { "name" : "apereo/phpcas", "description" : "Provides a simple API for authenticating users against a CAS server", "keywords" : [ "cas", "jasig", "apereo" ], "homepage" : "https://wiki.jasig.org/display/CASC/phpCAS", "type" : "library", "license" : "Apache-2.0", "authors" : [{ "name" : "Joachim Fritschi", "homepage" : "https://github.com/jfritschi", "email" : "jfritschi@freenet.de" }, { "name" : "Adam Franco", "homepage" : "https://github.com/adamfranco" }, { "name" : "Henry Pan", "homepage" : "https://github.com/phy25" } ], "require" : { "php" : ">=7.1.0", "ext-curl" : "*", "ext-dom" : "*", "psr/log" : "^1.0 || ^2.0 || ^3.0" }, "require-dev" : { "monolog/monolog" : "^1.0.0 || ^2.0.0", "phpunit/phpunit" : ">=7.5", "phpstan/phpstan" : "^1.5" }, "autoload" : { "classmap" : [ "source/" ] }, "autoload-dev" : { "files": ["source/CAS.php"], "psr-4" : { "PhpCas\\" : "test/CAS/" } }, "scripts" : { "test" : "phpunit", "phpstan" : "phpstan" }, "extra" : { "branch-alias" : { "dev-master" : "1.3.x-dev" } } } cas/CAS/vendor/apereo/phpcas/source/CAS.php 0000644 00000177512 15152311434 0014401 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * * * Interface class of the phpCAS library * PHP Version 7 * * @file CAS/CAS.php * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @author Olivier Berger <olivier.berger@it-sudparis.eu> * @author Brett Bieber <brett.bieber@gmail.com> * @author Joachim Fritschi <jfritschi@freenet.de> * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * @ingroup public */ use Psr\Log\LoggerInterface; // // hack by Vangelis Haniotakis to handle the absence of $_SERVER['REQUEST_URI'] // in IIS // if (!isset($_SERVER['REQUEST_URI']) && isset($_SERVER['SCRIPT_NAME']) && isset($_SERVER['QUERY_STRING'])) { $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['QUERY_STRING']; } // ######################################################################## // CONSTANTS // ######################################################################## // ------------------------------------------------------------------------ // CAS VERSIONS // ------------------------------------------------------------------------ /** * phpCAS version. accessible for the user by phpCAS::getVersion(). */ define('PHPCAS_VERSION', '1.6.0'); /** * @addtogroup public * @{ */ /** * phpCAS supported protocols. accessible for the user by phpCAS::getSupportedProtocols(). */ /** * CAS version 1.0 */ define("CAS_VERSION_1_0", '1.0'); /*! * CAS version 2.0 */ define("CAS_VERSION_2_0", '2.0'); /** * CAS version 3.0 */ define("CAS_VERSION_3_0", '3.0'); // ------------------------------------------------------------------------ // SAML defines // ------------------------------------------------------------------------ /** * SAML protocol */ define("SAML_VERSION_1_1", 'S1'); /** * XML header for SAML POST */ define("SAML_XML_HEADER", '<?xml version="1.0" encoding="UTF-8"?>'); /** * SOAP envelope for SAML POST */ define("SAML_SOAP_ENV", '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/>'); /** * SOAP body for SAML POST */ define("SAML_SOAP_BODY", '<SOAP-ENV:Body>'); /** * SAMLP request */ define("SAMLP_REQUEST", '<samlp:Request xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" MajorVersion="1" MinorVersion="1" RequestID="_192.168.16.51.1024506224022" IssueInstant="2002-06-19T17:03:44.022Z">'); define("SAMLP_REQUEST_CLOSE", '</samlp:Request>'); /** * SAMLP artifact tag (for the ticket) */ define("SAML_ASSERTION_ARTIFACT", '<samlp:AssertionArtifact>'); /** * SAMLP close */ define("SAML_ASSERTION_ARTIFACT_CLOSE", '</samlp:AssertionArtifact>'); /** * SOAP body close */ define("SAML_SOAP_BODY_CLOSE", '</SOAP-ENV:Body>'); /** * SOAP envelope close */ define("SAML_SOAP_ENV_CLOSE", '</SOAP-ENV:Envelope>'); /** * SAML Attributes */ define("SAML_ATTRIBUTES", 'SAMLATTRIBS'); /** @} */ /** * @addtogroup publicPGTStorage * @{ */ // ------------------------------------------------------------------------ // FILE PGT STORAGE // ------------------------------------------------------------------------ /** * Default path used when storing PGT's to file */ define("CAS_PGT_STORAGE_FILE_DEFAULT_PATH", session_save_path()); /** @} */ // ------------------------------------------------------------------------ // SERVICE ACCESS ERRORS // ------------------------------------------------------------------------ /** * @addtogroup publicServices * @{ */ /** * phpCAS::service() error code on success */ define("PHPCAS_SERVICE_OK", 0); /** * phpCAS::service() error code when the PT could not retrieve because * the CAS server did not respond. */ define("PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE", 1); /** * phpCAS::service() error code when the PT could not retrieve because * the response of the CAS server was ill-formed. */ define("PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE", 2); /** * phpCAS::service() error code when the PT could not retrieve because * the CAS server did not want to. */ define("PHPCAS_SERVICE_PT_FAILURE", 3); /** * phpCAS::service() error code when the service was not available. */ define("PHPCAS_SERVICE_NOT_AVAILABLE", 4); // ------------------------------------------------------------------------ // SERVICE TYPES // ------------------------------------------------------------------------ /** * phpCAS::getProxiedService() type for HTTP GET */ define("PHPCAS_PROXIED_SERVICE_HTTP_GET", 'CAS_ProxiedService_Http_Get'); /** * phpCAS::getProxiedService() type for HTTP POST */ define("PHPCAS_PROXIED_SERVICE_HTTP_POST", 'CAS_ProxiedService_Http_Post'); /** * phpCAS::getProxiedService() type for IMAP */ define("PHPCAS_PROXIED_SERVICE_IMAP", 'CAS_ProxiedService_Imap'); /** @} */ // ------------------------------------------------------------------------ // LANGUAGES // ------------------------------------------------------------------------ /** * @addtogroup publicLang * @{ */ define("PHPCAS_LANG_ENGLISH", 'CAS_Languages_English'); define("PHPCAS_LANG_FRENCH", 'CAS_Languages_French'); define("PHPCAS_LANG_GREEK", 'CAS_Languages_Greek'); define("PHPCAS_LANG_GERMAN", 'CAS_Languages_German'); define("PHPCAS_LANG_JAPANESE", 'CAS_Languages_Japanese'); define("PHPCAS_LANG_SPANISH", 'CAS_Languages_Spanish'); define("PHPCAS_LANG_CATALAN", 'CAS_Languages_Catalan'); define("PHPCAS_LANG_CHINESE_SIMPLIFIED", 'CAS_Languages_ChineseSimplified'); define("PHPCAS_LANG_GALEGO", 'CAS_Languages_Galego'); define("PHPCAS_LANG_PORTUGUESE", 'CAS_Languages_Portuguese'); /** @} */ /** * @addtogroup internalLang * @{ */ /** * phpCAS default language (when phpCAS::setLang() is not used) */ define("PHPCAS_LANG_DEFAULT", PHPCAS_LANG_ENGLISH); /** @} */ // ------------------------------------------------------------------------ // DEBUG // ------------------------------------------------------------------------ /** * @addtogroup publicDebug * @{ */ /** * The default directory for the debug file under Unix. * @return string directory for the debug file */ function gettmpdir() { if (!empty($_ENV['TMP'])) { return realpath($_ENV['TMP']); } if (!empty($_ENV['TMPDIR'])) { return realpath( $_ENV['TMPDIR']); } if (!empty($_ENV['TEMP'])) { return realpath( $_ENV['TEMP']); } return "/tmp"; } define('DEFAULT_DEBUG_DIR', gettmpdir()."/"); /** @} */ // include the class autoloader require_once __DIR__ . '/CAS/Autoload.php'; /** * The phpCAS class is a simple container for the phpCAS library. It provides CAS * authentication for web applications written in PHP. * * @ingroup public * @class phpCAS * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @author Olivier Berger <olivier.berger@it-sudparis.eu> * @author Brett Bieber <brett.bieber@gmail.com> * @author Joachim Fritschi <jfritschi@freenet.de> * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class phpCAS { /** * This variable is used by the interface class phpCAS. * * @var CAS_Client * @hideinitializer */ private static $_PHPCAS_CLIENT; /** * @var array * This variable is used to store where the initializer is called from * (to print a comprehensive error in case of multiple calls). * * @hideinitializer */ private static $_PHPCAS_INIT_CALL; /** * @var array * This variable is used to store phpCAS debug mode. * * @hideinitializer */ private static $_PHPCAS_DEBUG; /** * This variable is used to enable verbose mode * This pevents debug info to be show to the user. Since it's a security * feature the default is false * * @hideinitializer */ private static $_PHPCAS_VERBOSE = false; // ######################################################################## // INITIALIZATION // ######################################################################## /** * @addtogroup publicInit * @{ */ /** * phpCAS client initializer. * * @param string $server_version the version of the CAS server * @param string $server_hostname the hostname of the CAS server * @param int $server_port the port the CAS server is running on * @param string $server_uri the URI the CAS server is responding on * @param string|string[]|CAS_ServiceBaseUrl_Interface * $service_base_url the base URL (protocol, host and the * optional port) of the CAS client; pass * in an array to use auto discovery with * an allowlist; pass in * CAS_ServiceBaseUrl_Interface for custom * behavior. Added in 1.6.0. Similar to * serverName config in other CAS clients. * @param bool $changeSessionID Allow phpCAS to change the session_id * (Single Sign Out/handleLogoutRequests * is based on that change) * @param \SessionHandlerInterface $sessionHandler the session handler * * @return void a newly created CAS_Client object * @note Only one of the phpCAS::client() and phpCAS::proxy functions should be * called, only once, and before all other methods (except phpCAS::getVersion() * and phpCAS::setDebug()). */ public static function client($server_version, $server_hostname, $server_port, $server_uri, $service_base_url, $changeSessionID = true, \SessionHandlerInterface $sessionHandler = null ) { phpCAS :: traceBegin(); if (is_object(self::$_PHPCAS_CLIENT)) { phpCAS :: error(self::$_PHPCAS_INIT_CALL['method'] . '() has already been called (at ' . self::$_PHPCAS_INIT_CALL['file'] . ':' . self::$_PHPCAS_INIT_CALL['line'] . ')'); } // store where the initializer is called from $dbg = debug_backtrace(); self::$_PHPCAS_INIT_CALL = array ( 'done' => true, 'file' => $dbg[0]['file'], 'line' => $dbg[0]['line'], 'method' => __CLASS__ . '::' . __FUNCTION__ ); // initialize the object $_PHPCAS_CLIENT try { self::$_PHPCAS_CLIENT = new CAS_Client( $server_version, false, $server_hostname, $server_port, $server_uri, $service_base_url, $changeSessionID, $sessionHandler ); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * phpCAS proxy initializer. * * @param string $server_version the version of the CAS server * @param string $server_hostname the hostname of the CAS server * @param string $server_port the port the CAS server is running on * @param string $server_uri the URI the CAS server is responding on * @param string|string[]|CAS_ServiceBaseUrl_Interface * $service_base_url the base URL (protocol, host and the * optional port) of the CAS client; pass * in an array to use auto discovery with * an allowlist; pass in * CAS_ServiceBaseUrl_Interface for custom * behavior. Added in 1.6.0. Similar to * serverName config in other CAS clients. * @param bool $changeSessionID Allow phpCAS to change the session_id * (Single Sign Out/handleLogoutRequests * is based on that change) * @param \SessionHandlerInterface $sessionHandler the session handler * * @return void a newly created CAS_Client object * @note Only one of the phpCAS::client() and phpCAS::proxy functions should be * called, only once, and before all other methods (except phpCAS::getVersion() * and phpCAS::setDebug()). */ public static function proxy($server_version, $server_hostname, $server_port, $server_uri, $service_base_url, $changeSessionID = true, \SessionHandlerInterface $sessionHandler = null ) { phpCAS :: traceBegin(); if (is_object(self::$_PHPCAS_CLIENT)) { phpCAS :: error(self::$_PHPCAS_INIT_CALL['method'] . '() has already been called (at ' . self::$_PHPCAS_INIT_CALL['file'] . ':' . self::$_PHPCAS_INIT_CALL['line'] . ')'); } // store where the initialzer is called from $dbg = debug_backtrace(); self::$_PHPCAS_INIT_CALL = array ( 'done' => true, 'file' => $dbg[0]['file'], 'line' => $dbg[0]['line'], 'method' => __CLASS__ . '::' . __FUNCTION__ ); // initialize the object $_PHPCAS_CLIENT try { self::$_PHPCAS_CLIENT = new CAS_Client( $server_version, true, $server_hostname, $server_port, $server_uri, $service_base_url, $changeSessionID, $sessionHandler ); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * Answer whether or not the client or proxy has been initialized * * @return bool */ public static function isInitialized () { return (is_object(self::$_PHPCAS_CLIENT)); } /** @} */ // ######################################################################## // DEBUGGING // ######################################################################## /** * @addtogroup publicDebug * @{ */ /** * Set/unset PSR-3 logger * * @param LoggerInterface $logger the PSR-3 logger used for logging, or * null to stop logging. * * @return void */ public static function setLogger($logger = null) { if (empty(self::$_PHPCAS_DEBUG['unique_id'])) { self::$_PHPCAS_DEBUG['unique_id'] = substr(strtoupper(md5(uniqid(''))), 0, 4); } self::$_PHPCAS_DEBUG['logger'] = $logger; self::$_PHPCAS_DEBUG['indent'] = 0; phpCAS :: trace('START ('.date("Y-m-d H:i:s").') phpCAS-' . PHPCAS_VERSION . ' ******************'); } /** * Set/unset debug mode * * @param string $filename the name of the file used for logging, or false * to stop debugging. * * @return void * * @deprecated */ public static function setDebug($filename = '') { trigger_error('phpCAS::setDebug() is deprecated in favor of phpCAS::setLogger().', E_USER_DEPRECATED); if ($filename != false && gettype($filename) != 'string') { phpCAS :: error('type mismatched for parameter $dbg (should be false or the name of the log file)'); } if ($filename === false) { self::$_PHPCAS_DEBUG['filename'] = false; } else { if (empty ($filename)) { if (preg_match('/^Win.*/', getenv('OS'))) { if (isset ($_ENV['TMP'])) { $debugDir = $_ENV['TMP'] . '/'; } else { $debugDir = ''; } } else { $debugDir = DEFAULT_DEBUG_DIR; } $filename = $debugDir . 'phpCAS.log'; } if (empty (self::$_PHPCAS_DEBUG['unique_id'])) { self::$_PHPCAS_DEBUG['unique_id'] = substr(strtoupper(md5(uniqid(''))), 0, 4); } self::$_PHPCAS_DEBUG['filename'] = $filename; self::$_PHPCAS_DEBUG['indent'] = 0; phpCAS :: trace('START ('.date("Y-m-d H:i:s").') phpCAS-' . PHPCAS_VERSION . ' ******************'); } } /** * Enable verbose errors messages in the website output * This is a security relevant since internal status info may leak an may * help an attacker. Default is therefore false * * @param bool $verbose enable verbose output * * @return void */ public static function setVerbose($verbose) { if ($verbose === true) { self::$_PHPCAS_VERBOSE = true; } else { self::$_PHPCAS_VERBOSE = false; } } /** * Show is verbose mode is on * * @return bool verbose */ public static function getVerbose() { return self::$_PHPCAS_VERBOSE; } /** * Logs a string in debug mode. * * @param string $str the string to write * * @return void * @private */ public static function log($str) { $indent_str = "."; if (isset(self::$_PHPCAS_DEBUG['logger']) || !empty(self::$_PHPCAS_DEBUG['filename'])) { for ($i = 0; $i < self::$_PHPCAS_DEBUG['indent']; $i++) { $indent_str .= '| '; } // allow for multiline output with proper identing. Usefull for // dumping cas answers etc. $str2 = str_replace("\n", "\n" . self::$_PHPCAS_DEBUG['unique_id'] . ' ' . $indent_str, $str); $str3 = self::$_PHPCAS_DEBUG['unique_id'] . ' ' . $indent_str . $str2; if (isset(self::$_PHPCAS_DEBUG['logger'])) { self::$_PHPCAS_DEBUG['logger']->info($str3); } if (!empty(self::$_PHPCAS_DEBUG['filename'])) { // Check if file exists and modifiy file permissions to be only // readable by the webserver if (!file_exists(self::$_PHPCAS_DEBUG['filename'])) { touch(self::$_PHPCAS_DEBUG['filename']); // Chmod will fail on windows @chmod(self::$_PHPCAS_DEBUG['filename'], 0600); } error_log($str3 . "\n", 3, self::$_PHPCAS_DEBUG['filename']); } } } /** * This method is used by interface methods to print an error and where the * function was originally called from. * * @param string $msg the message to print * * @return void * @private */ public static function error($msg) { phpCAS :: traceBegin(); $dbg = debug_backtrace(); $function = '?'; $file = '?'; $line = '?'; if (is_array($dbg)) { for ($i = 1; $i < sizeof($dbg); $i++) { if (is_array($dbg[$i]) && isset($dbg[$i]['class']) ) { if ($dbg[$i]['class'] == __CLASS__) { $function = $dbg[$i]['function']; $file = $dbg[$i]['file']; $line = $dbg[$i]['line']; } } } } if (self::$_PHPCAS_VERBOSE) { echo "<br />\n<b>phpCAS error</b>: <font color=\"FF0000\"><b>" . __CLASS__ . "::" . $function . '(): ' . htmlentities($msg) . "</b></font> in <b>" . $file . "</b> on line <b>" . $line . "</b><br />\n"; } phpCAS :: trace($msg . ' in ' . $file . 'on line ' . $line ); phpCAS :: traceEnd(); throw new CAS_GracefullTerminationException(__CLASS__ . "::" . $function . '(): ' . $msg); } /** * This method is used to log something in debug mode. * * @param string $str string to log * * @return void */ public static function trace($str) { $dbg = debug_backtrace(); phpCAS :: log($str . ' [' . basename($dbg[0]['file']) . ':' . $dbg[0]['line'] . ']'); } /** * This method is used to indicate the start of the execution of a function * in debug mode. * * @return void */ public static function traceBegin() { $dbg = debug_backtrace(); $str = '=> '; if (!empty ($dbg[1]['class'])) { $str .= $dbg[1]['class'] . '::'; } $str .= $dbg[1]['function'] . '('; if (is_array($dbg[1]['args'])) { foreach ($dbg[1]['args'] as $index => $arg) { if ($index != 0) { $str .= ', '; } if (is_object($arg)) { $str .= get_class($arg); } else { $str .= str_replace(array("\r\n", "\n", "\r"), "", var_export($arg, true)); } } } if (isset($dbg[1]['file'])) { $file = basename($dbg[1]['file']); } else { $file = 'unknown_file'; } if (isset($dbg[1]['line'])) { $line = $dbg[1]['line']; } else { $line = 'unknown_line'; } $str .= ') [' . $file . ':' . $line . ']'; phpCAS :: log($str); if (!isset(self::$_PHPCAS_DEBUG['indent'])) { self::$_PHPCAS_DEBUG['indent'] = 0; } else { self::$_PHPCAS_DEBUG['indent']++; } } /** * This method is used to indicate the end of the execution of a function in * debug mode. * * @param mixed $res the result of the function * * @return void */ public static function traceEnd($res = '') { if (empty(self::$_PHPCAS_DEBUG['indent'])) { self::$_PHPCAS_DEBUG['indent'] = 0; } else { self::$_PHPCAS_DEBUG['indent']--; } $str = ''; if (is_object($res)) { $str .= '<= ' . get_class($res); } else { $str .= '<= ' . str_replace(array("\r\n", "\n", "\r"), "", var_export($res, true)); } phpCAS :: log($str); } /** * This method is used to indicate the end of the execution of the program * * @return void */ public static function traceExit() { phpCAS :: log('exit()'); while (self::$_PHPCAS_DEBUG['indent'] > 0) { phpCAS :: log('-'); self::$_PHPCAS_DEBUG['indent']--; } } /** @} */ // ######################################################################## // INTERNATIONALIZATION // ######################################################################## /** * @addtogroup publicLang * @{ */ /** * This method is used to set the language used by phpCAS. * * @param string $lang string representing the language. * * @return void * * @sa PHPCAS_LANG_FRENCH, PHPCAS_LANG_ENGLISH * @note Can be called only once. */ public static function setLang($lang) { phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setLang($lang); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** @} */ // ######################################################################## // VERSION // ######################################################################## /** * @addtogroup public * @{ */ /** * This method returns the phpCAS version. * * @return string the phpCAS version. */ public static function getVersion() { return PHPCAS_VERSION; } /** * This method returns supported protocols. * * @return array an array of all supported protocols. Use internal protocol name as array key. */ public static function getSupportedProtocols() { $supportedProtocols = array(); $supportedProtocols[CAS_VERSION_1_0] = 'CAS 1.0'; $supportedProtocols[CAS_VERSION_2_0] = 'CAS 2.0'; $supportedProtocols[CAS_VERSION_3_0] = 'CAS 3.0'; $supportedProtocols[SAML_VERSION_1_1] = 'SAML 1.1'; return $supportedProtocols; } /** @} */ // ######################################################################## // HTML OUTPUT // ######################################################################## /** * @addtogroup publicOutput * @{ */ /** * This method sets the HTML header used for all outputs. * * @param string $header the HTML header. * * @return void */ public static function setHTMLHeader($header) { phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setHTMLHeader($header); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** * This method sets the HTML footer used for all outputs. * * @param string $footer the HTML footer. * * @return void */ public static function setHTMLFooter($footer) { phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setHTMLFooter($footer); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** @} */ // ######################################################################## // PGT STORAGE // ######################################################################## /** * @addtogroup publicPGTStorage * @{ */ /** * This method can be used to set a custom PGT storage object. * * @param CAS_PGTStorage_AbstractStorage $storage a PGT storage object that inherits from the * CAS_PGTStorage_AbstractStorage class * * @return void */ public static function setPGTStorage($storage) { phpCAS :: traceBegin(); phpCAS::_validateProxyExists(); try { self::$_PHPCAS_CLIENT->setPGTStorage($storage); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * This method is used to tell phpCAS to store the response of the * CAS server to PGT requests in a database. * * @param string $dsn_or_pdo a dsn string to use for creating a PDO * object or a PDO object * @param string $username the username to use when connecting to the * database * @param string $password the password to use when connecting to the * database * @param string $table the table to use for storing and retrieving * PGT's * @param string $driver_options any driver options to use when connecting * to the database * * @return void */ public static function setPGTStorageDb($dsn_or_pdo, $username='', $password='', $table='', $driver_options=null ) { phpCAS :: traceBegin(); phpCAS::_validateProxyExists(); try { self::$_PHPCAS_CLIENT->setPGTStorageDb($dsn_or_pdo, $username, $password, $table, $driver_options); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * This method is used to tell phpCAS to store the response of the * CAS server to PGT requests onto the filesystem. * * @param string $path the path where the PGT's should be stored * * @return void */ public static function setPGTStorageFile($path = '') { phpCAS :: traceBegin(); phpCAS::_validateProxyExists(); try { self::$_PHPCAS_CLIENT->setPGTStorageFile($path); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** @} */ // ######################################################################## // ACCESS TO EXTERNAL SERVICES // ######################################################################## /** * @addtogroup publicServices * @{ */ /** * Answer a proxy-authenticated service handler. * * @param string $type The service type. One of * PHPCAS_PROXIED_SERVICE_HTTP_GET; PHPCAS_PROXIED_SERVICE_HTTP_POST; * PHPCAS_PROXIED_SERVICE_IMAP * * @return CAS_ProxiedService * @throws InvalidArgumentException If the service type is unknown. */ public static function getProxiedService ($type) { phpCAS :: traceBegin(); phpCAS::_validateProxyExists(); try { $res = self::$_PHPCAS_CLIENT->getProxiedService($type); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); return $res; } /** * Initialize a proxied-service handler with the proxy-ticket it should use. * * @param CAS_ProxiedService $proxiedService Proxied Service Handler * * @return void * @throws CAS_ProxyTicketException If there is a proxy-ticket failure. * The code of the Exception will be one of: * PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE * PHPCAS_SERVICE_PT_FAILURE */ public static function initializeProxiedService (CAS_ProxiedService $proxiedService) { phpCAS::_validateProxyExists(); try { self::$_PHPCAS_CLIENT->initializeProxiedService($proxiedService); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** * This method is used to access an HTTP[S] service. * * @param string $url the service to access. * @param int &$err_code an error code Possible values are * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE, * PHPCAS_SERVICE_NOT_AVAILABLE. * @param string &$output the output of the service (also used to give an * error message on failure). * * @return bool true on success, false otherwise (in this later case, * $err_code gives the reason why it failed and $output contains an error * message). */ public static function serviceWeb($url, & $err_code, & $output) { phpCAS :: traceBegin(); phpCAS::_validateProxyExists(); try { $res = self::$_PHPCAS_CLIENT->serviceWeb($url, $err_code, $output); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd($res); return $res; } /** * This method is used to access an IMAP/POP3/NNTP service. * * @param string $url a string giving the URL of the service, * including the mailing box for IMAP URLs, as accepted by imap_open(). * @param string $service a string giving for CAS retrieve Proxy ticket * @param string $flags options given to imap_open(). * @param int &$err_code an error code Possible values are * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE, * PHPCAS_SERVICE_NOT_AVAILABLE. * @param string &$err_msg an error message on failure * @param string &$pt the Proxy Ticket (PT) retrieved from the CAS * server to access the URL on success, false on error). * * @return object|false IMAP stream on success, false otherwise (in this later * case, $err_code gives the reason why it failed and $err_msg contains an * error message). */ public static function serviceMail($url, $service, $flags, & $err_code, & $err_msg, & $pt) { phpCAS :: traceBegin(); phpCAS::_validateProxyExists(); try { $res = self::$_PHPCAS_CLIENT->serviceMail($url, $service, $flags, $err_code, $err_msg, $pt); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd($res); return $res; } /** @} */ // ######################################################################## // AUTHENTICATION // ######################################################################## /** * @addtogroup publicAuth * @{ */ /** * Set the times authentication will be cached before really accessing the * CAS server in gateway mode: * - -1: check only once, and then never again (until you pree login) * - 0: always check * - n: check every "n" time * * @param int $n an integer. * * @return void */ public static function setCacheTimesForAuthRecheck($n) { phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setCacheTimesForAuthRecheck($n); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** * Set a callback function to be run when receiving CAS attributes * * The callback function will be passed an $success_elements * payload of the response (\DOMElement) as its first parameter. * * @param string $function Callback function * @param array $additionalArgs optional array of arguments * * @return void */ public static function setCasAttributeParserCallback($function, array $additionalArgs = array()) { phpCAS::_validateClientExists(); self::$_PHPCAS_CLIENT->setCasAttributeParserCallback($function, $additionalArgs); } /** * Set a callback function to be run when a user authenticates. * * The callback function will be passed a $logoutTicket as its first * parameter, followed by any $additionalArgs you pass. The $logoutTicket * parameter is an opaque string that can be used to map the session-id to * logout request in order to support single-signout in applications that * manage their own sessions (rather than letting phpCAS start the session). * * phpCAS::forceAuthentication() will always exit and forward client unless * they are already authenticated. To perform an action at the moment the user * logs in (such as registering an account, performing logging, etc), register * a callback function here. * * @param callable $function Callback function * @param array $additionalArgs optional array of arguments * * @return void */ public static function setPostAuthenticateCallback ($function, array $additionalArgs = array()) { phpCAS::_validateClientExists(); self::$_PHPCAS_CLIENT->setPostAuthenticateCallback($function, $additionalArgs); } /** * Set a callback function to be run when a single-signout request is * received. The callback function will be passed a $logoutTicket as its * first parameter, followed by any $additionalArgs you pass. The * $logoutTicket parameter is an opaque string that can be used to map a * session-id to the logout request in order to support single-signout in * applications that manage their own sessions (rather than letting phpCAS * start and destroy the session). * * @param callable $function Callback function * @param array $additionalArgs optional array of arguments * * @return void */ public static function setSingleSignoutCallback ($function, array $additionalArgs = array()) { phpCAS::_validateClientExists(); self::$_PHPCAS_CLIENT->setSingleSignoutCallback($function, $additionalArgs); } /** * This method is called to check if the user is already authenticated * locally or has a global cas session. A already existing cas session is * determined by a cas gateway call.(cas login call without any interactive * prompt) * * @return bool true when the user is authenticated, false when a previous * gateway login failed or the function will not return if the user is * redirected to the cas server for a gateway login attempt */ public static function checkAuthentication() { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); $auth = self::$_PHPCAS_CLIENT->checkAuthentication(); // store where the authentication has been checked and the result self::$_PHPCAS_CLIENT->markAuthenticationCall($auth); phpCAS :: traceEnd($auth); return $auth; } /** * This method is called to force authentication if the user was not already * authenticated. If the user is not authenticated, halt by redirecting to * the CAS server. * * @return bool Authentication */ public static function forceAuthentication() { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); $auth = self::$_PHPCAS_CLIENT->forceAuthentication(); // store where the authentication has been checked and the result self::$_PHPCAS_CLIENT->markAuthenticationCall($auth); /* if (!$auth) { phpCAS :: trace('user is not authenticated, redirecting to the CAS server'); self::$_PHPCAS_CLIENT->forceAuthentication(); } else { phpCAS :: trace('no need to authenticate (user `' . phpCAS :: getUser() . '\' is already authenticated)'); }*/ phpCAS :: traceEnd(); return $auth; } /** * This method is called to renew the authentication. * * @return void **/ public static function renewAuthentication() { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); $auth = self::$_PHPCAS_CLIENT->renewAuthentication(); // store where the authentication has been checked and the result self::$_PHPCAS_CLIENT->markAuthenticationCall($auth); //self::$_PHPCAS_CLIENT->renewAuthentication(); phpCAS :: traceEnd(); } /** * This method is called to check if the user is authenticated (previously or by * tickets given in the URL). * * @return bool true when the user is authenticated. */ public static function isAuthenticated() { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); // call the isAuthenticated method of the $_PHPCAS_CLIENT object $auth = self::$_PHPCAS_CLIENT->isAuthenticated(); // store where the authentication has been checked and the result self::$_PHPCAS_CLIENT->markAuthenticationCall($auth); phpCAS :: traceEnd($auth); return $auth; } /** * Checks whether authenticated based on $_SESSION. Useful to avoid * server calls. * * @return bool true if authenticated, false otherwise. * @since 0.4.22 by Brendan Arnold */ public static function isSessionAuthenticated() { phpCAS::_validateClientExists(); return (self::$_PHPCAS_CLIENT->isSessionAuthenticated()); } /** * This method returns the CAS user's login name. * * @return string the login name of the authenticated user * @warning should only be called after phpCAS::forceAuthentication() * or phpCAS::checkAuthentication(). * */ public static function getUser() { phpCAS::_validateClientExists(); try { return self::$_PHPCAS_CLIENT->getUser(); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** * Answer attributes about the authenticated user. * * @warning should only be called after phpCAS::forceAuthentication() * or phpCAS::checkAuthentication(). * * @return array */ public static function getAttributes() { phpCAS::_validateClientExists(); try { return self::$_PHPCAS_CLIENT->getAttributes(); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** * Answer true if there are attributes for the authenticated user. * * @warning should only be called after phpCAS::forceAuthentication() * or phpCAS::checkAuthentication(). * * @return bool */ public static function hasAttributes() { phpCAS::_validateClientExists(); try { return self::$_PHPCAS_CLIENT->hasAttributes(); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** * Answer true if an attribute exists for the authenticated user. * * @param string $key attribute name * * @return bool * @warning should only be called after phpCAS::forceAuthentication() * or phpCAS::checkAuthentication(). */ public static function hasAttribute($key) { phpCAS::_validateClientExists(); try { return self::$_PHPCAS_CLIENT->hasAttribute($key); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** * Answer an attribute for the authenticated user. * * @param string $key attribute name * * @return mixed string for a single value or an array if multiple values exist. * @warning should only be called after phpCAS::forceAuthentication() * or phpCAS::checkAuthentication(). */ public static function getAttribute($key) { phpCAS::_validateClientExists(); try { return self::$_PHPCAS_CLIENT->getAttribute($key); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** * Handle logout requests. * * @param bool $check_client additional safety check * @param array $allowed_clients array of allowed clients * * @return void */ public static function handleLogoutRequests($check_client = true, $allowed_clients = array()) { phpCAS::_validateClientExists(); return (self::$_PHPCAS_CLIENT->handleLogoutRequests($check_client, $allowed_clients)); } /** * This method returns the URL to be used to login. * * @return string the login URL */ public static function getServerLoginURL() { phpCAS::_validateClientExists(); return self::$_PHPCAS_CLIENT->getServerLoginURL(); } /** * Set the login URL of the CAS server. * * @param string $url the login URL * * @return void * @since 0.4.21 by Wyman Chan */ public static function setServerLoginURL($url = '') { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setServerLoginURL($url); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * Set the serviceValidate URL of the CAS server. * Used for all CAS versions of URL validations. * Examples: * CAS 1.0 http://www.exemple.com/validate * CAS 2.0 http://www.exemple.com/validateURL * CAS 3.0 http://www.exemple.com/p3/serviceValidate * * @param string $url the serviceValidate URL * * @return void */ public static function setServerServiceValidateURL($url = '') { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setServerServiceValidateURL($url); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * Set the proxyValidate URL of the CAS server. * Used for all CAS versions of proxy URL validations * Examples: * CAS 1.0 http://www.exemple.com/ * CAS 2.0 http://www.exemple.com/proxyValidate * CAS 3.0 http://www.exemple.com/p3/proxyValidate * * @param string $url the proxyValidate URL * * @return void */ public static function setServerProxyValidateURL($url = '') { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setServerProxyValidateURL($url); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * Set the samlValidate URL of the CAS server. * * @param string $url the samlValidate URL * * @return void */ public static function setServerSamlValidateURL($url = '') { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setServerSamlValidateURL($url); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * This method returns the URL to be used to logout. * * @return string the URL to use to log out */ public static function getServerLogoutURL() { phpCAS::_validateClientExists(); return self::$_PHPCAS_CLIENT->getServerLogoutURL(); } /** * Set the logout URL of the CAS server. * * @param string $url the logout URL * * @return void * @since 0.4.21 by Wyman Chan */ public static function setServerLogoutURL($url = '') { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setServerLogoutURL($url); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * This method is used to logout from CAS. * * @param string $params an array that contains the optional url and * service parameters that will be passed to the CAS server * * @return void */ public static function logout($params = "") { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); $parsedParams = array (); if ($params != "") { if (is_string($params)) { phpCAS :: error('method `phpCAS::logout($url)\' is now deprecated, use `phpCAS::logoutWithUrl($url)\' instead'); } if (!is_array($params)) { phpCAS :: error('type mismatched for parameter $params (should be `array\')'); } foreach ($params as $key => $value) { if ($key != "service" && $key != "url") { phpCAS :: error('only `url\' and `service\' parameters are allowed for method `phpCAS::logout($params)\''); } $parsedParams[$key] = $value; } } self::$_PHPCAS_CLIENT->logout($parsedParams); // never reached phpCAS :: traceEnd(); } /** * This method is used to logout from CAS. Halts by redirecting to the CAS * server. * * @param string $service a URL that will be transmitted to the CAS server * * @return void */ public static function logoutWithRedirectService($service) { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); if (!is_string($service)) { phpCAS :: error('type mismatched for parameter $service (should be `string\')'); } self::$_PHPCAS_CLIENT->logout(array ( "service" => $service )); // never reached phpCAS :: traceEnd(); } /** * This method is used to logout from CAS. Halts by redirecting to the CAS * server. * * @param string $url a URL that will be transmitted to the CAS server * * @return void * @deprecated The url parameter has been removed from the CAS server as of * version 3.3.5.1 */ public static function logoutWithUrl($url) { trigger_error('Function deprecated for cas servers >= 3.3.5.1', E_USER_DEPRECATED); phpCAS :: traceBegin(); if (!is_object(self::$_PHPCAS_CLIENT)) { phpCAS :: error('this method should only be called after ' . __CLASS__ . '::client() or' . __CLASS__ . '::proxy()'); } if (!is_string($url)) { phpCAS :: error('type mismatched for parameter $url (should be `string\')'); } self::$_PHPCAS_CLIENT->logout(array ( "url" => $url )); // never reached phpCAS :: traceEnd(); } /** * This method is used to logout from CAS. Halts by redirecting to the CAS * server. * * @param string $service a URL that will be transmitted to the CAS server * @param string $url a URL that will be transmitted to the CAS server * * @return void * * @deprecated The url parameter has been removed from the CAS server as of * version 3.3.5.1 */ public static function logoutWithRedirectServiceAndUrl($service, $url) { trigger_error('Function deprecated for cas servers >= 3.3.5.1', E_USER_DEPRECATED); phpCAS :: traceBegin(); phpCAS::_validateClientExists(); if (!is_string($service)) { phpCAS :: error('type mismatched for parameter $service (should be `string\')'); } if (!is_string($url)) { phpCAS :: error('type mismatched for parameter $url (should be `string\')'); } self::$_PHPCAS_CLIENT->logout( array ( "service" => $service, "url" => $url ) ); // never reached phpCAS :: traceEnd(); } /** * Set the fixed URL that will be used by the CAS server to transmit the * PGT. When this method is not called, a phpCAS script uses its own URL * for the callback. * * @param string $url the URL * * @return void */ public static function setFixedCallbackURL($url = '') { phpCAS :: traceBegin(); phpCAS::_validateProxyExists(); try { self::$_PHPCAS_CLIENT->setCallbackURL($url); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * Set the fixed URL that will be set as the CAS service parameter. When this * method is not called, a phpCAS script uses its own URL. * * @param string $url the URL * * @return void */ public static function setFixedServiceURL($url) { phpCAS :: traceBegin(); phpCAS::_validateProxyExists(); try { self::$_PHPCAS_CLIENT->setURL($url); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * Get the URL that is set as the CAS service parameter. * * @return string Service Url */ public static function getServiceURL() { phpCAS::_validateProxyExists(); return (self::$_PHPCAS_CLIENT->getURL()); } /** * Retrieve a Proxy Ticket from the CAS server. * * @param string $target_service Url string of service to proxy * @param int &$err_code error code * @param string &$err_msg error message * * @return string Proxy Ticket */ public static function retrievePT($target_service, & $err_code, & $err_msg) { phpCAS::_validateProxyExists(); try { return (self::$_PHPCAS_CLIENT->retrievePT($target_service, $err_code, $err_msg)); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } } /** * Set the certificate of the CAS server CA and if the CN should be properly * verified. * * @param string $cert CA certificate file name * @param bool $validate_cn Validate CN in certificate (default true) * * @return void */ public static function setCasServerCACert($cert, $validate_cn = true) { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->setCasServerCACert($cert, $validate_cn); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * Set no SSL validation for the CAS server. * * @return void */ public static function setNoCasServerValidation() { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); phpCAS :: trace('You have configured no validation of the legitimacy of the cas server. This is not recommended for production use.'); self::$_PHPCAS_CLIENT->setNoCasServerValidation(); phpCAS :: traceEnd(); } /** * Disable the removal of a CAS-Ticket from the URL when authenticating * DISABLING POSES A SECURITY RISK: * We normally remove the ticket by an additional redirect as a security * precaution to prevent a ticket in the HTTP_REFERRER or be carried over in * the URL parameter * * @return void */ public static function setNoClearTicketsFromUrl() { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); self::$_PHPCAS_CLIENT->setNoClearTicketsFromUrl(); phpCAS :: traceEnd(); } /** @} */ /** * Change CURL options. * CURL is used to connect through HTTPS to CAS server * * @param string $key the option key * @param string $value the value to set * * @return void */ public static function setExtraCurlOption($key, $value) { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); self::$_PHPCAS_CLIENT->setExtraCurlOption($key, $value); phpCAS :: traceEnd(); } /** * Set a salt/seed for the session-id hash to make it harder to guess. * * When $changeSessionID = true phpCAS will create a session-id that is derived * from the service ticket. Doing so allows phpCAS to look-up and destroy the * proper session on single-log-out requests. While the service tickets * provided by the CAS server may include enough data to generate a strong * hash, clients may provide an additional salt to ensure that session ids * are not guessable if the session tickets do not have enough entropy. * * @param string $salt The salt to combine with the session ticket. * * @return void */ public static function setSessionIdSalt($salt) { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); self::$_PHPCAS_CLIENT->setSessionIdSalt($salt); phpCAS :: traceEnd(); } /** * If you want your service to be proxied you have to enable it (default * disabled) and define an accepable list of proxies that are allowed to * proxy your service. * * Add each allowed proxy definition object. For the normal CAS_ProxyChain * class, the constructor takes an array of proxies to match. The list is in * reverse just as seen from the service. Proxies have to be defined in reverse * from the service to the user. If a user hits service A and gets proxied via * B to service C the list of acceptable on C would be array(B,A). The definition * of an individual proxy can be either a string or a regexp (preg_match is used) * that will be matched against the proxy list supplied by the cas server * when validating the proxy tickets. The strings are compared starting from * the beginning and must fully match with the proxies in the list. * Example: * phpCAS::allowProxyChain(new CAS_ProxyChain(array( * 'https://app.example.com/' * ))); * phpCAS::allowProxyChain(new CAS_ProxyChain(array( * '/^https:\/\/app[0-9]\.example\.com\/rest\//', * 'http://client.example.com/' * ))); * * For quick testing or in certain production screnarios you might want to * allow allow any other valid service to proxy your service. To do so, add * the "Any" chain: * phpCAS::allowProxyChain(new CAS_ProxyChain_Any); * THIS SETTING IS HOWEVER NOT RECOMMENDED FOR PRODUCTION AND HAS SECURITY * IMPLICATIONS: YOU ARE ALLOWING ANY SERVICE TO ACT ON BEHALF OF A USER * ON THIS SERVICE. * * @param CAS_ProxyChain_Interface $proxy_chain A proxy-chain that will be * matched against the proxies requesting access * * @return void */ public static function allowProxyChain(CAS_ProxyChain_Interface $proxy_chain) { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); if (self::$_PHPCAS_CLIENT->getServerVersion() !== CAS_VERSION_2_0 && self::$_PHPCAS_CLIENT->getServerVersion() !== CAS_VERSION_3_0 ) { phpCAS :: error('this method can only be used with the cas 2.0/3.0 protocols'); } self::$_PHPCAS_CLIENT->getAllowedProxyChains()->allowProxyChain($proxy_chain); phpCAS :: traceEnd(); } /** * Answer an array of proxies that are sitting in front of this application. * This method will only return a non-empty array if we have received and * validated a Proxy Ticket. * * @return array * @access public * @since 6/25/09 */ public static function getProxies () { phpCAS::_validateProxyExists(); return(self::$_PHPCAS_CLIENT->getProxies()); } // ######################################################################## // PGTIOU/PGTID and logoutRequest rebroadcasting // ######################################################################## /** * Add a pgtIou/pgtId and logoutRequest rebroadcast node. * * @param string $rebroadcastNodeUrl The rebroadcast node URL. Can be * hostname or IP. * * @return void */ public static function addRebroadcastNode($rebroadcastNodeUrl) { phpCAS::traceBegin(); phpCAS::log('rebroadcastNodeUrl:'.$rebroadcastNodeUrl); phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->addRebroadcastNode($rebroadcastNodeUrl); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS::traceEnd(); } /** * This method is used to add header parameters when rebroadcasting * pgtIou/pgtId or logoutRequest. * * @param String $header Header to send when rebroadcasting. * * @return void */ public static function addRebroadcastHeader($header) { phpCAS :: traceBegin(); phpCAS::_validateClientExists(); try { self::$_PHPCAS_CLIENT->addRebroadcastHeader($header); } catch (Exception $e) { phpCAS :: error(get_class($e) . ': ' . $e->getMessage()); } phpCAS :: traceEnd(); } /** * Checks if a client already exists * * @throws CAS_OutOfSequenceBeforeClientException * * @return void */ private static function _validateClientExists() { if (!is_object(self::$_PHPCAS_CLIENT)) { throw new CAS_OutOfSequenceBeforeClientException(); } } /** * Checks of a proxy client aready exists * * @throws CAS_OutOfSequenceBeforeProxyException * * @return void */ private static function _validateProxyExists() { if (!is_object(self::$_PHPCAS_CLIENT)) { throw new CAS_OutOfSequenceBeforeProxyException(); } } /** * @return CAS_Client */ public static function getCasClient() { return self::$_PHPCAS_CLIENT; } /** * For testing purposes, use this method to set the client to a test double * * @return void */ public static function setCasClient(\CAS_Client $client) { self::$_PHPCAS_CLIENT = $client; } } // ######################################################################## // DOCUMENTATION // ######################################################################## // ######################################################################## // MAIN PAGE /** * @mainpage * * The following pages only show the source documentation. * */ // ######################################################################## // MODULES DEFINITION /** @defgroup public User interface */ /** @defgroup publicInit Initialization * @ingroup public */ /** @defgroup publicAuth Authentication * @ingroup public */ /** @defgroup publicServices Access to external services * @ingroup public */ /** @defgroup publicConfig Configuration * @ingroup public */ /** @defgroup publicLang Internationalization * @ingroup publicConfig */ /** @defgroup publicOutput HTML output * @ingroup publicConfig */ /** @defgroup publicPGTStorage PGT storage * @ingroup publicConfig */ /** @defgroup publicDebug Debugging * @ingroup public */ /** @defgroup internal Implementation */ /** @defgroup internalAuthentication Authentication * @ingroup internal */ /** @defgroup internalBasic CAS Basic client features (CAS 1.0, Service Tickets) * @ingroup internal */ /** @defgroup internalProxy CAS Proxy features (CAS 2.0, Proxy Granting Tickets) * @ingroup internal */ /** @defgroup internalSAML CAS SAML features (SAML 1.1) * @ingroup internal */ /** @defgroup internalPGTStorage PGT storage * @ingroup internalProxy */ /** @defgroup internalPGTStorageDb PGT storage in a database * @ingroup internalPGTStorage */ /** @defgroup internalPGTStorageFile PGT storage on the filesystem * @ingroup internalPGTStorage */ /** @defgroup internalCallback Callback from the CAS server * @ingroup internalProxy */ /** @defgroup internalProxyServices Proxy other services * @ingroup internalProxy */ /** @defgroup internalService CAS client features (CAS 2.0, Proxied service) * @ingroup internal */ /** @defgroup internalConfig Configuration * @ingroup internal */ /** @defgroup internalBehave Internal behaviour of phpCAS * @ingroup internalConfig */ /** @defgroup internalOutput HTML output * @ingroup internalConfig */ /** @defgroup internalLang Internationalization * @ingroup internalConfig * * To add a new language: * - 1. define a new constant PHPCAS_LANG_XXXXXX in CAS/CAS.php * - 2. copy any file from CAS/languages to CAS/languages/XXXXXX.php * - 3. Make the translations */ /** @defgroup internalDebug Debugging * @ingroup internal */ /** @defgroup internalMisc Miscellaneous * @ingroup internal */ // ######################################################################## // EXAMPLES /** * @example example_simple.php */ /** * @example example_service.php */ /** * @example example_service_that_proxies.php */ /** * @example example_service_POST.php */ /** * @example example_proxy_serviceWeb.php */ /** * @example example_proxy_serviceWeb_chaining.php */ /** * @example example_proxy_POST.php */ /** * @example example_proxy_GET.php */ /** * @example example_lang.php */ /** * @example example_html.php */ /** * @example example_pgt_storage_file.php */ /** * @example example_pgt_storage_db.php */ /** * @example example_gateway.php */ /** * @example example_logout.php */ /** * @example example_rebroadcast.php */ /** * @example example_custom_urls.php */ /** * @example example_advanced_saml11.php */ cas/CAS/vendor/apereo/phpcas/source/CAS/ProxyChain/Interface.php 0000644 00000003235 15152311434 0020353 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxyChain/Interface.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * An interface for classes that define a list of allowed proxies in front of * the current application. * * @class CAS_ProxyChain_Interface * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ interface CAS_ProxyChain_Interface { /** * Match a list of proxies. * * @param array $list The list of proxies in front of this service. * * @return bool */ public function matches(array $list); } cas/CAS/vendor/apereo/phpcas/source/CAS/ProxyChain/Any.php 0000644 00000004074 15152311434 0017204 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxyChain/Any.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * A proxy-chain definition that will match any list of proxies. * * Use this class for quick testing or in certain production screnarios you * might want to allow allow any other valid service to proxy your service. * * THIS CLASS IS HOWEVER NOT RECOMMENDED FOR PRODUCTION AND HAS SECURITY * IMPLICATIONS: YOU ARE ALLOWING ANY SERVICE TO ACT ON BEHALF OF A USER * ON THIS SERVICE. * * @class CAS_ProxyChain_Any * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ProxyChain_Any implements CAS_ProxyChain_Interface { /** * Match a list of proxies. * * @param array $list The list of proxies in front of this service. * * @return bool */ public function matches(array $list) { phpCAS::trace("Using CAS_ProxyChain_Any. No proxy validation is performed."); return true; } } cas/CAS/vendor/apereo/phpcas/source/CAS/ProxyChain/Trusted.php 0000644 00000003534 15152311434 0020107 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxyChain/Trusted.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * A proxy-chain definition that defines a chain up to a trusted proxy and * delegates the resposibility of validating the rest of the chain to that * trusted proxy. * * @class CAS_ProxyChain_Trusted * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ProxyChain_Trusted extends CAS_ProxyChain implements CAS_ProxyChain_Interface { /** * Validate the size of the the list as compared to our chain. * * @param array $list list of proxies * * @return bool */ protected function isSizeValid (array $list) { return (sizeof($this->chain) <= sizeof($list)); } } cas/CAS/vendor/apereo/phpcas/source/CAS/ProxyChain/AllowedList.php 0000644 00000006743 15152311434 0020705 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxyChain/AllowedList.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * ProxyChain is a container for storing chains of valid proxies that can * be used to validate proxied requests to a service * * @class CAS_ProxyChain_AllowedList * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ProxyChain_AllowedList { private $_chains = array(); /** * Check whether proxies are allowed by configuration * * @return bool */ public function isProxyingAllowed() { return (count($this->_chains) > 0); } /** * Add a chain of proxies to the list of possible chains * * @param CAS_ProxyChain_Interface $chain A chain of proxies * * @return void */ public function allowProxyChain(CAS_ProxyChain_Interface $chain) { $this->_chains[] = $chain; } /** * Check if the proxies found in the response match the allowed proxies * * @param array $proxies list of proxies to check * * @return bool whether the proxies match the allowed proxies */ public function isProxyListAllowed(array $proxies) { phpCAS::traceBegin(); if (empty($proxies)) { phpCAS::trace("No proxies were found in the response"); phpCAS::traceEnd(true); return true; } elseif (!$this->isProxyingAllowed()) { phpCAS::trace("Proxies are not allowed"); phpCAS::traceEnd(false); return false; } else { $res = $this->contains($proxies); phpCAS::traceEnd($res); return $res; } } /** * Validate the proxies from the proxy ticket validation against the * chains that were definded. * * @param array $list List of proxies from the proxy ticket validation. * * @return bool if any chain fully matches the supplied list */ public function contains(array $list) { phpCAS::traceBegin(); $count = 0; foreach ($this->_chains as $chain) { phpCAS::trace("Checking chain ". $count++); if ($chain->matches($list)) { phpCAS::traceEnd(true); return true; } } phpCAS::trace("No proxy chain matches."); phpCAS::traceEnd(false); return false; } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/AuthenticationException.php 0000644 00000010521 15152311434 0021221 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/AuthenticationException.php * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This interface defines methods that allow proxy-authenticated service handlers * to interact with phpCAS. * * Proxy service handlers must implement this interface as well as call * phpCAS::initializeProxiedService($this) at some point in their implementation. * * While not required, proxy-authenticated service handlers are encouraged to * implement the CAS_ProxiedService_Testable interface to facilitate unit testing. * * @class CAS_AuthenticationException * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_AuthenticationException extends RuntimeException implements CAS_Exception { /** * This method is used to print the HTML output when the user was not * authenticated. * * @param CAS_Client $client phpcas client * @param string $failure the failure that occured * @param string $cas_url the URL the CAS server was asked for * @param bool $no_response the response from the CAS server (other * parameters are ignored if TRUE) * @param bool $bad_response bad response from the CAS server ($err_code * and $err_msg ignored if TRUE) * @param string $cas_response the response of the CAS server * @param int $err_code the error code given by the CAS server * @param string $err_msg the error message given by the CAS server */ public function __construct($client,$failure,$cas_url,$no_response, $bad_response=false,$cas_response='',$err_code=-1,$err_msg='' ) { $messages = array(); phpCAS::traceBegin(); $lang = $client->getLangObj(); $client->printHTMLHeader($lang->getAuthenticationFailed()); if (phpCAS::getVerbose()) { printf( $lang->getYouWereNotAuthenticated(), htmlentities($client->getURL()), $_SERVER['SERVER_ADMIN'] ?? '' ); } phpCAS::trace($messages[] = 'CAS URL: '.$cas_url); phpCAS::trace($messages[] = 'Authentication failure: '.$failure); if ( $no_response ) { phpCAS::trace($messages[] = 'Reason: no response from the CAS server'); } else { if ( $bad_response ) { phpCAS::trace($messages[] = 'Reason: bad response from the CAS server'); } else { switch ($client->getServerVersion()) { case CAS_VERSION_1_0: phpCAS::trace($messages[] = 'Reason: CAS error'); break; case CAS_VERSION_2_0: case CAS_VERSION_3_0: if ( $err_code === -1 ) { phpCAS::trace($messages[] = 'Reason: no CAS error'); } else { phpCAS::trace($messages[] = 'Reason: ['.$err_code.'] CAS error: '.$err_msg); } break; } } phpCAS::trace($messages[] = 'CAS response: '.$cas_response); } $client->printHTMLFooter(); phpCAS::traceExit(); parent::__construct(implode("\n", $messages)); } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/OutOfSequenceBeforeClientException.php 0000644 00000003675 15152311434 0023265 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * * PHP Version 7 * * @file CAS/OutOfSequenceBeforeClientException.php * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class defines Exceptions that should be thrown when the sequence of * operations is invalid. In this case it should be thrown when the client() or * proxy() call has not yet happened and no client or proxy object exists. * * @class CAS_OutOfSequenceBeforeClientException * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_OutOfSequenceBeforeClientException extends CAS_OutOfSequenceException implements CAS_Exception { /** * Return standard error message * * @return void */ public function __construct () { parent::__construct( 'this method cannot be called before phpCAS::client() or phpCAS::proxy()' ); } } cas/CAS/vendor/apereo/phpcas/source/CAS/Exception.php 0000644 00000003677 15152311434 0016337 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Exception.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * A root exception interface for all exceptions in phpCAS. * * All exceptions thrown in phpCAS should implement this interface to allow them * to be caught as a category by clients. Each phpCAS exception should extend * an appropriate SPL exception class that best fits its type. * * For example, an InvalidArgumentException in phpCAS should be defined as * * class CAS_InvalidArgumentException * extends InvalidArgumentException * implements CAS_Exception * { } * * This definition allows the CAS_InvalidArgumentException to be caught as either * an InvalidArgumentException or as a CAS_Exception. * * @class CAS_Exception * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * */ interface CAS_Exception { } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/Autoload.php 0000644 00000005652 15152311434 0016144 0 ustar 00 <?php /** * Autoloader Class * * PHP Version 7 * * @file CAS/Autoload.php * @category Authentication * @package SimpleCAS * @author Brett Bieber <brett.bieber@gmail.com> * @copyright 2008 Regents of the University of Nebraska * @license http://www1.unl.edu/wdn/wiki/Software_License BSD License * @link http://code.google.com/p/simplecas/ **/ /** * Autoload a class * * @param string $class Classname to load * * @return bool */ function CAS_autoload($class) { // Static to hold the Include Path to CAS static $include_path; // Check only for CAS classes if (substr($class, 0, 4) !== 'CAS_' && substr($class, 0, 7) !== 'PhpCas\\') { return false; } // Setup the include path if it's not already set from a previous call if (empty($include_path)) { $include_path = array(dirname(__DIR__)); } // Declare local variable to store the expected full path to the file foreach ($include_path as $path) { $class_path = str_replace('_', DIRECTORY_SEPARATOR, $class); // PhpCas namespace mapping if (substr($class_path, 0, 7) === 'PhpCas\\') { $class_path = 'CAS' . DIRECTORY_SEPARATOR . substr($class_path, 7); } $file_path = $path . DIRECTORY_SEPARATOR . $class_path . '.php'; $fp = @fopen($file_path, 'r', true); if ($fp) { fclose($fp); include $file_path; if (!class_exists($class, false) && !interface_exists($class, false)) { die( new Exception( 'Class ' . $class . ' was not present in ' . $file_path . ' [CAS_autoload]' ) ); } return true; } } $e = new Exception( 'Class ' . $class . ' could not be loaded from ' . $file_path . ', file does not exist (Path="' . implode(':', $include_path) .'") [CAS_autoload]' ); $trace = $e->getTrace(); if (isset($trace[2]) && isset($trace[2]['function']) && in_array($trace[2]['function'], array('class_exists', 'interface_exists', 'trait_exists')) ) { return false; } if (isset($trace[1]) && isset($trace[1]['function']) && in_array($trace[1]['function'], array('class_exists', 'interface_exists', 'trait_exists')) ) { return false; } die ((string) $e); } // Set up autoload if not already configured by composer. if (!class_exists('CAS_Client')) { trigger_error('phpCAS autoloader is deprecated. Install phpCAS using composer instead.', E_USER_DEPRECATED); spl_autoload_register('CAS_autoload'); if (function_exists('__autoload') && !in_array('__autoload', spl_autoload_functions()) ) { // __autoload() was being used, but now would be ignored, add // it to the autoload stack spl_autoload_register('__autoload'); } } cas/CAS/vendor/apereo/phpcas/source/CAS/ProxyTicketException.php 0000644 00000004411 15152311434 0020530 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @class CAS/ProxyTicketException.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * */ /** * An Exception for errors related to fetching or validating proxy tickets. * * @class CAS_ProxyTicketException * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ProxyTicketException extends BadMethodCallException implements CAS_Exception { /** * Constructor * * @param string $message Message text * @param int $code Error code * * @return void */ public function __construct ($message, $code = PHPCAS_SERVICE_PT_FAILURE) { // Warn if the code is not in our allowed list $ptCodes = array( PHPCAS_SERVICE_PT_FAILURE, PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, ); if (!in_array($code, $ptCodes)) { trigger_error( 'Invalid code '.$code .' passed. Must be one of PHPCAS_SERVICE_PT_FAILURE, PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, or PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE.' ); } parent::__construct($message, $code); } } cas/CAS/vendor/apereo/phpcas/source/CAS/OutOfSequenceBeforeProxyException.php 0000644 00000003620 15152311434 0023156 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * * PHP Version 7 * * @file CAS/OutOfSequenceBeforeProxyException.php * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class defines Exceptions that should be thrown when the sequence of * operations is invalid. In this case it should be thrown when the proxy() call * has not yet happened and no proxy object exists. * * @class CAS_OutOfSequenceBeforeProxyException * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_OutOfSequenceBeforeProxyException extends CAS_OutOfSequenceException implements CAS_Exception { /** * Return standard error message * * @return void */ public function __construct () { parent::__construct( 'this method cannot be called before phpCAS::proxy()' ); } } cas/CAS/vendor/apereo/phpcas/source/CAS/Session/PhpSession.php 0000644 00000003046 15152311434 0020105 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * * PHP Version 7 * * @file CAS/Session/PhpSession.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Empty class used as a default implementation for phpCAS. * * Implements the standard PHP session handler without no alterations. * * @class CAS_Session_PhpSession * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_Session_PhpSession extends SessionHandler implements SessionHandlerInterface { } cas/CAS/vendor/apereo/phpcas/source/CAS/ServiceBaseUrl/Interface.php 0000644 00000003636 15152311435 0021153 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ServerHostname/Interface.php * @category Authentication * @package PhpCAS * @author Henry Pan <git@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * An interface for classes that gets the server name of the PHP server. * This is used to generate service URL and PGT callback URL. * * @class CAS_ServiceBaseUrl_Interface * @category Authentication * @package PhpCAS * @author Henry Pan <git@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ interface CAS_ServiceBaseUrl_Interface { /** * Get PHP HTTP protocol and server name. * * @return string protocol, server hostname, and optionally port, * without trailing slash (https://localhost:8443) */ public function get(); /** * Check whether HTTPS is used. * * This is used to construct the protocol in the URL. * * @return bool true if HTTPS is used */ public function isHttps(); } cas/CAS/vendor/apereo/phpcas/source/CAS/ServiceBaseUrl/Static.php 0000644 00000004177 15152311435 0020503 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ServiceBaseUrl/Static.php * @category Authentication * @package PhpCAS * @author Henry Pan <git@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Class that gets the server name of the PHP server by statically set * hostname and port. This is used to generate service URL and PGT * callback URL. * * @class CAS_ServiceBaseUrl_Static * @category Authentication * @package PhpCAS * @author Henry Pan <git@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ServiceBaseUrl_Static extends CAS_ServiceBaseUrl_Base { private $_name = null; public function __construct($name) { if (is_string($name)) { $this->_name = $this->removeStandardPort($name); } else { throw new CAS_TypeMismatchException($name, '$name', 'string'); } } /** * Get the server name through static config. * * @return string the server hostname and port of the server configured */ public function get() { phpCAS::traceBegin(); phpCAS::trace("Returning static server name: " . $this->_name); phpCAS::traceEnd(true); return $this->_name; } } cas/CAS/vendor/apereo/phpcas/source/CAS/ServiceBaseUrl/AllowedListDiscovery.php 0000644 00000011673 15152311435 0023366 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ServiceBaseUrl/AllowedListDiscovery.php * @category Authentication * @package PhpCAS * @author Henry Pan <git@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Class that gets the service base URL of the PHP server by HTTP header * discovery and allowlist check. This is used to generate service URL * and PGT callback URL. * * @class CAS_ServiceBaseUrl_AllowedListDiscovery * @category Authentication * @package PhpCAS * @author Henry Pan <git@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ServiceBaseUrl_AllowedListDiscovery extends CAS_ServiceBaseUrl_Base { private $_list = array(); public function __construct($list) { if (is_array($list)) { if (count($list) === 0) { throw new CAS_InvalidArgumentException('$list should not be empty'); } foreach ($list as $value) { $this->allow($value); } } else { throw new CAS_TypeMismatchException($list, '$list', 'array'); } } /** * Add a base URL to the allowed list. * * @param $url protocol, host name and port to add to the allowed list * * @return void */ public function allow($url) { $this->_list[] = $this->removeStandardPort($url); } /** * Check if the server name is allowed by configuration. * * @param $name server name to check * * @return bool whether the allowed list contains the server name */ protected function isAllowed($name) { return in_array($name, $this->_list); } /** * Discover the server name through HTTP headers. * * We read: * - HTTP header X-Forwarded-Host * - HTTP header X-Forwarded-Server and X-Forwarded-Port * - HTTP header Host and SERVER_PORT * - PHP SERVER_NAME (which can change based on the HTTP server used) * * The standard port will be omitted (80 for HTTP, 443 for HTTPS). * * @return string the discovered, unsanitized server protocol, hostname and port */ protected function discover() { $isHttps = $this->isHttps(); $protocol = $isHttps ? 'https' : 'http'; $protocol .= '://'; if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) { // explode the host list separated by comma and use the first host $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']); // see rfc7239#5.3 and rfc7230#2.7.1: port is in HTTP_X_FORWARDED_HOST if non default return $protocol . $hosts[0]; } else if (!empty($_SERVER['HTTP_X_FORWARDED_SERVER'])) { $server_url = $_SERVER['HTTP_X_FORWARDED_SERVER']; } else { if (empty($_SERVER['SERVER_NAME'])) { $server_url = $_SERVER['HTTP_HOST']; } else { $server_url = $_SERVER['SERVER_NAME']; } } if (!strpos($server_url, ':')) { if (empty($_SERVER['HTTP_X_FORWARDED_PORT'])) { $server_port = $_SERVER['SERVER_PORT']; } else { $ports = explode(',', $_SERVER['HTTP_X_FORWARDED_PORT']); $server_port = $ports[0]; } $server_url .= ':'; $server_url .= $server_port; } return $protocol . $server_url; } /** * Get PHP server base URL. * * @return string the server protocol, hostname and port */ public function get() { phpCAS::traceBegin(); $result = $this->removeStandardPort($this->discover()); phpCAS::trace("Discovered server base URL: " . $result); if ($this->isAllowed($result)) { phpCAS::trace("Server base URL is allowed"); phpCAS::traceEnd(true); } else { $result = $this->_list[0]; phpCAS::trace("Server base URL is not allowed, using default: " . $result); phpCAS::traceEnd(false); } return $result; } } cas/CAS/vendor/apereo/phpcas/source/CAS/ServiceBaseUrl/Base.php 0000644 00000006202 15152311435 0020115 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ServiceBaseUrl/Base.php * @category Authentication * @package PhpCAS * @author Henry Pan <git@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Base class of CAS/ServiceBaseUrl that implements isHTTPS method. * * @class CAS_ServiceBaseUrl_Base * @category Authentication * @package PhpCAS * @author Henry Pan <git@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ abstract class CAS_ServiceBaseUrl_Base implements CAS_ServiceBaseUrl_Interface { /** * Get PHP server name. * * @return string the server hostname and port of the server */ abstract public function get(); /** * Check whether HTTPS is used. * * This is used to construct the protocol in the URL. * * @return bool true if HTTPS is used */ public function isHttps() { if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) { return ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'); } elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) { return ($_SERVER['HTTP_X_FORWARDED_PROTOCOL'] === 'https'); } elseif ( isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') !== 0 ) { return true; } return false; } /** * Remove standard HTTP and HTTPS port for discovery and allowlist input. * * @param $url URL as https://domain:port without trailing slash * @return standardized URL, or the original URL * @throws CAS_InvalidArgumentException if the URL does not include the protocol */ protected function removeStandardPort($url) { if (strpos($url, "://") === false) { throw new CAS_InvalidArgumentException( "Configured base URL should include the protocol string: " . $url); } $url = rtrim($url, '/'); if (strpos($url, "https://") === 0 && substr_compare($url, ':443', -4) === 0) { return substr($url, 0, -4); } if (strpos($url, "http://") === 0 && substr_compare($url, ':80', -3) === 0) { return substr($url, 0, -3); } return $url; } } cas/CAS/vendor/apereo/phpcas/source/CAS/Client.php 0000644 00000456331 15152311435 0015617 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Client.php * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @author Olivier Berger <olivier.berger@it-sudparis.eu> * @author Brett Bieber <brett.bieber@gmail.com> * @author Joachim Fritschi <jfritschi@freenet.de> * @author Adam Franco <afranco@middlebury.edu> * @author Tobias Schiebeck <tobias.schiebeck@manchester.ac.uk> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * The CAS_Client class is a client interface that provides CAS authentication * to PHP applications. * * @class CAS_Client * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @author Olivier Berger <olivier.berger@it-sudparis.eu> * @author Brett Bieber <brett.bieber@gmail.com> * @author Joachim Fritschi <jfritschi@freenet.de> * @author Adam Franco <afranco@middlebury.edu> * @author Tobias Schiebeck <tobias.schiebeck@manchester.ac.uk> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * */ class CAS_Client { // ######################################################################## // HTML OUTPUT // ######################################################################## /** * @addtogroup internalOutput * @{ */ /** * This method filters a string by replacing special tokens by appropriate values * and prints it. The corresponding tokens are taken into account: * - __CAS_VERSION__ * - __PHPCAS_VERSION__ * - __SERVER_BASE_URL__ * * Used by CAS_Client::PrintHTMLHeader() and CAS_Client::printHTMLFooter(). * * @param string $str the string to filter and output * * @return void */ private function _htmlFilterOutput($str) { $str = str_replace('__CAS_VERSION__', $this->getServerVersion(), $str); $str = str_replace('__PHPCAS_VERSION__', phpCAS::getVersion(), $str); $str = str_replace('__SERVER_BASE_URL__', $this->_getServerBaseURL(), $str); echo $str; } /** * A string used to print the header of HTML pages. Written by * CAS_Client::setHTMLHeader(), read by CAS_Client::printHTMLHeader(). * * @hideinitializer * @see CAS_Client::setHTMLHeader, CAS_Client::printHTMLHeader() */ private $_output_header = ''; /** * This method prints the header of the HTML output (after filtering). If * CAS_Client::setHTMLHeader() was not used, a default header is output. * * @param string $title the title of the page * * @return void * @see _htmlFilterOutput() */ public function printHTMLHeader($title) { if (!phpCAS::getVerbose()) { return; } $this->_htmlFilterOutput( str_replace( '__TITLE__', $title, (empty($this->_output_header) ? '<html><head><title>__TITLE__</title></head><body><h1>__TITLE__</h1>' : $this->_output_header) ) ); } /** * A string used to print the footer of HTML pages. Written by * CAS_Client::setHTMLFooter(), read by printHTMLFooter(). * * @hideinitializer * @see CAS_Client::setHTMLFooter, CAS_Client::printHTMLFooter() */ private $_output_footer = ''; /** * This method prints the footer of the HTML output (after filtering). If * CAS_Client::setHTMLFooter() was not used, a default footer is output. * * @return void * @see _htmlFilterOutput() */ public function printHTMLFooter() { if (!phpCAS::getVerbose()) { return; } $lang = $this->getLangObj(); $message = empty($this->_output_footer) ? '<hr><address>phpCAS __PHPCAS_VERSION__ ' . $lang->getUsingServer() . ' <a href="__SERVER_BASE_URL__">__SERVER_BASE_URL__</a> (CAS __CAS_VERSION__)</a></address></body></html>' : $this->_output_footer; $this->_htmlFilterOutput($message); } /** * This method set the HTML header used for all outputs. * * @param string $header the HTML header. * * @return void */ public function setHTMLHeader($header) { // Argument Validation if (gettype($header) != 'string') throw new CAS_TypeMismatchException($header, '$header', 'string'); $this->_output_header = $header; } /** * This method set the HTML footer used for all outputs. * * @param string $footer the HTML footer. * * @return void */ public function setHTMLFooter($footer) { // Argument Validation if (gettype($footer) != 'string') throw new CAS_TypeMismatchException($footer, '$footer', 'string'); $this->_output_footer = $footer; } /** * Simple wrapper for printf function, that respects * phpCAS verbosity setting. * * @param string $format * @param string|int|float ...$values * * @see printf() */ private function printf(string $format, ...$values): void { if (phpCAS::getVerbose()) { printf($format, ...$values); } } /** @} */ // ######################################################################## // INTERNATIONALIZATION // ######################################################################## /** * @addtogroup internalLang * @{ */ /** * A string corresponding to the language used by phpCAS. Written by * CAS_Client::setLang(), read by CAS_Client::getLang(). * @note debugging information is always in english (debug purposes only). */ private $_lang = PHPCAS_LANG_DEFAULT; /** * This method is used to set the language used by phpCAS. * * @param string $lang representing the language. * * @return void */ public function setLang($lang) { // Argument Validation if (gettype($lang) != 'string') throw new CAS_TypeMismatchException($lang, '$lang', 'string'); phpCAS::traceBegin(); $obj = new $lang(); if (!($obj instanceof CAS_Languages_LanguageInterface)) { throw new CAS_InvalidArgumentException( '$className must implement the CAS_Languages_LanguageInterface' ); } $this->_lang = $lang; phpCAS::traceEnd(); } /** * Create the language * * @return CAS_Languages_LanguageInterface object implementing the class */ public function getLangObj() { $classname = $this->_lang; return new $classname(); } /** @} */ // ######################################################################## // CAS SERVER CONFIG // ######################################################################## /** * @addtogroup internalConfig * @{ */ /** * a record to store information about the CAS server. * - $_server['version']: the version of the CAS server * - $_server['hostname']: the hostname of the CAS server * - $_server['port']: the port the CAS server is running on * - $_server['uri']: the base URI the CAS server is responding on * - $_server['base_url']: the base URL of the CAS server * - $_server['login_url']: the login URL of the CAS server * - $_server['service_validate_url']: the service validating URL of the * CAS server * - $_server['proxy_url']: the proxy URL of the CAS server * - $_server['proxy_validate_url']: the proxy validating URL of the CAS server * - $_server['logout_url']: the logout URL of the CAS server * * $_server['version'], $_server['hostname'], $_server['port'] and * $_server['uri'] are written by CAS_Client::CAS_Client(), read by * CAS_Client::getServerVersion(), CAS_Client::_getServerHostname(), * CAS_Client::_getServerPort() and CAS_Client::_getServerURI(). * * The other fields are written and read by CAS_Client::_getServerBaseURL(), * CAS_Client::getServerLoginURL(), CAS_Client::getServerServiceValidateURL(), * CAS_Client::getServerProxyValidateURL() and CAS_Client::getServerLogoutURL(). * * @hideinitializer */ private $_server = array( 'version' => '', 'hostname' => 'none', 'port' => -1, 'uri' => 'none'); /** * This method is used to retrieve the version of the CAS server. * * @return string the version of the CAS server. */ public function getServerVersion() { return $this->_server['version']; } /** * This method is used to retrieve the hostname of the CAS server. * * @return string the hostname of the CAS server. */ private function _getServerHostname() { return $this->_server['hostname']; } /** * This method is used to retrieve the port of the CAS server. * * @return int the port of the CAS server. */ private function _getServerPort() { return $this->_server['port']; } /** * This method is used to retrieve the URI of the CAS server. * * @return string a URI. */ private function _getServerURI() { return $this->_server['uri']; } /** * This method is used to retrieve the base URL of the CAS server. * * @return string a URL. */ private function _getServerBaseURL() { // the URL is build only when needed if ( empty($this->_server['base_url']) ) { $this->_server['base_url'] = 'https://' . $this->_getServerHostname(); if ($this->_getServerPort()!=443) { $this->_server['base_url'] .= ':' .$this->_getServerPort(); } $this->_server['base_url'] .= $this->_getServerURI(); } return $this->_server['base_url']; } /** * This method is used to retrieve the login URL of the CAS server. * * @param bool $gateway true to check authentication, false to force it * @param bool $renew true to force the authentication with the CAS server * * @return string a URL. * @note It is recommended that CAS implementations ignore the "gateway" * parameter if "renew" is set */ public function getServerLoginURL($gateway=false,$renew=false) { phpCAS::traceBegin(); // the URL is build only when needed if ( empty($this->_server['login_url']) ) { $this->_server['login_url'] = $this->_buildQueryUrl($this->_getServerBaseURL().'login','service='.urlencode($this->getURL())); } $url = $this->_server['login_url']; if ($renew) { // It is recommended that when the "renew" parameter is set, its // value be "true" $url = $this->_buildQueryUrl($url, 'renew=true'); } elseif ($gateway) { // It is recommended that when the "gateway" parameter is set, its // value be "true" $url = $this->_buildQueryUrl($url, 'gateway=true'); } phpCAS::traceEnd($url); return $url; } /** * This method sets the login URL of the CAS server. * * @param string $url the login URL * * @return string login url */ public function setServerLoginURL($url) { // Argument Validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); return $this->_server['login_url'] = $url; } /** * This method sets the serviceValidate URL of the CAS server. * * @param string $url the serviceValidate URL * * @return string serviceValidate URL */ public function setServerServiceValidateURL($url) { // Argument Validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); return $this->_server['service_validate_url'] = $url; } /** * This method sets the proxyValidate URL of the CAS server. * * @param string $url the proxyValidate URL * * @return string proxyValidate URL */ public function setServerProxyValidateURL($url) { // Argument Validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); return $this->_server['proxy_validate_url'] = $url; } /** * This method sets the samlValidate URL of the CAS server. * * @param string $url the samlValidate URL * * @return string samlValidate URL */ public function setServerSamlValidateURL($url) { // Argument Validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); return $this->_server['saml_validate_url'] = $url; } /** * This method is used to retrieve the service validating URL of the CAS server. * * @return string serviceValidate URL. */ public function getServerServiceValidateURL() { phpCAS::traceBegin(); // the URL is build only when needed if ( empty($this->_server['service_validate_url']) ) { switch ($this->getServerVersion()) { case CAS_VERSION_1_0: $this->_server['service_validate_url'] = $this->_getServerBaseURL() .'validate'; break; case CAS_VERSION_2_0: $this->_server['service_validate_url'] = $this->_getServerBaseURL() .'serviceValidate'; break; case CAS_VERSION_3_0: $this->_server['service_validate_url'] = $this->_getServerBaseURL() .'p3/serviceValidate'; break; } } $url = $this->_buildQueryUrl( $this->_server['service_validate_url'], 'service='.urlencode($this->getURL()) ); phpCAS::traceEnd($url); return $url; } /** * This method is used to retrieve the SAML validating URL of the CAS server. * * @return string samlValidate URL. */ public function getServerSamlValidateURL() { phpCAS::traceBegin(); // the URL is build only when needed if ( empty($this->_server['saml_validate_url']) ) { switch ($this->getServerVersion()) { case SAML_VERSION_1_1: $this->_server['saml_validate_url'] = $this->_getServerBaseURL().'samlValidate'; break; } } $url = $this->_buildQueryUrl( $this->_server['saml_validate_url'], 'TARGET='.urlencode($this->getURL()) ); phpCAS::traceEnd($url); return $url; } /** * This method is used to retrieve the proxy validating URL of the CAS server. * * @return string proxyValidate URL. */ public function getServerProxyValidateURL() { phpCAS::traceBegin(); // the URL is build only when needed if ( empty($this->_server['proxy_validate_url']) ) { switch ($this->getServerVersion()) { case CAS_VERSION_1_0: $this->_server['proxy_validate_url'] = ''; break; case CAS_VERSION_2_0: $this->_server['proxy_validate_url'] = $this->_getServerBaseURL().'proxyValidate'; break; case CAS_VERSION_3_0: $this->_server['proxy_validate_url'] = $this->_getServerBaseURL().'p3/proxyValidate'; break; } } $url = $this->_buildQueryUrl( $this->_server['proxy_validate_url'], 'service='.urlencode($this->getURL()) ); phpCAS::traceEnd($url); return $url; } /** * This method is used to retrieve the proxy URL of the CAS server. * * @return string proxy URL. */ public function getServerProxyURL() { // the URL is build only when needed if ( empty($this->_server['proxy_url']) ) { switch ($this->getServerVersion()) { case CAS_VERSION_1_0: $this->_server['proxy_url'] = ''; break; case CAS_VERSION_2_0: case CAS_VERSION_3_0: $this->_server['proxy_url'] = $this->_getServerBaseURL().'proxy'; break; } } return $this->_server['proxy_url']; } /** * This method is used to retrieve the logout URL of the CAS server. * * @return string logout URL. */ public function getServerLogoutURL() { // the URL is build only when needed if ( empty($this->_server['logout_url']) ) { $this->_server['logout_url'] = $this->_getServerBaseURL().'logout'; } return $this->_server['logout_url']; } /** * This method sets the logout URL of the CAS server. * * @param string $url the logout URL * * @return string logout url */ public function setServerLogoutURL($url) { // Argument Validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); return $this->_server['logout_url'] = $url; } /** * An array to store extra curl options. */ private $_curl_options = array(); /** * This method is used to set additional user curl options. * * @param string $key name of the curl option * @param string $value value of the curl option * * @return void */ public function setExtraCurlOption($key, $value) { $this->_curl_options[$key] = $value; } /** @} */ // ######################################################################## // Change the internal behaviour of phpcas // ######################################################################## /** * @addtogroup internalBehave * @{ */ /** * The class to instantiate for making web requests in readUrl(). * The class specified must implement the CAS_Request_RequestInterface. * By default CAS_Request_CurlRequest is used, but this may be overridden to * supply alternate request mechanisms for testing. */ private $_requestImplementation = 'CAS_Request_CurlRequest'; /** * Override the default implementation used to make web requests in readUrl(). * This class must implement the CAS_Request_RequestInterface. * * @param string $className name of the RequestImplementation class * * @return void */ public function setRequestImplementation ($className) { $obj = new $className; if (!($obj instanceof CAS_Request_RequestInterface)) { throw new CAS_InvalidArgumentException( '$className must implement the CAS_Request_RequestInterface' ); } $this->_requestImplementation = $className; } /** * @var boolean $_clearTicketsFromUrl; If true, phpCAS will clear session * tickets from the URL after a successful authentication. */ private $_clearTicketsFromUrl = true; /** * Configure the client to not send redirect headers and call exit() on * authentication success. The normal redirect is used to remove the service * ticket from the client's URL, but for running unit tests we need to * continue without exiting. * * Needed for testing authentication * * @return void */ public function setNoClearTicketsFromUrl () { $this->_clearTicketsFromUrl = false; } /** * @var callback $_attributeParserCallbackFunction; */ private $_casAttributeParserCallbackFunction = null; /** * @var array $_attributeParserCallbackArgs; */ private $_casAttributeParserCallbackArgs = array(); /** * Set a callback function to be run when parsing CAS attributes * * The callback function will be passed a XMLNode as its first parameter, * followed by any $additionalArgs you pass. * * @param string $function callback function to call * @param array $additionalArgs optional array of arguments * * @return void */ public function setCasAttributeParserCallback($function, array $additionalArgs = array()) { $this->_casAttributeParserCallbackFunction = $function; $this->_casAttributeParserCallbackArgs = $additionalArgs; } /** @var callable $_postAuthenticateCallbackFunction; */ private $_postAuthenticateCallbackFunction = null; /** * @var array $_postAuthenticateCallbackArgs; */ private $_postAuthenticateCallbackArgs = array(); /** * Set a callback function to be run when a user authenticates. * * The callback function will be passed a $logoutTicket as its first parameter, * followed by any $additionalArgs you pass. The $logoutTicket parameter is an * opaque string that can be used to map a session-id to the logout request * in order to support single-signout in applications that manage their own * sessions (rather than letting phpCAS start the session). * * phpCAS::forceAuthentication() will always exit and forward client unless * they are already authenticated. To perform an action at the moment the user * logs in (such as registering an account, performing logging, etc), register * a callback function here. * * @param callable $function callback function to call * @param array $additionalArgs optional array of arguments * * @return void */ public function setPostAuthenticateCallback ($function, array $additionalArgs = array()) { $this->_postAuthenticateCallbackFunction = $function; $this->_postAuthenticateCallbackArgs = $additionalArgs; } /** * @var callable $_signoutCallbackFunction; */ private $_signoutCallbackFunction = null; /** * @var array $_signoutCallbackArgs; */ private $_signoutCallbackArgs = array(); /** * Set a callback function to be run when a single-signout request is received. * * The callback function will be passed a $logoutTicket as its first parameter, * followed by any $additionalArgs you pass. The $logoutTicket parameter is an * opaque string that can be used to map a session-id to the logout request in * order to support single-signout in applications that manage their own sessions * (rather than letting phpCAS start and destroy the session). * * @param callable $function callback function to call * @param array $additionalArgs optional array of arguments * * @return void */ public function setSingleSignoutCallback ($function, array $additionalArgs = array()) { $this->_signoutCallbackFunction = $function; $this->_signoutCallbackArgs = $additionalArgs; } // ######################################################################## // Methods for supplying code-flow feedback to integrators. // ######################################################################## /** * Ensure that this is actually a proxy object or fail with an exception * * @throws CAS_OutOfSequenceBeforeProxyException * * @return void */ public function ensureIsProxy() { if (!$this->isProxy()) { throw new CAS_OutOfSequenceBeforeProxyException(); } } /** * Mark the caller of authentication. This will help client integraters determine * problems with their code flow if they call a function such as getUser() before * authentication has occurred. * * @param bool $auth True if authentication was successful, false otherwise. * * @return null */ public function markAuthenticationCall ($auth) { // store where the authentication has been checked and the result $dbg = debug_backtrace(); $this->_authentication_caller = array ( 'file' => $dbg[1]['file'], 'line' => $dbg[1]['line'], 'method' => $dbg[1]['class'] . '::' . $dbg[1]['function'], 'result' => (boolean)$auth ); } private $_authentication_caller; /** * Answer true if authentication has been checked. * * @return bool */ public function wasAuthenticationCalled () { return !empty($this->_authentication_caller); } /** * Ensure that authentication was checked. Terminate with exception if no * authentication was performed * * @throws CAS_OutOfSequenceBeforeAuthenticationCallException * * @return void */ private function _ensureAuthenticationCalled() { if (!$this->wasAuthenticationCalled()) { throw new CAS_OutOfSequenceBeforeAuthenticationCallException(); } } /** * Answer the result of the authentication call. * * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false * and markAuthenticationCall() didn't happen. * * @return bool */ public function wasAuthenticationCallSuccessful () { $this->_ensureAuthenticationCalled(); return $this->_authentication_caller['result']; } /** * Ensure that authentication was checked. Terminate with exception if no * authentication was performed * * @throws CAS_OutOfSequenceException * * @return void */ public function ensureAuthenticationCallSuccessful() { $this->_ensureAuthenticationCalled(); if (!$this->_authentication_caller['result']) { throw new CAS_OutOfSequenceException( 'authentication was checked (by ' . $this->getAuthenticationCallerMethod() . '() at ' . $this->getAuthenticationCallerFile() . ':' . $this->getAuthenticationCallerLine() . ') but the method returned false' ); } } /** * Answer information about the authentication caller. * * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false * and markAuthenticationCall() didn't happen. * * @return string the file that called authentication */ public function getAuthenticationCallerFile () { $this->_ensureAuthenticationCalled(); return $this->_authentication_caller['file']; } /** * Answer information about the authentication caller. * * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false * and markAuthenticationCall() didn't happen. * * @return int the line that called authentication */ public function getAuthenticationCallerLine () { $this->_ensureAuthenticationCalled(); return $this->_authentication_caller['line']; } /** * Answer information about the authentication caller. * * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false * and markAuthenticationCall() didn't happen. * * @return string the method that called authentication */ public function getAuthenticationCallerMethod () { $this->_ensureAuthenticationCalled(); return $this->_authentication_caller['method']; } /** @} */ // ######################################################################## // CONSTRUCTOR // ######################################################################## /** * @addtogroup internalConfig * @{ */ /** * CAS_Client constructor. * * @param string $server_version the version of the CAS server * @param bool $proxy true if the CAS client is a CAS proxy * @param string $server_hostname the hostname of the CAS server * @param int $server_port the port the CAS server is running on * @param string $server_uri the URI the CAS server is responding on * @param bool $changeSessionID Allow phpCAS to change the session_id * (Single Sign Out/handleLogoutRequests * is based on that change) * @param string|string[]|CAS_ServiceBaseUrl_Interface * $service_base_url the base URL (protocol, host and the * optional port) of the CAS client; pass * in an array to use auto discovery with * an allowlist; pass in * CAS_ServiceBaseUrl_Interface for custom * behavior. Added in 1.6.0. Similar to * serverName config in other CAS clients. * @param \SessionHandlerInterface $sessionHandler the session handler * * @return self a newly created CAS_Client object */ public function __construct( $server_version, $proxy, $server_hostname, $server_port, $server_uri, $service_base_url, $changeSessionID = true, \SessionHandlerInterface $sessionHandler = null ) { // Argument validation if (gettype($server_version) != 'string') throw new CAS_TypeMismatchException($server_version, '$server_version', 'string'); if (gettype($proxy) != 'boolean') throw new CAS_TypeMismatchException($proxy, '$proxy', 'boolean'); if (gettype($server_hostname) != 'string') throw new CAS_TypeMismatchException($server_hostname, '$server_hostname', 'string'); if (gettype($server_port) != 'integer') throw new CAS_TypeMismatchException($server_port, '$server_port', 'integer'); if (gettype($server_uri) != 'string') throw new CAS_TypeMismatchException($server_uri, '$server_uri', 'string'); if (gettype($changeSessionID) != 'boolean') throw new CAS_TypeMismatchException($changeSessionID, '$changeSessionID', 'boolean'); $this->_setServiceBaseUrl($service_base_url); if (empty($sessionHandler)) { $sessionHandler = new CAS_Session_PhpSession; } phpCAS::traceBegin(); // true : allow to change the session_id(), false session_id won't be // changed and logout won't be handled because of that $this->_setChangeSessionID($changeSessionID); $this->setSessionHandler($sessionHandler); if (!$this->_isLogoutRequest()) { if (session_id() === "") { // skip Session Handling for logout requests and if don't want it session_start(); phpCAS :: trace("Starting a new session " . session_id()); } // init phpCAS session array if (!isset($_SESSION[static::PHPCAS_SESSION_PREFIX]) || !is_array($_SESSION[static::PHPCAS_SESSION_PREFIX])) { $_SESSION[static::PHPCAS_SESSION_PREFIX] = array(); } } // Only for debug purposes if ($this->isSessionAuthenticated()){ phpCAS :: trace("Session is authenticated as: " . $this->getSessionValue('user')); } else { phpCAS :: trace("Session is not authenticated"); } // are we in proxy mode ? $this->_proxy = $proxy; // Make cookie handling available. if ($this->isProxy()) { if (!$this->hasSessionValue('service_cookies')) { $this->setSessionValue('service_cookies', array()); } // TODO remove explicit call to $_SESSION $this->_serviceCookieJar = new CAS_CookieJar( $_SESSION[static::PHPCAS_SESSION_PREFIX]['service_cookies'] ); } // check version $supportedProtocols = phpCAS::getSupportedProtocols(); if (isset($supportedProtocols[$server_version]) === false) { phpCAS::error( 'this version of CAS (`'.$server_version .'\') is not supported by phpCAS '.phpCAS::getVersion() ); } if ($server_version === CAS_VERSION_1_0 && $this->isProxy()) { phpCAS::error( 'CAS proxies are not supported in CAS '.$server_version ); } $this->_server['version'] = $server_version; // check hostname if ( empty($server_hostname) || !preg_match('/[\.\d\-a-z]*/', $server_hostname) ) { phpCAS::error('bad CAS server hostname (`'.$server_hostname.'\')'); } $this->_server['hostname'] = $server_hostname; // check port if ( $server_port == 0 || !is_int($server_port) ) { phpCAS::error('bad CAS server port (`'.$server_hostname.'\')'); } $this->_server['port'] = $server_port; // check URI if ( !preg_match('/[\.\d\-_a-z\/]*/', $server_uri) ) { phpCAS::error('bad CAS server URI (`'.$server_uri.'\')'); } // add leading and trailing `/' and remove doubles if(strstr($server_uri, '?') === false) $server_uri .= '/'; $server_uri = preg_replace('/\/\//', '/', '/'.$server_uri); $this->_server['uri'] = $server_uri; // set to callback mode if PgtIou and PgtId CGI GET parameters are provided if ( $this->isProxy() ) { if(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId'])) { $this->_setCallbackMode(true); $this->_setCallbackModeUsingPost(false); } elseif (!empty($_POST['pgtIou'])&&!empty($_POST['pgtId'])) { $this->_setCallbackMode(true); $this->_setCallbackModeUsingPost(true); } else { $this->_setCallbackMode(false); $this->_setCallbackModeUsingPost(false); } } if ( $this->_isCallbackMode() ) { //callback mode: check that phpCAS is secured if ( !$this->getServiceBaseUrl()->isHttps() ) { phpCAS::error( 'CAS proxies must be secured to use phpCAS; PGT\'s will not be received from the CAS server' ); } } else { //normal mode: get ticket and remove it from CGI parameters for // developers $ticket = (isset($_GET['ticket']) ? $_GET['ticket'] : ''); if (preg_match('/^[SP]T-/', $ticket) ) { phpCAS::trace('Ticket \''.$ticket.'\' found'); $this->setTicket($ticket); unset($_GET['ticket']); } else if ( !empty($ticket) ) { //ill-formed ticket, halt phpCAS::error( 'ill-formed ticket found in the URL (ticket=`' .htmlentities($ticket).'\')' ); } } phpCAS::traceEnd(); } /** @} */ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // XX XX // XX Session Handling XX // XX XX // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX /** * @addtogroup internalConfig * @{ */ /** The session prefix for phpCAS values */ const PHPCAS_SESSION_PREFIX = 'phpCAS'; /** * @var bool A variable to whether phpcas will use its own session handling. Default = true * @hideinitializer */ private $_change_session_id = true; /** * @var SessionHandlerInterface */ private $_sessionHandler; /** * Set a parameter whether to allow phpCAS to change session_id * * @param bool $allowed allow phpCAS to change session_id * * @return void */ private function _setChangeSessionID($allowed) { $this->_change_session_id = $allowed; } /** * Get whether phpCAS is allowed to change session_id * * @return bool */ public function getChangeSessionID() { return $this->_change_session_id; } /** * Set the session handler. * * @param \SessionHandlerInterface $sessionHandler * * @return bool */ public function setSessionHandler(\SessionHandlerInterface $sessionHandler) { $this->_sessionHandler = $sessionHandler; if (session_status() !== PHP_SESSION_ACTIVE) { return session_set_save_handler($this->_sessionHandler, true); } return true; } /** * Get a session value using the given key. * * @param string $key * @param mixed $default default value if the key is not set * * @return mixed */ protected function getSessionValue($key, $default = null) { $this->validateSession($key); if (isset($_SESSION[static::PHPCAS_SESSION_PREFIX][$key])) { return $_SESSION[static::PHPCAS_SESSION_PREFIX][$key]; } return $default; } /** * Determine whether a session value is set or not. * * To check if a session value is empty or not please use * !!(getSessionValue($key)). * * @param string $key * * @return bool */ protected function hasSessionValue($key) { $this->validateSession($key); return isset($_SESSION[static::PHPCAS_SESSION_PREFIX][$key]); } /** * Set a session value using the given key and value. * * @param string $key * @param mixed $value * * @return string */ protected function setSessionValue($key, $value) { $this->validateSession($key); $_SESSION[static::PHPCAS_SESSION_PREFIX][$key] = $value; } /** * Remove a session value with the given key. * * @param string $key */ protected function removeSessionValue($key) { $this->validateSession($key); if (isset($_SESSION[static::PHPCAS_SESSION_PREFIX][$key])) { unset($_SESSION[static::PHPCAS_SESSION_PREFIX][$key]); return true; } return false; } /** * Remove all phpCAS session values. */ protected function clearSessionValues() { unset($_SESSION[static::PHPCAS_SESSION_PREFIX]); } /** * Ensure $key is a string for session utils input * * @param string $key * * @return bool */ protected function validateSession($key) { if (!is_string($key)) { throw new InvalidArgumentException('Session key must be a string.'); } return true; } /** * Renaming the session * * @param string $ticket name of the ticket * * @return void */ protected function _renameSession($ticket) { phpCAS::traceBegin(); if ($this->getChangeSessionID()) { if (!empty($this->_user)) { $old_session = $_SESSION; phpCAS :: trace("Killing session: ". session_id()); session_destroy(); // set up a new session, of name based on the ticket $session_id = $this->_sessionIdForTicket($ticket); phpCAS :: trace("Starting session: ". $session_id); session_id($session_id); session_start(); phpCAS :: trace("Restoring old session vars"); $_SESSION = $old_session; } else { phpCAS :: trace ( 'Session should only be renamed after successfull authentication' ); } } else { phpCAS :: trace( "Skipping session rename since phpCAS is not handling the session." ); } phpCAS::traceEnd(); } /** @} */ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // XX XX // XX AUTHENTICATION XX // XX XX // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX /** * @addtogroup internalAuthentication * @{ */ /** * The Authenticated user. Written by CAS_Client::_setUser(), read by * CAS_Client::getUser(). * * @hideinitializer */ private $_user = ''; /** * This method sets the CAS user's login name. * * @param string $user the login name of the authenticated user. * * @return void */ private function _setUser($user) { $this->_user = $user; } /** * This method returns the CAS user's login name. * * @return string the login name of the authenticated user * * @warning should be called only after CAS_Client::forceAuthentication() or * CAS_Client::isAuthenticated(), otherwise halt with an error. */ public function getUser() { // Sequence validation $this->ensureAuthenticationCallSuccessful(); return $this->_getUser(); } /** * This method returns the CAS user's login name. * * @return string the login name of the authenticated user * * @warning should be called only after CAS_Client::forceAuthentication() or * CAS_Client::isAuthenticated(), otherwise halt with an error. */ private function _getUser() { // This is likely a duplicate check that could be removed.... if ( empty($this->_user) ) { phpCAS::error( 'this method should be used only after '.__CLASS__ .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()' ); } return $this->_user; } /** * The Authenticated users attributes. Written by * CAS_Client::setAttributes(), read by CAS_Client::getAttributes(). * @attention client applications should use phpCAS::getAttributes(). * * @hideinitializer */ private $_attributes = array(); /** * Set an array of attributes * * @param array $attributes a key value array of attributes * * @return void */ public function setAttributes($attributes) { $this->_attributes = $attributes; } /** * Get an key values arry of attributes * * @return array of attributes */ public function getAttributes() { // Sequence validation $this->ensureAuthenticationCallSuccessful(); // This is likely a duplicate check that could be removed.... if ( empty($this->_user) ) { // if no user is set, there shouldn't be any attributes also... phpCAS::error( 'this method should be used only after '.__CLASS__ .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()' ); } return $this->_attributes; } /** * Check whether attributes are available * * @return bool attributes available */ public function hasAttributes() { // Sequence validation $this->ensureAuthenticationCallSuccessful(); return !empty($this->_attributes); } /** * Check whether a specific attribute with a name is available * * @param string $key name of attribute * * @return bool is attribute available */ public function hasAttribute($key) { // Sequence validation $this->ensureAuthenticationCallSuccessful(); return $this->_hasAttribute($key); } /** * Check whether a specific attribute with a name is available * * @param string $key name of attribute * * @return bool is attribute available */ private function _hasAttribute($key) { return (is_array($this->_attributes) && array_key_exists($key, $this->_attributes)); } /** * Get a specific attribute by name * * @param string $key name of attribute * * @return string attribute values */ public function getAttribute($key) { // Sequence validation $this->ensureAuthenticationCallSuccessful(); if ($this->_hasAttribute($key)) { return $this->_attributes[$key]; } } /** * This method is called to renew the authentication of the user * If the user is authenticated, renew the connection * If not, redirect to CAS * * @return bool true when the user is authenticated; otherwise halt. */ public function renewAuthentication() { phpCAS::traceBegin(); // Either way, the user is authenticated by CAS $this->removeSessionValue('auth_checked'); if ( $this->isAuthenticated(true) ) { phpCAS::trace('user already authenticated'); $res = true; } else { $this->redirectToCas(false, true); // never reached $res = false; } phpCAS::traceEnd(); return $res; } /** * This method is called to be sure that the user is authenticated. When not * authenticated, halt by redirecting to the CAS server; otherwise return true. * * @return bool true when the user is authenticated; otherwise halt. */ public function forceAuthentication() { phpCAS::traceBegin(); if ( $this->isAuthenticated() ) { // the user is authenticated, nothing to be done. phpCAS::trace('no need to authenticate'); $res = true; } else { // the user is not authenticated, redirect to the CAS server $this->removeSessionValue('auth_checked'); $this->redirectToCas(false/* no gateway */); // never reached $res = false; } phpCAS::traceEnd($res); return $res; } /** * An integer that gives the number of times authentication will be cached * before rechecked. * * @hideinitializer */ private $_cache_times_for_auth_recheck = 0; /** * Set the number of times authentication will be cached before rechecked. * * @param int $n number of times to wait for a recheck * * @return void */ public function setCacheTimesForAuthRecheck($n) { if (gettype($n) != 'integer') throw new CAS_TypeMismatchException($n, '$n', 'string'); $this->_cache_times_for_auth_recheck = $n; } /** * This method is called to check whether the user is authenticated or not. * * @return bool true when the user is authenticated, false when a previous * gateway login failed or the function will not return if the user is * redirected to the cas server for a gateway login attempt */ public function checkAuthentication() { phpCAS::traceBegin(); $res = false; // default if ( $this->isAuthenticated() ) { phpCAS::trace('user is authenticated'); /* The 'auth_checked' variable is removed just in case it's set. */ $this->removeSessionValue('auth_checked'); $res = true; } else if ($this->getSessionValue('auth_checked')) { // the previous request has redirected the client to the CAS server // with gateway=true $this->removeSessionValue('auth_checked'); } else { // avoid a check against CAS on every request // we need to write this back to session later $unauth_count = $this->getSessionValue('unauth_count', -2); if (($unauth_count != -2 && $this->_cache_times_for_auth_recheck == -1) || ($unauth_count >= 0 && $unauth_count < $this->_cache_times_for_auth_recheck) ) { if ($this->_cache_times_for_auth_recheck != -1) { $unauth_count++; phpCAS::trace( 'user is not authenticated (cached for ' .$unauth_count.' times of ' .$this->_cache_times_for_auth_recheck.')' ); } else { phpCAS::trace( 'user is not authenticated (cached for until login pressed)' ); } $this->setSessionValue('unauth_count', $unauth_count); } else { $this->setSessionValue('unauth_count', 0); $this->setSessionValue('auth_checked', true); phpCAS::trace('user is not authenticated (cache reset)'); $this->redirectToCas(true/* gateway */); // never reached } } phpCAS::traceEnd($res); return $res; } /** * This method is called to check if the user is authenticated (previously or by * tickets given in the URL). * * @param bool $renew true to force the authentication with the CAS server * * @return bool true when the user is authenticated. Also may redirect to the * same URL without the ticket. */ public function isAuthenticated($renew=false) { phpCAS::traceBegin(); $res = false; if ( $this->_wasPreviouslyAuthenticated() ) { if ($this->hasTicket()) { // User has a additional ticket but was already authenticated phpCAS::trace( 'ticket was present and will be discarded, use renewAuthenticate()' ); if ($this->_clearTicketsFromUrl) { phpCAS::trace("Prepare redirect to : ".$this->getURL()); session_write_close(); header('Location: '.$this->getURL()); flush(); phpCAS::traceExit(); throw new CAS_GracefullTerminationException(); } else { phpCAS::trace( 'Already authenticated, but skipping ticket clearing since setNoClearTicketsFromUrl() was used.' ); $res = true; } } else { // the user has already (previously during the session) been // authenticated, nothing to be done. phpCAS::trace( 'user was already authenticated, no need to look for tickets' ); $res = true; } // Mark the auth-check as complete to allow post-authentication // callbacks to make use of phpCAS::getUser() and similar methods $this->markAuthenticationCall($res); } else { if ($this->hasTicket()) { $validate_url = ''; $text_response = ''; $tree_response = ''; switch ($this->getServerVersion()) { case CAS_VERSION_1_0: // if a Service Ticket was given, validate it phpCAS::trace( 'CAS 1.0 ticket `'.$this->getTicket().'\' is present' ); $this->validateCAS10( $validate_url, $text_response, $tree_response, $renew ); // if it fails, it halts phpCAS::trace( 'CAS 1.0 ticket `'.$this->getTicket().'\' was validated' ); $this->setSessionValue('user', $this->_getUser()); $res = true; $logoutTicket = $this->getTicket(); break; case CAS_VERSION_2_0: case CAS_VERSION_3_0: // if a Proxy Ticket was given, validate it phpCAS::trace( 'CAS '.$this->getServerVersion().' ticket `'.$this->getTicket().'\' is present' ); $this->validateCAS20( $validate_url, $text_response, $tree_response, $renew ); // note: if it fails, it halts phpCAS::trace( 'CAS '.$this->getServerVersion().' ticket `'.$this->getTicket().'\' was validated' ); if ( $this->isProxy() ) { $this->_validatePGT( $validate_url, $text_response, $tree_response ); // idem phpCAS::trace('PGT `'.$this->_getPGT().'\' was validated'); $this->setSessionValue('pgt', $this->_getPGT()); } $this->setSessionValue('user', $this->_getUser()); if (!empty($this->_attributes)) { $this->setSessionValue('attributes', $this->_attributes); } $proxies = $this->getProxies(); if (!empty($proxies)) { $this->setSessionValue('proxies', $this->getProxies()); } $res = true; $logoutTicket = $this->getTicket(); break; case SAML_VERSION_1_1: // if we have a SAML ticket, validate it. phpCAS::trace( 'SAML 1.1 ticket `'.$this->getTicket().'\' is present' ); $this->validateSA( $validate_url, $text_response, $tree_response, $renew ); // if it fails, it halts phpCAS::trace( 'SAML 1.1 ticket `'.$this->getTicket().'\' was validated' ); $this->setSessionValue('user', $this->_getUser()); $this->setSessionValue('attributes', $this->_attributes); $res = true; $logoutTicket = $this->getTicket(); break; default: phpCAS::trace('Protocol error'); break; } } else { // no ticket given, not authenticated phpCAS::trace('no ticket found'); } // Mark the auth-check as complete to allow post-authentication // callbacks to make use of phpCAS::getUser() and similar methods $this->markAuthenticationCall($res); if ($res) { // call the post-authenticate callback if registered. if ($this->_postAuthenticateCallbackFunction) { $args = $this->_postAuthenticateCallbackArgs; array_unshift($args, $logoutTicket); call_user_func_array( $this->_postAuthenticateCallbackFunction, $args ); } // if called with a ticket parameter, we need to redirect to the // app without the ticket so that CAS-ification is transparent // to the browser (for later POSTS) most of the checks and // errors should have been made now, so we're safe for redirect // without masking error messages. remove the ticket as a // security precaution to prevent a ticket in the HTTP_REFERRER if ($this->_clearTicketsFromUrl) { phpCAS::trace("Prepare redirect to : ".$this->getURL()); session_write_close(); header('Location: '.$this->getURL()); flush(); phpCAS::traceExit(); throw new CAS_GracefullTerminationException(); } } } phpCAS::traceEnd($res); return $res; } /** * This method tells if the current session is authenticated. * * @return bool true if authenticated based soley on $_SESSION variable */ public function isSessionAuthenticated () { return !!$this->getSessionValue('user'); } /** * This method tells if the user has already been (previously) authenticated * by looking into the session variables. * * @note This function switches to callback mode when needed. * * @return bool true when the user has already been authenticated; false otherwise. */ private function _wasPreviouslyAuthenticated() { phpCAS::traceBegin(); if ( $this->_isCallbackMode() ) { // Rebroadcast the pgtIou and pgtId to all nodes if ($this->_rebroadcast&&!isset($_POST['rebroadcast'])) { $this->_rebroadcast(self::PGTIOU); } $this->_callback(); } $auth = false; if ( $this->isProxy() ) { // CAS proxy: username and PGT must be present if ( $this->isSessionAuthenticated() && $this->getSessionValue('pgt') ) { // authentication already done $this->_setUser($this->getSessionValue('user')); if ($this->hasSessionValue('attributes')) { $this->setAttributes($this->getSessionValue('attributes')); } $this->_setPGT($this->getSessionValue('pgt')); phpCAS::trace( 'user = `'.$this->getSessionValue('user').'\', PGT = `' .$this->getSessionValue('pgt').'\'' ); // Include the list of proxies if ($this->hasSessionValue('proxies')) { $this->_setProxies($this->getSessionValue('proxies')); phpCAS::trace( 'proxies = "' .implode('", "', $this->getSessionValue('proxies')).'"' ); } $auth = true; } elseif ( $this->isSessionAuthenticated() && !$this->getSessionValue('pgt') ) { // these two variables should be empty or not empty at the same time phpCAS::trace( 'username found (`'.$this->getSessionValue('user') .'\') but PGT is empty' ); // unset all tickets to enforce authentication $this->clearSessionValues(); $this->setTicket(''); } elseif ( !$this->isSessionAuthenticated() && $this->getSessionValue('pgt') ) { // these two variables should be empty or not empty at the same time phpCAS::trace( 'PGT found (`'.$this->getSessionValue('pgt') .'\') but username is empty' ); // unset all tickets to enforce authentication $this->clearSessionValues(); $this->setTicket(''); } else { phpCAS::trace('neither user nor PGT found'); } } else { // `simple' CAS client (not a proxy): username must be present if ( $this->isSessionAuthenticated() ) { // authentication already done $this->_setUser($this->getSessionValue('user')); if ($this->hasSessionValue('attributes')) { $this->setAttributes($this->getSessionValue('attributes')); } phpCAS::trace('user = `'.$this->getSessionValue('user').'\''); // Include the list of proxies if ($this->hasSessionValue('proxies')) { $this->_setProxies($this->getSessionValue('proxies')); phpCAS::trace( 'proxies = "' .implode('", "', $this->getSessionValue('proxies')).'"' ); } $auth = true; } else { phpCAS::trace('no user found'); } } phpCAS::traceEnd($auth); return $auth; } /** * This method is used to redirect the client to the CAS server. * It is used by CAS_Client::forceAuthentication() and * CAS_Client::checkAuthentication(). * * @param bool $gateway true to check authentication, false to force it * @param bool $renew true to force the authentication with the CAS server * * @return void */ public function redirectToCas($gateway=false,$renew=false) { phpCAS::traceBegin(); $cas_url = $this->getServerLoginURL($gateway, $renew); session_write_close(); if (php_sapi_name() === 'cli') { @header('Location: '.$cas_url); } else { header('Location: '.$cas_url); } phpCAS::trace("Redirect to : ".$cas_url); $lang = $this->getLangObj(); $this->printHTMLHeader($lang->getAuthenticationWanted()); $this->printf('<p>'. $lang->getShouldHaveBeenRedirected(). '</p>', $cas_url); $this->printHTMLFooter(); phpCAS::traceExit(); throw new CAS_GracefullTerminationException(); } /** * This method is used to logout from CAS. * * @param array $params an array that contains the optional url and service * parameters that will be passed to the CAS server * * @return void */ public function logout($params) { phpCAS::traceBegin(); $cas_url = $this->getServerLogoutURL(); $paramSeparator = '?'; if (isset($params['url'])) { $cas_url = $cas_url . $paramSeparator . "url=" . urlencode($params['url']); $paramSeparator = '&'; } if (isset($params['service'])) { $cas_url = $cas_url . $paramSeparator . "service=" . urlencode($params['service']); } header('Location: '.$cas_url); phpCAS::trace("Prepare redirect to : ".$cas_url); phpCAS::trace("Destroying session : ".session_id()); session_unset(); session_destroy(); if (session_status() === PHP_SESSION_NONE) { phpCAS::trace("Session terminated"); } else { phpCAS::error("Session was not terminated"); phpCAS::trace("Session was not terminated"); } $lang = $this->getLangObj(); $this->printHTMLHeader($lang->getLogout()); $this->printf('<p>'.$lang->getShouldHaveBeenRedirected(). '</p>', $cas_url); $this->printHTMLFooter(); phpCAS::traceExit(); throw new CAS_GracefullTerminationException(); } /** * Check of the current request is a logout request * * @return bool is logout request. */ private function _isLogoutRequest() { return !empty($_POST['logoutRequest']); } /** * This method handles logout requests. * * @param bool $check_client true to check the client bofore handling * the request, false not to perform any access control. True by default. * @param array $allowed_clients an array of host names allowed to send * logout requests. * * @return void */ public function handleLogoutRequests($check_client=true, $allowed_clients=array()) { phpCAS::traceBegin(); if (!$this->_isLogoutRequest()) { phpCAS::trace("Not a logout request"); phpCAS::traceEnd(); return; } if (!$this->getChangeSessionID() && is_null($this->_signoutCallbackFunction) ) { phpCAS::trace( "phpCAS can't handle logout requests if it is not allowed to change session_id." ); } phpCAS::trace("Logout requested"); $decoded_logout_rq = urldecode($_POST['logoutRequest']); phpCAS::trace("SAML REQUEST: ".$decoded_logout_rq); $allowed = false; if ($check_client) { if ($allowed_clients === array()) { $allowed_clients = array( $this->_getServerHostname() ); } $client_ip = $_SERVER['REMOTE_ADDR']; $client = gethostbyaddr($client_ip); phpCAS::trace("Client: ".$client."/".$client_ip); foreach ($allowed_clients as $allowed_client) { if (($client == $allowed_client) || ($client_ip == $allowed_client) ) { phpCAS::trace( "Allowed client '".$allowed_client ."' matches, logout request is allowed" ); $allowed = true; break; } else { phpCAS::trace( "Allowed client '".$allowed_client."' does not match" ); } } } else { phpCAS::trace("No access control set"); $allowed = true; } // If Logout command is permitted proceed with the logout if ($allowed) { phpCAS::trace("Logout command allowed"); // Rebroadcast the logout request if ($this->_rebroadcast && !isset($_POST['rebroadcast'])) { $this->_rebroadcast(self::LOGOUT); } // Extract the ticket from the SAML Request preg_match( "|<samlp:SessionIndex>(.*)</samlp:SessionIndex>|", $decoded_logout_rq, $tick, PREG_OFFSET_CAPTURE, 3 ); $wrappedSamlSessionIndex = preg_replace( '|<samlp:SessionIndex>|', '', $tick[0][0] ); $ticket2logout = preg_replace( '|</samlp:SessionIndex>|', '', $wrappedSamlSessionIndex ); phpCAS::trace("Ticket to logout: ".$ticket2logout); // call the post-authenticate callback if registered. if ($this->_signoutCallbackFunction) { $args = $this->_signoutCallbackArgs; array_unshift($args, $ticket2logout); call_user_func_array($this->_signoutCallbackFunction, $args); } // If phpCAS is managing the session_id, destroy session thanks to // session_id. if ($this->getChangeSessionID()) { $session_id = $this->_sessionIdForTicket($ticket2logout); phpCAS::trace("Session id: ".$session_id); // destroy a possible application session created before phpcas if (session_id() !== "") { session_unset(); session_destroy(); } // fix session ID session_id($session_id); $_COOKIE[session_name()]=$session_id; $_GET[session_name()]=$session_id; // Overwrite session session_start(); session_unset(); session_destroy(); phpCAS::trace("Session ". $session_id . " destroyed"); } } else { phpCAS::error("Unauthorized logout request from client '".$client."'"); phpCAS::trace("Unauthorized logout request from client '".$client."'"); } flush(); phpCAS::traceExit(); throw new CAS_GracefullTerminationException(); } /** @} */ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // XX XX // XX BASIC CLIENT FEATURES (CAS 1.0) XX // XX XX // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // ######################################################################## // ST // ######################################################################## /** * @addtogroup internalBasic * @{ */ /** * The Ticket provided in the URL of the request if present * (empty otherwise). Written by CAS_Client::CAS_Client(), read by * CAS_Client::getTicket() and CAS_Client::_hasPGT(). * * @hideinitializer */ private $_ticket = ''; /** * This method returns the Service Ticket provided in the URL of the request. * * @return string service ticket. */ public function getTicket() { return $this->_ticket; } /** * This method stores the Service Ticket. * * @param string $st The Service Ticket. * * @return void */ public function setTicket($st) { $this->_ticket = $st; } /** * This method tells if a Service Ticket was stored. * * @return bool if a Service Ticket has been stored. */ public function hasTicket() { return !empty($this->_ticket); } /** @} */ // ######################################################################## // ST VALIDATION // ######################################################################## /** * @addtogroup internalBasic * @{ */ /** * @var string the certificate of the CAS server CA. * * @hideinitializer */ private $_cas_server_ca_cert = null; /** * validate CN of the CAS server certificate * * @hideinitializer */ private $_cas_server_cn_validate = true; /** * Set to true not to validate the CAS server. * * @hideinitializer */ private $_no_cas_server_validation = false; /** * Set the CA certificate of the CAS server. * * @param string $cert the PEM certificate file name of the CA that emited * the cert of the server * @param bool $validate_cn valiate CN of the CAS server certificate * * @return void */ public function setCasServerCACert($cert, $validate_cn) { // Argument validation if (gettype($cert) != 'string') { throw new CAS_TypeMismatchException($cert, '$cert', 'string'); } if (gettype($validate_cn) != 'boolean') { throw new CAS_TypeMismatchException($validate_cn, '$validate_cn', 'boolean'); } if (!file_exists($cert)) { throw new CAS_InvalidArgumentException("Certificate file does not exist " . $this->_requestImplementation); } $this->_cas_server_ca_cert = $cert; $this->_cas_server_cn_validate = $validate_cn; } /** * Set no SSL validation for the CAS server. * * @return void */ public function setNoCasServerValidation() { $this->_no_cas_server_validation = true; } /** * This method is used to validate a CAS 1,0 ticket; halt on failure, and * sets $validate_url, $text_reponse and $tree_response on success. * * @param string &$validate_url reference to the the URL of the request to * the CAS server. * @param string &$text_response reference to the response of the CAS * server, as is (XML text). * @param string &$tree_response reference to the response of the CAS * server, as a DOM XML tree. * @param bool $renew true to force the authentication with the CAS server * * @return bool true when successfull and issue a CAS_AuthenticationException * and false on an error * @throws CAS_AuthenticationException */ public function validateCAS10(&$validate_url,&$text_response,&$tree_response,$renew=false) { phpCAS::traceBegin(); // build the URL to validate the ticket $validate_url = $this->getServerServiceValidateURL() .'&ticket='.urlencode($this->getTicket()); if ( $renew ) { // pass the renew $validate_url .= '&renew=true'; } $headers = ''; $err_msg = ''; // open and read the URL if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) { phpCAS::trace( 'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')' ); throw new CAS_AuthenticationException( $this, 'CAS 1.0 ticket not validated', $validate_url, true/*$no_response*/ ); } if (preg_match('/^no\n/', $text_response)) { phpCAS::trace('Ticket has not been validated'); throw new CAS_AuthenticationException( $this, 'ST not validated', $validate_url, false/*$no_response*/, false/*$bad_response*/, $text_response ); } else if (!preg_match('/^yes\n/', $text_response)) { phpCAS::trace('ill-formed response'); throw new CAS_AuthenticationException( $this, 'Ticket not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } // ticket has been validated, extract the user name $arr = preg_split('/\n/', $text_response); $this->_setUser(trim($arr[1])); $this->_renameSession($this->getTicket()); // at this step, ticket has been validated and $this->_user has been set, phpCAS::traceEnd(true); return true; } /** @} */ // ######################################################################## // SAML VALIDATION // ######################################################################## /** * @addtogroup internalSAML * @{ */ /** * This method is used to validate a SAML TICKET; halt on failure, and sets * $validate_url, $text_reponse and $tree_response on success. These * parameters are used later by CAS_Client::_validatePGT() for CAS proxies. * * @param string &$validate_url reference to the the URL of the request to * the CAS server. * @param string &$text_response reference to the response of the CAS * server, as is (XML text). * @param string &$tree_response reference to the response of the CAS * server, as a DOM XML tree. * @param bool $renew true to force the authentication with the CAS server * * @return bool true when successfull and issue a CAS_AuthenticationException * and false on an error * * @throws CAS_AuthenticationException */ public function validateSA(&$validate_url,&$text_response,&$tree_response,$renew=false) { phpCAS::traceBegin(); $result = false; // build the URL to validate the ticket $validate_url = $this->getServerSamlValidateURL(); if ( $renew ) { // pass the renew $validate_url .= '&renew=true'; } $headers = ''; $err_msg = ''; // open and read the URL if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) { phpCAS::trace( 'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')' ); throw new CAS_AuthenticationException( $this, 'SA not validated', $validate_url, true/*$no_response*/ ); } phpCAS::trace('server version: '.$this->getServerVersion()); // analyze the result depending on the version switch ($this->getServerVersion()) { case SAML_VERSION_1_1: // create new DOMDocument Object $dom = new DOMDocument(); // Fix possible whitspace problems $dom->preserveWhiteSpace = false; // read the response of the CAS server into a DOM object if (!($dom->loadXML($text_response))) { phpCAS::trace('dom->loadXML() failed'); throw new CAS_AuthenticationException( $this, 'SA not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } // read the root node of the XML tree if (!($tree_response = $dom->documentElement)) { phpCAS::trace('documentElement() failed'); throw new CAS_AuthenticationException( $this, 'SA not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } else if ( $tree_response->localName != 'Envelope' ) { // insure that tag name is 'Envelope' phpCAS::trace( 'bad XML root node (should be `Envelope\' instead of `' .$tree_response->localName.'\'' ); throw new CAS_AuthenticationException( $this, 'SA not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } else if ($tree_response->getElementsByTagName("NameIdentifier")->length != 0) { // check for the NameIdentifier tag in the SAML response $success_elements = $tree_response->getElementsByTagName("NameIdentifier"); phpCAS::trace('NameIdentifier found'); $user = trim($success_elements->item(0)->nodeValue); phpCAS::trace('user = `'.$user.'`'); $this->_setUser($user); $this->_setSessionAttributes($text_response); $result = true; } else { phpCAS::trace('no <NameIdentifier> tag found in SAML payload'); throw new CAS_AuthenticationException( $this, 'SA not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } } if ($result) { $this->_renameSession($this->getTicket()); } // at this step, ST has been validated and $this->_user has been set, phpCAS::traceEnd($result); return $result; } /** * This method will parse the DOM and pull out the attributes from the SAML * payload and put them into an array, then put the array into the session. * * @param string $text_response the SAML payload. * * @return bool true when successfull and false if no attributes a found */ private function _setSessionAttributes($text_response) { phpCAS::traceBegin(); $result = false; $attr_array = array(); // create new DOMDocument Object $dom = new DOMDocument(); // Fix possible whitspace problems $dom->preserveWhiteSpace = false; if (($dom->loadXML($text_response))) { $xPath = new DOMXPath($dom); $xPath->registerNamespace('samlp', 'urn:oasis:names:tc:SAML:1.0:protocol'); $xPath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:1.0:assertion'); $nodelist = $xPath->query("//saml:Attribute"); if ($nodelist) { foreach ($nodelist as $node) { $xres = $xPath->query("saml:AttributeValue", $node); $name = $node->getAttribute("AttributeName"); $value_array = array(); foreach ($xres as $node2) { $value_array[] = $node2->nodeValue; } $attr_array[$name] = $value_array; } // UGent addition... foreach ($attr_array as $attr_key => $attr_value) { if (count($attr_value) > 1) { $this->_attributes[$attr_key] = $attr_value; phpCAS::trace("* " . $attr_key . "=" . print_r($attr_value, true)); } else { $this->_attributes[$attr_key] = $attr_value[0]; phpCAS::trace("* " . $attr_key . "=" . $attr_value[0]); } } $result = true; } else { phpCAS::trace("SAML Attributes are empty"); $result = false; } } phpCAS::traceEnd($result); return $result; } /** @} */ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // XX XX // XX PROXY FEATURES (CAS 2.0) XX // XX XX // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // ######################################################################## // PROXYING // ######################################################################## /** * @addtogroup internalProxy * @{ */ /** * @var bool is the client a proxy * A boolean telling if the client is a CAS proxy or not. Written by * CAS_Client::CAS_Client(), read by CAS_Client::isProxy(). */ private $_proxy; /** * @var CAS_CookieJar Handler for managing service cookies. */ private $_serviceCookieJar; /** * Tells if a CAS client is a CAS proxy or not * * @return bool true when the CAS client is a CAS proxy, false otherwise */ public function isProxy() { return $this->_proxy; } /** @} */ // ######################################################################## // PGT // ######################################################################## /** * @addtogroup internalProxy * @{ */ /** * the Proxy Grnting Ticket given by the CAS server (empty otherwise). * Written by CAS_Client::_setPGT(), read by CAS_Client::_getPGT() and * CAS_Client::_hasPGT(). * * @hideinitializer */ private $_pgt = ''; /** * This method returns the Proxy Granting Ticket given by the CAS server. * * @return string the Proxy Granting Ticket. */ private function _getPGT() { return $this->_pgt; } /** * This method stores the Proxy Granting Ticket. * * @param string $pgt The Proxy Granting Ticket. * * @return void */ private function _setPGT($pgt) { $this->_pgt = $pgt; } /** * This method tells if a Proxy Granting Ticket was stored. * * @return bool true if a Proxy Granting Ticket has been stored. */ private function _hasPGT() { return !empty($this->_pgt); } /** @} */ // ######################################################################## // CALLBACK MODE // ######################################################################## /** * @addtogroup internalCallback * @{ */ /** * each PHP script using phpCAS in proxy mode is its own callback to get the * PGT back from the CAS server. callback_mode is detected by the constructor * thanks to the GET parameters. */ /** * @var bool a boolean to know if the CAS client is running in callback mode. Written by * CAS_Client::setCallBackMode(), read by CAS_Client::_isCallbackMode(). * * @hideinitializer */ private $_callback_mode = false; /** * This method sets/unsets callback mode. * * @param bool $callback_mode true to set callback mode, false otherwise. * * @return void */ private function _setCallbackMode($callback_mode) { $this->_callback_mode = $callback_mode; } /** * This method returns true when the CAS client is running in callback mode, * false otherwise. * * @return bool A boolean. */ private function _isCallbackMode() { return $this->_callback_mode; } /** * @var bool a boolean to know if the CAS client is using POST parameters when in callback mode. * Written by CAS_Client::_setCallbackModeUsingPost(), read by CAS_Client::_isCallbackModeUsingPost(). * * @hideinitializer */ private $_callback_mode_using_post = false; /** * This method sets/unsets usage of POST parameters in callback mode (default/false is GET parameters) * * @param bool $callback_mode_using_post true to use POST, false to use GET (default). * * @return void */ private function _setCallbackModeUsingPost($callback_mode_using_post) { $this->_callback_mode_using_post = $callback_mode_using_post; } /** * This method returns true when the callback mode is using POST, false otherwise. * * @return bool A boolean. */ private function _isCallbackModeUsingPost() { return $this->_callback_mode_using_post; } /** * the URL that should be used for the PGT callback (in fact the URL of the * current request without any CGI parameter). Written and read by * CAS_Client::_getCallbackURL(). * * @hideinitializer */ private $_callback_url = ''; /** * This method returns the URL that should be used for the PGT callback (in * fact the URL of the current request without any CGI parameter, except if * phpCAS::setFixedCallbackURL() was used). * * @return string The callback URL */ private function _getCallbackURL() { // the URL is built when needed only if ( empty($this->_callback_url) ) { // remove the ticket if present in the URL $final_uri = $this->getServiceBaseUrl()->get(); $request_uri = $_SERVER['REQUEST_URI']; $request_uri = preg_replace('/\?.*$/', '', $request_uri); $final_uri .= $request_uri; $this->_callback_url = $final_uri; } return $this->_callback_url; } /** * This method sets the callback url. * * @param string $url url to set callback * * @return string the callback url */ public function setCallbackURL($url) { // Sequence validation $this->ensureIsProxy(); // Argument Validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); return $this->_callback_url = $url; } /** * This method is called by CAS_Client::CAS_Client() when running in callback * mode. It stores the PGT and its PGT Iou, prints its output and halts. * * @return void */ private function _callback() { phpCAS::traceBegin(); if ($this->_isCallbackModeUsingPost()) { $pgtId = $_POST['pgtId']; $pgtIou = $_POST['pgtIou']; } else { $pgtId = $_GET['pgtId']; $pgtIou = $_GET['pgtIou']; } if (preg_match('/^PGTIOU-[\.\-\w]+$/', $pgtIou)) { if (preg_match('/^[PT]GT-[\.\-\w]+$/', $pgtId)) { phpCAS::trace('Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\')'); $this->_storePGT($pgtId, $pgtIou); if ($this->isXmlResponse()) { echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n"; echo '<proxySuccess xmlns="http://www.yale.edu/tp/cas" />'; phpCAS::traceExit("XML response sent"); } else { $this->printHTMLHeader('phpCAS callback'); echo '<p>Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\').</p>'; $this->printHTMLFooter(); phpCAS::traceExit("HTML response sent"); } phpCAS::traceExit("Successfull Callback"); } else { phpCAS::error('PGT format invalid' . $pgtId); phpCAS::traceExit('PGT format invalid' . $pgtId); } } else { phpCAS::error('PGTiou format invalid' . $pgtIou); phpCAS::traceExit('PGTiou format invalid' . $pgtIou); } // Flush the buffer to prevent from sending anything other then a 200 // Success Status back to the CAS Server. The Exception would normally // report as a 500 error. flush(); throw new CAS_GracefullTerminationException(); } /** * Check if application/xml or text/xml is pressent in HTTP_ACCEPT header values * when return value is complex and contains attached q parameters. * Example: HTTP_ACCEPT = text/html,application/xhtml+xml,application/xml;q=0.9 * @return bool */ private function isXmlResponse() { if (!array_key_exists('HTTP_ACCEPT', $_SERVER)) { return false; } if (strpos($_SERVER['HTTP_ACCEPT'], 'application/xml') === false && strpos($_SERVER['HTTP_ACCEPT'], 'text/xml') === false) { return false; } return true; } /** @} */ // ######################################################################## // PGT STORAGE // ######################################################################## /** * @addtogroup internalPGTStorage * @{ */ /** * @var CAS_PGTStorage_AbstractStorage * an instance of a class inheriting of PGTStorage, used to deal with PGT * storage. Created by CAS_Client::setPGTStorageFile(), used * by CAS_Client::setPGTStorageFile() and CAS_Client::_initPGTStorage(). * * @hideinitializer */ private $_pgt_storage = null; /** * This method is used to initialize the storage of PGT's. * Halts on error. * * @return void */ private function _initPGTStorage() { // if no SetPGTStorageXxx() has been used, default to file if ( !is_object($this->_pgt_storage) ) { $this->setPGTStorageFile(); } // initializes the storage $this->_pgt_storage->init(); } /** * This method stores a PGT. Halts on error. * * @param string $pgt the PGT to store * @param string $pgt_iou its corresponding Iou * * @return void */ private function _storePGT($pgt,$pgt_iou) { // ensure that storage is initialized $this->_initPGTStorage(); // writes the PGT $this->_pgt_storage->write($pgt, $pgt_iou); } /** * This method reads a PGT from its Iou and deletes the corresponding * storage entry. * * @param string $pgt_iou the PGT Iou * * @return string mul The PGT corresponding to the Iou, false when not found. */ private function _loadPGT($pgt_iou) { // ensure that storage is initialized $this->_initPGTStorage(); // read the PGT return $this->_pgt_storage->read($pgt_iou); } /** * This method can be used to set a custom PGT storage object. * * @param CAS_PGTStorage_AbstractStorage $storage a PGT storage object that * inherits from the CAS_PGTStorage_AbstractStorage class * * @return void */ public function setPGTStorage($storage) { // Sequence validation $this->ensureIsProxy(); // check that the storage has not already been set if ( is_object($this->_pgt_storage) ) { phpCAS::error('PGT storage already defined'); } // check to make sure a valid storage object was specified if ( !($storage instanceof CAS_PGTStorage_AbstractStorage) ) throw new CAS_TypeMismatchException($storage, '$storage', 'CAS_PGTStorage_AbstractStorage object'); // store the PGTStorage object $this->_pgt_storage = $storage; } /** * This method is used to tell phpCAS to store the response of the * CAS server to PGT requests in a database. * * @param string|PDO $dsn_or_pdo a dsn string to use for creating a PDO * object or a PDO object * @param string $username the username to use when connecting to the * database * @param string $password the password to use when connecting to the * database * @param string $table the table to use for storing and retrieving * PGTs * @param string $driver_options any driver options to use when connecting * to the database * * @return void */ public function setPGTStorageDb( $dsn_or_pdo, $username='', $password='', $table='', $driver_options=null ) { // Sequence validation $this->ensureIsProxy(); // Argument validation if (!(is_object($dsn_or_pdo) && $dsn_or_pdo instanceof PDO) && !is_string($dsn_or_pdo)) throw new CAS_TypeMismatchException($dsn_or_pdo, '$dsn_or_pdo', 'string or PDO object'); if (gettype($username) != 'string') throw new CAS_TypeMismatchException($username, '$username', 'string'); if (gettype($password) != 'string') throw new CAS_TypeMismatchException($password, '$password', 'string'); if (gettype($table) != 'string') throw new CAS_TypeMismatchException($table, '$password', 'string'); // create the storage object $this->setPGTStorage( new CAS_PGTStorage_Db( $this, $dsn_or_pdo, $username, $password, $table, $driver_options ) ); } /** * This method is used to tell phpCAS to store the response of the * CAS server to PGT requests onto the filesystem. * * @param string $path the path where the PGT's should be stored * * @return void */ public function setPGTStorageFile($path='') { // Sequence validation $this->ensureIsProxy(); // Argument validation if (gettype($path) != 'string') throw new CAS_TypeMismatchException($path, '$path', 'string'); // create the storage object $this->setPGTStorage(new CAS_PGTStorage_File($this, $path)); } // ######################################################################## // PGT VALIDATION // ######################################################################## /** * This method is used to validate a PGT; halt on failure. * * @param string &$validate_url the URL of the request to the CAS server. * @param string $text_response the response of the CAS server, as is * (XML text); result of * CAS_Client::validateCAS10() or * CAS_Client::validateCAS20(). * @param DOMElement $tree_response the response of the CAS server, as a DOM XML * tree; result of CAS_Client::validateCAS10() or CAS_Client::validateCAS20(). * * @return bool true when successfull and issue a CAS_AuthenticationException * and false on an error * * @throws CAS_AuthenticationException */ private function _validatePGT(&$validate_url,$text_response,$tree_response) { phpCAS::traceBegin(); if ( $tree_response->getElementsByTagName("proxyGrantingTicket")->length == 0) { phpCAS::trace('<proxyGrantingTicket> not found'); // authentication succeded, but no PGT Iou was transmitted throw new CAS_AuthenticationException( $this, 'Ticket validated but no PGT Iou transmitted', $validate_url, false/*$no_response*/, false/*$bad_response*/, $text_response ); } else { // PGT Iou transmitted, extract it $pgt_iou = trim( $tree_response->getElementsByTagName("proxyGrantingTicket")->item(0)->nodeValue ); if (preg_match('/^PGTIOU-[\.\-\w]+$/', $pgt_iou)) { $pgt = $this->_loadPGT($pgt_iou); if ( $pgt == false ) { phpCAS::trace('could not load PGT'); throw new CAS_AuthenticationException( $this, 'PGT Iou was transmitted but PGT could not be retrieved', $validate_url, false/*$no_response*/, false/*$bad_response*/, $text_response ); } $this->_setPGT($pgt); } else { phpCAS::trace('PGTiou format error'); throw new CAS_AuthenticationException( $this, 'PGT Iou was transmitted but has wrong format', $validate_url, false/*$no_response*/, false/*$bad_response*/, $text_response ); } } phpCAS::traceEnd(true); return true; } // ######################################################################## // PGT VALIDATION // ######################################################################## /** * This method is used to retrieve PT's from the CAS server thanks to a PGT. * * @param string $target_service the service to ask for with the PT. * @param int &$err_code an error code (PHPCAS_SERVICE_OK on success). * @param string &$err_msg an error message (empty on success). * * @return string|false a Proxy Ticket, or false on error. */ public function retrievePT($target_service,&$err_code,&$err_msg) { // Argument validation if (gettype($target_service) != 'string') throw new CAS_TypeMismatchException($target_service, '$target_service', 'string'); phpCAS::traceBegin(); // by default, $err_msg is set empty and $pt to true. On error, $pt is // set to false and $err_msg to an error message. At the end, if $pt is false // and $error_msg is still empty, it is set to 'invalid response' (the most // commonly encountered error). $err_msg = ''; // build the URL to retrieve the PT $cas_url = $this->getServerProxyURL().'?targetService=' .urlencode($target_service).'&pgt='.$this->_getPGT(); $headers = ''; $cas_response = ''; // open and read the URL if ( !$this->_readURL($cas_url, $headers, $cas_response, $err_msg) ) { phpCAS::trace( 'could not open URL \''.$cas_url.'\' to validate ('.$err_msg.')' ); $err_code = PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE; $err_msg = 'could not retrieve PT (no response from the CAS server)'; phpCAS::traceEnd(false); return false; } $bad_response = false; // create new DOMDocument object $dom = new DOMDocument(); // Fix possible whitspace problems $dom->preserveWhiteSpace = false; // read the response of the CAS server into a DOM object if ( !($dom->loadXML($cas_response))) { phpCAS::trace('dom->loadXML() failed'); // read failed $bad_response = true; } if ( !$bad_response ) { // read the root node of the XML tree if ( !($root = $dom->documentElement) ) { phpCAS::trace('documentElement failed'); // read failed $bad_response = true; } } if ( !$bad_response ) { // insure that tag name is 'serviceResponse' if ( $root->localName != 'serviceResponse' ) { phpCAS::trace('localName failed'); // bad root node $bad_response = true; } } if ( !$bad_response ) { // look for a proxySuccess tag if ( $root->getElementsByTagName("proxySuccess")->length != 0) { $proxy_success_list = $root->getElementsByTagName("proxySuccess"); // authentication succeded, look for a proxyTicket tag if ( $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->length != 0) { $err_code = PHPCAS_SERVICE_OK; $err_msg = ''; $pt = trim( $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->item(0)->nodeValue ); phpCAS::trace('original PT: '.trim($pt)); phpCAS::traceEnd($pt); return $pt; } else { phpCAS::trace('<proxySuccess> was found, but not <proxyTicket>'); } } else if ($root->getElementsByTagName("proxyFailure")->length != 0) { // look for a proxyFailure tag $proxy_failure_list = $root->getElementsByTagName("proxyFailure"); // authentication failed, extract the error $err_code = PHPCAS_SERVICE_PT_FAILURE; $err_msg = 'PT retrieving failed (code=`' .$proxy_failure_list->item(0)->getAttribute('code') .'\', message=`' .trim($proxy_failure_list->item(0)->nodeValue) .'\')'; phpCAS::traceEnd(false); return false; } else { phpCAS::trace('neither <proxySuccess> nor <proxyFailure> found'); } } // at this step, we are sure that the response of the CAS server was // illformed $err_code = PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE; $err_msg = 'Invalid response from the CAS server (response=`' .$cas_response.'\')'; phpCAS::traceEnd(false); return false; } /** @} */ // ######################################################################## // READ CAS SERVER ANSWERS // ######################################################################## /** * @addtogroup internalMisc * @{ */ /** * This method is used to acces a remote URL. * * @param string $url the URL to access. * @param string &$headers an array containing the HTTP header lines of the * response (an empty array on failure). * @param string &$body the body of the response, as a string (empty on * failure). * @param string &$err_msg an error message, filled on failure. * * @return bool true on success, false otherwise (in this later case, $err_msg * contains an error message). */ private function _readURL($url, &$headers, &$body, &$err_msg) { phpCAS::traceBegin(); $className = $this->_requestImplementation; $request = new $className(); if (count($this->_curl_options)) { $request->setCurlOptions($this->_curl_options); } $request->setUrl($url); if (empty($this->_cas_server_ca_cert) && !$this->_no_cas_server_validation) { phpCAS::error( 'one of the methods phpCAS::setCasServerCACert() or phpCAS::setNoCasServerValidation() must be called.' ); } if ($this->_cas_server_ca_cert != '') { $request->setSslCaCert( $this->_cas_server_ca_cert, $this->_cas_server_cn_validate ); } // add extra stuff if SAML if ($this->getServerVersion() == SAML_VERSION_1_1) { $request->addHeader("soapaction: http://www.oasis-open.org/committees/security"); $request->addHeader("cache-control: no-cache"); $request->addHeader("pragma: no-cache"); $request->addHeader("accept: text/xml"); $request->addHeader("connection: keep-alive"); $request->addHeader("content-type: text/xml"); $request->makePost(); $request->setPostBody($this->_buildSAMLPayload()); } if ($request->send()) { $headers = $request->getResponseHeaders(); $body = $request->getResponseBody(); $err_msg = ''; phpCAS::traceEnd(true); return true; } else { $headers = ''; $body = ''; $err_msg = $request->getErrorMessage(); phpCAS::traceEnd(false); return false; } } /** * This method is used to build the SAML POST body sent to /samlValidate URL. * * @return string the SOAP-encased SAMLP artifact (the ticket). */ private function _buildSAMLPayload() { phpCAS::traceBegin(); //get the ticket $sa = urlencode($this->getTicket()); $body = SAML_SOAP_ENV.SAML_SOAP_BODY.SAMLP_REQUEST .SAML_ASSERTION_ARTIFACT.$sa.SAML_ASSERTION_ARTIFACT_CLOSE .SAMLP_REQUEST_CLOSE.SAML_SOAP_BODY_CLOSE.SAML_SOAP_ENV_CLOSE; phpCAS::traceEnd($body); return ($body); } /** @} **/ // ######################################################################## // ACCESS TO EXTERNAL SERVICES // ######################################################################## /** * @addtogroup internalProxyServices * @{ */ /** * Answer a proxy-authenticated service handler. * * @param string $type The service type. One of: * PHPCAS_PROXIED_SERVICE_HTTP_GET, PHPCAS_PROXIED_SERVICE_HTTP_POST, * PHPCAS_PROXIED_SERVICE_IMAP * * @return CAS_ProxiedService * @throws InvalidArgumentException If the service type is unknown. */ public function getProxiedService ($type) { // Sequence validation $this->ensureIsProxy(); $this->ensureAuthenticationCallSuccessful(); // Argument validation if (gettype($type) != 'string') throw new CAS_TypeMismatchException($type, '$type', 'string'); switch ($type) { case PHPCAS_PROXIED_SERVICE_HTTP_GET: case PHPCAS_PROXIED_SERVICE_HTTP_POST: $requestClass = $this->_requestImplementation; $request = new $requestClass(); if (count($this->_curl_options)) { $request->setCurlOptions($this->_curl_options); } $proxiedService = new $type($request, $this->_serviceCookieJar); if ($proxiedService instanceof CAS_ProxiedService_Testable) { $proxiedService->setCasClient($this); } return $proxiedService; case PHPCAS_PROXIED_SERVICE_IMAP; $proxiedService = new CAS_ProxiedService_Imap($this->_getUser()); if ($proxiedService instanceof CAS_ProxiedService_Testable) { $proxiedService->setCasClient($this); } return $proxiedService; default: throw new CAS_InvalidArgumentException( "Unknown proxied-service type, $type." ); } } /** * Initialize a proxied-service handler with the proxy-ticket it should use. * * @param CAS_ProxiedService $proxiedService service handler * * @return void * * @throws CAS_ProxyTicketException If there is a proxy-ticket failure. * The code of the Exception will be one of: * PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE * PHPCAS_SERVICE_PT_FAILURE * @throws CAS_ProxiedService_Exception If there is a failure getting the * url from the proxied service. */ public function initializeProxiedService (CAS_ProxiedService $proxiedService) { // Sequence validation $this->ensureIsProxy(); $this->ensureAuthenticationCallSuccessful(); $url = $proxiedService->getServiceUrl(); if (!is_string($url)) { throw new CAS_ProxiedService_Exception( "Proxied Service ".get_class($proxiedService) ."->getServiceUrl() should have returned a string, returned a " .gettype($url)." instead." ); } $pt = $this->retrievePT($url, $err_code, $err_msg); if (!$pt) { throw new CAS_ProxyTicketException($err_msg, $err_code); } $proxiedService->setProxyTicket($pt); } /** * This method is used to access an HTTP[S] service. * * @param string $url the service to access. * @param int &$err_code an error code Possible values are * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE, * PHPCAS_SERVICE_NOT_AVAILABLE. * @param string &$output the output of the service (also used to give an error * message on failure). * * @return bool true on success, false otherwise (in this later case, $err_code * gives the reason why it failed and $output contains an error message). */ public function serviceWeb($url,&$err_code,&$output) { // Sequence validation $this->ensureIsProxy(); $this->ensureAuthenticationCallSuccessful(); // Argument validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); try { $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_HTTP_GET); $service->setUrl($url); $service->send(); $output = $service->getResponseBody(); $err_code = PHPCAS_SERVICE_OK; return true; } catch (CAS_ProxyTicketException $e) { $err_code = $e->getCode(); $output = $e->getMessage(); return false; } catch (CAS_ProxiedService_Exception $e) { $lang = $this->getLangObj(); $output = sprintf( $lang->getServiceUnavailable(), $url, $e->getMessage() ); $err_code = PHPCAS_SERVICE_NOT_AVAILABLE; return false; } } /** * This method is used to access an IMAP/POP3/NNTP service. * * @param string $url a string giving the URL of the service, including * the mailing box for IMAP URLs, as accepted by imap_open(). * @param string $serviceUrl a string giving for CAS retrieve Proxy ticket * @param string $flags options given to imap_open(). * @param int &$err_code an error code Possible values are * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE, * PHPCAS_SERVICE_NOT_AVAILABLE. * @param string &$err_msg an error message on failure * @param string &$pt the Proxy Ticket (PT) retrieved from the CAS * server to access the URL on success, false on error). * * @return object|false an IMAP stream on success, false otherwise (in this later * case, $err_code gives the reason why it failed and $err_msg contains an * error message). */ public function serviceMail($url,$serviceUrl,$flags,&$err_code,&$err_msg,&$pt) { // Sequence validation $this->ensureIsProxy(); $this->ensureAuthenticationCallSuccessful(); // Argument validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); if (gettype($serviceUrl) != 'string') throw new CAS_TypeMismatchException($serviceUrl, '$serviceUrl', 'string'); if (gettype($flags) != 'integer') throw new CAS_TypeMismatchException($flags, '$flags', 'string'); try { $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_IMAP); $service->setServiceUrl($serviceUrl); $service->setMailbox($url); $service->setOptions($flags); $stream = $service->open(); $err_code = PHPCAS_SERVICE_OK; $pt = $service->getImapProxyTicket(); return $stream; } catch (CAS_ProxyTicketException $e) { $err_msg = $e->getMessage(); $err_code = $e->getCode(); $pt = false; return false; } catch (CAS_ProxiedService_Exception $e) { $lang = $this->getLangObj(); $err_msg = sprintf( $lang->getServiceUnavailable(), $url, $e->getMessage() ); $err_code = PHPCAS_SERVICE_NOT_AVAILABLE; $pt = false; return false; } } /** @} **/ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // XX XX // XX PROXIED CLIENT FEATURES (CAS 2.0) XX // XX XX // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // ######################################################################## // PT // ######################################################################## /** * @addtogroup internalService * @{ */ /** * This array will store a list of proxies in front of this application. This * property will only be populated if this script is being proxied rather than * accessed directly. * * It is set in CAS_Client::validateCAS20() and can be read by * CAS_Client::getProxies() * * @access private */ private $_proxies = array(); /** * Answer an array of proxies that are sitting in front of this application. * * This method will only return a non-empty array if we have received and * validated a Proxy Ticket. * * @return array * @access public */ public function getProxies() { return $this->_proxies; } /** * Set the Proxy array, probably from persistant storage. * * @param array $proxies An array of proxies * * @return void * @access private */ private function _setProxies($proxies) { $this->_proxies = $proxies; if (!empty($proxies)) { // For proxy-authenticated requests people are not viewing the URL // directly since the client is another application making a // web-service call. // Because of this, stripping the ticket from the URL is unnecessary // and causes another web-service request to be performed. Additionally, // if session handling on either the client or the server malfunctions // then the subsequent request will not complete successfully. $this->setNoClearTicketsFromUrl(); } } /** * A container of patterns to be allowed as proxies in front of the cas client. * * @var CAS_ProxyChain_AllowedList */ private $_allowed_proxy_chains; /** * Answer the CAS_ProxyChain_AllowedList object for this client. * * @return CAS_ProxyChain_AllowedList */ public function getAllowedProxyChains () { if (empty($this->_allowed_proxy_chains)) { $this->_allowed_proxy_chains = new CAS_ProxyChain_AllowedList(); } return $this->_allowed_proxy_chains; } /** @} */ // ######################################################################## // PT VALIDATION // ######################################################################## /** * @addtogroup internalProxied * @{ */ /** * This method is used to validate a cas 2.0 ST or PT; halt on failure * Used for all CAS 2.0 validations * * @param string &$validate_url the url of the reponse * @param string &$text_response the text of the repsones * @param DOMElement &$tree_response the domxml tree of the respones * @param bool $renew true to force the authentication with the CAS server * * @return bool true when successfull and issue a CAS_AuthenticationException * and false on an error * * @throws CAS_AuthenticationException */ public function validateCAS20(&$validate_url,&$text_response,&$tree_response, $renew=false) { phpCAS::traceBegin(); phpCAS::trace($text_response); // build the URL to validate the ticket if ($this->getAllowedProxyChains()->isProxyingAllowed()) { $validate_url = $this->getServerProxyValidateURL().'&ticket=' .urlencode($this->getTicket()); } else { $validate_url = $this->getServerServiceValidateURL().'&ticket=' .urlencode($this->getTicket()); } if ( $this->isProxy() ) { // pass the callback url for CAS proxies $validate_url .= '&pgtUrl='.urlencode($this->_getCallbackURL()); } if ( $renew ) { // pass the renew $validate_url .= '&renew=true'; } // open and read the URL if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) { phpCAS::trace( 'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')' ); throw new CAS_AuthenticationException( $this, 'Ticket not validated', $validate_url, true/*$no_response*/ ); } // create new DOMDocument object $dom = new DOMDocument(); // Fix possible whitspace problems $dom->preserveWhiteSpace = false; // CAS servers should only return data in utf-8 $dom->encoding = "utf-8"; // read the response of the CAS server into a DOMDocument object if ( !($dom->loadXML($text_response))) { // read failed throw new CAS_AuthenticationException( $this, 'Ticket not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } else if ( !($tree_response = $dom->documentElement) ) { // read the root node of the XML tree // read failed throw new CAS_AuthenticationException( $this, 'Ticket not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } else if ($tree_response->localName != 'serviceResponse') { // insure that tag name is 'serviceResponse' // bad root node throw new CAS_AuthenticationException( $this, 'Ticket not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) { // authentication failed, extract the error code and message and throw exception $auth_fail_list = $tree_response ->getElementsByTagName("authenticationFailure"); throw new CAS_AuthenticationException( $this, 'Ticket not validated', $validate_url, false/*$no_response*/, false/*$bad_response*/, $text_response, $auth_fail_list->item(0)->getAttribute('code')/*$err_code*/, trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/ ); } else if ($tree_response->getElementsByTagName("authenticationSuccess")->length != 0) { // authentication succeded, extract the user name $success_elements = $tree_response ->getElementsByTagName("authenticationSuccess"); if ( $success_elements->item(0)->getElementsByTagName("user")->length == 0) { // no user specified => error throw new CAS_AuthenticationException( $this, 'Ticket not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } else { $this->_setUser( trim( $success_elements->item(0)->getElementsByTagName("user")->item(0)->nodeValue ) ); $this->_readExtraAttributesCas20($success_elements); // Store the proxies we are sitting behind for authorization checking $proxyList = array(); if ( sizeof($arr = $success_elements->item(0)->getElementsByTagName("proxy")) > 0) { foreach ($arr as $proxyElem) { phpCAS::trace("Found Proxy: ".$proxyElem->nodeValue); $proxyList[] = trim($proxyElem->nodeValue); } $this->_setProxies($proxyList); phpCAS::trace("Storing Proxy List"); } // Check if the proxies in front of us are allowed if (!$this->getAllowedProxyChains()->isProxyListAllowed($proxyList)) { throw new CAS_AuthenticationException( $this, 'Proxy not allowed', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } else { $result = true; } } } else { throw new CAS_AuthenticationException( $this, 'Ticket not validated', $validate_url, false/*$no_response*/, true/*$bad_response*/, $text_response ); } $this->_renameSession($this->getTicket()); // at this step, Ticket has been validated and $this->_user has been set, phpCAS::traceEnd($result); return $result; } /** * This method recursively parses the attribute XML. * It also collapses name-value pairs into a single * array entry. It parses all common formats of * attributes and well formed XML files. * * @param string $root the DOM root element to be parsed * @param string $namespace namespace of the elements * * @return an array of the parsed XML elements * * Formats tested: * * "Jasig Style" Attributes: * * <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> * <cas:authenticationSuccess> * <cas:user>jsmith</cas:user> * <cas:attributes> * <cas:attraStyle>RubyCAS</cas:attraStyle> * <cas:surname>Smith</cas:surname> * <cas:givenName>John</cas:givenName> * <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf> * <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf> * </cas:attributes> * <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket> * </cas:authenticationSuccess> * </cas:serviceResponse> * * "Jasig Style" Attributes (longer version): * * <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> * <cas:authenticationSuccess> * <cas:user>jsmith</cas:user> * <cas:attributes> * <cas:attribute> * <cas:name>surname</cas:name> * <cas:value>Smith</cas:value> * </cas:attribute> * <cas:attribute> * <cas:name>givenName</cas:name> * <cas:value>John</cas:value> * </cas:attribute> * <cas:attribute> * <cas:name>memberOf</cas:name> * <cas:value>['CN=Staff,OU=Groups,DC=example,DC=edu', 'CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu']</cas:value> * </cas:attribute> * </cas:attributes> * <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket> * </cas:authenticationSuccess> * </cas:serviceResponse> * * "RubyCAS Style" attributes * * <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> * <cas:authenticationSuccess> * <cas:user>jsmith</cas:user> * * <cas:attraStyle>RubyCAS</cas:attraStyle> * <cas:surname>Smith</cas:surname> * <cas:givenName>John</cas:givenName> * <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf> * <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf> * * <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket> * </cas:authenticationSuccess> * </cas:serviceResponse> * * "Name-Value" attributes. * * Attribute format from these mailing list thread: * http://jasig.275507.n4.nabble.com/CAS-attributes-and-how-they-appear-in-the-CAS-response-td264272.html * Note: This is a less widely used format, but in use by at least two institutions. * * <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> * <cas:authenticationSuccess> * <cas:user>jsmith</cas:user> * * <cas:attribute name='attraStyle' value='Name-Value' /> * <cas:attribute name='surname' value='Smith' /> * <cas:attribute name='givenName' value='John' /> * <cas:attribute name='memberOf' value='CN=Staff,OU=Groups,DC=example,DC=edu' /> * <cas:attribute name='memberOf' value='CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu' /> * * <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket> * </cas:authenticationSuccess> * </cas:serviceResponse> * * result: * * Array ( * [surname] => Smith * [givenName] => John * [memberOf] => Array ( * [0] => CN=Staff, OU=Groups, DC=example, DC=edu * [1] => CN=Spanish Department, OU=Departments, OU=Groups, DC=example, DC=edu * ) * ) */ private function _xml_to_array($root, $namespace = "cas") { $result = array(); if ($root->hasAttributes()) { $attrs = $root->attributes; $pair = array(); foreach ($attrs as $attr) { if ($attr->name === "name") { $pair['name'] = $attr->value; } elseif ($attr->name === "value") { $pair['value'] = $attr->value; } else { $result[$attr->name] = $attr->value; } if (array_key_exists('name', $pair) && array_key_exists('value', $pair)) { $result[$pair['name']] = $pair['value']; } } } if ($root->hasChildNodes()) { $children = $root->childNodes; if ($children->length == 1) { $child = $children->item(0); if ($child->nodeType == XML_TEXT_NODE) { $result['_value'] = $child->nodeValue; return (count($result) == 1) ? $result['_value'] : $result; } } $groups = array(); foreach ($children as $child) { $child_nodeName = str_ireplace($namespace . ":", "", $child->nodeName); if (in_array($child_nodeName, array("user", "proxies", "proxyGrantingTicket"))) { continue; } if (!isset($result[$child_nodeName])) { $res = $this->_xml_to_array($child, $namespace); if (!empty($res)) { $result[$child_nodeName] = $this->_xml_to_array($child, $namespace); } } else { if (!isset($groups[$child_nodeName])) { $result[$child_nodeName] = array($result[$child_nodeName]); $groups[$child_nodeName] = 1; } $result[$child_nodeName][] = $this->_xml_to_array($child, $namespace); } } } return $result; } /** * This method parses a "JSON-like array" of strings * into an array of strings * * @param string $json_value the json-like string: * e.g.: * ['CN=Staff,OU=Groups,DC=example,DC=edu', 'CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu'] * * @return array of strings Description * e.g.: * Array ( * [0] => CN=Staff,OU=Groups,DC=example,DC=edu * [1] => CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu * ) */ private function _parse_json_like_array_value($json_value) { $parts = explode(",", trim($json_value, "[]")); $out = array(); $quote = ''; foreach ($parts as $part) { $part = trim($part); if ($quote === '') { $value = ""; if ($this->_startsWith($part, '\'')) { $quote = '\''; } elseif ($this->_startsWith($part, '"')) { $quote = '"'; } else { $out[] = $part; } $part = ltrim($part, $quote); } if ($quote !== '') { $value .= $part; if ($this->_endsWith($part, $quote)) { $out[] = rtrim($value, $quote); $quote = ''; } else { $value .= ", "; }; } } return $out; } /** * This method recursively removes unneccessary hirarchy levels in array-trees. * into an array of strings * * @param array $arr the array to flatten * e.g.: * Array ( * [attributes] => Array ( * [attribute] => Array ( * [0] => Array ( * [name] => surname * [value] => Smith * ) * [1] => Array ( * [name] => givenName * [value] => John * ) * [2] => Array ( * [name] => memberOf * [value] => ['CN=Staff,OU=Groups,DC=example,DC=edu', 'CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu'] * ) * ) * ) * ) * * @return array the flattened array * e.g.: * Array ( * [attribute] => Array ( * [surname] => Smith * [givenName] => John * [memberOf] => Array ( * [0] => CN=Staff, OU=Groups, DC=example, DC=edu * [1] => CN=Spanish Department, OU=Departments, OU=Groups, DC=example, DC=edu * ) * ) * ) */ private function _flatten_array($arr) { if (!is_array($arr)) { if ($this->_startsWith($arr, '[') && $this->_endsWith($arr, ']')) { return $this->_parse_json_like_array_value($arr); } else { return $arr; } } $out = array(); foreach ($arr as $key => $val) { if (!is_array($val)) { $out[$key] = $val; } else { switch (count($val)) { case 1 : { $key = key($val); if (array_key_exists($key, $out)) { $value = $out[$key]; if (!is_array($value)) { $out[$key] = array(); $out[$key][] = $value; } $out[$key][] = $this->_flatten_array($val[$key]); } else { $out[$key] = $this->_flatten_array($val[$key]); }; break; }; case 2 : { if (array_key_exists("name", $val) && array_key_exists("value", $val)) { $key = $val['name']; if (array_key_exists($key, $out)) { $value = $out[$key]; if (!is_array($value)) { $out[$key] = array(); $out[$key][] = $value; } $out[$key][] = $this->_flatten_array($val['value']); } else { $out[$key] = $this->_flatten_array($val['value']); }; } else { $out[$key] = $this->_flatten_array($val); } break; }; default: { $out[$key] = $this->_flatten_array($val); } } } } return $out; } /** * This method will parse the DOM and pull out the attributes from the XML * payload and put them into an array, then put the array into the session. * * @param DOMNodeList $success_elements payload of the response * * @return bool true when successfull, halt otherwise by calling * CAS_Client::_authError(). */ private function _readExtraAttributesCas20($success_elements) { phpCAS::traceBegin(); $extra_attributes = array(); if ($this->_casAttributeParserCallbackFunction !== null && is_callable($this->_casAttributeParserCallbackFunction) ) { array_unshift($this->_casAttributeParserCallbackArgs, $success_elements->item(0)); phpCAS :: trace("Calling attritubeParser callback"); $extra_attributes = call_user_func_array( $this->_casAttributeParserCallbackFunction, $this->_casAttributeParserCallbackArgs ); } else { phpCAS :: trace("Parse extra attributes: "); $attributes = $this->_xml_to_array($success_elements->item(0)); phpCAS :: trace(print_r($attributes,true). "\nFLATTEN Array: "); $extra_attributes = $this->_flatten_array($attributes); phpCAS :: trace(print_r($extra_attributes, true)."\nFILTER : "); if (array_key_exists("attribute", $extra_attributes)) { $extra_attributes = $extra_attributes["attribute"]; } elseif (array_key_exists("attributes", $extra_attributes)) { $extra_attributes = $extra_attributes["attributes"]; }; phpCAS :: trace(print_r($extra_attributes, true)."return"); } $this->setAttributes($extra_attributes); phpCAS::traceEnd(); return true; } /** * Add an attribute value to an array of attributes. * * @param array &$attributeArray reference to array * @param string $name name of attribute * @param string $value value of attribute * * @return void */ private function _addAttributeToArray(array &$attributeArray, $name, $value) { // If multiple attributes exist, add as an array value if (isset($attributeArray[$name])) { // Initialize the array with the existing value if (!is_array($attributeArray[$name])) { $existingValue = $attributeArray[$name]; $attributeArray[$name] = array($existingValue); } $attributeArray[$name][] = trim($value); } else { $attributeArray[$name] = trim($value); } } /** @} */ // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // XX XX // XX MISC XX // XX XX // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX /** * @addtogroup internalMisc * @{ */ // ######################################################################## // URL // ######################################################################## /** * the URL of the current request (without any ticket CGI parameter). Written * and read by CAS_Client::getURL(). * * @hideinitializer */ private $_url = ''; /** * This method sets the URL of the current request * * @param string $url url to set for service * * @return void */ public function setURL($url) { // Argument Validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); $this->_url = $url; } /** * This method returns the URL of the current request (without any ticket * CGI parameter). * * @return string The URL */ public function getURL() { phpCAS::traceBegin(); // the URL is built when needed only if ( empty($this->_url) ) { // remove the ticket if present in the URL $final_uri = $this->getServiceBaseUrl()->get(); $request_uri = explode('?', $_SERVER['REQUEST_URI'], 2); $final_uri .= $request_uri[0]; if (isset($request_uri[1]) && $request_uri[1]) { $query_string= $this->_removeParameterFromQueryString('ticket', $request_uri[1]); // If the query string still has anything left, // append it to the final URI if ($query_string !== '') { $final_uri .= "?$query_string"; } } phpCAS::trace("Final URI: $final_uri"); $this->setURL($final_uri); } phpCAS::traceEnd($this->_url); return $this->_url; } /** * This method sets the base URL of the CAS server. * * @param string $url the base URL * * @return string base url */ public function setBaseURL($url) { // Argument Validation if (gettype($url) != 'string') throw new CAS_TypeMismatchException($url, '$url', 'string'); return $this->_server['base_url'] = $url; } /** * The ServiceBaseUrl object that provides base URL during service URL * discovery process. * * @var CAS_ServiceBaseUrl_Interface * * @hideinitializer */ private $_serviceBaseUrl = null; /** * Answer the CAS_ServiceBaseUrl_Interface object for this client. * * @return CAS_ServiceBaseUrl_Interface */ public function getServiceBaseUrl() { if (empty($this->_serviceBaseUrl)) { phpCAS::error("ServiceBaseUrl object is not initialized"); } return $this->_serviceBaseUrl; } /** * This method sets the service base URL used during service URL discovery process. * * This is required since phpCAS 1.6.0 to protect the integrity of the authentication. * * @since phpCAS 1.6.0 * * @param $name can be any of the following: * - A base URL string. The service URL discovery will always use this (protocol, * hostname and optional port number) without using any external host names. * - An array of base URL strings. The service URL discovery will check against * this list before using the auto discovered base URL. If there is no match, * the first base URL in the array will be used as the default. This option is * helpful if your PHP website is accessible through multiple domains without a * canonical name, or through both HTTP and HTTPS. * - A class that implements CAS_ServiceBaseUrl_Interface. If you need to customize * the base URL discovery behavior, you can pass in a class that implements the * interface. * * @return void */ private function _setServiceBaseUrl($name) { if (is_array($name)) { $this->_serviceBaseUrl = new CAS_ServiceBaseUrl_AllowedListDiscovery($name); } else if (is_string($name)) { $this->_serviceBaseUrl = new CAS_ServiceBaseUrl_Static($name); } else if ($name instanceof CAS_ServiceBaseUrl_Interface) { $this->_serviceBaseUrl = $name; } else { throw new CAS_TypeMismatchException($name, '$name', 'array, string, or CAS_ServiceBaseUrl_Interface object'); } } /** * Removes a parameter from a query string * * @param string $parameterName name of parameter * @param string $queryString query string * * @return string new query string * * @link http://stackoverflow.com/questions/1842681/regular-expression-to-remove-one-parameter-from-query-string */ private function _removeParameterFromQueryString($parameterName, $queryString) { $parameterName = preg_quote($parameterName); return preg_replace( "/&$parameterName(=[^&]*)?|^$parameterName(=[^&]*)?&?/", '', $queryString ); } /** * This method is used to append query parameters to an url. Since the url * might already contain parameter it has to be detected and to build a proper * URL * * @param string $url base url to add the query params to * @param string $query params in query form with & separated * * @return string url with query params */ private function _buildQueryUrl($url, $query) { $url .= (strstr($url, '?') === false) ? '?' : '&'; $url .= $query; return $url; } /** * This method tests if a string starts with a given character. * * @param string $text text to test * @param string $char character to test for * * @return bool true if the $text starts with $char */ private function _startsWith($text, $char) { return (strpos($text, $char) === 0); } /** * This method tests if a string ends with a given character * * @param string $text text to test * @param string $char character to test for * * @return bool true if the $text ends with $char */ private function _endsWith($text, $char) { return (strpos(strrev($text), $char) === 0); } /** * Answer a valid session-id given a CAS ticket. * * The output must be deterministic to allow single-log-out when presented with * the ticket to log-out. * * * @param string $ticket name of the ticket * * @return string */ private function _sessionIdForTicket($ticket) { // Hash the ticket to ensure that the value meets the PHP 7.1 requirement // that session-ids have a length between 22 and 256 characters. return hash('sha256', $this->_sessionIdSalt . $ticket); } /** * Set a salt/seed for the session-id hash to make it harder to guess. * * @var string $_sessionIdSalt */ private $_sessionIdSalt = ''; /** * Set a salt/seed for the session-id hash to make it harder to guess. * * @param string $salt * * @return void */ public function setSessionIdSalt($salt) { $this->_sessionIdSalt = (string)$salt; } // ######################################################################## // AUTHENTICATION ERROR HANDLING // ######################################################################## /** * This method is used to print the HTML output when the user was not * authenticated. * * @param string $failure the failure that occured * @param string $cas_url the URL the CAS server was asked for * @param bool $no_response the response from the CAS server (other * parameters are ignored if true) * @param bool $bad_response bad response from the CAS server ($err_code * and $err_msg ignored if true) * @param string $cas_response the response of the CAS server * @param int $err_code the error code given by the CAS server * @param string $err_msg the error message given by the CAS server * * @return void */ private function _authError( $failure, $cas_url, $no_response=false, $bad_response=false, $cas_response='', $err_code=-1, $err_msg='' ) { phpCAS::traceBegin(); $lang = $this->getLangObj(); $this->printHTMLHeader($lang->getAuthenticationFailed()); $this->printf( $lang->getYouWereNotAuthenticated(), htmlentities($this->getURL()), isset($_SERVER['SERVER_ADMIN']) ? $_SERVER['SERVER_ADMIN']:'' ); phpCAS::trace('CAS URL: '.$cas_url); phpCAS::trace('Authentication failure: '.$failure); if ( $no_response ) { phpCAS::trace('Reason: no response from the CAS server'); } else { if ( $bad_response ) { phpCAS::trace('Reason: bad response from the CAS server'); } else { switch ($this->getServerVersion()) { case CAS_VERSION_1_0: phpCAS::trace('Reason: CAS error'); break; case CAS_VERSION_2_0: case CAS_VERSION_3_0: if ( $err_code === -1 ) { phpCAS::trace('Reason: no CAS error'); } else { phpCAS::trace( 'Reason: ['.$err_code.'] CAS error: '.$err_msg ); } break; } } phpCAS::trace('CAS response: '.$cas_response); } $this->printHTMLFooter(); phpCAS::traceExit(); throw new CAS_GracefullTerminationException(); } // ######################################################################## // PGTIOU/PGTID and logoutRequest rebroadcasting // ######################################################################## /** * Boolean of whether to rebroadcast pgtIou/pgtId and logoutRequest, and * array of the nodes. */ private $_rebroadcast = false; private $_rebroadcast_nodes = array(); /** * Constants used for determining rebroadcast node type. */ const HOSTNAME = 0; const IP = 1; /** * Determine the node type from the URL. * * @param String $nodeURL The node URL. * * @return int hostname * */ private function _getNodeType($nodeURL) { phpCAS::traceBegin(); if (preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $nodeURL)) { phpCAS::traceEnd(self::IP); return self::IP; } else { phpCAS::traceEnd(self::HOSTNAME); return self::HOSTNAME; } } /** * Store the rebroadcast node for pgtIou/pgtId and logout requests. * * @param string $rebroadcastNodeUrl The rebroadcast node URL. * * @return void */ public function addRebroadcastNode($rebroadcastNodeUrl) { // Argument validation if ( !(bool)preg_match("/^(http|https):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?(\d+)?\/?/i", $rebroadcastNodeUrl)) throw new CAS_TypeMismatchException($rebroadcastNodeUrl, '$rebroadcastNodeUrl', 'url'); // Store the rebroadcast node and set flag $this->_rebroadcast = true; $this->_rebroadcast_nodes[] = $rebroadcastNodeUrl; } /** * An array to store extra rebroadcast curl options. */ private $_rebroadcast_headers = array(); /** * This method is used to add header parameters when rebroadcasting * pgtIou/pgtId or logoutRequest. * * @param string $header Header to send when rebroadcasting. * * @return void */ public function addRebroadcastHeader($header) { if (gettype($header) != 'string') throw new CAS_TypeMismatchException($header, '$header', 'string'); $this->_rebroadcast_headers[] = $header; } /** * Constants used for determining rebroadcast type (logout or pgtIou/pgtId). */ const LOGOUT = 0; const PGTIOU = 1; /** * This method rebroadcasts logout/pgtIou requests. Can be LOGOUT,PGTIOU * * @param int $type type of rebroadcasting. * * @return void */ private function _rebroadcast($type) { phpCAS::traceBegin(); $rebroadcast_curl_options = array( CURLOPT_FAILONERROR => 1, CURLOPT_FOLLOWLOCATION => 1, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 1, CURLOPT_TIMEOUT => 4); // Try to determine the IP address of the server if (!empty($_SERVER['SERVER_ADDR'])) { $ip = $_SERVER['SERVER_ADDR']; } else if (!empty($_SERVER['LOCAL_ADDR'])) { // IIS 7 $ip = $_SERVER['LOCAL_ADDR']; } // Try to determine the DNS name of the server if (!empty($ip)) { $dns = gethostbyaddr($ip); } $multiClassName = 'CAS_Request_CurlMultiRequest'; $multiRequest = new $multiClassName(); for ($i = 0; $i < sizeof($this->_rebroadcast_nodes); $i++) { if ((($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::HOSTNAME) && !empty($dns) && (stripos($this->_rebroadcast_nodes[$i], $dns) === false)) || (($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::IP) && !empty($ip) && (stripos($this->_rebroadcast_nodes[$i], $ip) === false)) ) { phpCAS::trace( 'Rebroadcast target URL: '.$this->_rebroadcast_nodes[$i] .$_SERVER['REQUEST_URI'] ); $className = $this->_requestImplementation; $request = new $className(); $url = $this->_rebroadcast_nodes[$i].$_SERVER['REQUEST_URI']; $request->setUrl($url); if (count($this->_rebroadcast_headers)) { $request->addHeaders($this->_rebroadcast_headers); } $request->makePost(); if ($type == self::LOGOUT) { // Logout request $request->setPostBody( 'rebroadcast=false&logoutRequest='.$_POST['logoutRequest'] ); } else if ($type == self::PGTIOU) { // pgtIou/pgtId rebroadcast $request->setPostBody('rebroadcast=false'); } $request->setCurlOptions($rebroadcast_curl_options); $multiRequest->addRequest($request); } else { phpCAS::trace( 'Rebroadcast not sent to self: ' .$this->_rebroadcast_nodes[$i].' == '.(!empty($ip)?$ip:'') .'/'.(!empty($dns)?$dns:'') ); } } // We need at least 1 request if ($multiRequest->getNumRequests() > 0) { $multiRequest->send(); } phpCAS::traceEnd(); } /** @} */ } cas/CAS/vendor/apereo/phpcas/source/CAS/InvalidArgumentException.php 0000644 00000002754 15152311435 0021345 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/InvalidArgumentException.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Exception that denotes invalid arguments were passed. * * @class CAS_InvalidArgumentException * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_InvalidArgumentException extends InvalidArgumentException implements CAS_Exception { } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/TypeMismatchException.php 0000644 00000004373 15152311435 0020662 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/InvalidArgumentException.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Exception that denotes invalid arguments were passed. * * @class CAS_InvalidArgumentException * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_TypeMismatchException extends CAS_InvalidArgumentException { /** * Constructor, provides a nice message. * * @param mixed $argument Argument * @param string $argumentName Argument Name * @param string $type Type * @param string $message Error Message * @param integer $code Code * * @return void */ public function __construct ( $argument, $argumentName, $type, $message = '', $code = 0 ) { if (is_object($argument)) { $foundType = get_class($argument).' object'; } else { $foundType = gettype($argument); } parent::__construct( 'type mismatched for parameter ' . $argumentName . ' (should be \'' . $type .' \'), ' . $foundType . ' given. ' . $message, $code ); } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxiedService.php 0000644 00000004647 15152311435 0017333 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxiedService.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This interface defines methods that allow proxy-authenticated service handlers * to interact with phpCAS. * * Proxy service handlers must implement this interface as well as call * phpCAS::initializeProxiedService($this) at some point in their implementation. * * While not required, proxy-authenticated service handlers are encouraged to * implement the CAS_ProxiedService_Testable interface to facilitate unit testing. * * @class CAS_ProxiedService * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ interface CAS_ProxiedService { /** * Answer a service identifier (URL) for whom we should fetch a proxy ticket. * * @return string * @throws Exception If no service url is available. */ public function getServiceUrl (); /** * Register a proxy ticket with the ProxiedService that it can use when * making requests. * * @param string $proxyTicket Proxy ticket string * * @return void * @throws InvalidArgumentException If the $proxyTicket is invalid. * @throws CAS_OutOfSequenceException If called after a proxy ticket has * already been initialized/set. */ public function setProxyTicket ($proxyTicket); } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/OutOfSequenceBeforeAuthenticationCallException.php 0000644 00000003605 15152311435 0025614 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * * PHP Version 7 * * @file CAS/OutOfSequenceBeforeAuthenticationCallException.php * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class defines Exceptions that should be thrown when the sequence of * operations is invalid. In this case it should be thrown when an * authentication call has not yet happened. * * @class CAS_OutOfSequenceBeforeAuthenticationCallException * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_OutOfSequenceBeforeAuthenticationCallException extends CAS_OutOfSequenceException implements CAS_Exception { /** * Return standard error meessage * * @return void */ public function __construct () { parent::__construct('An authentication call hasn\'t happened yet.'); } } cas/CAS/vendor/apereo/phpcas/source/CAS/CookieJar.php 0000644 00000027760 15152311435 0016247 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/CookieJar.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class provides access to service cookies and handles parsing of response * headers to pull out cookie values. * * @class CAS_CookieJar * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_CookieJar { private $_cookies; /** * Create a new cookie jar by passing it a reference to an array in which it * should store cookies. * * @param array &$storageArray Array to store cookies * * @return void */ public function __construct (array &$storageArray) { $this->_cookies =& $storageArray; } /** * Store cookies for a web service request. * Cookie storage is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt * * @param string $request_url The URL that generated the response headers. * @param array $response_headers An array of the HTTP response header strings. * * @return void * * @access private */ public function storeCookies ($request_url, $response_headers) { $urlParts = parse_url($request_url); $defaultDomain = $urlParts['host']; $cookies = $this->parseCookieHeaders($response_headers, $defaultDomain); foreach ($cookies as $cookie) { // Enforce the same-origin policy by verifying that the cookie // would match the url that is setting it if (!$this->cookieMatchesTarget($cookie, $urlParts)) { continue; } // store the cookie $this->storeCookie($cookie); phpCAS::trace($cookie['name'].' -> '.$cookie['value']); } } /** * Retrieve cookies applicable for a web service request. * Cookie applicability is based on RFC 2965: http://www.ietf.org/rfc/rfc2965.txt * * @param string $request_url The url that the cookies will be for. * * @return array An array containing cookies. E.g. array('name' => 'val'); * * @access private */ public function getCookies ($request_url) { if (!count($this->_cookies)) { return array(); } // If our request URL can't be parsed, no cookies apply. $target = parse_url($request_url); if ($target === false) { return array(); } $this->expireCookies(); $matching_cookies = array(); foreach ($this->_cookies as $key => $cookie) { if ($this->cookieMatchesTarget($cookie, $target)) { $matching_cookies[$cookie['name']] = $cookie['value']; } } return $matching_cookies; } /** * Parse Cookies without PECL * From the comments in http://php.net/manual/en/function.http-parse-cookie.php * * @param array $header array of header lines. * @param string $defaultDomain The domain to use if none is specified in * the cookie. * * @return array of cookies */ protected function parseCookieHeaders( $header, $defaultDomain ) { phpCAS::traceBegin(); $cookies = array(); foreach ( $header as $line ) { if ( preg_match('/^Set-Cookie2?: /i', $line)) { $cookies[] = $this->parseCookieHeader($line, $defaultDomain); } } phpCAS::traceEnd($cookies); return $cookies; } /** * Parse a single cookie header line. * * Based on RFC2965 http://www.ietf.org/rfc/rfc2965.txt * * @param string $line The header line. * @param string $defaultDomain The domain to use if none is specified in * the cookie. * * @return array */ protected function parseCookieHeader ($line, $defaultDomain) { if (!$defaultDomain) { throw new CAS_InvalidArgumentException( '$defaultDomain was not provided.' ); } // Set our default values $cookie = array( 'domain' => $defaultDomain, 'path' => '/', 'secure' => false, ); $line = preg_replace('/^Set-Cookie2?: /i', '', trim($line)); // trim any trailing semicolons. $line = trim($line, ';'); phpCAS::trace("Cookie Line: $line"); // This implementation makes the assumption that semicolons will not // be present in quoted attribute values. While attribute values that // contain semicolons are allowed by RFC2965, they are hopefully rare // enough to ignore for our purposes. Most browsers make the same // assumption. $attributeStrings = explode(';', $line); foreach ( $attributeStrings as $attributeString ) { // split on the first equals sign and use the rest as value $attributeParts = explode('=', $attributeString, 2); $attributeName = trim($attributeParts[0]); $attributeNameLC = strtolower($attributeName); if (isset($attributeParts[1])) { $attributeValue = trim($attributeParts[1]); // Values may be quoted strings. if (strpos($attributeValue, '"') === 0) { $attributeValue = trim($attributeValue, '"'); // unescape any escaped quotes: $attributeValue = str_replace('\"', '"', $attributeValue); } } else { $attributeValue = null; } switch ($attributeNameLC) { case 'expires': $cookie['expires'] = strtotime($attributeValue); break; case 'max-age': $cookie['max-age'] = (int)$attributeValue; // Set an expiry time based on the max-age if ($cookie['max-age']) { $cookie['expires'] = time() + $cookie['max-age']; } else { // If max-age is zero, then the cookie should be removed // imediately so set an expiry before now. $cookie['expires'] = time() - 1; } break; case 'secure': $cookie['secure'] = true; break; case 'domain': case 'path': case 'port': case 'version': case 'comment': case 'commenturl': case 'discard': case 'httponly': case 'samesite': $cookie[$attributeNameLC] = $attributeValue; break; default: $cookie['name'] = $attributeName; $cookie['value'] = $attributeValue; } } return $cookie; } /** * Add, update, or remove a cookie. * * @param array $cookie A cookie array as created by parseCookieHeaders() * * @return void * * @access protected */ protected function storeCookie ($cookie) { // Discard any old versions of this cookie. $this->discardCookie($cookie); $this->_cookies[] = $cookie; } /** * Discard an existing cookie * * @param array $cookie An cookie * * @return void * * @access protected */ protected function discardCookie ($cookie) { if (!isset($cookie['domain']) || !isset($cookie['path']) || !isset($cookie['path']) ) { throw new CAS_InvalidArgumentException('Invalid Cookie array passed.'); } foreach ($this->_cookies as $key => $old_cookie) { if ( $cookie['domain'] == $old_cookie['domain'] && $cookie['path'] == $old_cookie['path'] && $cookie['name'] == $old_cookie['name'] ) { unset($this->_cookies[$key]); } } } /** * Go through our stored cookies and remove any that are expired. * * @return void * * @access protected */ protected function expireCookies () { foreach ($this->_cookies as $key => $cookie) { if (isset($cookie['expires']) && $cookie['expires'] < time()) { unset($this->_cookies[$key]); } } } /** * Answer true if cookie is applicable to a target. * * @param array $cookie An array of cookie attributes. * @param array|false $target An array of URL attributes as generated by parse_url(). * * @return bool * * @access private */ protected function cookieMatchesTarget ($cookie, $target) { if (!is_array($target)) { throw new CAS_InvalidArgumentException( '$target must be an array of URL attributes as generated by parse_url().' ); } if (!isset($target['host'])) { throw new CAS_InvalidArgumentException( '$target must be an array of URL attributes as generated by parse_url().' ); } // Verify that the scheme matches if ($cookie['secure'] && $target['scheme'] != 'https') { return false; } // Verify that the host matches // Match domain and mulit-host cookies if (strpos($cookie['domain'], '.') === 0) { // .host.domain.edu cookies are valid for host.domain.edu if (substr($cookie['domain'], 1) == $target['host']) { // continue with other checks } else { // non-exact host-name matches. // check that the target host a.b.c.edu is within .b.c.edu $pos = strripos($target['host'], $cookie['domain']); if (!$pos) { return false; } // verify that the cookie domain is the last part of the host. if ($pos + strlen($cookie['domain']) != strlen($target['host'])) { return false; } // verify that the host name does not contain interior dots as per // RFC 2965 section 3.3.2 Rejecting Cookies // http://www.ietf.org/rfc/rfc2965.txt $hostname = substr($target['host'], 0, $pos); if (strpos($hostname, '.') !== false) { return false; } } } else { // If the cookie host doesn't begin with '.', // the host must case-insensitive match exactly if (strcasecmp($target['host'], $cookie['domain']) !== 0) { return false; } } // Verify that the port matches if (isset($cookie['ports']) && !in_array($target['port'], $cookie['ports']) ) { return false; } // Verify that the path matches if (strpos($target['path'], $cookie['path']) !== 0) { return false; } return true; } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxiedService/Exception.php 0000644 00000002747 15152311435 0021270 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxiedService/Exception.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * An Exception for problems communicating with a proxied service. * * @class CAS_ProxiedService_Exception * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ProxiedService_Exception extends Exception implements CAS_Exception { } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxiedService/Testable.php 0000644 00000004755 15152311435 0021076 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxiedService/Testabel.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This interface defines methods that allow proxy-authenticated service handlers * to be tested in unit tests. * * Classes implementing this interface SHOULD store the CAS_Client passed and * initialize themselves with that client rather than via the static phpCAS * method. For example: * * / ** * * Fetch our proxy ticket. * * / * protected function initializeProxyTicket() { * // Allow usage of a particular CAS_Client for unit testing. * if (is_null($this->casClient)) * phpCAS::initializeProxiedService($this); * else * $this->casClient->initializeProxiedService($this); * } * * @class CAS_ProxiedService_Testabel * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ interface CAS_ProxiedService_Testable { /** * Use a particular CAS_Client->initializeProxiedService() rather than the * static phpCAS::initializeProxiedService(). * * This method should not be called in standard operation, but is needed for unit * testing. * * @param CAS_Client $casClient Cas client object * * @return void * @throws CAS_OutOfSequenceException If called after a proxy ticket has * already been initialized/set. */ public function setCasClient (CAS_Client $casClient); } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxiedService/Abstract.php 0000644 00000011201 15152311435 0021056 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxiedService/Abstract.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class implements common methods for ProxiedService implementations included * with phpCAS. * * @class CAS_ProxiedService_Abstract * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ abstract class CAS_ProxiedService_Abstract implements CAS_ProxiedService, CAS_ProxiedService_Testable { /** * The proxy ticket that can be used when making service requests. * @var string $_proxyTicket; */ private $_proxyTicket; /** * Register a proxy ticket with the Proxy that it can use when making requests. * * @param string $proxyTicket proxy ticket * * @return void * @throws InvalidArgumentException If the $proxyTicket is invalid. * @throws CAS_OutOfSequenceException If called after a proxy ticket has * already been initialized/set. */ public function setProxyTicket ($proxyTicket) { if (empty($proxyTicket)) { throw new CAS_InvalidArgumentException( 'Trying to initialize with an empty proxy ticket.' ); } if (!empty($this->_proxyTicket)) { throw new CAS_OutOfSequenceException( 'Already initialized, cannot change the proxy ticket.' ); } $this->_proxyTicket = $proxyTicket; } /** * Answer the proxy ticket to be used when making requests. * * @return string * @throws CAS_OutOfSequenceException If called before a proxy ticket has * already been initialized/set. */ protected function getProxyTicket () { if (empty($this->_proxyTicket)) { throw new CAS_OutOfSequenceException( 'No proxy ticket yet. Call $this->initializeProxyTicket() to aquire the proxy ticket.' ); } return $this->_proxyTicket; } /** * @var CAS_Client $_casClient; */ private $_casClient; /** * Use a particular CAS_Client->initializeProxiedService() rather than the * static phpCAS::initializeProxiedService(). * * This method should not be called in standard operation, but is needed for unit * testing. * * @param CAS_Client $casClient cas client * * @return void * @throws CAS_OutOfSequenceException If called after a proxy ticket has * already been initialized/set. */ public function setCasClient (CAS_Client $casClient) { if (!empty($this->_proxyTicket)) { throw new CAS_OutOfSequenceException( 'Already initialized, cannot change the CAS_Client.' ); } $this->_casClient = $casClient; } /** * Fetch our proxy ticket. * * Descendent classes should call this method once their service URL is available * to initialize their proxy ticket. * * @return void * @throws CAS_OutOfSequenceException If called after a proxy ticket has * already been initialized. */ protected function initializeProxyTicket() { if (!empty($this->_proxyTicket)) { throw new CAS_OutOfSequenceException( 'Already initialized, cannot initialize again.' ); } // Allow usage of a particular CAS_Client for unit testing. if (empty($this->_casClient)) { phpCAS::initializeProxiedService($this); } else { $this->_casClient->initializeProxiedService($this); } } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxiedService/Http/Get.php 0000644 00000005737 15152311435 0020772 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxiedService/Http/Get.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class is used to make proxied service requests via the HTTP GET method. * * Usage Example: * * try { * $service = phpCAS::getProxiedService(PHPCAS_PROXIED_SERVICE_HTTP_GET); * $service->setUrl('http://www.example.com/path/'); * $service->send(); * if ($service->getResponseStatusCode() == 200) * return $service->getResponseBody(); * else * // The service responded with an error code 404, 500, etc. * throw new Exception('The service responded with an error.'); * * } catch (CAS_ProxyTicketException $e) { * if ($e->getCode() == PHPCAS_SERVICE_PT_FAILURE) * return "Your login has timed out. You need to log in again."; * else * // Other proxy ticket errors are from bad request format * // (shouldn't happen) or CAS server failure (unlikely) * // so lets just stop if we hit those. * throw $e; * } catch (CAS_ProxiedService_Exception $e) { * // Something prevented the service request from being sent or received. * // We didn't even get a valid error response (404, 500, etc), so this * // might be caused by a network error or a DNS resolution failure. * // We could handle it in some way, but for now we will just stop. * throw $e; * } * * @class CAS_ProxiedService_Http_Get * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ProxiedService_Http_Get extends CAS_ProxiedService_Http_Abstract { /** * Add any other parts of the request needed by concrete classes * * @param CAS_Request_RequestInterface $request request interface * * @return void */ protected function populateRequest (CAS_Request_RequestInterface $request) { // do nothing, since the URL has already been sent and that is our // only data. } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxiedService/Http/Abstract.php 0000644 00000024735 15152311435 0022015 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxiedService/Http/Abstract.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class implements common methods for ProxiedService implementations included * with phpCAS. * * @class CAS_ProxiedService_Http_Abstract * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ abstract class CAS_ProxiedService_Http_Abstract extends CAS_ProxiedService_Abstract implements CAS_ProxiedService_Http { /** * The HTTP request mechanism talking to the target service. * * @var CAS_Request_RequestInterface $requestHandler */ protected $requestHandler; /** * The storage mechanism for cookies set by the target service. * * @var CAS_CookieJar $_cookieJar */ private $_cookieJar; /** * Constructor. * * @param CAS_Request_RequestInterface $requestHandler request handler object * @param CAS_CookieJar $cookieJar cookieJar object * * @return void */ public function __construct(CAS_Request_RequestInterface $requestHandler, CAS_CookieJar $cookieJar ) { $this->requestHandler = $requestHandler; $this->_cookieJar = $cookieJar; } /** * The target service url. * @var string $_url; */ private $_url; /** * Answer a service identifier (URL) for whom we should fetch a proxy ticket. * * @return string * @throws Exception If no service url is available. */ public function getServiceUrl() { if (empty($this->_url)) { throw new CAS_ProxiedService_Exception( 'No URL set via ' . get_class($this) . '->setUrl($url).' ); } return $this->_url; } /********************************************************* * Configure the Request *********************************************************/ /** * Set the URL of the Request * * @param string $url url to set * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setUrl($url) { if ($this->hasBeenSent()) { throw new CAS_OutOfSequenceException( 'Cannot set the URL, request already sent.' ); } if (!is_string($url)) { throw new CAS_InvalidArgumentException('$url must be a string.'); } $this->_url = $url; } /********************************************************* * 2. Send the Request *********************************************************/ /** * Perform the request. * * @return void * @throws CAS_OutOfSequenceException If called multiple times. * @throws CAS_ProxyTicketException If there is a proxy-ticket failure. * The code of the Exception will be one of: * PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE * PHPCAS_SERVICE_PT_FAILURE * @throws CAS_ProxiedService_Exception If there is a failure sending the * request to the target service. */ public function send() { if ($this->hasBeenSent()) { throw new CAS_OutOfSequenceException( 'Cannot send, request already sent.' ); } phpCAS::traceBegin(); // Get our proxy ticket and append it to our URL. $this->initializeProxyTicket(); $url = $this->getServiceUrl(); if (strstr($url, '?') === false) { $url = $url . '?ticket=' . $this->getProxyTicket(); } else { $url = $url . '&ticket=' . $this->getProxyTicket(); } try { $this->makeRequest($url); } catch (Exception $e) { phpCAS::traceEnd(); throw $e; } } /** * Indicator of the number of requests (including redirects performed. * * @var int $_numRequests; */ private $_numRequests = 0; /** * The response headers. * * @var array $_responseHeaders; */ private $_responseHeaders = array(); /** * The response status code. * * @var int $_responseStatusCode; */ private $_responseStatusCode = ''; /** * The response headers. * * @var string $_responseBody; */ private $_responseBody = ''; /** * Build and perform a request, following redirects * * @param string $url url for the request * * @return void * @throws CAS_ProxyTicketException If there is a proxy-ticket failure. * The code of the Exception will be one of: * PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE * PHPCAS_SERVICE_PT_FAILURE * @throws CAS_ProxiedService_Exception If there is a failure sending the * request to the target service. */ protected function makeRequest($url) { // Verify that we are not in a redirect loop $this->_numRequests++; if ($this->_numRequests > 4) { $message = 'Exceeded the maximum number of redirects (3) in proxied service request.'; phpCAS::trace($message); throw new CAS_ProxiedService_Exception($message); } // Create a new request. $request = clone $this->requestHandler; $request->setUrl($url); // Add any cookies to the request. $request->addCookies($this->_cookieJar->getCookies($url)); // Add any other parts of the request needed by concrete classes $this->populateRequest($request); // Perform the request. phpCAS::trace('Performing proxied service request to \'' . $url . '\''); if (!$request->send()) { $message = 'Could not perform proxied service request to URL`' . $url . '\'. ' . $request->getErrorMessage(); phpCAS::trace($message); throw new CAS_ProxiedService_Exception($message); } // Store any cookies from the response; $this->_cookieJar->storeCookies($url, $request->getResponseHeaders()); // Follow any redirects if ($redirectUrl = $this->getRedirectUrl($request->getResponseHeaders()) ) { phpCAS::trace('Found redirect:' . $redirectUrl); $this->makeRequest($redirectUrl); } else { $this->_responseHeaders = $request->getResponseHeaders(); $this->_responseBody = $request->getResponseBody(); $this->_responseStatusCode = $request->getResponseStatusCode(); } } /** * Add any other parts of the request needed by concrete classes * * @param CAS_Request_RequestInterface $request request interface object * * @return void */ abstract protected function populateRequest( CAS_Request_RequestInterface $request ); /** * Answer a redirect URL if a redirect header is found, otherwise null. * * @param array $responseHeaders response header to extract a redirect from * * @return string|null */ protected function getRedirectUrl(array $responseHeaders) { // Check for the redirect after authentication foreach ($responseHeaders as $header) { if ( preg_match('/^(Location:|URI:)\s*([^\s]+.*)$/', $header, $matches) ) { return trim(array_pop($matches)); } } return null; } /********************************************************* * 3. Access the response *********************************************************/ /** * Answer true if our request has been sent yet. * * @return bool */ protected function hasBeenSent() { return ($this->_numRequests > 0); } /** * Answer the headers of the response. * * @return array An array of header strings. * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseHeaders() { if (!$this->hasBeenSent()) { throw new CAS_OutOfSequenceException( 'Cannot access response, request not sent yet.' ); } return $this->_responseHeaders; } /** * Answer HTTP status code of the response * * @return int * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseStatusCode() { if (!$this->hasBeenSent()) { throw new CAS_OutOfSequenceException( 'Cannot access response, request not sent yet.' ); } return $this->_responseStatusCode; } /** * Answer the body of response. * * @return string * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseBody() { if (!$this->hasBeenSent()) { throw new CAS_OutOfSequenceException( 'Cannot access response, request not sent yet.' ); } return $this->_responseBody; } /** * Answer the cookies from the response. This may include cookies set during * redirect responses. * * @return array An array containing cookies. E.g. array('name' => 'val'); */ public function getCookies() { return $this->_cookieJar->getCookies($this->getServiceUrl()); } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxiedService/Http/Post.php 0000644 00000011433 15152311435 0021166 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxiedService/Http/Post.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class is used to make proxied service requests via the HTTP POST method. * * Usage Example: * * try { * $service = phpCAS::getProxiedService(PHPCAS_PROXIED_SERVICE_HTTP_POST); * $service->setUrl('http://www.example.com/path/'); * $service->setContentType('text/xml'); * $service->setBody('<?xml version="1.0"?'.'><methodCall><methodName>example.search</methodName></methodCall>'); * $service->send(); * if ($service->getResponseStatusCode() == 200) * return $service->getResponseBody(); * else * // The service responded with an error code 404, 500, etc. * throw new Exception('The service responded with an error.'); * * } catch (CAS_ProxyTicketException $e) { * if ($e->getCode() == PHPCAS_SERVICE_PT_FAILURE) * return "Your login has timed out. You need to log in again."; * else * // Other proxy ticket errors are from bad request format * // (shouldn't happen) or CAS server failure (unlikely) so lets just * // stop if we hit those. * throw $e; * } catch (CAS_ProxiedService_Exception $e) { * // Something prevented the service request from being sent or received. * // We didn't even get a valid error response (404, 500, etc), so this * // might be caused by a network error or a DNS resolution failure. * // We could handle it in some way, but for now we will just stop. * throw $e; * } * * @class CAS_ProxiedService_Http_Post * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ProxiedService_Http_Post extends CAS_ProxiedService_Http_Abstract { /** * The content-type of this request * * @var string $_contentType */ private $_contentType; /** * The body of the this request * * @var string $_body */ private $_body; /** * Set the content type of this POST request. * * @param string $contentType content type * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setContentType ($contentType) { if ($this->hasBeenSent()) { throw new CAS_OutOfSequenceException( 'Cannot set the content type, request already sent.' ); } $this->_contentType = $contentType; } /** * Set the body of this POST request. * * @param string $body body to set * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setBody ($body) { if ($this->hasBeenSent()) { throw new CAS_OutOfSequenceException( 'Cannot set the body, request already sent.' ); } $this->_body = $body; } /** * Add any other parts of the request needed by concrete classes * * @param CAS_Request_RequestInterface $request request interface class * * @return void */ protected function populateRequest (CAS_Request_RequestInterface $request) { if (empty($this->_contentType) && !empty($this->_body)) { throw new CAS_ProxiedService_Exception( "If you pass a POST body, you must specify a content type via " .get_class($this).'->setContentType($contentType).' ); } $request->makePost(); if (!empty($this->_body)) { $request->addHeader('Content-Type: '.$this->_contentType); $request->addHeader('Content-Length: '.strlen($this->_body)); $request->setPostBody($this->_body); } } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxiedService/Imap.php 0000644 00000020013 15152311435 0020202 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxiedService/Imap.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Provides access to a proxy-authenticated IMAP stream * * @class CAS_ProxiedService_Imap * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ProxiedService_Imap extends CAS_ProxiedService_Abstract { /** * The username to send via imap_open. * * @var string $_username; */ private $_username; /** * Constructor. * * @param string $username Username * * @return void */ public function __construct ($username) { if (!is_string($username) || !strlen($username)) { throw new CAS_InvalidArgumentException('Invalid username.'); } $this->_username = $username; } /** * The target service url. * @var string $_url; */ private $_url; /** * Answer a service identifier (URL) for whom we should fetch a proxy ticket. * * @return string * @throws Exception If no service url is available. */ public function getServiceUrl () { if (empty($this->_url)) { throw new CAS_ProxiedService_Exception( 'No URL set via '.get_class($this).'->getServiceUrl($url).' ); } return $this->_url; } /********************************************************* * Configure the Stream *********************************************************/ /** * Set the URL of the service to pass to CAS for proxy-ticket retrieval. * * @param string $url Url to set * * @return void * @throws CAS_OutOfSequenceException If called after the stream has been opened. */ public function setServiceUrl ($url) { if ($this->hasBeenOpened()) { throw new CAS_OutOfSequenceException( 'Cannot set the URL, stream already opened.' ); } if (!is_string($url) || !strlen($url)) { throw new CAS_InvalidArgumentException('Invalid url.'); } $this->_url = $url; } /** * The mailbox to open. See the $mailbox parameter of imap_open(). * * @var string $_mailbox */ private $_mailbox; /** * Set the mailbox to open. See the $mailbox parameter of imap_open(). * * @param string $mailbox Mailbox to set * * @return void * @throws CAS_OutOfSequenceException If called after the stream has been opened. */ public function setMailbox ($mailbox) { if ($this->hasBeenOpened()) { throw new CAS_OutOfSequenceException( 'Cannot set the mailbox, stream already opened.' ); } if (!is_string($mailbox) || !strlen($mailbox)) { throw new CAS_InvalidArgumentException('Invalid mailbox.'); } $this->_mailbox = $mailbox; } /** * A bit mask of options to pass to imap_open() as the $options parameter. * * @var int $_options */ private $_options = null; /** * Set the options for opening the stream. See the $options parameter of * imap_open(). * * @param int $options Options for the stream * * @return void * @throws CAS_OutOfSequenceException If called after the stream has been opened. */ public function setOptions ($options) { if ($this->hasBeenOpened()) { throw new CAS_OutOfSequenceException( 'Cannot set options, stream already opened.' ); } if (!is_int($options)) { throw new CAS_InvalidArgumentException('Invalid options.'); } $this->_options = $options; } /********************************************************* * 2. Open the stream *********************************************************/ /** * Open the IMAP stream (similar to imap_open()). * * @return resource Returns an IMAP stream on success * @throws CAS_OutOfSequenceException If called multiple times. * @throws CAS_ProxyTicketException If there is a proxy-ticket failure. * The code of the Exception will be one of: * PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE * PHPCAS_SERVICE_PT_FAILURE * @throws CAS_ProxiedService_Exception If there is a failure sending the * request to the target service. */ public function open () { if ($this->hasBeenOpened()) { throw new CAS_OutOfSequenceException('Stream already opened.'); } if (empty($this->_mailbox)) { throw new CAS_ProxiedService_Exception( 'You must specify a mailbox via '.get_class($this) .'->setMailbox($mailbox)' ); } phpCAS::traceBegin(); // Get our proxy ticket and append it to our URL. $this->initializeProxyTicket(); phpCAS::trace('opening IMAP mailbox `'.$this->_mailbox.'\'...'); $this->_stream = @imap_open( $this->_mailbox, $this->_username, $this->getProxyTicket(), $this->_options ); if ($this->_stream) { phpCAS::trace('ok'); } else { phpCAS::trace('could not open mailbox'); // @todo add localization integration. $message = 'IMAP Error: '.$this->_url.' '. var_export(imap_errors(), true); phpCAS::trace($message); throw new CAS_ProxiedService_Exception($message); } phpCAS::traceEnd(); return $this->_stream; } /** * Answer true if our request has been sent yet. * * @return bool */ protected function hasBeenOpened () { return !empty($this->_stream); } /********************************************************* * 3. Access the result *********************************************************/ /** * The IMAP stream * * @var resource $_stream */ private $_stream; /** * Answer the IMAP stream * * @return resource * @throws CAS_OutOfSequenceException if stream is not opened yet */ public function getStream () { if (!$this->hasBeenOpened()) { throw new CAS_OutOfSequenceException( 'Cannot access stream, not opened yet.' ); } return $this->_stream; } /** * CAS_Client::serviceMail() needs to return the proxy ticket for some reason, * so this method provides access to it. * * @return string * @throws CAS_OutOfSequenceException If called before the stream has been * opened. */ public function getImapProxyTicket () { if (!$this->hasBeenOpened()) { throw new CAS_OutOfSequenceException( 'Cannot access errors, stream not opened yet.' ); } return $this->getProxyTicket(); } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxiedService/Http.php 0000644 00000005500 15152311435 0020237 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxiedService/Http.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This interface defines methods that clients should use for configuring, sending, * and receiving proxied HTTP requests. * * @class CAS_ProxiedService_Http * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ interface CAS_ProxiedService_Http { /********************************************************* * Configure the Request *********************************************************/ /** * Set the URL of the Request * * @param string $url Url to set * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setUrl ($url); /********************************************************* * 2. Send the Request *********************************************************/ /** * Perform the request. * * @return bool TRUE on success, FALSE on failure. * @throws CAS_OutOfSequenceException If called multiple times. */ public function send (); /********************************************************* * 3. Access the response *********************************************************/ /** * Answer the headers of the response. * * @return array An array of header strings. * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseHeaders (); /** * Answer the body of response. * * @return string * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseBody (); } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/ProxyChain.php 0000644 00000010402 15152311435 0016446 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/ProxyChain.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * A normal proxy-chain definition that lists each level of the chain as either * a string or regular expression. * * @class CAS_ProxyChain * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_ProxyChain implements CAS_ProxyChain_Interface { protected $chain = array(); /** * A chain is an array of strings or regexp strings that will be matched * against. Regexp will be matched with preg_match and strings will be * matched from the beginning. A string must fully match the beginning of * an proxy url. So you can define a full domain as acceptable or go further * down. * Proxies have to be defined in reverse from the service to the user. If a * user hits service A get proxied via B to service C the list of acceptable * proxies on C would be array(B,A); * * @param array $chain A chain of proxies */ public function __construct(array $chain) { // Ensure that we have an indexed array $this->chain = array_values($chain); } /** * Match a list of proxies. * * @param array $list The list of proxies in front of this service. * * @return bool */ public function matches(array $list) { $list = array_values($list); // Ensure that we have an indexed array if ($this->isSizeValid($list)) { $mismatch = false; foreach ($this->chain as $i => $search) { $proxy_url = $list[$i]; if (preg_match('/^\/.*\/[ixASUXu]*$/s', $search)) { if (preg_match($search, $proxy_url)) { phpCAS::trace( "Found regexp " . $search . " matching " . $proxy_url ); } else { phpCAS::trace( "No regexp match " . $search . " != " . $proxy_url ); $mismatch = true; break; } } else { if (strncasecmp($search, $proxy_url, strlen($search)) == 0) { phpCAS::trace( "Found string " . $search . " matching " . $proxy_url ); } else { phpCAS::trace( "No match " . $search . " != " . $proxy_url ); $mismatch = true; break; } } } if (!$mismatch) { phpCAS::trace("Proxy chain matches"); return true; } } else { phpCAS::trace("Proxy chain skipped: size mismatch"); } return false; } /** * Validate the size of the the list as compared to our chain. * * @param array $list List of proxies * * @return bool */ protected function isSizeValid (array $list) { return (sizeof($this->chain) == sizeof($list)); } } cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/Portuguese.php 0000644 00000006452 15152311435 0020444 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/Portuguese.php * @category Authentication * @package PhpCAS * @author Sherwin Harris <sherwin.harris@gmail.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://apereo.atlassian.net/wiki/spaces/CASC/pages/103252517/phpCAS */ /** * Portuguese language class * * @class CAS_Languages_Portuguese * @category Authentication * @package PhpCAS * @author Sherwin Harris <sherwin.harris@gmail.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://apereo.atlassian.net/wiki/spaces/CASC/pages/103252517/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ class CAS_Languages_Portuguese implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return 'Usando o servidor'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return 'A autenticação do servidor CAS desejado!'; } /** * Get logout string * * @return string logout */ public function getLogout() { return 'Saida do servidor CAS desejado!'; } /** * Get the should have been redirected string * * @return string should have been redirected */ public function getShouldHaveBeenRedirected() { return 'Você já deve ter sido redirecionado para o servidor CAS. Clique <a href="%s">aqui</a> para continuar'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return 'A autenticação do servidor CAS falheu!'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return '<p>Você não foi autenticado.</p><p>Você pode enviar sua solicitação novamente clicando <a href="%s">aqui</a>. </p><p>Se o problema persistir, você pode entrar em contato com <a href="mailto:%s">o administrador deste site</a>.</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return 'O serviço `<b>%s</b>\' não está disponível (<b>%s</b>).'; } } cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/Spanish.php 0000644 00000006336 15152311435 0017710 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/Spanish.php * @category Authentication * @package PhpCAS * @author Iván-Benjamín García Torà <ivaniclixx@gmail.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Spanish language class * * @class CAS_Languages_Spanish * @category Authentication * @package PhpCAS * @author Iván-Benjamín García Torà <ivaniclixx@gmail.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ class CAS_Languages_Spanish implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return 'usando servidor'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return '¡Autentificación CAS necesaria!'; } /** * Get logout string * * @return string logout */ public function getLogout() { return '¡Salida CAS necesaria!'; } /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected() { return 'Ya debería haber sido redireccionado al servidor CAS. Haga click <a href="%s">aquí</a> para continuar.'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return '¡Autentificación CAS fallida!'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return '<p>No estás autentificado.</p><p>Puedes volver a intentarlo haciendo click <a href="%s">aquí</a>.</p><p>Si el problema persiste debería contactar con el <a href="mailto:%s">administrador de este sitio</a>.</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return 'El servicio `<b>%s</b>\' no está disponible (<b>%s</b>).'; } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/Galego.php 0000644 00000006303 15152311435 0017473 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/Galego.php * @category Authentication * @package PhpCAS * @author Enrique Huelva Rivero enrique.huelvarivero@plexus.es * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Galego language class * * @class CAS_Languages_Galego * @category Authentication * @package PhpCAS * @author Enrique Huelva Rivero enrique.huelvarivero@plexus.es * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ class CAS_Languages_Galego implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return 'usando servidor'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return 'Autenticación CAS necesaria!'; } /** * Get logout string * * @return string logout */ public function getLogout() { return 'Saída CAS necesaria!'; } /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected() { return 'Xa debería ser redireccionado ao servidor CAS. Faga click <a href="%s">aquí</a> para continuar'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return 'Autenticación CAS errada!'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return ' <p>Non estás autenticado</p><p>Podes volver tentalo facendo click <a href="%s">aquí</a>.</p><p>Se o problema persiste debería contactar con el <a href="mailto:%s">administrador deste sitio</a>.</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return 'O servizo `<b>%s</b>\' non está dispoñible (<b>%s</b>).'; } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/Catalan.php 0000644 00000006273 15152311435 0017646 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/Catalan.php * @category Authentication * @package PhpCAS * @author Iván-Benjamín García Torà <ivaniclixx@gmail.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Catalan language class * * @class CAS_Languages_Catalan * @category Authentication * @package PhpCAS * @author Iván-Benjamín García Torà <ivaniclixx@gmail.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ class CAS_Languages_Catalan implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return 'usant servidor'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return 'Autentificació CAS necessària!'; } /** * Get logout string * * @return string logout */ public function getLogout() { return 'Sortida de CAS necessària!'; } /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected() { return 'Ja hauria d\ haver estat redireccionat al servidor CAS. Feu click <a href="%s">aquí</a> per a continuar.'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return 'Autentificació CAS fallida!'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return '<p>No estàs autentificat.</p><p>Pots tornar a intentar-ho fent click <a href="%s">aquí</a>.</p><p>Si el problema persisteix hauría de contactar amb l\'<a href="mailto:%s">administrador d\'aquest llocc</a>.</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return 'El servei `<b>%s</b>\' no està disponible (<b>%s</b>).'; } } cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/LanguageInterface.php 0000644 00000005063 15152311435 0021643 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/LanguageInterface.php * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Language Interface class for all internationalization files * * @class CAS_Languages_LanguageInterface * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ interface CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer(); /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted(); /** * Get logout string * * @return string logout */ public function getLogout(); /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected(); /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed(); /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated(); /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable(); } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/Japanese.php 0000644 00000006425 15152311435 0020030 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/Japanese.php * @category Authentication * @package PhpCAS * @author fnorif <fnorif@yahoo.co.jp> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Japanese language class. Now Encoding is UTF-8. * * @class CAS_Languages_Japanese * @category Authentication * @package PhpCAS * @author fnorif <fnorif@yahoo.co.jp> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * **/ class CAS_Languages_Japanese implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return 'サーバーを使っています。'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return 'CASによる認証を行います。'; } /** * Get logout string * * @return string logout */ public function getLogout() { return 'CASからログアウトします!'; } /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected() { return 'CASサーバに行く必要があります。自動的に転送されない場合は <a href="%s">こちら</a> をクリックして続行します。'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return 'CASによる認証に失敗しました。'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return '<p>認証できませんでした。</p><p>もう一度リクエストを送信する場合は<a href="%s">こちら</a>をクリック。</p><p>問題が解決しない場合は <a href="mailto:%s">このサイトの管理者</a>に問い合わせてください。</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return 'サービス `<b>%s</b>\' は利用できません (<b>%s</b>)。'; } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/Greek.php 0000644 00000006752 15152311435 0017342 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/Greek.php * @category Authentication * @package PhpCAS * @author Vangelis Haniotakis <haniotak@ucnet.uoc.gr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Greek language class * * @class CAS_Languages_Greek * @category Authentication * @package PhpCAS * @author Vangelis Haniotakis <haniotak@ucnet.uoc.gr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ class CAS_Languages_Greek implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return 'χρησιμοποιείται ο εξυπηρετητής'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return 'Απαιτείται η ταυτοποίηση CAS!'; } /** * Get logout string * * @return string logout */ public function getLogout() { return 'Απαιτείται η αποσύνδεση από CAS!'; } /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected() { return 'Θα έπρεπε να είχατε ανακατευθυνθεί στον εξυπηρετητή CAS. Κάντε κλίκ <a href="%s">εδώ</a> για να συνεχίσετε.'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return 'Η ταυτοποίηση CAS απέτυχε!'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return '<p>Δεν ταυτοποιηθήκατε.</p><p>Μπορείτε να ξαναπροσπαθήσετε, κάνοντας κλίκ <a href="%s">εδώ</a>.</p><p>Εαν το πρόβλημα επιμείνει, ελάτε σε επαφή με τον <a href="mailto:%s">διαχειριστή</a>.</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return 'Η υπηρεσία `<b>%s</b>\' δεν είναι διαθέσιμη (<b>%s</b>).'; } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/ChineseSimplified.php 0000644 00000006275 15152311435 0021671 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/ChineseSimplified.php * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr>, Phy25 <caslang@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Chinese Simplified language class * * @class CAS_Languages_ChineseSimplified * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr>, Phy25 <caslang@phy25.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ class CAS_Languages_ChineseSimplified implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return '连接的服务器'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return '请进行 CAS 认证!'; } /** * Get logout string * * @return string logout */ public function getLogout() { return '请进行 CAS 登出!'; } /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected() { return '你正被重定向到 CAS 服务器。<a href="%s">点击这里</a>继续。'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return 'CAS 认证失败!'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return '<p>你没有成功登录。</p><p>你可以<a href="%s">点击这里重新登录</a>。</p><p>如果问题依然存在,请<a href="mailto:%s">联系本站管理员</a>。</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return '服务器 <b>%s</b> 不可用(<b>%s</b>)。'; } } cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/German.php 0000644 00000006306 15152311435 0017511 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/German.php * @category Authentication * @package PhpCAS * @author Henrik Genssen <hg@mediafactory.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * German language class * * @class CAS_Languages_German * @category Authentication * @package PhpCAS * @author Henrik Genssen <hg@mediafactory.de> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ class CAS_Languages_German implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return 'via Server'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return 'CAS Authentifizierung erforderlich!'; } /** * Get logout string * * @return string logout */ public function getLogout() { return 'CAS Abmeldung!'; } /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected() { return 'eigentlich häten Sie zum CAS Server weitergeleitet werden sollen. Drücken Sie <a href="%s">hier</a> um fortzufahren.'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return 'CAS Anmeldung fehlgeschlagen!'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return '<p>Sie wurden nicht angemeldet.</p><p>Um es erneut zu versuchen klicken Sie <a href="%s">hier</a>.</p><p>Wenn das Problem bestehen bleibt, kontaktieren Sie den <a href="mailto:%s">Administrator</a> dieser Seite.</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return 'Der Dienst `<b>%s</b>\' ist nicht verfügbar (<b>%s</b>).'; } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/French.php 0000644 00000006361 15152311435 0017506 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/French.php * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * French language class * * @class CAS_Languages_French * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ class CAS_Languages_French implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return 'utilisant le serveur'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return 'Authentication CAS nécessaire !'; } /** * Get logout string * * @return string logout */ public function getLogout() { return 'Déconnexion demandée !'; } /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected() { return 'Vous auriez du etre redirigé(e) vers le serveur CAS. Cliquez <a href="%s">ici</a> pour continuer.'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return 'Authentification CAS infructueuse !'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return '<p>Vous n\'avez pas été authentifié(e).</p><p>Vous pouvez soumettre votre requete à nouveau en cliquant <a href="%s">ici</a>.</p><p>Si le problème persiste, vous pouvez contacter <a href="mailto:%s">l\'administrateur de ce site</a>.</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return 'Le service `<b>%s</b>\' est indisponible (<b>%s</b>)'; } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/Languages/English.php 0000644 00000006237 15152311435 0017674 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Language/English.php * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * English language class * * @class CAS_Languages_English * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @sa @link internalLang Internationalization @endlink * @ingroup internalLang */ class CAS_Languages_English implements CAS_Languages_LanguageInterface { /** * Get the using server string * * @return string using server */ public function getUsingServer() { return 'using server'; } /** * Get authentication wanted string * * @return string authentication wanted */ public function getAuthenticationWanted() { return 'CAS Authentication wanted!'; } /** * Get logout string * * @return string logout */ public function getLogout() { return 'CAS logout wanted!'; } /** * Get the should have been redirected string * * @return string should habe been redirected */ public function getShouldHaveBeenRedirected() { return 'You should already have been redirected to the CAS server. Click <a href="%s">here</a> to continue.'; } /** * Get authentication failed string * * @return string authentication failed */ public function getAuthenticationFailed() { return 'CAS Authentication failed!'; } /** * Get the your were not authenticated string * * @return string not authenticated */ public function getYouWereNotAuthenticated() { return '<p>You were not authenticated.</p><p>You may submit your request again by clicking <a href="%s">here</a>.</p><p>If the problem persists, you may contact <a href="mailto:%s">the administrator of this site</a>.</p>'; } /** * Get the service unavailable string * * @return string service unavailable */ public function getServiceUnavailable() { return 'The service `<b>%s</b>\' is not available (<b>%s</b>).'; } } cas/CAS/vendor/apereo/phpcas/source/CAS/OutOfSequenceException.php 0000644 00000003232 15152311435 0020771 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * * PHP Version 7 * * @file CAS/OutOfSequenceException.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This class defines Exceptions that should be thrown when the sequence of * operations is invalid. Examples are: * - Requesting the response before executing a request. * - Changing the URL of a request after executing the request. * * @class CAS_OutOfSequenceException * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_OutOfSequenceException extends BadMethodCallException implements CAS_Exception { } cas/CAS/vendor/apereo/phpcas/source/CAS/PGTStorage/File.php 0000644 00000017206 15152311435 0017231 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/PGTStorage/AbstractStorage.php * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * The CAS_PGTStorage_File class is a class for PGT file storage. An instance of * this class is returned by CAS_Client::SetPGTStorageFile(). * * @class CAS_PGTStorage_File * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * * @ingroup internalPGTStorageFile */ class CAS_PGTStorage_File extends CAS_PGTStorage_AbstractStorage { /** * @addtogroup internalPGTStorageFile * @{ */ /** * a string telling where PGT's should be stored on the filesystem. Written by * PGTStorageFile::PGTStorageFile(), read by getPath(). * * @private */ var $_path; /** * This method returns the name of the directory where PGT's should be stored * on the filesystem. * * @return string the name of a directory (with leading and trailing '/') * * @private */ function getPath() { return $this->_path; } // ######################################################################## // DEBUGGING // ######################################################################## /** * This method returns an informational string giving the type of storage * used by the object (used for debugging purposes). * * @return string an informational string. * @public */ function getStorageType() { return "file"; } /** * This method returns an informational string giving informations on the * parameters of the storage.(used for debugging purposes). * * @return string an informational string. * @public */ function getStorageInfo() { return 'path=`'.$this->getPath().'\''; } // ######################################################################## // CONSTRUCTOR // ######################################################################## /** * The class constructor, called by CAS_Client::SetPGTStorageFile(). * * @param CAS_Client $cas_parent the CAS_Client instance that creates the object. * @param string $path the path where the PGT's should be stored * * @return void * * @public */ function __construct($cas_parent,$path) { phpCAS::traceBegin(); // call the ancestor's constructor parent::__construct($cas_parent); if (empty($path)) { $path = CAS_PGT_STORAGE_FILE_DEFAULT_PATH; } // check that the path is an absolute path if (getenv("OS")=="Windows_NT" || strtoupper(substr(PHP_OS,0,3)) == 'WIN') { if (!preg_match('`^[a-zA-Z]:`', $path)) { phpCAS::error('an absolute path is needed for PGT storage to file'); } } else { if ( $path[0] != '/' ) { phpCAS::error('an absolute path is needed for PGT storage to file'); } // store the path (with a leading and trailing '/') $path = preg_replace('|[/]*$|', '/', $path); $path = preg_replace('|^[/]*|', '/', $path); } $this->_path = $path; phpCAS::traceEnd(); } // ######################################################################## // INITIALIZATION // ######################################################################## /** * This method is used to initialize the storage. Halts on error. * * @return void * @public */ function init() { phpCAS::traceBegin(); // if the storage has already been initialized, return immediatly if ($this->isInitialized()) { return; } // call the ancestor's method (mark as initialized) parent::init(); phpCAS::traceEnd(); } // ######################################################################## // PGT I/O // ######################################################################## /** * This method returns the filename corresponding to a PGT Iou. * * @param string $pgt_iou the PGT iou. * * @return string a filename * @private */ function getPGTIouFilename($pgt_iou) { phpCAS::traceBegin(); $filename = $this->getPath()."phpcas-".hash("sha256", $pgt_iou); // $filename = $this->getPath().$pgt_iou.'.plain'; phpCAS::trace("Sha256 filename:" . $filename); phpCAS::traceEnd(); return $filename; } /** * This method stores a PGT and its corresponding PGT Iou into a file. Echoes a * warning on error. * * @param string $pgt the PGT * @param string $pgt_iou the PGT iou * * @return void * * @public */ function write($pgt,$pgt_iou) { phpCAS::traceBegin(); $fname = $this->getPGTIouFilename($pgt_iou); if (!file_exists($fname)) { touch($fname); // Chmod will fail on windows @chmod($fname, 0600); if ($f=fopen($fname, "w")) { if (fputs($f, $pgt) === false) { phpCAS::error('could not write PGT to `'.$fname.'\''); } phpCAS::trace('Successful write of PGT to `'.$fname.'\''); fclose($f); } else { phpCAS::error('could not open `'.$fname.'\''); } } else { phpCAS::error('File exists: `'.$fname.'\''); } phpCAS::traceEnd(); } /** * This method reads a PGT corresponding to a PGT Iou and deletes the * corresponding file. * * @param string $pgt_iou the PGT iou * * @return string|false the corresponding PGT, or FALSE on error * * @public */ function read($pgt_iou) { phpCAS::traceBegin(); $pgt = false; $fname = $this->getPGTIouFilename($pgt_iou); if (file_exists($fname)) { if (!($f=fopen($fname, "r"))) { phpCAS::error('could not open `'.$fname.'\''); } else { if (($pgt=fgets($f)) === false) { phpCAS::error('could not read PGT from `'.$fname.'\''); } phpCAS::trace('Successful read of PGT to `'.$fname.'\''); fclose($f); } // delete the PGT file @unlink($fname); } else { phpCAS::error('No such file `'.$fname.'\''); } phpCAS::traceEnd($pgt); return $pgt; } /** @} */ } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/PGTStorage/Db.php 0000644 00000031366 15152311435 0016702 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/PGTStorage/Db.php * @category Authentication * @package PhpCAS * @author Daniel Frett <daniel.frett@gmail.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ define('CAS_PGT_STORAGE_DB_DEFAULT_TABLE', 'cas_pgts'); /** * Basic class for PGT database storage * The CAS_PGTStorage_Db class is a class for PGT database storage. * * @class CAS_PGTStorage_Db * @category Authentication * @package PhpCAS * @author Daniel Frett <daniel.frett@gmail.com> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @ingroup internalPGTStorageDb */ class CAS_PGTStorage_Db extends CAS_PGTStorage_AbstractStorage { /** * @addtogroup internalCAS_PGTStorageDb * @{ */ /** * the PDO object to use for database interactions */ private $_pdo; /** * This method returns the PDO object to use for database interactions. * * @return PDO object */ private function _getPdo() { return $this->_pdo; } /** * database connection options to use when creating a new PDO object */ private $_dsn; private $_username; private $_password; private $_driver_options; /** * @var string the table to use for storing/retrieving pgt's */ private $_table; /** * This method returns the table to use when storing/retrieving PGT's * * @return string the name of the pgt storage table. */ private function _getTable() { return $this->_table; } // ######################################################################## // DEBUGGING // ######################################################################## /** * This method returns an informational string giving the type of storage * used by the object (used for debugging purposes). * * @return string an informational string. */ public function getStorageType() { return "db"; } /** * This method returns an informational string giving informations on the * parameters of the storage.(used for debugging purposes). * * @return string an informational string. * @public */ public function getStorageInfo() { return 'table=`'.$this->_getTable().'\''; } // ######################################################################## // CONSTRUCTOR // ######################################################################## /** * The class constructor. * * @param CAS_Client $cas_parent the CAS_Client instance that creates * the object. * @param string $dsn_or_pdo a dsn string to use for creating a PDO * object or a PDO object * @param string $username the username to use when connecting to * the database * @param string $password the password to use when connecting to * the database * @param string $table the table to use for storing and * retrieving PGT's * @param string $driver_options any driver options to use when * connecting to the database */ public function __construct( $cas_parent, $dsn_or_pdo, $username='', $password='', $table='', $driver_options=null ) { phpCAS::traceBegin(); // call the ancestor's constructor parent::__construct($cas_parent); // set default values if ( empty($table) ) { $table = CAS_PGT_STORAGE_DB_DEFAULT_TABLE; } if ( !is_array($driver_options) ) { $driver_options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); } // store the specified parameters if ($dsn_or_pdo instanceof PDO) { $this->_pdo = $dsn_or_pdo; } else { $this->_dsn = $dsn_or_pdo; $this->_username = $username; $this->_password = $password; $this->_driver_options = $driver_options; } // store the table name $this->_table = $table; phpCAS::traceEnd(); } // ######################################################################## // INITIALIZATION // ######################################################################## /** * This method is used to initialize the storage. Halts on error. * * @return void */ public function init() { phpCAS::traceBegin(); // if the storage has already been initialized, return immediatly if ($this->isInitialized()) { return; } // initialize the base object parent::init(); // create the PDO object if it doesn't exist already if (!($this->_pdo instanceof PDO)) { try { $this->_pdo = new PDO( $this->_dsn, $this->_username, $this->_password, $this->_driver_options ); } catch(PDOException $e) { phpCAS::error('Database connection error: ' . $e->getMessage()); } } phpCAS::traceEnd(); } // ######################################################################## // PDO database interaction // ######################################################################## /** * attribute that stores the previous error mode for the PDO handle while * processing a transaction */ private $_errMode; /** * This method will enable the Exception error mode on the PDO object * * @return void */ private function _setErrorMode() { // get PDO object and enable exception error mode $pdo = $this->_getPdo(); $this->_errMode = $pdo->getAttribute(PDO::ATTR_ERRMODE); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } /** * this method will reset the error mode on the PDO object * * @return void */ private function _resetErrorMode() { // get PDO object and reset the error mode to what it was originally $pdo = $this->_getPdo(); $pdo->setAttribute(PDO::ATTR_ERRMODE, $this->_errMode); } // ######################################################################## // database queries // ######################################################################## // these queries are potentially unsafe because the person using this library // can set the table to use, but there is no reliable way to escape SQL // fieldnames in PDO yet /** * This method returns the query used to create a pgt storage table * * @return string the create table SQL, no bind params in query */ protected function createTableSql() { return 'CREATE TABLE ' . $this->_getTable() . ' (pgt_iou VARCHAR(255) NOT NULL PRIMARY KEY, pgt VARCHAR(255) NOT NULL)'; } /** * This method returns the query used to store a pgt * * @return string the store PGT SQL, :pgt and :pgt_iou are the bind params contained * in the query */ protected function storePgtSql() { return 'INSERT INTO ' . $this->_getTable() . ' (pgt_iou, pgt) VALUES (:pgt_iou, :pgt)'; } /** * This method returns the query used to retrieve a pgt. the first column * of the first row should contain the pgt * * @return string the retrieve PGT SQL, :pgt_iou is the only bind param contained * in the query */ protected function retrievePgtSql() { return 'SELECT pgt FROM ' . $this->_getTable() . ' WHERE pgt_iou = :pgt_iou'; } /** * This method returns the query used to delete a pgt. * * @return string the delete PGT SQL, :pgt_iou is the only bind param contained in * the query */ protected function deletePgtSql() { return 'DELETE FROM ' . $this->_getTable() . ' WHERE pgt_iou = :pgt_iou'; } // ######################################################################## // PGT I/O // ######################################################################## /** * This method creates the database table used to store pgt's and pgtiou's * * @return void */ public function createTable() { phpCAS::traceBegin(); // initialize this PGTStorage object if it hasn't been initialized yet if ( !$this->isInitialized() ) { $this->init(); } // initialize the PDO object for this method $pdo = $this->_getPdo(); $this->_setErrorMode(); try { $pdo->beginTransaction(); $query = $pdo->query($this->createTableSQL()); $query->closeCursor(); $pdo->commit(); } catch(PDOException $e) { // attempt rolling back the transaction before throwing a phpCAS error try { $pdo->rollBack(); } catch(PDOException $e) { } phpCAS::error('error creating PGT storage table: ' . $e->getMessage()); } // reset the PDO object $this->_resetErrorMode(); phpCAS::traceEnd(); } /** * This method stores a PGT and its corresponding PGT Iou in the database. * Echoes a warning on error. * * @param string $pgt the PGT * @param string $pgt_iou the PGT iou * * @return void */ public function write($pgt, $pgt_iou) { phpCAS::traceBegin(); // initialize the PDO object for this method $pdo = $this->_getPdo(); $this->_setErrorMode(); try { $pdo->beginTransaction(); $query = $pdo->prepare($this->storePgtSql()); $query->bindValue(':pgt', $pgt, PDO::PARAM_STR); $query->bindValue(':pgt_iou', $pgt_iou, PDO::PARAM_STR); $query->execute(); $query->closeCursor(); $pdo->commit(); } catch(PDOException $e) { // attempt rolling back the transaction before throwing a phpCAS error try { $pdo->rollBack(); } catch(PDOException $e) { } phpCAS::error('error writing PGT to database: ' . $e->getMessage()); } // reset the PDO object $this->_resetErrorMode(); phpCAS::traceEnd(); } /** * This method reads a PGT corresponding to a PGT Iou and deletes the * corresponding db entry. * * @param string $pgt_iou the PGT iou * * @return string|false the corresponding PGT, or FALSE on error */ public function read($pgt_iou) { phpCAS::traceBegin(); $pgt = false; // initialize the PDO object for this method $pdo = $this->_getPdo(); $this->_setErrorMode(); try { $pdo->beginTransaction(); // fetch the pgt for the specified pgt_iou $query = $pdo->prepare($this->retrievePgtSql()); $query->bindValue(':pgt_iou', $pgt_iou, PDO::PARAM_STR); $query->execute(); $pgt = $query->fetchColumn(0); $query->closeCursor(); // delete the specified pgt_iou from the database $query = $pdo->prepare($this->deletePgtSql()); $query->bindValue(':pgt_iou', $pgt_iou, PDO::PARAM_STR); $query->execute(); $query->closeCursor(); $pdo->commit(); } catch(PDOException $e) { // attempt rolling back the transaction before throwing a phpCAS error try { $pdo->rollBack(); } catch(PDOException $e) { } phpCAS::trace('error reading PGT from database: ' . $e->getMessage()); } // reset the PDO object $this->_resetErrorMode(); phpCAS::traceEnd(); return $pgt; } /** @} */ } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/PGTStorage/AbstractStorage.php 0000644 00000014007 15152311435 0021436 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/PGTStorage/AbstractStorage.php * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Basic class for PGT storage * The CAS_PGTStorage_AbstractStorage class is a generic class for PGT storage. * This class should not be instanciated itself but inherited by specific PGT * storage classes. * * @class CAS_PGTStorage_AbstractStorage * @category Authentication * @package PhpCAS * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS * * @ingroup internalPGTStorage */ abstract class CAS_PGTStorage_AbstractStorage { /** * @addtogroup internalPGTStorage * @{ */ // ######################################################################## // CONSTRUCTOR // ######################################################################## /** * The constructor of the class, should be called only by inherited classes. * * @param CAS_Client $cas_parent the CAS _client instance that creates the * current object. * * @return void * * @protected */ function __construct($cas_parent) { phpCAS::traceBegin(); if ( !$cas_parent->isProxy() ) { phpCAS::error( 'defining PGT storage makes no sense when not using a CAS proxy' ); } phpCAS::traceEnd(); } // ######################################################################## // DEBUGGING // ######################################################################## /** * This virtual method returns an informational string giving the type of storage * used by the object (used for debugging purposes). * * @return string * * @public */ function getStorageType() { phpCAS::error(__CLASS__.'::'.__FUNCTION__.'() should never be called'); } /** * This virtual method returns an informational string giving informations on the * parameters of the storage.(used for debugging purposes). * * @return string * * @public */ function getStorageInfo() { phpCAS::error(__CLASS__.'::'.__FUNCTION__.'() should never be called'); } // ######################################################################## // ERROR HANDLING // ######################################################################## /** * string used to store an error message. Written by * PGTStorage::setErrorMessage(), read by PGTStorage::getErrorMessage(). * * @hideinitializer * @deprecated not used. */ var $_error_message=false; /** * This method sets en error message, which can be read later by * PGTStorage::getErrorMessage(). * * @param string $error_message an error message * * @return void * * @deprecated not used. */ function setErrorMessage($error_message) { $this->_error_message = $error_message; } /** * This method returns an error message set by PGTStorage::setErrorMessage(). * * @return string an error message when set by PGTStorage::setErrorMessage(), FALSE * otherwise. * * @deprecated not used. */ function getErrorMessage() { return $this->_error_message; } // ######################################################################## // INITIALIZATION // ######################################################################## /** * a boolean telling if the storage has already been initialized. Written by * PGTStorage::init(), read by PGTStorage::isInitialized(). * * @hideinitializer */ var $_initialized = false; /** * This method tells if the storage has already been intialized. * * @return bool * * @protected */ function isInitialized() { return $this->_initialized; } /** * This virtual method initializes the object. * * @return void */ function init() { $this->_initialized = true; } // ######################################################################## // PGT I/O // ######################################################################## /** * This virtual method stores a PGT and its corresponding PGT Iuo. * * @param string $pgt the PGT * @param string $pgt_iou the PGT iou * * @return void * * @note Should never be called. * */ function write($pgt,$pgt_iou) { phpCAS::error(__CLASS__.'::'.__FUNCTION__.'() should never be called'); } /** * This virtual method reads a PGT corresponding to a PGT Iou and deletes * the corresponding storage entry. * * @param string $pgt_iou the PGT iou * * @return string * * @note Should never be called. */ function read($pgt_iou) { phpCAS::error(__CLASS__.'::'.__FUNCTION__.'() should never be called'); } /** @} */ } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/GracefullTerminationException.php 0000644 00000005565 15152311435 0022375 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/GracefullTerminationException.php * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * An exception for terminatinating execution or to throw for unit testing * * @class CAS_GracefullTerminationException.php * @category Authentication * @package PhpCAS * @author Joachim Fritschi <jfritschi@freenet.de> * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_GracefullTerminationException extends RuntimeException implements CAS_Exception { /** * Test if exceptions should be thrown or if we should just exit. * In production usage we want to just exit cleanly when prompting the user * for a redirect without filling the error logs with uncaught exceptions. * In unit testing scenarios we cannot exit or we won't be able to continue * with our tests. * * @param string $message Message Text * @param int $code Error code * * @return self */ public function __construct ($message = 'Terminate Gracefully', $code = 0) { // Exit cleanly to avoid filling up the logs with uncaught exceptions. if (self::$_exitWhenThrown) { exit; } else { // Throw exceptions to allow unit testing to continue; parent::__construct($message, $code); } } private static $_exitWhenThrown = true; /** * Force phpcas to thow Exceptions instead of calling exit() * Needed for unit testing. Generally shouldn't be used in production due to * an increase in Apache error logging if CAS_GracefulTerminiationExceptions * are not caught and handled. * * @return void */ public static function throwInsteadOfExiting() { self::$_exitWhenThrown = false; } } ?> cas/CAS/vendor/apereo/phpcas/source/CAS/Request/RequestInterface.php 0000644 00000012511 15152311435 0021266 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Request/RequestInterface.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This interface defines a class library for performing web requests. * * @class CAS_Request_RequestInterface * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ interface CAS_Request_RequestInterface { /********************************************************* * Configure the Request *********************************************************/ /** * Set the URL of the Request * * @param string $url url to set * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setUrl ($url); /** * Add a cookie to the request. * * @param string $name name of cookie * @param string $value value of cookie * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function addCookie ($name, $value); /** * Add an array of cookies to the request. * The cookie array is of the form * array('cookie_name' => 'cookie_value', 'cookie_name2' => cookie_value2') * * @param array $cookies cookies to add * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function addCookies (array $cookies); /** * Add a header string to the request. * * @param string $header header to add * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function addHeader ($header); /** * Add an array of header strings to the request. * * @param array $headers headers to add * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function addHeaders (array $headers); /** * Make the request a POST request rather than the default GET request. * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function makePost (); /** * Add a POST body to the request * * @param string $body body to add * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setPostBody ($body); /** * Specify the path to an SSL CA certificate to validate the server with. * * @param string $caCertPath path to cert file * @param boolean $validate_cn validate CN of SSL certificate * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setSslCaCert ($caCertPath, $validate_cn = true); /********************************************************* * 2. Send the Request *********************************************************/ /** * Perform the request. * * @return bool TRUE on success, FALSE on failure. * @throws CAS_OutOfSequenceException If called multiple times. */ public function send (); /********************************************************* * 3. Access the response *********************************************************/ /** * Answer the headers of the response. * * @return array An array of header strings. * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseHeaders (); /** * Answer HTTP status code of the response * * @return int * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseStatusCode (); /** * Answer the body of response. * * @return string * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseBody (); /** * Answer a message describing any errors if the request failed. * * @return string * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getErrorMessage (); } cas/CAS/vendor/apereo/phpcas/source/CAS/Request/CurlRequest.php 0000644 00000014766 15152311435 0020311 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Request/CurlRequest.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Provides support for performing web-requests via curl * * @class CAS_Request_CurlRequest * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_Request_CurlRequest extends CAS_Request_AbstractRequest implements CAS_Request_RequestInterface { /** * Set additional curl options * * @param array $options option to set * * @return void */ public function setCurlOptions (array $options) { $this->_curlOptions = $options; } private $_curlOptions = array(); /** * Send the request and store the results. * * @return bool true on success, false on failure. */ protected function sendRequest () { phpCAS::traceBegin(); /********************************************************* * initialize the CURL session *********************************************************/ $ch = $this->initAndConfigure(); /********************************************************* * Perform the query *********************************************************/ $buf = curl_exec($ch); if ( $buf === false ) { phpCAS::trace('curl_exec() failed'); $this->storeErrorMessage( 'CURL error #'.curl_errno($ch).': '.curl_error($ch) ); $res = false; } else { $this->storeResponseBody($buf); phpCAS::trace("Response Body: \n".$buf."\n"); $res = true; } // close the CURL session curl_close($ch); phpCAS::traceEnd($res); return $res; } /** * Internal method to initialize our cURL handle and configure the request. * This method should NOT be used outside of the CurlRequest or the * CurlMultiRequest. * * @return resource|false The cURL handle on success, false on failure */ public function initAndConfigure() { /********************************************************* * initialize the CURL session *********************************************************/ $ch = curl_init($this->url); curl_setopt_array($ch, $this->_curlOptions); /********************************************************* * Set SSL configuration *********************************************************/ if ($this->caCertPath) { if ($this->validateCN) { curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); } else { curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); } curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); curl_setopt($ch, CURLOPT_CAINFO, $this->caCertPath); phpCAS::trace('CURL: Set CURLOPT_CAINFO ' . $this->caCertPath); } else { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); } /********************************************************* * Configure curl to capture our output. *********************************************************/ // return the CURL output into a variable curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // get the HTTP header with a callback curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, '_curlReadHeaders')); /********************************************************* * Add cookie headers to our request. *********************************************************/ if (count($this->cookies)) { $cookieStrings = array(); foreach ($this->cookies as $name => $val) { $cookieStrings[] = $name.'='.$val; } curl_setopt($ch, CURLOPT_COOKIE, implode(';', $cookieStrings)); } /********************************************************* * Add any additional headers *********************************************************/ if (count($this->headers)) { curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers); } /********************************************************* * Flag and Body for POST requests *********************************************************/ if ($this->isPost) { curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $this->postBody); } /********************************************************* * Set User Agent *********************************************************/ curl_setopt($ch, CURLOPT_USERAGENT, 'phpCAS/' . phpCAS::getVersion()); return $ch; } /** * Store the response body. * This method should NOT be used outside of the CurlRequest or the * CurlMultiRequest. * * @param string $body body to stor * * @return void */ public function _storeResponseBody ($body) { $this->storeResponseBody($body); } /** * Internal method for capturing the headers from a curl request. * * @param resource $ch handle of curl * @param string $header header * * @return int */ public function _curlReadHeaders ($ch, $header) { $this->storeResponseHeader($header); return strlen($header); } } cas/CAS/vendor/apereo/phpcas/source/CAS/Request/Exception.php 0000644 00000002675 15152311435 0017765 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Request/Exception.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * An Exception for problems performing requests * * @class CAS_Request_Exception * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_Request_Exception extends Exception implements CAS_Exception { } cas/CAS/vendor/apereo/phpcas/source/CAS/Request/MultiRequestInterface.php 0000644 00000005463 15152311435 0022311 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Request/MultiRequestInterface.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This interface defines a class library for performing multiple web requests * in batches. Implementations of this interface may perform requests serially * or in parallel. * * @class CAS_Request_MultiRequestInterface * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ interface CAS_Request_MultiRequestInterface { /********************************************************* * Add Requests *********************************************************/ /** * Add a new Request to this batch. * Note, implementations will likely restrict requests to their own concrete * class hierarchy. * * @param CAS_Request_RequestInterface $request request interface * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been * sent. * @throws CAS_InvalidArgumentException If passed a Request of the wrong * implmentation. */ public function addRequest (CAS_Request_RequestInterface $request); /** * Retrieve the number of requests added to this batch. * * @return int number of request elements */ public function getNumRequests (); /********************************************************* * 2. Send the Request *********************************************************/ /** * Perform the request. After sending, all requests will have their * responses poulated. * * @return bool TRUE on success, FALSE on failure. * @throws CAS_OutOfSequenceException If called multiple times. */ public function send (); } cas/CAS/vendor/apereo/phpcas/source/CAS/Request/CurlMultiRequest.php 0000644 00000011742 15152311435 0021313 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Request/AbstractRequest.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * This interface defines a class library for performing multiple web requests * in batches. Implementations of this interface may perform requests serially * or in parallel. * * @class CAS_Request_CurlMultiRequest * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ class CAS_Request_CurlMultiRequest implements CAS_Request_MultiRequestInterface { private $_requests = array(); private $_sent = false; /********************************************************* * Add Requests *********************************************************/ /** * Add a new Request to this batch. * Note, implementations will likely restrict requests to their own concrete * class hierarchy. * * @param CAS_Request_RequestInterface $request reqest to add * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. * @throws CAS_InvalidArgumentException If passed a Request of the wrong * implmentation. */ public function addRequest (CAS_Request_RequestInterface $request) { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } if (!$request instanceof CAS_Request_CurlRequest) { throw new CAS_InvalidArgumentException( 'As a CAS_Request_CurlMultiRequest, I can only work with CAS_Request_CurlRequest objects.' ); } $this->_requests[] = $request; } /** * Retrieve the number of requests added to this batch. * * @return int number of request elements * @throws CAS_OutOfSequenceException if the request has already been sent */ public function getNumRequests() { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } return count($this->_requests); } /********************************************************* * 2. Send the Request *********************************************************/ /** * Perform the request. After sending, all requests will have their * responses poulated. * * @return bool TRUE on success, FALSE on failure. * @throws CAS_OutOfSequenceException If called multiple times. */ public function send () { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot send again.' ); } if (!count($this->_requests)) { throw new CAS_OutOfSequenceException( 'At least one request must be added via addRequest() before the multi-request can be sent.' ); } $this->_sent = true; // Initialize our handles and configure all requests. $handles = array(); $multiHandle = curl_multi_init(); foreach ($this->_requests as $i => $request) { $handle = $request->initAndConfigure(); curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); $handles[$i] = $handle; curl_multi_add_handle($multiHandle, $handle); } // Execute the requests in parallel. do { curl_multi_exec($multiHandle, $running); } while ($running > 0); // Populate all of the responses or errors back into the request objects. foreach ($this->_requests as $i => $request) { $buf = curl_multi_getcontent($handles[$i]); $request->_storeResponseBody($buf); curl_multi_remove_handle($multiHandle, $handles[$i]); curl_close($handles[$i]); } curl_multi_close($multiHandle); } } cas/CAS/vendor/apereo/phpcas/source/CAS/Request/AbstractRequest.php 0000644 00000024770 15152311435 0021143 0 ustar 00 <?php /** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * PHP Version 7 * * @file CAS/Request/AbstractRequest.php * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ /** * Provides support for performing web-requests via curl * * @class CAS_Request_AbstractRequest * @category Authentication * @package PhpCAS * @author Adam Franco <afranco@middlebury.edu> * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://wiki.jasig.org/display/CASC/phpCAS */ abstract class CAS_Request_AbstractRequest implements CAS_Request_RequestInterface { protected $url = null; protected $cookies = array(); protected $headers = array(); protected $isPost = false; protected $postBody = null; protected $caCertPath = null; protected $validateCN = true; private $_sent = false; private $_responseHeaders = array(); private $_responseBody = null; private $_errorMessage = ''; /********************************************************* * Configure the Request *********************************************************/ /** * Set the URL of the Request * * @param string $url Url to set * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setUrl ($url) { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } $this->url = $url; } /** * Add a cookie to the request. * * @param string $name Name of entry * @param string $value value of entry * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function addCookie ($name, $value) { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } $this->cookies[$name] = $value; } /** * Add an array of cookies to the request. * The cookie array is of the form * array('cookie_name' => 'cookie_value', 'cookie_name2' => cookie_value2') * * @param array $cookies cookies to add * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function addCookies (array $cookies) { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } $this->cookies = array_merge($this->cookies, $cookies); } /** * Add a header string to the request. * * @param string $header Header to add * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function addHeader ($header) { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } $this->headers[] = $header; } /** * Add an array of header strings to the request. * * @param array $headers headers to add * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function addHeaders (array $headers) { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } $this->headers = array_merge($this->headers, $headers); } /** * Make the request a POST request rather than the default GET request. * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function makePost () { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } $this->isPost = true; } /** * Add a POST body to the request * * @param string $body body to add * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setPostBody ($body) { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } if (!$this->isPost) { throw new CAS_OutOfSequenceException( 'Cannot add a POST body to a GET request, use makePost() first.' ); } $this->postBody = $body; } /** * Specify the path to an SSL CA certificate to validate the server with. * * @param string $caCertPath path to cert * @param bool $validate_cn valdiate CN of certificate * * @return void * @throws CAS_OutOfSequenceException If called after the Request has been sent. */ public function setSslCaCert ($caCertPath,$validate_cn=true) { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot '.__METHOD__ ); } $this->caCertPath = $caCertPath; $this->validateCN = $validate_cn; } /********************************************************* * 2. Send the Request *********************************************************/ /** * Perform the request. * * @return bool TRUE on success, FALSE on failure. * @throws CAS_OutOfSequenceException If called multiple times. */ public function send () { if ($this->_sent) { throw new CAS_OutOfSequenceException( 'Request has already been sent cannot send again.' ); } if (is_null($this->url) || !$this->url) { throw new CAS_OutOfSequenceException( 'A url must be specified via setUrl() before the request can be sent.' ); } $this->_sent = true; return $this->sendRequest(); } /** * Send the request and store the results. * * @return bool TRUE on success, FALSE on failure. */ abstract protected function sendRequest (); /** * Store the response headers. * * @param array $headers headers to store * * @return void */ protected function storeResponseHeaders (array $headers) { $this->_responseHeaders = array_merge($this->_responseHeaders, $headers); } /** * Store a single response header to our array. * * @param string $header header to store * * @return void */ protected function storeResponseHeader ($header) { $this->_responseHeaders[] = $header; } /** * Store the response body. * * @param string $body body to store * * @return void */ protected function storeResponseBody ($body) { $this->_responseBody = $body; } /** * Add a string to our error message. * * @param string $message message to add * * @return void */ protected function storeErrorMessage ($message) { $this->_errorMessage .= $message; } /********************************************************* * 3. Access the response *********************************************************/ /** * Answer the headers of the response. * * @return array An array of header strings. * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseHeaders () { if (!$this->_sent) { throw new CAS_OutOfSequenceException( 'Request has not been sent yet. Cannot '.__METHOD__ ); } return $this->_responseHeaders; } /** * Answer HTTP status code of the response * * @return int * @throws CAS_OutOfSequenceException If called before the Request has been sent. * @throws CAS_Request_Exception if the response did not contain a status code */ public function getResponseStatusCode () { if (!$this->_sent) { throw new CAS_OutOfSequenceException( 'Request has not been sent yet. Cannot '.__METHOD__ ); } if (!preg_match( '/HTTP\/[0-9.]+\s+([0-9]+)\s*(.*)/', $this->_responseHeaders[0], $matches ) ) { throw new CAS_Request_Exception( 'Bad response, no status code was found in the first line.' ); } return intval($matches[1]); } /** * Answer the body of response. * * @return string * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getResponseBody () { if (!$this->_sent) { throw new CAS_OutOfSequenceException( 'Request has not been sent yet. Cannot '.__METHOD__ ); } return $this->_responseBody; } /** * Answer a message describing any errors if the request failed. * * @return string * @throws CAS_OutOfSequenceException If called before the Request has been sent. */ public function getErrorMessage () { if (!$this->_sent) { throw new CAS_OutOfSequenceException( 'Request has not been sent yet. Cannot '.__METHOD__ ); } return $this->_errorMessage; } } cas/CAS/vendor/composer/autoload_real.php 0000644 00000002161 15152311435 0014370 0 ustar 00 <?php // autoload_real.php @generated by Composer class ComposerAutoloaderInit8c729390e3f26f25c6e8fe4b9504a4d9 { private static $loader; public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } } /** * @return \Composer\Autoload\ClassLoader */ public static function getLoader() { if (null !== self::$loader) { return self::$loader; } require __DIR__ . '/platform_check.php'; spl_autoload_register(array('ComposerAutoloaderInit8c729390e3f26f25c6e8fe4b9504a4d9', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInit8c729390e3f26f25c6e8fe4b9504a4d9', 'loadClassLoader')); require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit8c729390e3f26f25c6e8fe4b9504a4d9::getInitializer($loader)); $loader->register(true); return $loader; } } cas/CAS/vendor/composer/autoload_classmap.php 0000644 00000012637 15152311435 0015261 0 ustar 00 <?php // autoload_classmap.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( 'CAS_AuthenticationException' => $vendorDir . '/apereo/phpcas/source/CAS/AuthenticationException.php', 'CAS_Client' => $vendorDir . '/apereo/phpcas/source/CAS/Client.php', 'CAS_CookieJar' => $vendorDir . '/apereo/phpcas/source/CAS/CookieJar.php', 'CAS_Exception' => $vendorDir . '/apereo/phpcas/source/CAS/Exception.php', 'CAS_GracefullTerminationException' => $vendorDir . '/apereo/phpcas/source/CAS/GracefullTerminationException.php', 'CAS_InvalidArgumentException' => $vendorDir . '/apereo/phpcas/source/CAS/InvalidArgumentException.php', 'CAS_Languages_Catalan' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/Catalan.php', 'CAS_Languages_ChineseSimplified' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/ChineseSimplified.php', 'CAS_Languages_English' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/English.php', 'CAS_Languages_French' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/French.php', 'CAS_Languages_Galego' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/Galego.php', 'CAS_Languages_German' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/German.php', 'CAS_Languages_Greek' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/Greek.php', 'CAS_Languages_Japanese' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/Japanese.php', 'CAS_Languages_LanguageInterface' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/LanguageInterface.php', 'CAS_Languages_Portuguese' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/Portuguese.php', 'CAS_Languages_Spanish' => $vendorDir . '/apereo/phpcas/source/CAS/Languages/Spanish.php', 'CAS_OutOfSequenceBeforeAuthenticationCallException' => $vendorDir . '/apereo/phpcas/source/CAS/OutOfSequenceBeforeAuthenticationCallException.php', 'CAS_OutOfSequenceBeforeClientException' => $vendorDir . '/apereo/phpcas/source/CAS/OutOfSequenceBeforeClientException.php', 'CAS_OutOfSequenceBeforeProxyException' => $vendorDir . '/apereo/phpcas/source/CAS/OutOfSequenceBeforeProxyException.php', 'CAS_OutOfSequenceException' => $vendorDir . '/apereo/phpcas/source/CAS/OutOfSequenceException.php', 'CAS_PGTStorage_AbstractStorage' => $vendorDir . '/apereo/phpcas/source/CAS/PGTStorage/AbstractStorage.php', 'CAS_PGTStorage_Db' => $vendorDir . '/apereo/phpcas/source/CAS/PGTStorage/Db.php', 'CAS_PGTStorage_File' => $vendorDir . '/apereo/phpcas/source/CAS/PGTStorage/File.php', 'CAS_ProxiedService' => $vendorDir . '/apereo/phpcas/source/CAS/ProxiedService.php', 'CAS_ProxiedService_Abstract' => $vendorDir . '/apereo/phpcas/source/CAS/ProxiedService/Abstract.php', 'CAS_ProxiedService_Exception' => $vendorDir . '/apereo/phpcas/source/CAS/ProxiedService/Exception.php', 'CAS_ProxiedService_Http' => $vendorDir . '/apereo/phpcas/source/CAS/ProxiedService/Http.php', 'CAS_ProxiedService_Http_Abstract' => $vendorDir . '/apereo/phpcas/source/CAS/ProxiedService/Http/Abstract.php', 'CAS_ProxiedService_Http_Get' => $vendorDir . '/apereo/phpcas/source/CAS/ProxiedService/Http/Get.php', 'CAS_ProxiedService_Http_Post' => $vendorDir . '/apereo/phpcas/source/CAS/ProxiedService/Http/Post.php', 'CAS_ProxiedService_Imap' => $vendorDir . '/apereo/phpcas/source/CAS/ProxiedService/Imap.php', 'CAS_ProxiedService_Testable' => $vendorDir . '/apereo/phpcas/source/CAS/ProxiedService/Testable.php', 'CAS_ProxyChain' => $vendorDir . '/apereo/phpcas/source/CAS/ProxyChain.php', 'CAS_ProxyChain_AllowedList' => $vendorDir . '/apereo/phpcas/source/CAS/ProxyChain/AllowedList.php', 'CAS_ProxyChain_Any' => $vendorDir . '/apereo/phpcas/source/CAS/ProxyChain/Any.php', 'CAS_ProxyChain_Interface' => $vendorDir . '/apereo/phpcas/source/CAS/ProxyChain/Interface.php', 'CAS_ProxyChain_Trusted' => $vendorDir . '/apereo/phpcas/source/CAS/ProxyChain/Trusted.php', 'CAS_ProxyTicketException' => $vendorDir . '/apereo/phpcas/source/CAS/ProxyTicketException.php', 'CAS_Request_AbstractRequest' => $vendorDir . '/apereo/phpcas/source/CAS/Request/AbstractRequest.php', 'CAS_Request_CurlMultiRequest' => $vendorDir . '/apereo/phpcas/source/CAS/Request/CurlMultiRequest.php', 'CAS_Request_CurlRequest' => $vendorDir . '/apereo/phpcas/source/CAS/Request/CurlRequest.php', 'CAS_Request_Exception' => $vendorDir . '/apereo/phpcas/source/CAS/Request/Exception.php', 'CAS_Request_MultiRequestInterface' => $vendorDir . '/apereo/phpcas/source/CAS/Request/MultiRequestInterface.php', 'CAS_Request_RequestInterface' => $vendorDir . '/apereo/phpcas/source/CAS/Request/RequestInterface.php', 'CAS_ServiceBaseUrl_AllowedListDiscovery' => $vendorDir . '/apereo/phpcas/source/CAS/ServiceBaseUrl/AllowedListDiscovery.php', 'CAS_ServiceBaseUrl_Base' => $vendorDir . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Base.php', 'CAS_ServiceBaseUrl_Interface' => $vendorDir . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Interface.php', 'CAS_ServiceBaseUrl_Static' => $vendorDir . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Static.php', 'CAS_Session_PhpSession' => $vendorDir . '/apereo/phpcas/source/CAS/Session/PhpSession.php', 'CAS_TypeMismatchException' => $vendorDir . '/apereo/phpcas/source/CAS/TypeMismatchException.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'phpCAS' => $vendorDir . '/apereo/phpcas/source/CAS.php', ); cas/CAS/vendor/composer/installed.php 0000644 00000002554 15152311435 0013542 0 ustar 00 <?php return array( 'root' => array( 'name' => '__root__', 'pretty_version' => '1.0.0+no-version-set', 'version' => '1.0.0.0', 'reference' => NULL, 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => true, ), 'versions' => array( '__root__' => array( 'pretty_version' => '1.0.0+no-version-set', 'version' => '1.0.0.0', 'reference' => NULL, 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), 'apereo/phpcas' => array( 'pretty_version' => '1.6.0', 'version' => '1.6.0.0', 'reference' => 'f817c72a961484afef95ac64a9257c8e31f063b9', 'type' => 'library', 'install_path' => __DIR__ . '/../apereo/phpcas', 'aliases' => array(), 'dev_requirement' => false, ), 'psr/log' => array( 'pretty_version' => '1.1.4', 'version' => '1.1.4.0', 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), 'dev_requirement' => false, ), ), ); cas/CAS/vendor/composer/autoload_psr4.php 0000644 00000000301 15152311435 0014327 0 ustar 00 <?php // autoload_psr4.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), ); cas/CAS/vendor/composer/ClassLoader.php 0000644 00000037304 15152311435 0013760 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier <fabien@symfony.com> * @author Jordi Boggiano <j.boggiano@seld.be> * @see https://www.php-fig.org/psr/psr-0/ * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { /** @var ?string */ private $vendorDir; // PSR-4 /** * @var array[] * @psalm-var array<string, array<string, int>> */ private $prefixLengthsPsr4 = array(); /** * @var array[] * @psalm-var array<string, array<int, string>> */ private $prefixDirsPsr4 = array(); /** * @var array[] * @psalm-var array<string, string> */ private $fallbackDirsPsr4 = array(); // PSR-0 /** * @var array[] * @psalm-var array<string, array<string, string[]>> */ private $prefixesPsr0 = array(); /** * @var array[] * @psalm-var array<string, string> */ private $fallbackDirsPsr0 = array(); /** @var bool */ private $useIncludePath = false; /** * @var string[] * @psalm-var array<string, string> */ private $classMap = array(); /** @var bool */ private $classMapAuthoritative = false; /** * @var bool[] * @psalm-var array<string, bool> */ private $missingClasses = array(); /** @var ?string */ private $apcuPrefix; /** * @var self[] */ private static $registeredLoaders = array(); /** * @param ?string $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; } /** * @return string[] */ public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } /** * @return array[] * @psalm-return array<string, array<int, string>> */ public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } /** * @return array[] * @psalm-return array<string, string> */ public function getFallbackDirs() { return $this->fallbackDirsPsr0; } /** * @return array[] * @psalm-return array<string, string> */ public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } /** * @return string[] Array of classname => path * @psalm-return array<string, string> */ public function getClassMap() { return $this->classMap; } /** * @param string[] $classMap Class to filename map * @psalm-param array<string, string> $classMap * * @return void */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param string[]|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, (array) $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], (array) $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param string[]|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * * @return void */ public function addPsr4($prefix, $paths, $prepend = false) { if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], (array) $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param string[]|string $paths The PSR-0 base directories * * @return void */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param string[]|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * * @return void */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath * * @return void */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative * * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix * * @return void */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not * * @return void */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); if (null === $this->vendorDir) { return; } if ($prepend) { self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; } else { unset(self::$registeredLoaders[$this->vendorDir]); self::$registeredLoaders[$this->vendorDir] = $this; } } /** * Unregisters this instance as an autoloader. * * @return void */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); if (null !== $this->vendorDir) { unset(self::$registeredLoaders[$this->vendorDir]); } } /** * Loads the given class or interface. * * @param string $class The name of the class * @return true|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } return null; } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } /** * Returns the currently registered loaders indexed by their corresponding vendor directories. * * @return self[] */ public static function getRegisteredLoaders() { return self::$registeredLoaders; } /** * @param string $class * @param string $ext * @return string|false */ private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } } /** * Scope isolated include. * * Prevents access to $this/self from included files. * * @param string $file * @return void * @private */ function includeFile($file) { include $file; } cas/CAS/vendor/composer/autoload_static.php 0000644 00000015271 15152311435 0014742 0 ustar 00 <?php // autoload_static.php @generated by Composer namespace Composer\Autoload; class ComposerStaticInit8c729390e3f26f25c6e8fe4b9504a4d9 { public static $prefixLengthsPsr4 = array ( 'P' => array ( 'Psr\\Log\\' => 8, ), ); public static $prefixDirsPsr4 = array ( 'Psr\\Log\\' => array ( 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', ), ); public static $classMap = array ( 'CAS_AuthenticationException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/AuthenticationException.php', 'CAS_Client' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Client.php', 'CAS_CookieJar' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/CookieJar.php', 'CAS_Exception' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Exception.php', 'CAS_GracefullTerminationException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/GracefullTerminationException.php', 'CAS_InvalidArgumentException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/InvalidArgumentException.php', 'CAS_Languages_Catalan' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/Catalan.php', 'CAS_Languages_ChineseSimplified' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/ChineseSimplified.php', 'CAS_Languages_English' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/English.php', 'CAS_Languages_French' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/French.php', 'CAS_Languages_Galego' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/Galego.php', 'CAS_Languages_German' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/German.php', 'CAS_Languages_Greek' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/Greek.php', 'CAS_Languages_Japanese' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/Japanese.php', 'CAS_Languages_LanguageInterface' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/LanguageInterface.php', 'CAS_Languages_Portuguese' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/Portuguese.php', 'CAS_Languages_Spanish' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Languages/Spanish.php', 'CAS_OutOfSequenceBeforeAuthenticationCallException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/OutOfSequenceBeforeAuthenticationCallException.php', 'CAS_OutOfSequenceBeforeClientException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/OutOfSequenceBeforeClientException.php', 'CAS_OutOfSequenceBeforeProxyException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/OutOfSequenceBeforeProxyException.php', 'CAS_OutOfSequenceException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/OutOfSequenceException.php', 'CAS_PGTStorage_AbstractStorage' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/PGTStorage/AbstractStorage.php', 'CAS_PGTStorage_Db' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/PGTStorage/Db.php', 'CAS_PGTStorage_File' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/PGTStorage/File.php', 'CAS_ProxiedService' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxiedService.php', 'CAS_ProxiedService_Abstract' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxiedService/Abstract.php', 'CAS_ProxiedService_Exception' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxiedService/Exception.php', 'CAS_ProxiedService_Http' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxiedService/Http.php', 'CAS_ProxiedService_Http_Abstract' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxiedService/Http/Abstract.php', 'CAS_ProxiedService_Http_Get' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxiedService/Http/Get.php', 'CAS_ProxiedService_Http_Post' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxiedService/Http/Post.php', 'CAS_ProxiedService_Imap' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxiedService/Imap.php', 'CAS_ProxiedService_Testable' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxiedService/Testable.php', 'CAS_ProxyChain' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxyChain.php', 'CAS_ProxyChain_AllowedList' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxyChain/AllowedList.php', 'CAS_ProxyChain_Any' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxyChain/Any.php', 'CAS_ProxyChain_Interface' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxyChain/Interface.php', 'CAS_ProxyChain_Trusted' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxyChain/Trusted.php', 'CAS_ProxyTicketException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ProxyTicketException.php', 'CAS_Request_AbstractRequest' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Request/AbstractRequest.php', 'CAS_Request_CurlMultiRequest' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Request/CurlMultiRequest.php', 'CAS_Request_CurlRequest' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Request/CurlRequest.php', 'CAS_Request_Exception' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Request/Exception.php', 'CAS_Request_MultiRequestInterface' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Request/MultiRequestInterface.php', 'CAS_Request_RequestInterface' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Request/RequestInterface.php', 'CAS_ServiceBaseUrl_AllowedListDiscovery' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ServiceBaseUrl/AllowedListDiscovery.php', 'CAS_ServiceBaseUrl_Base' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Base.php', 'CAS_ServiceBaseUrl_Interface' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Interface.php', 'CAS_ServiceBaseUrl_Static' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/ServiceBaseUrl/Static.php', 'CAS_Session_PhpSession' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/Session/PhpSession.php', 'CAS_TypeMismatchException' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS/TypeMismatchException.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'phpCAS' => __DIR__ . '/..' . '/apereo/phpcas/source/CAS.php', ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit8c729390e3f26f25c6e8fe4b9504a4d9::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit8c729390e3f26f25c6e8fe4b9504a4d9::$prefixDirsPsr4; $loader->classMap = ComposerStaticInit8c729390e3f26f25c6e8fe4b9504a4d9::$classMap; }, null, ClassLoader::class); } } cas/CAS/vendor/composer/platform_check.php 0000644 00000001635 15152311435 0014543 0 ustar 00 <?php // platform_check.php @generated by Composer $issues = array(); if (!(PHP_VERSION_ID >= 70100)) { $issues[] = 'Your Composer dependencies require a PHP version ">= 7.1.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } if (!ini_get('display_errors')) { if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); } elseif (!headers_sent()) { echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } trigger_error( 'Composer detected issues in your platform: ' . implode(' ', $issues), E_USER_ERROR ); } cas/CAS/vendor/composer/installed.json 0000644 00000010453 15152311435 0013721 0 ustar 00 { "packages": [ { "name": "apereo/phpcas", "version": "1.6.0", "version_normalized": "1.6.0.0", "source": { "type": "git", "url": "https://github.com/apereo/phpCAS.git", "reference": "f817c72a961484afef95ac64a9257c8e31f063b9" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/apereo/phpCAS/zipball/f817c72a961484afef95ac64a9257c8e31f063b9", "reference": "f817c72a961484afef95ac64a9257c8e31f063b9", "shasum": "" }, "require": { "ext-curl": "*", "ext-dom": "*", "php": ">=7.1.0", "psr/log": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "monolog/monolog": "^1.0.0 || ^2.0.0", "phpstan/phpstan": "^1.5", "phpunit/phpunit": ">=7.5" }, "time": "2022-10-31T20:39:27+00:00", "type": "library", "extra": { "branch-alias": { "dev-master": "1.3.x-dev" } }, "installation-source": "dist", "autoload": { "classmap": [ "source/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "Apache-2.0" ], "authors": [ { "name": "Joachim Fritschi", "email": "jfritschi@freenet.de", "homepage": "https://github.com/jfritschi" }, { "name": "Adam Franco", "homepage": "https://github.com/adamfranco" }, { "name": "Henry Pan", "homepage": "https://github.com/phy25" } ], "description": "Provides a simple API for authenticating users against a CAS server", "homepage": "https://wiki.jasig.org/display/CASC/phpCAS", "keywords": [ "apereo", "cas", "jasig" ], "support": { "issues": "https://github.com/apereo/phpCAS/issues", "source": "https://github.com/apereo/phpCAS/tree/1.6.0" }, "install-path": "../apereo/phpcas" }, { "name": "psr/log", "version": "1.1.4", "version_normalized": "1.1.4.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { "php": ">=5.3.0" }, "time": "2021-05-03T11:20:27+00:00", "type": "library", "extra": { "branch-alias": { "dev-master": "1.1.x-dev" } }, "installation-source": "dist", "autoload": { "psr-4": { "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { "name": "PHP-FIG", "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", "homepage": "https://github.com/php-fig/log", "keywords": [ "log", "psr", "psr-3" ], "support": { "source": "https://github.com/php-fig/log/tree/1.1.4" }, "install-path": "../psr/log" } ], "dev": true, "dev-package-names": [] } cas/CAS/vendor/composer/LICENSE 0000644 00000002056 15152311435 0012054 0 ustar 00 Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cas/CAS/vendor/composer/InstalledVersions.php 0000644 00000035335 15152311435 0015236 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Autoload\ClassLoader; use Composer\Semver\VersionParser; /** * This class is copied in every Composer installed project and available to all * * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * * To require its presence, you can require `composer-runtime-api ^2.0` * * @final */ class InstalledVersions { /** * @var mixed[]|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null */ private static $installed; /** * @var bool|null */ private static $canGetVendors; /** * @var array[] * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ private static $installedByVendor = array(); /** * Returns a list of all package names which are present, either by being installed, replaced or provided * * @return string[] * @psalm-return list<string> */ public static function getInstalledPackages() { $packages = array(); foreach (self::getInstalled() as $installed) { $packages[] = array_keys($installed['versions']); } if (1 === \count($packages)) { return $packages[0]; } return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); } /** * Returns a list of all package names with a specific type e.g. 'library' * * @param string $type * @return string[] * @psalm-return list<string> */ public static function getInstalledPackagesByType($type) { $packagesByType = array(); foreach (self::getInstalled() as $installed) { foreach ($installed['versions'] as $name => $package) { if (isset($package['type']) && $package['type'] === $type) { $packagesByType[] = $name; } } } return $packagesByType; } /** * Checks whether the given package is installed * * This also returns true if the package name is provided or replaced by another package * * @param string $packageName * @param bool $includeDevRequirements * @return bool */ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); } } return false; } /** * Checks whether the given package satisfies a version constraint * * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: * * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') * * @param VersionParser $parser Install composer/semver to have access to this class and functionality * @param string $packageName * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package * @return bool */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { $constraint = $parser->parseConstraints($constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); } /** * Returns a version constraint representing all the range(s) which are installed for a given package * * It is easier to use this via isInstalled() with the $constraint argument if you need to check * whether a given version of a package is installed, and not just whether it exists * * @param string $packageName * @return string Version constraint usable with composer/semver */ public static function getVersionRanges($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } $ranges = array(); if (isset($installed['versions'][$packageName]['pretty_version'])) { $ranges[] = $installed['versions'][$packageName]['pretty_version']; } if (array_key_exists('aliases', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); } if (array_key_exists('replaced', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); } if (array_key_exists('provided', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); } return implode(' || ', $ranges); } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['version'])) { return null; } return $installed['versions'][$packageName]['version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getPrettyVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['pretty_version'])) { return null; } return $installed['versions'][$packageName]['pretty_version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference */ public static function getReference($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['reference'])) { return null; } return $installed['versions'][$packageName]['reference']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. */ public static function getInstallPath($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @return array * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} */ public static function getRootPackage() { $installed = self::getInstalled(); return $installed[0]['root']; } /** * Returns the raw installed.php data for custom implementations * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} */ public static function getRawData() { @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); } } return self::$installed; } /** * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ public static function getAllRawData() { return self::getInstalled(); } /** * Lets you reload the static array from another file * * This is only useful for complex integrations in which a project needs to use * this class but then also needs to execute another project's autoloader in process, * and wants to ensure both projects have access to their version of installed.php. * * A typical case would be PHPUnit, where it would need to make sure it reads all * the data it needs from this class, then call reload() with * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure * the project in which it runs can then also use this class safely, without * interference between PHPUnit's dependencies and the project's dependencies. * * @param array[] $data A vendor/composer/installed.php data set * @return void * * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data */ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); } /** * @return array[] * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ private static function getInstalled() { if (null === self::$canGetVendors) { self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); } $installed = array(); if (self::$canGetVendors) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $installed[count($installed) - 1]; } } } } if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = require __DIR__ . '/installed.php'; } else { self::$installed = array(); } } $installed[] = self::$installed; return $installed; } } cas/CAS/vendor/composer/autoload_namespaces.php 0000644 00000000213 15152311435 0015560 0 ustar 00 <?php // autoload_namespaces.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( ); cas/CAS/vendor/psr/log/Psr/Log/LogLevel.php 0000644 00000000520 15152311435 0014306 0 ustar 00 <?php namespace Psr\Log; /** * Describes log levels. */ class LogLevel { const EMERGENCY = 'emergency'; const ALERT = 'alert'; const CRITICAL = 'critical'; const ERROR = 'error'; const WARNING = 'warning'; const NOTICE = 'notice'; const INFO = 'info'; const DEBUG = 'debug'; } cas/CAS/vendor/psr/log/Psr/Log/LoggerTrait.php 0000644 00000006527 15152311435 0015035 0 ustar 00 <?php namespace Psr\Log; /** * This is a simple Logger trait that classes unable to extend AbstractLogger * (because they extend another class, etc) can include. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ trait LoggerTrait { /** * System is unusable. * * @param string $message * @param array $context * * @return void */ public function emergency($message, array $context = array()) { $this->log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ abstract public function log($level, $message, array $context = array()); } cas/CAS/vendor/psr/log/Psr/Log/LoggerAwareInterface.php 0000644 00000000451 15152311435 0016620 0 ustar 00 <?php namespace Psr\Log; /** * Describes a logger-aware instance. */ interface LoggerAwareInterface { /** * Sets a logger instance on the object. * * @param LoggerInterface $logger * * @return void */ public function setLogger(LoggerInterface $logger); } cas/CAS/vendor/psr/log/Psr/Log/LoggerAwareTrait.php 0000644 00000000622 15152311435 0016003 0 ustar 00 <?php namespace Psr\Log; /** * Basic Implementation of LoggerAwareInterface. */ trait LoggerAwareTrait { /** * The logger instance. * * @var LoggerInterface|null */ protected $logger; /** * Sets a logger. * * @param LoggerInterface $logger */ public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } } cas/CAS/vendor/psr/log/Psr/Log/AbstractLogger.php 0000644 00000006040 15152311435 0015503 0 ustar 00 <?php namespace Psr\Log; /** * This is a simple Logger implementation that other Loggers can inherit from. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ abstract class AbstractLogger implements LoggerInterface { /** * System is unusable. * * @param string $message * @param mixed[] $context * * @return void */ public function emergency($message, array $context = array()) { $this->log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param mixed[] $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param mixed[] $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param mixed[] $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param mixed[] $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param mixed[] $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param mixed[] $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param mixed[] $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } } cas/CAS/vendor/psr/log/Psr/Log/InvalidArgumentException.php 0000644 00000000140 15152311435 0017543 0 ustar 00 <?php namespace Psr\Log; class InvalidArgumentException extends \InvalidArgumentException { } cas/CAS/vendor/psr/log/Psr/Log/NullLogger.php 0000644 00000001303 15152311435 0014647 0 ustar 00 <?php namespace Psr\Log; /** * This Logger can be used to avoid conditional log calls. * * Logging should always be optional, and if no logger is provided to your * library creating a NullLogger instance to have something to throw logs at * is a good way to avoid littering your code with `if ($this->logger) { }` * blocks. */ class NullLogger extends AbstractLogger { /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, $message, array $context = array()) { // noop } } cas/CAS/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php 0000644 00000011051 15152311435 0017415 0 ustar 00 <?php namespace Psr\Log\Test; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use PHPUnit\Framework\TestCase; /** * Provides a base test class for ensuring compliance with the LoggerInterface. * * Implementors can extend the class and implement abstract methods to run this * as part of their test suite. */ abstract class LoggerInterfaceTest extends TestCase { /** * @return LoggerInterface */ abstract public function getLogger(); /** * This must return the log messages in order. * * The simple formatting of the messages is: "<LOG LEVEL> <MESSAGE>". * * Example ->error('Foo') would yield "error Foo". * * @return string[] */ abstract public function getLogs(); public function testImplements() { $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger()); } /** * @dataProvider provideLevelsAndMessages */ public function testLogsAtAllLevels($level, $message) { $logger = $this->getLogger(); $logger->{$level}($message, array('user' => 'Bob')); $logger->log($level, $message, array('user' => 'Bob')); $expected = array( $level.' message of level '.$level.' with context: Bob', $level.' message of level '.$level.' with context: Bob', ); $this->assertEquals($expected, $this->getLogs()); } public function provideLevelsAndMessages() { return array( LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'), LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'), LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'), LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'), LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'), LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'), LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'), LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'), ); } /** * @expectedException \Psr\Log\InvalidArgumentException */ public function testThrowsOnInvalidLevel() { $logger = $this->getLogger(); $logger->log('invalid level', 'Foo'); } public function testContextReplacement() { $logger = $this->getLogger(); $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); $expected = array('info {Message {nothing} Bob Bar a}'); $this->assertEquals($expected, $this->getLogs()); } public function testObjectCastToString() { if (method_exists($this, 'createPartialMock')) { $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString')); } else { $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString')); } $dummy->expects($this->once()) ->method('__toString') ->will($this->returnValue('DUMMY')); $this->getLogger()->warning($dummy); $expected = array('warning DUMMY'); $this->assertEquals($expected, $this->getLogs()); } public function testContextCanContainAnything() { $closed = fopen('php://memory', 'r'); fclose($closed); $context = array( 'bool' => true, 'null' => null, 'string' => 'Foo', 'int' => 0, 'float' => 0.5, 'nested' => array('with object' => new DummyTest), 'object' => new \DateTime, 'resource' => fopen('php://memory', 'r'), 'closed' => $closed, ); $this->getLogger()->warning('Crazy context data', $context); $expected = array('warning Crazy context data'); $this->assertEquals($expected, $this->getLogs()); } public function testContextExceptionKeyCanBeExceptionOrOtherValues() { $logger = $this->getLogger(); $logger->warning('Random message', array('exception' => 'oops')); $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail'))); $expected = array( 'warning Random message', 'critical Uncaught Exception!' ); $this->assertEquals($expected, $this->getLogs()); } } cas/CAS/vendor/psr/log/Psr/Log/Test/TestLogger.php 0000644 00000010657 15152311435 0015607 0 ustar 00 <?php namespace Psr\Log\Test; use Psr\Log\AbstractLogger; /** * Used for testing purposes. * * It records all records and gives you access to them for verification. * * @method bool hasEmergency($record) * @method bool hasAlert($record) * @method bool hasCritical($record) * @method bool hasError($record) * @method bool hasWarning($record) * @method bool hasNotice($record) * @method bool hasInfo($record) * @method bool hasDebug($record) * * @method bool hasEmergencyRecords() * @method bool hasAlertRecords() * @method bool hasCriticalRecords() * @method bool hasErrorRecords() * @method bool hasWarningRecords() * @method bool hasNoticeRecords() * @method bool hasInfoRecords() * @method bool hasDebugRecords() * * @method bool hasEmergencyThatContains($message) * @method bool hasAlertThatContains($message) * @method bool hasCriticalThatContains($message) * @method bool hasErrorThatContains($message) * @method bool hasWarningThatContains($message) * @method bool hasNoticeThatContains($message) * @method bool hasInfoThatContains($message) * @method bool hasDebugThatContains($message) * * @method bool hasEmergencyThatMatches($message) * @method bool hasAlertThatMatches($message) * @method bool hasCriticalThatMatches($message) * @method bool hasErrorThatMatches($message) * @method bool hasWarningThatMatches($message) * @method bool hasNoticeThatMatches($message) * @method bool hasInfoThatMatches($message) * @method bool hasDebugThatMatches($message) * * @method bool hasEmergencyThatPasses($message) * @method bool hasAlertThatPasses($message) * @method bool hasCriticalThatPasses($message) * @method bool hasErrorThatPasses($message) * @method bool hasWarningThatPasses($message) * @method bool hasNoticeThatPasses($message) * @method bool hasInfoThatPasses($message) * @method bool hasDebugThatPasses($message) */ class TestLogger extends AbstractLogger { /** * @var array */ public $records = []; public $recordsByLevel = []; /** * @inheritdoc */ public function log($level, $message, array $context = []) { $record = [ 'level' => $level, 'message' => $message, 'context' => $context, ]; $this->recordsByLevel[$record['level']][] = $record; $this->records[] = $record; } public function hasRecords($level) { return isset($this->recordsByLevel[$level]); } public function hasRecord($record, $level) { if (is_string($record)) { $record = ['message' => $record]; } return $this->hasRecordThatPasses(function ($rec) use ($record) { if ($rec['message'] !== $record['message']) { return false; } if (isset($record['context']) && $rec['context'] !== $record['context']) { return false; } return true; }, $level); } public function hasRecordThatContains($message, $level) { return $this->hasRecordThatPasses(function ($rec) use ($message) { return strpos($rec['message'], $message) !== false; }, $level); } public function hasRecordThatMatches($regex, $level) { return $this->hasRecordThatPasses(function ($rec) use ($regex) { return preg_match($regex, $rec['message']) > 0; }, $level); } public function hasRecordThatPasses(callable $predicate, $level) { if (!isset($this->recordsByLevel[$level])) { return false; } foreach ($this->recordsByLevel[$level] as $i => $rec) { if (call_user_func($predicate, $rec, $i)) { return true; } } return false; } public function __call($method, $args) { if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; $level = strtolower($matches[2]); if (method_exists($this, $genericMethod)) { $args[] = $level; return call_user_func_array([$this, $genericMethod], $args); } } throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); } public function reset() { $this->records = []; $this->recordsByLevel = []; } } cas/CAS/vendor/psr/log/Psr/Log/Test/DummyTest.php 0000644 00000000373 15152311435 0015455 0 ustar 00 <?php namespace Psr\Log\Test; /** * This class is internal and does not follow the BC promise. * * Do NOT use this class in any way. * * @internal */ class DummyTest { public function __toString() { return 'DummyTest'; } } cas/CAS/vendor/psr/log/Psr/Log/LoggerInterface.php 0000644 00000006052 15152311435 0015643 0 ustar 00 <?php namespace Psr\Log; /** * Describes a logger instance. * * The message MUST be a string or object implementing __toString(). * * The message MAY contain placeholders in the form: {foo} where foo * will be replaced by the context data in key "foo". * * The context array can contain arbitrary data. The only assumption that * can be made by implementors is that if an Exception instance is given * to produce a stack trace, it MUST be in a key named "exception". * * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md * for the full interface specification. */ interface LoggerInterface { /** * System is unusable. * * @param string $message * @param mixed[] $context * * @return void */ public function emergency($message, array $context = array()); /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param mixed[] $context * * @return void */ public function alert($message, array $context = array()); /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param mixed[] $context * * @return void */ public function critical($message, array $context = array()); /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param mixed[] $context * * @return void */ public function error($message, array $context = array()); /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param mixed[] $context * * @return void */ public function warning($message, array $context = array()); /** * Normal but significant events. * * @param string $message * @param mixed[] $context * * @return void */ public function notice($message, array $context = array()); /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param mixed[] $context * * @return void */ public function info($message, array $context = array()); /** * Detailed debug information. * * @param string $message * @param mixed[] $context * * @return void */ public function debug($message, array $context = array()); /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param mixed[] $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, $message, array $context = array()); } cas/CAS/vendor/psr/log/LICENSE 0000644 00000002075 15152311435 0011613 0 ustar 00 Copyright (c) 2012 PHP Framework Interoperability Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cas/CAS/vendor/psr/log/README.md 0000644 00000002502 15152311435 0012060 0 ustar 00 PSR Log ======= This repository holds all interfaces/classes/traits related to [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). Note that this is not a logger of its own. It is merely an interface that describes a logger. See the specification for more details. Installation ------------ ```bash composer require psr/log ``` Usage ----- If you need a logger, you can use the interface like this: ```php <?php use Psr\Log\LoggerInterface; class Foo { private $logger; public function __construct(LoggerInterface $logger = null) { $this->logger = $logger; } public function doSomething() { if ($this->logger) { $this->logger->info('Doing work'); } try { $this->doSomethingElse(); } catch (Exception $exception) { $this->logger->error('Oh no!', array('exception' => $exception)); } // do something useful } } ``` You can then pick one of the implementations of the interface to get a logger. If you want to implement the interface, you can require this package and implement `Psr\Log\LoggerInterface` in your code. Please read the [specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) for details. cas/CAS/vendor/psr/log/composer.json 0000644 00000001062 15152311435 0013323 0 ustar 00 { "name": "psr/log", "description": "Common interface for logging libraries", "keywords": ["psr", "psr-3", "log"], "homepage": "https://github.com/php-fig/log", "license": "MIT", "authors": [ { "name": "PHP-FIG", "homepage": "https://www.php-fig.org/" } ], "require": { "php": ">=5.3.0" }, "autoload": { "psr-4": { "Psr\\Log\\": "Psr/Log/" } }, "extra": { "branch-alias": { "dev-master": "1.1.x-dev" } } } cas/CAS/vendor/autoload.php 0000644 00000001403 15152311435 0011534 0 ustar 00 <?php // autoload.php @generated by Composer if (PHP_VERSION_ID < 50600) { if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; if (!ini_get('display_errors')) { if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { fwrite(STDERR, $err); } elseif (!headers_sent()) { echo $err; } } trigger_error( $err, E_USER_ERROR ); } require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit8c729390e3f26f25c6e8fe4b9504a4d9::getLoader(); cas/lib.php 0000644 00000004257 15152311435 0006601 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: CAS Authentication. * * Authentication using CAS (Central Authentication Server). * * @package auth_cas * @copyright 2018 Fabrice Ménard <menard.fabrice@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Serves the logo file settings. * * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool false|void */ function auth_cas_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []) { if ($context->contextlevel != CONTEXT_SYSTEM) { return false; } if ($filearea !== 'logo' ) { return false; } // Extract the filename / filepath from the $args array. $filename = array_pop($args); if (!$args) { $filepath = '/'; } else { $filepath = '/' . implode('/', $args) . '/'; } // Retrieve the file from the Files API. $itemid = 0; $fs = get_file_storage(); $file = $fs->get_file($context->id, 'auth_cas', $filearea, $itemid, $filepath, $filename); if (!$file) { return false; // The file does not exist. } send_stored_file($file, null, 0, $forcedownload, $options); } cas/version.php 0000644 00000002355 15152311435 0007515 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version details * * @package auth_cas * @author Martin Dougiamas * @author Jerome GUTIERREZ * @author Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_cas'; // Full name of the plugin (used for diagnostics) $plugin->dependencies = array('auth_ldap' => 2022111800); cas/classes/privacy/provider.php 0000644 00000002741 15152311435 0012773 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_cas. * * @package auth_cas * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_cas\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_cas implementing null_provider. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } cas/classes/task/sync_task.php 0000644 00000003056 15152311435 0012424 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A scheduled task for CAS user sync. * * @package auth_cas * @copyright 2015 Vadim Dvorovenko <Vadimon@mail.ru> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_cas\task; /** * A scheduled task class for CAS user sync. * * @copyright 2015 Vadim Dvorovenko <Vadimon@mail.ru> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_task extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('synctask', 'auth_cas'); } /** * Run users sync. */ public function execute() { global $CFG; if (is_enabled_auth('cas')) { $auth = get_auth_plugin('cas'); $auth->sync_users(true); } } } cas/lang/en/auth_cas.php 0000644 00000013255 15152311435 0011143 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_cas', language 'en'. * * @package auth_cas * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_cas_auth_name'] = 'Authentication method name'; $string['auth_cas_auth_name_description'] = 'Provide a name for the CAS authentication method that is familiar to your users.'; $string['auth_cas_auth_logo'] = 'Authentication method logo'; $string['auth_cas_auth_logo_description'] = 'Provide a logo for the CAS authentication method that is familiar to your users.'; $string['auth_cas_auth_user_create'] = 'Create users externally'; $string['auth_cas_auth_service'] = 'CAS'; $string['auth_cas_baseuri'] = 'URI of the server (nothing if no baseUri)<br />For example, if the CAS server responds to host.domaine.fr/CAS/ then<br />cas_baseuri = CAS/'; $string['auth_cas_baseuri_key'] = 'Base URI'; $string['auth_cas_broken_password'] = 'You cannot proceed without changing your password, however there is no available page for changing it. Please contact your Moodle Administrator.'; $string['auth_cas_cantconnect'] = 'LDAP part of CAS-module cannot connect to server: {$a}'; $string['auth_cas_casversion'] = 'CAS protocol version'; $string['auth_cas_certificate_check'] = 'Select \'yes\' if you want to validate the server certificate'; $string['auth_cas_certificate_path_empty'] = 'If you turn on Server validation, you need to specify a certificate path'; $string['auth_cas_certificate_check_key'] = 'Server validation'; $string['auth_cas_certificate_path'] = 'Path of the CA chain file (PEM Format) to validate the server certificate'; $string['auth_cas_certificate_path_key'] = 'Certificate path'; $string['auth_cas_create_user'] = 'Turn this on if you want to insert CAS-authenticated users in Moodle database. If not then only users who already exist in the Moodle database can log in.'; $string['auth_cas_create_user_key'] = 'Create user'; $string['auth_cas_curl_ssl_version'] = 'The SSL version (2 or 3) to use. By default PHP will try to determine this itself, although in some cases this must be set manually.'; $string['auth_cas_curl_ssl_version_default'] = 'Default'; $string['auth_cas_curl_ssl_version_key'] = 'cURL SSL Version'; $string['auth_cas_curl_ssl_version_SSLv2'] = 'SSLv2'; $string['auth_cas_curl_ssl_version_SSLv3'] = 'SSLv3'; $string['auth_cas_curl_ssl_version_TLSv1x'] = 'TLSv1.x'; $string['auth_cas_curl_ssl_version_TLSv10'] = 'TLSv1.0'; $string['auth_cas_curl_ssl_version_TLSv11'] = 'TLSv1.1'; $string['auth_cas_curl_ssl_version_TLSv12'] = 'TLSv1.2'; $string['auth_casdescription'] = 'This method uses a CAS server (Central Authentication Service) to authenticate users in a Single Sign On environment (SSO). You can also use a simple LDAP authentication. If the given username and password are valid according to CAS, Moodle creates a new user entry in its database, taking user attributes from LDAP if required. On following logins only the username and password are checked.'; $string['auth_cas_enabled'] = 'Turn this on if you want to use CAS authentication.'; $string['auth_cas_hostname'] = 'Hostname of the CAS server <br />eg: host.domain.fr'; $string['auth_cas_hostname_key'] = 'Hostname'; $string['auth_cas_changepasswordurl'] = 'Password-change URL'; $string['auth_cas_invalidcaslogin'] = 'Sorry, your login has failed - you could not be authorised'; $string['auth_cas_language'] = 'Select language for authentication pages'; $string['auth_cas_language_key'] = 'Language'; $string['auth_cas_logincas'] = 'Secure connection access'; $string['auth_cas_logout_return_url_key'] = 'Alternative logout return URL'; $string['auth_cas_logout_return_url'] = 'Provide the URL that CAS users shall be redirected to after logging out.<br />If left empty, users will be redirected to the location that moodle will redirect users to'; $string['auth_cas_logoutcas'] = 'Select \'yes\' if you want to logout from CAS when you disconnect from Moodle'; $string['auth_cas_logoutcas_key'] = 'CAS logout option'; $string['auth_cas_multiauth'] = 'Select \'yes\' if you want to have multi-authentication (CAS + other authentication)'; $string['auth_cas_multiauth_key'] = 'Multi-authentication'; $string['auth_casnotinstalled'] = 'Cannot use CAS authentication. The PHP LDAP module is not installed.'; $string['auth_cas_port'] = 'Port of the CAS server'; $string['auth_cas_port_key'] = 'Port'; $string['auth_cas_proxycas'] = 'Select \'yes\' if you use CAS in proxy-mode'; $string['auth_cas_proxycas_key'] = 'Proxy mode'; $string['auth_cas_server_settings'] = 'CAS server configuration'; $string['auth_cas_text'] = 'Secure connection'; $string['auth_cas_use_cas'] = 'Use CAS'; $string['auth_cas_version'] = 'CAS protocol version to use'; $string['CASform'] = 'Authentication choice'; $string['noldapserver'] = 'No LDAP server configured for CAS! Syncing disabled.'; $string['pluginname'] = 'CAS server (SSO)'; $string['synctask'] = 'CAS users sync job'; $string['privacy:metadata'] = 'The CAS server (SSO) authentication plugin does not store any personal data.'; lti/db/upgrade.php 0000644 00000007445 15152311435 0010073 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * LTI authentication plugin upgrade code * * @package auth_lti * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Upgrade function. * * @param int $oldversion the version we are upgrading from. * @return bool result. */ function xmldb_auth_lti_upgrade($oldversion) { global $DB; $dbman = $DB->get_manager(); if ($oldversion < 2021100500) { // Define table auth_lti_linked_login to be created. $table = new xmldb_table('auth_lti_linked_login'); // Adding fields to table auth_lti_linked_login. $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); $table->add_field('issuer', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); $table->add_field('issuer256', XMLDB_TYPE_CHAR, '64', null, XMLDB_NOTNULL, null, null); $table->add_field('sub', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); $table->add_field('sub256', XMLDB_TYPE_CHAR, '64', null, XMLDB_NOTNULL, null, null); $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); // Adding keys to table auth_lti_linked_login. $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); $table->add_key('userid_key', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']); $table->add_key('unique_key', XMLDB_KEY_UNIQUE, ['userid', 'issuer256', 'sub256']); // Conditionally launch create table for auth_lti_linked_login. if (!$dbman->table_exists($table)) { $dbman->create_table($table); } // Auth LTI savepoint reached. upgrade_plugin_savepoint(true, 2021100500, 'auth', 'lti'); } if ($oldversion < 2022030900) { // Fix the unique key made up of {userid, issuer256, sub256}. // This was improperly defined as ['userid, issuer256, sub256'] in the upgrade step above (note the quotes), // resulting in the potential for missing keys on some databases. // Drop and re-add the key to make sure we have it in place. // Define table auth_lti_linked_login to be modified. $table = new xmldb_table('auth_lti_linked_login'); // Define the key to be dropped and re-added. $key = new xmldb_key('unique_key', XMLDB_KEY_UNIQUE, ['userid', 'issuer256', 'sub256'], 'auth_lti_linked_login'); // Drop the key. $dbman->drop_key($table, $key); // Create the key. $dbman->add_key($table, $key); // Auth LTI savepoint reached. upgrade_plugin_savepoint(true, 2022030900, 'auth', 'lti'); } // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } lti/db/install.xml 0000644 00000003046 15152311435 0010114 0 ustar 00 <?xml version="1.0" encoding="UTF-8" ?> <XMLDB PATH="auth/lti/db" VERSION="20211005" COMMENT="XMLDB file for Moodle auth/lti" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" > <TABLES> <TABLE NAME="auth_lti_linked_login" COMMENT="Accounts linked to a users Moodle account."> <FIELDS> <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The user account the LTI user is linked to."/> <FIELD NAME="issuer" TYPE="text" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="issuer256" TYPE="char" LENGTH="64" NOTNULL="true" SEQUENCE="false" COMMENT="SHA256 hash of the issuer from which the platform user originates."/> <FIELD NAME="sub" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="sub256" TYPE="char" LENGTH="64" NOTNULL="true" SEQUENCE="false" COMMENT="SHA256 hash of the subject identifying the user for the issuer."/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> <KEY NAME="userid_key" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/> <KEY NAME="unique_key" TYPE="unique" FIELDS="userid, issuer256, sub256"/> </KEYS> </TABLE> </TABLES> </XMLDB> lti/tests/auth_test.php 0000644 00000137676 15152311435 0011233 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_lti; /** * Tests for the auth_plugin_lti class. * * @package auth_lti * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \auth_plugin_lti */ class auth_test extends \advanced_testcase { /** @var string issuer URL used for test cases. */ protected $issuer = 'https://lms.example.org'; /** @var int const representing cases where no PII is present. */ protected const PII_NONE = 0; /** @var int const representing cases where only names are included in PII. */ protected const PII_NAMES_ONLY = 1; /** @var int const representing cases where only email is included in PII. */ protected const PII_EMAILS_ONLY = 2; /** @var int const representing cases where both names and email are included in PII. */ protected const PII_ALL = 3; /** * Verify the user's profile picture has been set, which is useful to verify picture syncs. * * @param int $userid the id of the Moodle user. */ protected function verify_user_profile_image_updated(int $userid): void { global $CFG; $user = \core_user::get_user($userid); $usercontext = \context_user::instance($user->id); $expected = $CFG->wwwroot . '/pluginfile.php/' . $usercontext->id . '/user/icon/boost/f2?rev='. $user->picture; $page = new \moodle_page(); $page->set_url('/user/profile.php'); $page->set_context(\context_system::instance()); $renderer = $page->get_renderer('core'); $userpicture = new \user_picture($user); $this->assertEquals($expected, $userpicture->get_url($page, $renderer)->out(false)); } /** * Get a list of users ready for use with mock authentication requests by providing an array of user ids. * * @param array $ids the platform user_ids for the users. * @param string $role the LTI role to include in the user data. * @param bool $includenames whether to include the firstname and lastname of the user * @param bool $includeemail whether to include the email of the user * @param bool $includepicture whether to include a profile picture or not (slows tests, so defaults to false). * @return array the users list. */ protected function get_mock_users_with_ids(array $ids, string $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', bool $includenames = true, bool $includeemail = true, bool $includepicture = false): array { $users = []; foreach ($ids as $id) { $user = [ 'user_id' => $id, 'given_name' => 'Firstname' . $id, 'family_name' => 'Surname' . $id, 'email' => "firstname.surname{$id}@lms.example.org", 'roles' => [$role] ]; if (!$includenames) { unset($user['given_name']); unset($user['family_name']); } if (!$includeemail) { unset($user['email']); } if ($includepicture) { $user['picture'] = $this->getExternalTestFileUrl('/test.jpg'); } $users[] = $user; } return $users; } /** * Get a mock member structure based on a mock user and, optionally, a legacy user id. * * @param array $mockuser the user data * @param string $legacyuserid the id of the user in the platform in 1.1, if different from the id used in 1.3. * @return array */ protected function get_mock_member_data_for_user(array $mockuser, string $legacyuserid = ''): array { $data = [ 'user_id' => $mockuser['user_id'], 'roles' => $mockuser['roles'] ]; if (isset($mockuser['given_name'])) { $data['given_name'] = $mockuser['given_name']; } if (isset($mockuser['family_name'])) { $data['family_name'] = $mockuser['family_name']; } if (isset($mockuser['email'])) { $data['email'] = $mockuser['email']; } if (!empty($mockuser['picture'])) { $data['picture'] = $mockuser['picture']; } if (!empty($legacyuserid)) { $data['lti11_legacy_user_id'] = $legacyuserid; } return $data; } /** * Get mocked JWT data for the given user, including optionally the migration claim information if provided. * * @param array $mockuser the user data * @param array $mockmigration information needed to mock the migration claim * @return array the mock JWT data */ protected function get_mock_launchdata_for_user(array $mockuser, array $mockmigration = []): array { $data = [ 'iss' => $this->issuer, // Must match registration in create_test_environment. 'aud' => '123', // Must match registration in create_test_environment. 'sub' => $mockuser['user_id'], // User id on the platform site. 'exp' => time() + 60, 'nonce' => 'some-nonce-value-123', 'https://purl.imsglobal.org/spec/lti/claim/deployment_id' => '1', // Must match registration. 'https://purl.imsglobal.org/spec/lti/claim/roles' => $mockuser['roles'], 'https://purl.imsglobal.org/spec/lti/claim/resource_link' => [ 'title' => "Res link title", 'id' => 'res-link-id-123', ], "https://purl.imsglobal.org/spec/lti/claim/context" => [ "id" => "context-id-12345", "label" => "ITS 123", "title" => "ITS 123 Machine Learning", "type" => ["http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering"] ], 'https://purl.imsglobal.org/spec/lti/claim/target_link_uri' => 'https://this-moodle-tool.example.org/context/24/resource/14', 'https://purl.imsglobal.org/spec/lti/claim/custom' => [ 'id' => '1' ] ]; if (isset($mockuser['given_name'])) { $data['given_name'] = $mockuser['given_name']; } if (isset($mockuser['family_name'])) { $data['family_name'] = $mockuser['family_name']; } if (isset($mockuser['email'])) { $data['email'] = $mockuser['email']; } if (!empty($mockuser['picture'])) { $data['picture'] = $mockuser['picture']; } if ($mockmigration) { if (isset($mockmigration['consumer_key'])) { $base = [ $mockmigration['consumer_key'], $data['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], $data['iss'], $data['aud'], $data['exp'], $data['nonce'] ]; $basestring = implode('&', $base); $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] = [ 'oauth_consumer_key' => $mockmigration['consumer_key'], ]; if (isset($mockmigration['signing_secret'])) { $sig = base64_encode(hash_hmac('sha256', $basestring, $mockmigration['signing_secret'])); $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['oauth_consumer_key_sign'] = $sig; } } if (isset($mockmigration['user_id'])) { $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['user_id'] = $mockmigration['user_id']; } } return $data; } /** * Test which verifies a user account can be created/found using the find_or_create_user_from_launch() method. * * @dataProvider launch_data_provider * @param array|null $legacydata legacy user and tool data, if testing migration cases. * @param array $launchdata data describing the launch, including user data and migration claim data. * @param array $expected the test case expectations. * @covers ::find_or_create_user_from_launch */ public function test_find_or_create_user_from_launch(?array $legacydata, array $launchdata, array $expected) { $this->resetAfterTest(); global $DB; $auth = get_auth_plugin('lti'); // When testing platform users who have authenticated before, make that first auth call. if (!empty($launchdata['has_authenticated_before']) && $launchdata['has_authenticated_before']) { $mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user']); $firstauthuser = $auth->find_or_create_user_from_launch($mockjwtdata); } // Create legacy users and mocked tool secrets if desired. $legacysecrets = []; if ($legacydata) { $legacyusers = []; $generator = $this->getDataGenerator(); foreach ($legacydata['users'] as $legacyuser) { $username = 'enrol_lti' . sha1($legacydata['consumer_key'] . '::' . $legacydata['consumer_key'] . ':' . $legacyuser['user_id']); $legacyusers[] = $generator->create_user([ 'username' => $username, 'auth' => 'lti' ]); } // In a real usage, legacy tool secrets are only passed for a consumer, as indicated in the migration claim. if (!empty($launchdata['migration_claim'])) { $legacysecrets = array_column($legacydata['tools'], 'secret'); } } // Mock the launchdata. $mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user'], $launchdata['migration_claim'] ?? []); // Authenticate the platform user. $countusersbefore = $DB->count_records('user'); $user = $auth->find_or_create_user_from_launch($mockjwtdata, true, $legacysecrets); if (!empty($expected['migration_debugging'])) { $this->assertDebuggingCalled(); } $countusersafter = $DB->count_records('user'); // Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when // the user has authenticated with the tool before. $numnewusers = (!empty($expected['migrated']) && $expected['migrated']) ? 0 : 1; $numnewusers = (!empty($launchdata['has_authenticated_before']) && $launchdata['has_authenticated_before']) ? 0 : $numnewusers; $this->assertEquals($numnewusers, $countusersafter - $countusersbefore); // Verify PII is updated appropriately. switch ($expected['PII']) { case self::PII_ALL: $this->assertEquals($launchdata['user']['given_name'], $user->firstname); $this->assertEquals($launchdata['user']['family_name'], $user->lastname); $this->assertEquals($launchdata['user']['email'], $user->email); break; case self::PII_NAMES_ONLY: $this->assertEquals($launchdata['user']['given_name'], $user->firstname); $this->assertEquals($launchdata['user']['family_name'], $user->lastname); $email = 'enrol_lti_13_' . sha1($mockjwtdata['iss'] . '_' . $mockjwtdata['sub']) . "@example.com"; $this->assertEquals($email, $user->email); break; case self::PII_EMAILS_ONLY: $this->assertEquals($mockjwtdata['iss'], $user->lastname); $this->assertEquals($mockjwtdata['sub'], $user->firstname); $this->assertEquals($launchdata['user']['email'], $user->email); break; default: case self::PII_NONE: $this->assertEquals($mockjwtdata['iss'], $user->lastname); $this->assertEquals($mockjwtdata['sub'], $user->firstname); $email = 'enrol_lti_13_' . sha1($mockjwtdata['iss'] . '_' . $mockjwtdata['sub']) . "@example.com"; $this->assertEquals($email, $user->email); break; } // Verify picture sync occurs, if expected. if (!empty($expected['syncpicture']) && $expected['syncpicture']) { $this->verify_user_profile_image_updated($user->id); } // If migrated, verify the user account is reusing the legacy user account. if (!empty($expected['migrated']) && $expected['migrated']) { $legacyuserids = array_column($legacyusers, 'id'); $this->assertContains($user->id, $legacyuserids); } // If the user is authenticating a second time, confirm the same account is being returned. if (isset($firstauthuser)) { $this->assertEquals($firstauthuser->id, $user->id); } } /** * Data provider for testing launch-based authentication. * * @return array the test case data. */ public function launch_data_provider(): array { return [ 'New (unlinked) platform learner including PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_ALL, ] ], 'New (unlinked) platform learner excluding names, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_EMAILS_ONLY, ] ], 'New (unlinked) platform learner excluding emails, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, false )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_NAMES_ONLY, ] ], 'New (unlinked) platform learner excluding all PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_NONE, ] ], 'New (unlinked) platform learner including PII, existing legacy user, valid migration claim' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => true ] ], 'New (unlinked) platform learner including PII, existing legacy user, no migration claim' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => null, ], 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration missing consumer_key' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, 'migration_debugging' => true, ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration bad consumer_key' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_BAD', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration user not matched' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '234-bcd', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform learner including PII, existing legacy user, valid migration claim secret2' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret2', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => true ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration claim bad secret' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'bad_secret', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, 'migration_debugging' => true, ] ], 'New (unlinked) platform learner including PII, no legacy user, valid migration claim' => [ 'legacy_data' => [ 'users' => [], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret2', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform learner excluding PII, existing legacy user, valid migration claim' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'PII' => self::PII_NONE, 'migrated' => true ] ], 'New (unlinked) platform instructor including PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_ALL, ] ], 'New (unlinked) platform instructor excluding PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_NONE, ] ], 'New (unlinked) platform instructor including PII, existing legacy user, valid migration claim' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launch_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => true ] ], 'Existing (linked) platform learner including PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => true, 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_ALL, ] ], 'Existing (linked) platform learner excluding PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => true, 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_NONE, ] ], 'Existing (linked) platform instructor including PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => true, 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_ALL, ] ], 'Existing (linked) platform instructor excluding PII, no legacy user, no migration claim' => [ 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => true, 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_NONE, ] ], 'New (unlinked) platform instructor excluding PII, picture included' => [ 'legacy_data' => null, 'launch_data' => [ 'has_authenticated_before' => false, 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false, true )[0], 'migration_claim' => null ], 'expected' => [ 'PII' => self::PII_NONE, 'syncpicture' => true ] ] ]; } /** * Test which verifies a user account can be created/found using the find_or_create_user_from_membership() method. * * @dataProvider membership_data_provider * @param array|null $legacydata legacy user and tool data, if testing migration cases. * @param array $memberdata data describing the membership data, including user data and legacy user id info. * @param string $iss the issuer URL string * @param string|null $legacyconsumerkey optional legacy consumer_key value for testing user migration * @param array $expected the test case expectations. * @covers ::find_or_create_user_from_membership */ public function test_find_or_create_user_from_membership(?array $legacydata, array $memberdata, string $iss, ?string $legacyconsumerkey, array $expected) { $this->resetAfterTest(); global $DB; $auth = get_auth_plugin('lti'); // When testing platform users who have authenticated before, make that first auth call. if (!empty($memberdata['has_authenticated_before']) && $memberdata['has_authenticated_before']) { $mockmemberdata = $this->get_mock_member_data_for_user($memberdata['user'], $memberdata['legacy_user_id'] ?? ''); $firstauthuser = $auth->find_or_create_user_from_membership($mockmemberdata, $iss, $legacyconsumerkey ?? ''); } // Create legacy users and mocked tool secrets if desired. if ($legacydata) { $legacyusers = []; $generator = $this->getDataGenerator(); foreach ($legacydata['users'] as $legacyuser) { $username = 'enrol_lti' . sha1($legacydata['consumer_key'] . '::' . $legacydata['consumer_key'] . ':' . $legacyuser['user_id']); $legacyusers[] = $generator->create_user([ 'username' => $username, 'auth' => 'lti' ]); } } // Mock the membership data. $mockmemberdata = $this->get_mock_member_data_for_user($memberdata['user'], $memberdata['legacy_user_id'] ?? ''); // Authenticate the platform user. $countusersbefore = $DB->count_records('user'); $user = $auth->find_or_create_user_from_membership($mockmemberdata, $iss, $legacyconsumerkey ?? ''); $countusersafter = $DB->count_records('user'); // Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when // the user has authenticated with the tool before. $numnewusers = (!empty($expected['migrated']) && $expected['migrated']) ? 0 : 1; $numnewusers = (!empty($memberdata['has_authenticated_before']) && $memberdata['has_authenticated_before']) ? 0 : $numnewusers; $this->assertEquals($numnewusers, $countusersafter - $countusersbefore); // Verify PII is updated appropriately. switch ($expected['PII']) { case self::PII_ALL: $this->assertEquals($memberdata['user']['given_name'], $user->firstname); $this->assertEquals($memberdata['user']['family_name'], $user->lastname); $this->assertEquals($memberdata['user']['email'], $user->email); break; case self::PII_NAMES_ONLY: $this->assertEquals($memberdata['user']['given_name'], $user->firstname); $this->assertEquals($memberdata['user']['family_name'], $user->lastname); $email = 'enrol_lti_13_' . sha1($iss . '_' . $mockmemberdata['user_id']) . "@example.com"; $this->assertEquals($email, $user->email); break; case self::PII_EMAILS_ONLY: $this->assertEquals($iss, $user->lastname); $this->assertEquals($mockmemberdata['user_id'], $user->firstname); $this->assertEquals($memberdata['user']['email'], $user->email); break; default: case self::PII_NONE: $this->assertEquals($iss, $user->lastname); $this->assertEquals($mockmemberdata['user_id'], $user->firstname); $email = 'enrol_lti_13_' . sha1($iss . '_' . $mockmemberdata['user_id']) . "@example.com"; $this->assertEquals($email, $user->email); break; } // If migrated, verify the user account is reusing the legacy user account. if (!empty($expected['migrated']) && $expected['migrated']) { $legacyuserids = array_column($legacyusers, 'id'); $this->assertContains($user->id, $legacyuserids); } // If the user is authenticating a second time, confirm the same account is being returned. if (isset($firstauthuser)) { $this->assertEquals($firstauthuser->id, $user->id); } } /** * Data provider for testing membership-service-based authentication. * * @return array the test case data. */ public function membership_data_provider(): array { return [ 'New (unlinked) platform learner including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => $this->issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform learner excluding PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], ], 'iss' => $this->issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_NONE, 'migrated' => false ] ], 'New (unlinked) platform learner excluding names, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, )[0], ], 'iss' => $this->issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_EMAILS_ONLY, 'migrated' => false ] ], 'New (unlinked) platform learner excluding email, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, false )[0], ], 'iss' => $this->issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_NAMES_ONLY, 'migrated' => false ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key bound, legacy user id sent' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], 'iss' => $this->issuer, 'legacy_consumer_key' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => true ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key bound, legacy user id omitted' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => $this->issuer, 'legacy_consumer_key' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key bound, no change in user id' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['123-abc'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => $this->issuer, 'legacy_consumer_key' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => true ] ], 'New (unlinked) platform learner including PII, legacy user, unexpected consumer key bound, no change in user id' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['123-abc'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => $this->issuer, 'legacy_consumer_key' => 'CONSUMER_ABCDEF', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key not bound, legacy user id sent' => [ 'legacy_data' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], 'iss' => $this->issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform learner including PII, no legacy data, consumer key bound, legacy user id sent' => [ 'legacy_data' => null, 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], 'iss' => $this->issuer, 'legacy_consumer_key' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform instructor including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], ], 'iss' => $this->issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform instructor excluding PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacy_data' => null, 'membership_data' => [ 'user' => $this->get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], ], 'iss' => $this->issuer, 'legacy_consumer_key' => null, 'expected' => [ 'PII' => self::PII_NONE, 'migrated' => false ] ] ]; } /** * Test the behaviour of create_user_binding(). * * @covers ::create_user_binding */ public function test_create_user_binding() { $this->resetAfterTest(); global $DB; $auth = get_auth_plugin('lti'); $user = $this->getDataGenerator()->create_user(); $mockiss = $this->issuer; $mocksub = '1'; // Create a binding and verify it exists. $this->assertFalse($DB->record_exists('auth_lti_linked_login', ['userid' => $user->id])); $auth->create_user_binding($mockiss, $mocksub, $user->id); $this->assertTrue($DB->record_exists('auth_lti_linked_login', ['userid' => $user->id])); // Now, try to get an authenticated user USING that binding. Verify the bound user is returned. $numusersbefore = $DB->count_records('user'); $matcheduser = $auth->find_or_create_user_from_launch( $this->get_mock_launchdata_for_user( $this->get_mock_users_with_ids([$mocksub])[0] ) ); $numusersafter = $DB->count_records('user'); $this->assertEquals($numusersafter, $numusersbefore); $this->assertEquals($user->id, $matcheduser->id); // Assert idempotency of the bind call. $this->assertNull($auth->create_user_binding($mockiss, $mocksub, $user->id)); } } lti/tests/privacy/provider_test.php 0000644 00000023273 15152311435 0013564 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_lti\privacy; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; use core_privacy\local\request\approved_userlist; /** * Test for the auth_lti privacy provider. * * @package auth_lti * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \auth_lti\privacy\provider */ class provider_test extends provider_testcase { /** * Set up method. */ public function setUp(): void { $this->resetAfterTest(); $this->setAdminUser(); } /** * Check that a user context is returned if there is any user data for this user. * * @covers ::get_contexts_for_userid */ public function test_get_contexts_for_userid() { $user = $this->getDataGenerator()->create_user(); $this->assertEmpty(provider::get_contexts_for_userid($user->id)); $auth = get_auth_plugin('lti'); $auth->create_user_binding('https://lms.example.com', 'abc123', $user->id); $contextlist = provider::get_contexts_for_userid($user->id); // Check that we only get back one context. $this->assertCount(1, $contextlist); // Check that a context is returned is the expected. $usercontext = \context_user::instance($user->id); $this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]); } /** * Test that user data is exported correctly. * * @covers ::export_user_data */ public function test_export_user_data() { $user = $this->getDataGenerator()->create_user(); $auth = get_auth_plugin('lti'); $auth->create_user_binding('https://lms.example.com', 'abc123', $user->id); $usercontext = \context_user::instance($user->id); $writer = writer::with_context($usercontext); $this->assertFalse($writer->has_any_data()); $approvedlist = new approved_contextlist($user, 'auth_lti', [$usercontext->id]); provider::export_user_data($approvedlist); $data = $writer->get_data([get_string('privacy:metadata:auth_lti', 'auth_lti'), 'https://lms.example.com']); $this->assertEquals('https://lms.example.com', $data->issuer); $this->assertEquals(hash('sha256', 'https://lms.example.com'), $data->issuer256); $this->assertEquals('abc123', $data->sub); $this->assertEquals(hash('sha256', 'abc123'), $data->sub256); } /** * Test deleting all user data for a specific context. * * @covers ::delete_data_for_all_users_in_context */ public function test_delete_data_for_all_users_in_context() { global $DB; $auth = get_auth_plugin('lti'); $user1 = $this->getDataGenerator()->create_user(); $auth->create_user_binding('https://lms.example.com', 'abc123', $user1->id); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $auth->create_user_binding('https://lms.example.com', 'def456', $user2->id); // Verify there are two linked logins. $ltiaccounts = $DB->get_records('auth_lti_linked_login'); $this->assertCount(2, $ltiaccounts); // Delete everything for the first user context. provider::delete_data_for_all_users_in_context($user1context); // Get all LTI linked accounts match with user1. $ltiaccounts = $DB->get_records('auth_lti_linked_login', ['userid' => $user1->id]); $this->assertCount(0, $ltiaccounts); // Verify there is now only one linked login. $ltiaccounts = $DB->get_records('auth_lti_linked_login'); $this->assertCount(1, $ltiaccounts); } /** * This should work identical to the above test. * * @covers ::delete_data_for_user */ public function test_delete_data_for_user() { global $DB; $auth = get_auth_plugin('lti'); $user1 = $this->getDataGenerator()->create_user(); $auth->create_user_binding('https://lms.example.com', 'abc123', $user1->id); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $auth->create_user_binding('https://lms.example.com', 'def456', $user2->id); // Verify there are two linked logins. $ltiaccounts = $DB->get_records('auth_lti_linked_login'); $this->assertCount(2, $ltiaccounts); // Delete everything for the first user. $approvedlist = new approved_contextlist($user1, 'auth_lti', [$user1context->id]); provider::delete_data_for_user($approvedlist); // Get all LTI accounts linked with user1. $ltiaccounts = $DB->get_records('auth_lti_linked_login', ['userid' => $user1->id]); $this->assertCount(0, $ltiaccounts); // Verify there is only one linked login now. $ltiaccounts = $DB->get_records('auth_lti_linked_login', array()); $this->assertCount(1, $ltiaccounts); } /** * Test that only users with a user context are fetched. * * @covers ::get_users_in_context */ public function test_get_users_in_context() { $auth = get_auth_plugin('lti'); $component = 'auth_lti'; $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (no linked login yet). $userlist = new userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $auth->create_user_binding('https://lms.example.com', 'abc123', $user->id); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. * * @covers ::delete_data_for_users */ public function test_delete_data_for_users() { $auth = get_auth_plugin('lti'); $component = 'auth_lti'; $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $auth->create_user_binding('https://lms.example.com', 'abc123', $user1->id); $auth->create_user_binding('https://lms.example.com', 'def456', $user2->id); // The list of users for usercontext1 should return user1. $userlist1 = new userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); } } lti/templates/local/ltiadvantage/account_binding_complete.mustache 0000644 00000003334 15152311435 0021643 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template auth_lti/account_binding_complete Template which displays the confirmation after the user has either signed in and has their account linked, or has had an account automatically provisioned and linked. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * notification * returnurl Example context (json): { "notification": { "message": "Your account was successfully linked!", "extraclasses": "", "announce": true, "closebutton": false, "issuccess": true, "isinfo": false, "iswarning": false, "iserror": false }, "returnurl": "https://your.site/enrol/lti/launch_deeplink.php?id=123abc" } }} <div id="lti_adv_account_binding_complete"> {{#notification}} {{> core/notification}} {{/notification}} <a class="btn btn-primary" href="{{returnurl}}">{{#str}}continue, core{{/str}}</a> </div> lti/templates/local/ltiadvantage/login.mustache 0000644 00000013073 15152311435 0015736 0 ustar 00 {{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template auth_lti/login Template which displays a choice screen for instructors on first launch, allowing them to select whether to use an existing account in the tool, or to auto provision a new one. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * formaction * sesskey * info - a notification describing the first launch options * cancreateaccounts - whether or not the user is allowed to create auth_lti accounts * accountinfo - information about the user, importantly whether they are logged in or not. * noauthnotice - a notification telling the user they must be authenticated to link accounts. Only relevant when not logged in. Example context (json): { "formaction": "auth/lti/login.php", "sesskey": "1a2b3c4dfg", "info": { "message": "Looks like this is your first time here...", "extraclasses": "", "announce": false, "closebutton": false, "issuccess": true }, "cancreateaccounts": true, "accountinfo": { "isloggedin": true, "firstname": "John", "lastname": "Smith", "email": "john@example.com", "picturehtml": "<img src=\"http://site.example.com/pluginfile.php/5/user/icon/boost/f2?rev=99\" class=\"round\" alt=\"\" width=\"35\" height=\"35\">" }, "noauthnotice": { "message": "To link your existing account you must be logged in to the site...", "extraclasses": "", "announce": false, "closebutton": false, "iswarning": true } } }} <div id="lti_adv_account_binding_options"> <form action="{{formaction}}" method="POST"> <input type="hidden" name="sesskey" value="{{sesskey}}"> <div class="container-fluid"> <h2>{{#str}} welcome, auth_lti {{/str}}</h2> {{#info}} {{> core/notification}} {{/info}} <div class="row"> <div class="{{#cancreateaccounts}}col-sm-6{{/cancreateaccounts}}{{^cancreateaccounts}}col-sm-12{{/cancreateaccounts}} d-flex"> <div class="card w-100"> <div class="card-header"> {{#str}} haveexistingaccount, auth_lti {{/str}} </div> <div class="card-body text-center d-flex flex-column"> <i class="fa fa-user-circle-o fa-2x link"></i> <h4 class="card-title">{{#str}} useexistingaccount, auth_lti {{/str}}</h4> {{#accountinfo}} {{#isloggedin}} <p class="card-text mt-2"> <span class="text-muted"> {{#str}} currentlyloggedinas, auth_lti {{/str}} </span> <br> {{{picturehtml}}} {{firstname}} {{lastname}} ({{email}}) </p> <input type="submit" class="btn btn-primary mt-auto" name="existing_account" value="{{#str}} linkthisaccount, auth_lti {{/str}}"> {{/isloggedin}} {{^isloggedin}} <p class="card-text text-muted">{{#str}} mustbeloggedin, auth_lti {{/str}}</p> {{#noauthnotice}} {{> core/notification}} {{/noauthnotice}} {{/isloggedin}} {{/accountinfo}} </div> </div> </div> {{#cancreateaccounts}} <div class="col-sm-6 d-flex"> <div class="card w-100"> <div class="card-header"> {{#str}} createnewaccount, auth_lti {{/str}} </div> <div class="card-body text-center d-flex flex-column"> <i class="fa fa-user-plus fa-2x"></i> <h4 class="card-title">{{#str}} createaccount, auth_lti {{/str}}</h4> <p class="card-text text-muted">{{#str}} getstartedwithnewaccount, auth_lti {{/str}}</p> <input type="submit" class="btn btn-secondary mt-auto" name="new_account" value="{{#str}} createaccountforme, auth_lti {{/str}}"> </div> </div> </div> {{/cancreateaccounts}} </div> </div> </form> </div> lti/auth.php 0000644 00000052425 15152311435 0007016 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. use auth_lti\local\ltiadvantage\entity\user_migration_claim; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); require_once($CFG->libdir.'/accesslib.php'); /** * LTI Authentication plugin. * * @package auth_lti * @copyright 2016 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_plugin_lti extends \auth_plugin_base { /** * @var int constant representing the automatic account provisioning mode. * On first launch, for a previously unbound user, this mode dictates that a new Moodle account will be created automatically * for the user and bound to their platform credentials {iss, sub}. */ public const PROVISIONING_MODE_AUTO_ONLY = 1; /** * @var int constant representing the prompt for new or existing provisioning mode. * On first launch, for a previously unbound user, the mode dictates that the launch user will be presented with an options * view, allowing them to select either 'link an existing account' or 'create a new account for me'. */ public const PROVISIONING_MODE_PROMPT_NEW_EXISTING = 2; /** * @var int constant representing the prompt for existing only provisioning mode. * On first launch, for a previously unbound user, the mode dictates that the launch user will be presented with a view allowing * them to link an existing account only. This is useful for situations like deep linking, where an existing account is needed. */ public const PROVISIONING_MODE_PROMPT_EXISTING_ONLY = 3; /** * Constructor. */ public function __construct() { $this->authtype = 'lti'; } /** * Users can not log in via the traditional login form. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure */ public function user_login($username, $password) { return false; } /** * Authenticate the user based on the unique {iss, sub} tuple present in the OIDC JWT. * * This method ensures a Moodle user account has been found or is created, that the user is linked to the relevant * LTI Advantage credentials (iss, sub) and that the user account is logged in. * * Launch code can therefore rely on this method to get a session before doing things like calling require_login(). * * This method supports two workflows: * 1. Automatic account provisioning - where the complete_login() call will ALWAYS create/find a user and return to * calling code directly. No user interaction is required. * * 2. Manual account provisioning - where the complete_login() call will redirect ONLY ONCE to a login page, * where the user can decide whether to use an automatically provisioned account, or bind an existing user account. * When the decision has been made, the relevant account is bound and the user is redirected back to $returnurl. * Once an account has been bound via this selection process, subsequent calls to complete_login() will return to * calling code directly. Any calling code must provide its $returnurl to support the return from the account * selection process and must also take care to cache any JWT data appropriately, since the return will not inlude * that information. * * Which workflow is chosen depends on the roles present in the JWT. * For teachers/admins, manual provisioning will take place. These user type are likely to have existing accounts. * For learners, automatic provisioning will take place. * * Migration of legacy users is supported, however, only for the Learner role (automatic provisioning). Admins and * teachers are likely to have existing accounts and we want them to be able to select and bind these, rather than * binding an automatically provisioned legacy account which doesn't represent their real user account. * * The JWT data must be verified elsewhere. The code here assumes its integrity/authenticity. * * @param array $launchdata the JWT data provided in the link launch. * @param moodle_url $returnurl the local URL to return to if authentication workflows are required. * @param int $provisioningmode the desired account provisioning mode, which controls the auth flow for unbound users. * @param array $legacyconsumersecrets an array of secrets used by the legacy consumer if a migration claim exists. * @throws coding_exception if the specified provisioning mode is invalid. */ public function complete_login(array $launchdata, moodle_url $returnurl, int $provisioningmode, array $legacyconsumersecrets = []): void { // The platform user is already linked with a user account. if ($this->get_user_binding($launchdata['iss'], $launchdata['sub'])) { // Always sync the PII, regardless of whether we're already authenticated as this user or not. $user = $this->find_or_create_user_from_launch($launchdata, true); if (isloggedin()) { // If a different user is currently logged in, authenticate the linked user instead. global $USER; if ((int) $USER->id !== $user->id) { complete_user_login($user); } // If the linked user is already logged in, skip the call to complete_user_login() because this affects deep linking // workflows on sites publishing and consuming resources on the same site, due to the regenerated sesskey. return; } else { complete_user_login($user); return; } } // The platform user is not bound to a user account, check provisioning mode now. if (!$this->is_valid_provisioning_mode($provisioningmode)) { throw new coding_exception('Invalid account provisioning mode provided to complete_login().'); } switch ($provisioningmode) { case self::PROVISIONING_MODE_AUTO_ONLY: // Automatic provisioning - this will create/migrate a user account and log the user in. complete_user_login($this->find_or_create_user_from_launch($launchdata, true, $legacyconsumersecrets)); break; case self::PROVISIONING_MODE_PROMPT_NEW_EXISTING: case self::PROVISIONING_MODE_PROMPT_EXISTING_ONLY: default: // Allow linking an existing account or creation of a new account via an intermediate account options page. // Cache the relevant data and take the user to the account options page. // Note: This mode also depends on the global auth config 'authpreventaccountcreation'. If set, only existing // accounts can be bound in this provisioning mode. global $SESSION; $SESSION->auth_lti = (object)[ 'launchdata' => $launchdata, 'returnurl' => $returnurl, 'provisioningmode' => $provisioningmode ]; redirect(new moodle_url('/auth/lti/login.php', [ 'sesskey' => sesskey(), ])); break; } } /** * Get a Moodle user account for the LTI user based on the user details returned by a NRPS 2 membership call. * * This method expects a single member structure, in array format, as defined here: * See: https://www.imsglobal.org/spec/lti-nrps/v2p0#membership-container-media-type. * * This method supports migration of user accounts used in legacy launches, provided the legacy consumerkey corresponding to * to the legacy consumer is provided. Calling code will have verified the migration claim during initial launches and should * have the consumer key mapped to the deployment, ready to pass in. * * @param array $member the member data, in array format. * @param string $iss the issuer to which the member relates. * @param string $legacyconsumerkey optional consumer key mapped to the deployment to facilitate user migration. * @return stdClass a Moodle user record. */ public function find_or_create_user_from_membership(array $member, string $iss, string $legacyconsumerkey = ''): stdClass { // Picture is not synced with membership-based auths because sync tasks may wish to perform slow operations like this a // different way. unset($member['picture']); if ($binduser = $this->get_user_binding($iss, $member['user_id'])) { $user = \core_user::get_user((int) $binduser); $this->update_user_account($user, $member, $iss); return \core_user::get_user($user->id); } else { if (!empty($legacyconsumerkey)) { // Consumer key is required to attempt user migration because legacy users were identified by a // username consisting of the consumer key and user id. // See the legacy \enrol_lti\helper::create_username() for details. $legacyuserid = $member['lti11_legacy_user_id'] ?? $member['user_id']; $username = 'enrol_lti' . sha1($legacyconsumerkey . '::' . $legacyconsumerkey . ':' . $legacyuserid); if ($user = \core_user::get_user_by_username($username)) { $this->create_user_binding($iss, $member['user_id'], $user->id); $this->update_user_account($user, $member, $iss); return \core_user::get_user($user->id); } } $user = $this->create_new_account($member, $iss); $this->update_user_account($user, $member, $iss); return \core_user::get_user($user->id); } } /** * Get a Moodle user account for the LTI user corresponding to the user defined in a link launch. * * This method supports migration of user accounts used in legacy launches, provided the legacy consumer secrets corresponding * to the legacy consumer are provided. If calling code wishes migration to be role-specific, it should check roles accordingly * itself and pass relevant data in - as auth_plugin_lti::complete_login() does. * * @param array $launchdata all data in the decoded JWT including iss and sub. * @param bool $syncpicture whether to sync the user's picture with the picture sent in the launch. * @param array $legacyconsumersecrets all secrets found for the legacy consumer, facilitating user migration. * @return stdClass the Moodle user who is mapped to the platform user identified in the JWT data. */ public function find_or_create_user_from_launch(array $launchdata, bool $syncpicture = false, array $legacyconsumersecrets = []): stdClass { if (!$syncpicture) { unset($launchdata['picture']); } if ($binduser = $this->get_user_binding($launchdata['iss'], $launchdata['sub'])) { $user = \core_user::get_user((int) $binduser); $this->update_user_account($user, $launchdata, $launchdata['iss']); return \core_user::get_user($user->id); } else { // Is the intent to migrate a user account used in legacy launches? if (!empty($legacyconsumersecrets)) { try { // Validate the migration claim and try to find a legacy user. $usermigrationclaim = new user_migration_claim($launchdata, $legacyconsumersecrets); $username = 'enrol_lti' . sha1($usermigrationclaim->get_consumer_key() . '::' . $usermigrationclaim->get_consumer_key() .':' .$usermigrationclaim->get_user_id()); if ($user = \core_user::get_user_by_username($username)) { $this->create_user_binding($launchdata['iss'], $launchdata['sub'], $user->id); $this->update_user_account($user, $launchdata, $launchdata['iss']); return \core_user::get_user($user->id); } } catch (Exception $e) { // There was an issue validating the user migration claim. We don't want to fail auth entirely though. // Rather, we want to fall back to new account creation and log the attempt. debugging("There was a problem migrating the LTI user '{$launchdata['sub']}' on the platform " . "'{$launchdata['iss']}'. The migration claim could not be validated. A new account will be created."); } } $user = $this->create_new_account($launchdata, $launchdata['iss']); $this->update_user_account($user, $launchdata, $launchdata['iss']); return \core_user::get_user($user->id); } } /** * Create a binding between the LTI user, as identified by {iss, sub} tuple and the user id. * * @param string $iss the issuer URL identifying the platform to which to user belongs. * @param string $sub the sub string identifying the user on the platform. * @param int $userid the id of the Moodle user account to bind. */ public function create_user_binding(string $iss, string $sub, int $userid): void { global $DB; $timenow = time(); $issuer256 = hash('sha256', $iss); $sub256 = hash('sha256', $sub); if ($DB->record_exists('auth_lti_linked_login', ['issuer256' => $issuer256, 'sub256' => $sub256])) { return; } $rec = [ 'userid' => $userid, 'issuer' => $iss, 'issuer256' => $issuer256, 'sub' => $sub, 'sub256' => $sub256, 'timecreated' => $timenow, 'timemodified' => $timenow ]; $DB->insert_record('auth_lti_linked_login', $rec); } /** * Gets the id of the linked Moodle user account for an LTI user, or null if not found. * * @param string $issuer the issuer to which the user belongs. * @param string $sub the sub string identifying the user on the issuer. * @return int|null the id of the corresponding Moodle user record, or null if not found. */ public function get_user_binding(string $issuer, string $sub): ?int { global $DB; $issuer256 = hash('sha256', $issuer); $sub256 = hash('sha256', $sub); try { $binduser = $DB->get_field('auth_lti_linked_login', 'userid', ['issuer256' => $issuer256, 'sub256' => $sub256], MUST_EXIST); } catch (\dml_exception $e) { $binduser = null; } return $binduser; } /** * Check whether a provisioning mode is valid or not. * * @param int $mode the mode * @return bool true if valid for use, false otherwise. */ protected function is_valid_provisioning_mode(int $mode): bool { $validmodes = [ self::PROVISIONING_MODE_AUTO_ONLY, self::PROVISIONING_MODE_PROMPT_NEW_EXISTING, self::PROVISIONING_MODE_PROMPT_EXISTING_ONLY ]; return in_array($mode, $validmodes); } /** * Create a new user account based on the user data either in the launch JWT or from a membership call. * * @param array $userdata the user data coming from either a launch or membership service call. * @param string $iss the issuer to which the user belongs. * @return stdClass a complete Moodle user record. */ protected function create_new_account(array $userdata, string $iss): stdClass { global $CFG; require_once($CFG->dirroot.'/user/lib.php'); // Launches and membership calls handle the user id differently. // Launch uses 'sub', whereas member uses 'user_id'. $userid = !empty($userdata['sub']) ? $userdata['sub'] : $userdata['user_id']; $user = new stdClass(); $user->username = 'enrol_lti_13_' . sha1($iss . '_' . $userid); // If the email was stripped/not set then fill it with a default one. // This stops the user from being redirected to edit their profile page. $email = !empty($userdata['email']) ? $userdata['email'] : 'enrol_lti_13_' . sha1($iss . '_' . $userid) . "@example.com"; $email = \core_user::clean_field($email, 'email'); $user->email = $email; $user->auth = 'lti'; $user->mnethostid = $CFG->mnet_localhost_id; $user->firstname = $userdata['given_name'] ?? $userid; $user->lastname = $userdata['family_name'] ?? $iss; $user->password = ''; $user->confirmed = 1; $user->id = user_create_user($user, false); // Link this user with the LTI credentials for future use. $this->create_user_binding($iss, $userid, $user->id); return (object) get_complete_user_data('id', $user->id); } /** * Update the personal fields of the user account, based on data present in either a launch of member sync call. * * @param stdClass $user the Moodle user account to update. * @param array $userdata the user data coming from either a launch or membership service call. * @param string $iss the issuer to which the user belongs. */ protected function update_user_account(stdClass $user, array $userdata, string $iss): void { global $CFG; require_once($CFG->dirroot.'/user/lib.php'); if ($user->auth !== 'lti') { return; } // Launches and membership calls handle the user id differently. // Launch uses 'sub', whereas member uses 'user_id'. $platformuserid = !empty($userdata['sub']) ? $userdata['sub'] : $userdata['user_id']; $email = !empty($userdata['email']) ? $userdata['email'] : 'enrol_lti_13_' . sha1($iss . '_' . $platformuserid) . "@example.com"; $email = \core_user::clean_field($email, 'email'); $update = [ 'id' => $user->id, 'firstname' => $userdata['given_name'] ?? $platformuserid, 'lastname' => $userdata['family_name'] ?? $iss, 'email' => $email ]; user_update_user($update); if (!empty($userdata['picture'])) { try { $this->update_user_picture($user->id, $userdata['picture']); } catch (Exception $e) { debugging("Error syncing the profile picture for user '$user->id' during LTI authentication."); } } } /** * Update the user's picture with the image stored at $url. * * @param int $userid the id of the user to update. * @param string $url the string URL where the new image can be found. * @throws moodle_exception if there were any problems updating the picture. */ protected function update_user_picture(int $userid, string $url): void { global $CFG, $DB; require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/gdlib.php'); $fs = get_file_storage(); $context = \context_user::instance($userid, MUST_EXIST); $fs->delete_area_files($context->id, 'user', 'newicon'); $filerecord = array( 'contextid' => $context->id, 'component' => 'user', 'filearea' => 'newicon', 'itemid' => 0, 'filepath' => '/' ); $urlparams = array( 'calctimeout' => false, 'timeout' => 5, 'skipcertverify' => true, 'connecttimeout' => 5 ); try { $fs->create_file_from_url($filerecord, $url, $urlparams); } catch (\file_exception $e) { throw new moodle_exception(get_string($e->errorcode, $e->module, $e->a)); } $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false); // There should only be one. $iconfile = reset($iconfile); // Something went wrong while creating temp file - remove the uploaded file. if (!$iconfile = $iconfile->copy_content_to_temp()) { $fs->delete_area_files($context->id, 'user', 'newicon'); throw new moodle_exception('There was a problem copying the profile picture to temp.'); } // Copy file to temporary location and the send it for processing icon. $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile); // Delete temporary file. @unlink($iconfile); // Remove uploaded file. $fs->delete_area_files($context->id, 'user', 'newicon'); // Set the user's picture. $DB->set_field('user', 'picture', $newpicture, array('id' => $userid)); } } lti/login.php 0000644 00000012104 15152311435 0007153 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Page allowing a platform user, identified by their {iss, sub} tuple, to be bound to a new or existing Moodle account. * * This is an LTI Advantage specific login feature. * * The auth flow defined in auth_lti\auth::complete_login() redirects here when a launching user does not have an * account binding yet. This page prompts the user to select between: * a) An auto provisioned account. * An account with auth type 'lti' is created for the user. This account is bound to the launch credentials. * Or * b) Use an existing account * The standard Moodle auth flow is leveraged to get an existing user account. This account is then bound to the launch * credentials. * * @package auth_lti * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\event\user_login_failed; use core\output\notification; require_once(__DIR__ . '/../../config.php'); global $OUTPUT, $PAGE, $SESSION; // Form fields dealing with the user's choice about account types (new, existing). $newaccount = optional_param('new_account', false, PARAM_BOOL); $existingaccount = optional_param('existing_account', false, PARAM_BOOL); if (empty($SESSION->auth_lti) || empty($SESSION->auth_lti->launchdata)) { throw new coding_exception('Missing LTI launch credentials.'); } if (empty($SESSION->auth_lti->returnurl)) { throw new coding_exception('Missing return URL.'); } if ($newaccount) { require_sesskey(); $launchdata = $SESSION->auth_lti->launchdata; $returnurl = $SESSION->auth_lti->returnurl; unset($SESSION->auth_lti); if (!empty($CFG->authpreventaccountcreation)) { // If 'authpreventaccountcreation' is enabled, the option to create a new account isn't presented to users in the form. // This just ensures no action is taken were the 'newaccount' value to be present in the submitted data. // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = user_login_failed::create(['other' => ['reason' => $failurereason]]); $event->trigger(); // Site settings prevent creating new accounts. $errormsg = get_string('cannotcreateaccounts', 'auth_lti'); $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url('/login/index.php')); } else { // Create a new account and link it, logging the user in. $auth = get_auth_plugin('lti'); $newuser = $auth->find_or_create_user_from_launch($launchdata, true); complete_user_login($newuser); $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/auth/lti/login.php')); $PAGE->set_pagelayout('popup'); $renderer = $PAGE->get_renderer('auth_lti'); echo $OUTPUT->header(); echo $renderer->render_account_binding_complete( new notification(get_string('accountcreatedsuccess', 'auth_lti'), notification::NOTIFY_SUCCESS, false), $returnurl ); echo $OUTPUT->footer(); exit(); } } else if ($existingaccount) { // Only when authenticated can an account be bound, allowing the user to continue to the original launch action. require_login(null, false); require_sesskey(); $launchdata = $SESSION->auth_lti->launchdata; $returnurl = $SESSION->auth_lti->returnurl; unset($SESSION->auth_lti); global $USER; $auth = get_auth_plugin('lti'); $auth->create_user_binding($launchdata['iss'], $launchdata['sub'], $USER->id); $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/auth/lti/login.php')); $PAGE->set_pagelayout('popup'); $renderer = $PAGE->get_renderer('auth_lti'); echo $OUTPUT->header(); echo $renderer->render_account_binding_complete( new notification(get_string('accountlinkedsuccess', 'auth_lti'), notification::NOTIFY_SUCCESS, false), $returnurl ); echo $OUTPUT->footer(); exit(); } // Render the relevant account provisioning page, based on the provisioningmode set in the calling code. $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/auth/lti/login.php')); $PAGE->set_pagelayout('popup'); $renderer = $PAGE->get_renderer('auth_lti'); echo $OUTPUT->header(); require_once($CFG->dirroot . '/auth/lti/auth.php'); echo $renderer->render_account_binding_options_page($SESSION->auth_lti->provisioningmode); echo $OUTPUT->footer(); lti/lib.php 0000644 00000002223 15152311435 0006612 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Callbacks for auth_lti. * * @package auth_lti * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Callback to remove linked logins for deleted users. * * @param stdClass $user the user being deleted. */ function auth_lti_pre_user_delete($user) { global $DB; $DB->delete_records('auth_lti_linked_login', ['userid' => $user->id]); } lti/version.php 0000644 00000002207 15152311435 0007533 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * LTI authentication plugin version information * * @package auth_lti * @copyright 2016 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_lti'; // Full name of the plugin (used for diagnostics). lti/classes/output/renderer.php 0000644 00000010005 15152311435 0012644 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_lti\output; use core\output\notification; /** * Renderer class for auth_lti. * * @package auth_lti * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends \plugin_renderer_base { /** * Render the account options view, displayed to instructors on first launch if no account binding exists. * * @param int $provisioningmode the desired account provisioning mode, see auth_plugin_lti constants for details. * @return string the html. */ public function render_account_binding_options_page(int $provisioningmode): string { $formaction = new \moodle_url('/auth/lti/login.php'); $notification = new notification(get_string('firstlaunchnotice', 'auth_lti'), \core\notification::INFO, false); $noauthnotice = new notification(get_string('firstlaunchnoauthnotice', 'auth_lti', get_docs_url('Publish_as_LTI_tool')), \core\notification::WARNING, false); $cancreateaccounts = !get_config('moodle', 'authpreventaccountcreation'); if ($provisioningmode == \auth_plugin_lti::PROVISIONING_MODE_PROMPT_EXISTING_ONLY) { $cancreateaccounts = false; } $accountinfo = ['isloggedin' => isloggedin()]; if (isloggedin()) { global $USER; $accountinfo = array_merge($accountinfo, [ 'firstname' => $USER->firstname, 'lastname' => $USER->lastname, 'email' => $USER->email, 'picturehtml' => $this->output->user_picture($USER, ['size' => 35, 'class' => 'round']), ]); } $context = [ 'info' => $notification->export_for_template($this), 'formaction' => $formaction->out(), 'sesskey' => sesskey(), 'accountinfo' => $accountinfo, 'cancreateaccounts' => $cancreateaccounts, 'noauthnotice' => $noauthnotice->export_for_template($this) ]; return parent::render_from_template('auth_lti/local/ltiadvantage/login', $context); } /** * Render the page displayed when the account binding is complete, letting the user continue to the launch. * * Callers can provide different messages depending on which type of binding took place. For example, a newly * provisioned account may require a slightly different message to an existing account being linked. * * The return URL is the page the user will be taken back to when they click 'Continue'. This is likely the launch * or deeplink launch endpoint but could be any calling code in LTI which wants to use the account binding workflow. * * @param notification $notification the notification containing the message describing the binding success. * @param \moodle_url $returnurl the URL to return to when the user clicks continue on the rendered page. * @return string the rendered HTML */ public function render_account_binding_complete(notification $notification, \moodle_url $returnurl): string { $context = (object) [ 'notification' => $notification->export_for_template($this), 'returnurl' => $returnurl->out() ]; return parent::render_from_template('auth_lti/local/ltiadvantage/account_binding_complete', $context); } } lti/classes/privacy/provider.php 0000644 00000015243 15152311435 0013016 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_lti\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\context; use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; /** * Privacy Subsystem for auth_lti implementing null_provider. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @package auth_lti * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider, \core_privacy\local\request\core_userlist_provider { /** * Get all contexts contain user information for the given user. * * @param int $userid the id of the user. * @return contextlist the list of contexts containing user information. */ public static function get_contexts_for_userid(int $userid): contextlist { $sql = "SELECT ctx.id FROM {auth_lti_linked_login} ll JOIN {context} ctx ON ctx.instanceid = ll.userid AND ctx.contextlevel = :contextlevel WHERE ll.userid = :userid"; $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Export all user data for the user in the identified contexts. * * @param approved_contextlist $contextlist the list of approved contexts for the user. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; $user = $contextlist->get_user(); $linkedlogins = $DB->get_records('auth_lti_linked_login', ['userid' => $user->id], '', 'issuer, issuer256, sub, sub256, timecreated, timemodified'); foreach ($linkedlogins as $login) { $data = (object)[ 'timecreated' => transform::datetime($login->timecreated), 'timemodified' => transform::datetime($login->timemodified), 'issuer' => $login->issuer, 'issuer256' => $login->issuer256, 'sub' => $login->sub, 'sub256' => $login->sub256 ]; writer::with_context(\context_user::instance($user->id))->export_data([ get_string('privacy:metadata:auth_lti', 'auth_lti'), $login->issuer ], $data); } } /** * Delete all user data for this context. * * @param \context $context The context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { if ($context->contextlevel != CONTEXT_USER) { return; } static::delete_user_data($context->instanceid); } /** * Delete user data in the list of given contexts. * * @param approved_contextlist $contextlist the list of contexts. */ public static function delete_data_for_user(approved_contextlist $contextlist) { if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel != CONTEXT_USER) { continue; } if ($context->instanceid == $userid) { static::delete_user_data($context->instanceid); } } } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT userid FROM {auth_lti_linked_login} WHERE userid = ?"; $params = [$context->instanceid]; $userlist->add_from_sql('userid', $sql, $params); } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_user_data($context->instanceid); } } /** * Description of the metadata stored for users in auth_lti. * * @param collection $collection a collection to add to. * @return collection the collection, with relevant metadata descriptions for auth_lti added. */ public static function get_metadata(collection $collection): collection { $authfields = [ 'userid' => 'privacy:metadata:auth_lti:userid', 'issuer' => 'privacy:metadata:auth_lti:issuer', 'issuer256' => 'privacy:metadata:auth_lti:issuer256', 'sub' => 'privacy:metadata:auth_lti:sub', 'sub256' => 'privacy:metadata:auth_lti:sub256', 'timecreated' => 'privacy:metadata:auth_lti:timecreated', 'timemodified' => 'privacy:metadata:auth_lti:timemodified' ]; $collection->add_database_table('auth_lti_linked_login', $authfields, 'privacy:metadata:auth_lti:tableexplanation'); $collection->link_subsystem('core_auth', 'privacy:metadata:auth_lti:authsubsystem'); return $collection; } /** * Delete user data for the user. * * @param int $userid The id of the user. */ protected static function delete_user_data(int $userid) { global $DB; // Because we only use user contexts the instance ID is the user ID. $DB->delete_records('auth_lti_linked_login', ['userid' => $userid]); } } lti/classes/local/ltiadvantage/entity/user_migration_claim.php 0000644 00000014744 15152311435 0020761 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_lti\local\ltiadvantage\entity; /** * A simplified representation of a 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' migration claim. * * This serves the purpose of migrating a legacy user account only. Claim properties that do not relate to user migration are not * included or handled by this representation. * * See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim * * @package auth_lti * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_migration_claim { /** @var string the LTI 1.1 consumer key */ private $consumerkey; /** @var string the LTI 1.1 user identifier. * This is only included in the claim if it differs to the value included in the LTI 1.3 'sub' claim. * If not included, the value will be taken from 'sub'. */ private $userid; /** * The migration_claim constructor. * * The signature of a migration claim must be verifiable. To achieve this, the constructor takes a list of secrets * corresponding to the 'oauth_consumer_key' provided in the 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' * claim. How these secrets are determined is not the responsibility of this class. The constructor assumes these * correspond. * * @param array $jwt the array of claim data, as received in a resource link launch JWT. * @param array $consumersecrets a list of consumer secrets for the consumerkey included in the migration claim. * @throws \coding_exception if the claim data is invalid. */ public function __construct(array $jwt, array $consumersecrets) { // Can't get a claim instance without the claim data. if (empty($jwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1'])) { throw new \coding_exception("Missing the 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' JWT claim"); } $claim = $jwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1']; // The oauth_consumer_key property MUST be sent. // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key. if (empty($claim['oauth_consumer_key'])) { throw new \coding_exception("Missing 'oauth_consumer_key' property in lti1p1 migration claim."); } // The oauth_consumer_key_sign property MAY be sent. // For user migration to take place, however, this is deemed a required property since Moodle identified its // legacy users through a combination of consumerkey and userid. // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign. if (empty($claim['oauth_consumer_key_sign'])) { throw new \coding_exception("Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim."); } if (!$this->verify_signature( $claim['oauth_consumer_key'], $claim['oauth_consumer_key_sign'], $jwt['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], $jwt['iss'], $jwt['aud'], $jwt['exp'], $jwt['nonce'], $consumersecrets )) { throw new \coding_exception("Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim."); } $this->consumerkey = $claim['oauth_consumer_key']; $this->userid = $claim['user_id'] ?? $jwt['sub']; } /** * Verify the claim signature by recalculating it using the launch data and cross-checking consumer secrets. * * @param string $consumerkey the LTI 1.1 consumer key. * @param string $signature a signature of the LTI 1.1 consumer key and associated launch data. * @param string $deploymentid the deployment id included in the launch. * @param string $platform the platform included in the launch. * @param string $clientid the client id included in the launch. * @param string $exp the exp included in the launch. * @param string $nonce the nonce included in the launch. * @param array $consumersecrets the list of consumer secrets used with the given $consumerkey param * @return bool true if the signature was verified, false otherwise. */ private function verify_signature(string $consumerkey, string $signature, string $deploymentid, string $platform, string $clientid, string $exp, string $nonce, array $consumersecrets): bool { $base = [ $consumerkey, $deploymentid, $platform, $clientid, $exp, $nonce ]; $basestring = implode('&', $base); // Legacy enrol_lti code permits tools to share a consumer key but use different secrets. This results in // potentially many secrets per mapped tool consumer. As such, when generating the migration claim it's // impossible to know which secret the platform will use to sign the consumer key. The consumer key in the // migration claim is thus verified by trying all known secrets for the consumer, until either a match is found // or no signatures match. foreach ($consumersecrets as $consumersecret) { $calculatedsignature = base64_encode(hash_hmac('sha256', $basestring, $consumersecret)); if ($signature === $calculatedsignature) { return true; } } return false; } /** * Return the consumer key stored in the claim. * * @return string the consumer key included in the claim. */ public function get_consumer_key(): string { return $this->consumerkey; } /** * Return the LTI 1.1 user id stored in the claim. * * @return string the user id, or null if not provided in the claim. */ public function get_user_id(): string { return $this->userid; } } lti/lang/en/deprecated.txt 0000644 00000000032 15152311435 0011513 0 ustar 00 privacy:metadata,auth_lti lti/lang/en/auth_lti.php 0000644 00000007426 15152311435 0011212 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_lti', language 'en'. * * @package auth_lti * @copyright 2016 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['accountcreatedsuccess'] = 'Your account has been created and is now ready to use.'; $string['accountlinkedsuccess'] = 'Your existing account has been successfully linked.'; $string['auth_ltidescription'] = 'The LTI authentication plugin, together with the \'Publish as LTI tool\' enrolment plugin, allows remote users to access selected courses and activities. In other words, Moodle functions as an LTI tool provider.'; $string['cannotcreateaccounts'] = 'Account creation is currently prohibited on this site.'; $string['createaccount'] = 'Create account'; $string['createaccountforme'] = 'Create an account for me'; $string['createnewaccount'] = 'I\'d like to create a new account'; $string['currentlyloggedinas'] = 'You are currently logged in as:'; $string['firstlaunchnotice'] = 'It looks like this is your first time here. Please select from one of the following account options.'; $string['firstlaunchnoauthnotice'] = 'To link your existing account you must be logged in to the site. Please log in to the site in a new tab/window and then relaunch the tool here. For further information, see the documentation <a href="{$a}" target="_blank">Publish as LTI tool</a>.'; $string['getstartedwithnewaccount'] = 'Get started with a new account'; $string['haveexistingaccount'] = 'I have an existing account'; $string['linkthisaccount'] = 'Link this account'; $string['mustbeloggedin'] = 'You need to be logged in to your existing account'; $string['pluginname'] = 'LTI'; $string['privacy:metadata:auth_lti'] = 'LTI authentication'; $string['privacy:metadata:auth_lti:authsubsystem'] = 'This plugin is connected to the authentication subsystem.'; $string['privacy:metadata:auth_lti:issuer'] = 'The issuer URL identifying the platform to which the linked user belongs.'; $string['privacy:metadata:auth_lti:issuer256'] = 'The SHA256 hash of the issuer URL.'; $string['privacy:metadata:auth_lti:sub'] = 'The subject string identifying the user on the issuer.'; $string['privacy:metadata:auth_lti:sub256'] = 'The SHA256 hash of the subject string identifying the user on the issuer.'; $string['privacy:metadata:auth_lti:tableexplanation'] = 'LTI accounts linked to a user\'s Moodle account.'; $string['privacy:metadata:auth_lti:timecreated'] = 'The timestamp when the user account was linked to the LTI login.'; $string['privacy:metadata:auth_lti:timemodified'] = 'The timestamp when this record was modified.'; $string['privacy:metadata:auth_lti:userid'] = 'The ID of the user account which the LTI login is linked to'; $string['provisioningmodeauto'] = 'New accounts only (automatic)'; $string['provisioningmodenewexisting'] = 'Existing and new accounts (prompt)'; $string['provisioningmodeexistingonly'] = 'Existing accounts only (prompt)'; $string['useexistingaccount'] = 'Use existing account'; $string['welcome'] = 'Welcome!'; // Deprecated since Moodle 4.0. $string['privacy:metadata'] = 'The LTI authentication plugin does not store any personal data.'; ldap/ntlmsso_finish.php 0000644 00000002231 15152311435 0011232 0 ustar 00 <?php require(__DIR__.'/../../config.php'); $PAGE->set_url('/auth/ldap/ntlmsso_finish.php'); $PAGE->set_context(context_system::instance()); // Define variables used in page $site = get_site(); $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. if (!in_array('ldap', $authsequence, true)) { throw new \moodle_exception('ldap_isdisabled', 'auth'); } $authplugin = get_auth_plugin('ldap'); if (empty($authplugin->config->ntlmsso_enabled)) { throw new \moodle_exception('ntlmsso_isdisabled', 'auth_ldap'); } // If ntlmsso_finish() succeeds, then the code never returns, // so we only worry about failure. if (!$authplugin->ntlmsso_finish()) { // Redirect to login, saying "don't try again!" // Display the page header. This makes redirect respect the timeout we specify // here (and not add 3 more secs). $loginsite = get_string("loginsite"); $PAGE->navbar->add($loginsite); $PAGE->set_title("$site->fullname: $loginsite"); $PAGE->set_heading($site->fullname); echo $OUTPUT->header(); redirect($CFG->wwwroot . '/login/index.php?authldap_skipntlmsso=1', get_string('ntlmsso_failed','auth_ldap'), 3); } ldap/db/upgrade.php 0000644 00000003340 15152311435 0010211 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * LDAP authentication plugin upgrade code * * @package auth_ldap * @copyright 2013 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Function to upgrade auth_ldap. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_ldap_upgrade($oldversion) { global $CFG; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2021052501) { // Normalize the memberattribute_isdn plugin config. set_config('memberattribute_isdn', !empty(get_config('auth_ldap', 'memberattribute_isdn')), 'auth_ldap'); upgrade_plugin_savepoint(true, 2021052501, 'auth', 'ldap'); } // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } ldap/db/install.php 0000644 00000000105 15152311435 0010224 0 ustar 00 <?php function xmldb_auth_ldap_install() { global $CFG, $DB; } ldap/db/tasks.php 0000644 00000002645 15152311435 0007716 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Definition of auth_ldap tasks. * * @package auth_ldap * @category task * @copyright 2015 Vadim Dvorovenko <Vadimon@mail.ru> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => 'auth_ldap\task\sync_roles', 'blocking' => 0, 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'dayofweek' => '*', 'disabled' => 1 ), array( 'classname' => 'auth_ldap\task\sync_task', 'blocking' => 0, 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'dayofweek' => '*', 'disabled' => 1 ) ); ldap/locallib.php 0000644 00000004311 15152311435 0007755 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Internal library of functions for module auth_ldap * * @package auth_ldap * @author David Balch <david.balch@conted.ox.ac.uk> * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Get a list of system roles assignable by the current or a specified user, including their localised names. * * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user. * @return array $roles, each role as an array with id, shortname, localname, and settingname for the config value. */ function get_ldap_assignable_role_names($user = null) { $roles = array(); if ($assignableroles = get_assignable_roles(context_system::instance(), ROLENAME_SHORT, false, $user)) { $systemroles = role_fix_names(get_all_roles(), context_system::instance(), ROLENAME_ORIGINAL); foreach ($assignableroles as $shortname) { foreach ($systemroles as $systemrole) { if ($systemrole->shortname == $shortname) { $roles[] = array('id' => $systemrole->id, 'shortname' => $shortname, 'localname' => $systemrole->localname, 'settingname' => $shortname . 'context'); break; } } } } return $roles; } ldap/README-LDAP 0000644 00000000112 15152311435 0007054 0 ustar 00 LDAP-module README Please read comments from auth.php in this directory. ldap/cli/sync_users.php 0000644 00000005676 15152311435 0011157 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * CAS user sync script. * * This script is meant to be called from a cronjob to sync moodle with the LDAP * backend in those setups where the LDAP backend acts as 'master'. * * Notes: * - it is required to use the web server account when executing PHP CLI scripts * - you need to change the "www-data" to match the apache user account * - use "su" if "sudo" not available * - If you have a large number of users, you may want to raise the memory limits * by passing -d momory_limit=256M * - For debugging & better logging, you are encouraged to use in the command line: * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 * - If you have a large number of users, you may want to raise the memory limits * by passing -d momory_limit=256M * - For debugging & better logging, you are encouraged to use in the command line: * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 * * Performance notes: * We have optimized it as best as we could for PostgreSQL and MySQL, with 27K students * we have seen this take 10 minutes. * * @package auth_ldap * @copyright 2004 Martin Langhoff * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 3.0 MDL-51824 - please do not use this CLI script any more, use scheduled task instead. * @todo MDL-50264 This will be deleted in Moodle 3.2. */ define('CLI_SCRIPT', true); require(__DIR__.'/../../../config.php'); // global moodle config file. require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->libdir.'/clilib.php'); // Ensure errors are well explained set_debugging(DEBUG_DEVELOPER, true); if (!is_enabled_auth('ldap')) { error_log('[AUTH LDAP] '.get_string('pluginnotenabled', 'auth_ldap')); die; } cli_problem('[AUTH LDAP] The users sync cron has been deprecated. Please use the scheduled task instead.'); // Abort execution of the CLI script if the auth_ldap\task\sync_task is enabled. $taskdisabled = \core\task\manager::get_scheduled_task('auth_ldap\task\sync_task'); if (!$taskdisabled->get_disabled()) { cli_error('[AUTH LDAP] The scheduled task sync_task is enabled, the cron execution has been aborted.'); } $ldapauth = get_auth_plugin('ldap'); $ldapauth->sync_users(true); ldap/ntlmsso_magic.php 0000644 00000002417 15152311435 0011040 0 ustar 00 <?php // Don't let lib/setup.php set any cookies // as we will be executing under the OS security // context of the user we are trying to login, rather than // of the webserver. define('NO_MOODLE_COOKIES', true); require(__DIR__.'/../../config.php'); $PAGE->set_context(context_system::instance()); $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. if (!in_array('ldap', $authsequence, true)) { throw new \moodle_exception('ldap_isdisabled', 'auth'); } $authplugin = get_auth_plugin('ldap'); if (empty($authplugin->config->ntlmsso_enabled)) { throw new \moodle_exception('ntlmsso_isdisabled', 'auth_ldap'); } $sesskey = required_param('sesskey', PARAM_RAW); $file = $CFG->dirroot.'/pix/spacer.gif'; if ($authplugin->ntlmsso_magic($sesskey) && file_exists($file)) { if (!empty($authplugin->config->ntlmsso_ie_fastpath)) { if (core_useragent::is_ie()) { redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_finish.php'); } } // Serve GIF // Type header('Content-Type: image/gif'); header('Content-Length: '.filesize($file)); // Output file $handle = fopen($file, 'r'); fpassthru($handle); fclose($handle); exit; } else { throw new \moodle_exception('ntlmsso_iwamagicnotenabled', 'auth_ldap'); } ldap/tests/plugin_test.php 0000644 00000061023 15152311435 0011676 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_ldap; /** * LDAP authentication plugin tests. * * NOTE: in order to execute this test you need to set up * OpenLDAP server with core, cosine, nis and internet schemas * and add configuration constants to config.php or phpunit.xml configuration file: * * define('TEST_AUTH_LDAP_HOST_URL', 'ldap://127.0.0.1'); * define('TEST_AUTH_LDAP_BIND_DN', 'cn=someuser,dc=example,dc=local'); * define('TEST_AUTH_LDAP_BIND_PW', 'somepassword'); * define('TEST_AUTH_LDAP_DOMAIN', 'dc=example,dc=local'); * * @package auth_ldap * @category phpunit * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class plugin_test extends \advanced_testcase { /** * Data provider for auth_ldap tests * * Used to ensure that all the paged stuff works properly, irrespectively * of the pagesize configured (that implies all the chunking and paging * built in the plugis is doing its work consistently). Both searching and * not searching within subcontexts. * * @return array[] */ public function auth_ldap_provider() { $pagesizes = [1, 3, 5, 1000]; $subcontexts = [0, 1]; $combinations = []; foreach ($pagesizes as $pagesize) { foreach ($subcontexts as $subcontext) { $combinations["pagesize {$pagesize}, subcontexts {$subcontext}"] = [$pagesize, $subcontext]; } } return $combinations; } /** * General auth_ldap testcase * * @dataProvider auth_ldap_provider * @param int $pagesize Value to be configured in settings controlling page size. * @param int $subcontext Value to be configured in settings controlling searching in subcontexts. */ public function test_auth_ldap(int $pagesize, int $subcontext) { global $CFG, $DB; if (!extension_loaded('ldap')) { $this->markTestSkipped('LDAP extension is not loaded.'); } $this->resetAfterTest(); require_once($CFG->dirroot.'/auth/ldap/auth.php'); require_once($CFG->libdir.'/ldaplib.php'); if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) { $this->markTestSkipped('External LDAP test server not configured.'); } // Make sure we can connect the server. $debuginfo = ''; if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) { $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo); } $this->enable_plugin(); // Create new empty test container. $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN; $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); $o = array(); $o['objectClass'] = array('dcObject', 'organizationalUnit'); $o['dc'] = 'moodletest'; $o['ou'] = 'MOODLETEST'; if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) { $this->markTestSkipped('Can not create test LDAP container.'); } // Create a few users. $o = array(); $o['objectClass'] = array('organizationalUnit'); $o['ou'] = 'users'; ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o); $createdusers = array(); for ($i=1; $i<=5; $i++) { $this->create_ldap_user($connection, $topdn, $i); $createdusers[] = 'username' . $i; } // Set up creators group. $assignedroles = array('username1', 'username2'); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'creators'; $o['gidNumber'] = 1; $o['memberUid'] = $assignedroles; ldap_add($connection, 'cn='.$o['cn'].','.$topdn, $o); $creatorrole = $DB->get_record('role', array('shortname'=>'coursecreator')); $this->assertNotEmpty($creatorrole); // Configure the plugin a bit. set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap'); set_config('start_tls', 0, 'auth_ldap'); set_config('ldap_version', 3, 'auth_ldap'); set_config('ldapencoding', 'utf-8', 'auth_ldap'); set_config('pagesize', $pagesize, 'auth_ldap'); set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap'); set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap'); set_config('user_type', 'rfc2307', 'auth_ldap'); set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap'); set_config('search_sub', $subcontext, 'auth_ldap'); set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap'); set_config('user_attribute', 'cn', 'auth_ldap'); set_config('memberattribute', 'memberuid', 'auth_ldap'); set_config('memberattribute_isdn', 0, 'auth_ldap'); set_config('coursecreatorcontext', 'cn=creators,'.$topdn, 'auth_ldap'); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap'); set_config('field_map_email', 'mail', 'auth_ldap'); set_config('field_updatelocal_email', 'oncreate', 'auth_ldap'); set_config('field_updateremote_email', '0', 'auth_ldap'); set_config('field_lock_email', 'unlocked', 'auth_ldap'); set_config('field_map_firstname', 'givenName', 'auth_ldap'); set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_firstname', '0', 'auth_ldap'); set_config('field_lock_firstname', 'unlocked', 'auth_ldap'); set_config('field_map_lastname', 'sn', 'auth_ldap'); set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_lastname', '0', 'auth_ldap'); set_config('field_lock_lastname', 'unlocked', 'auth_ldap'); $this->assertEquals(2, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('role_assignments')); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 5 users created with 2 users having roles. $this->assertCount(7, $events); foreach ($events as $index => $event) { $username = $DB->get_field('user', 'username', array('id' => $event->relateduserid)); // Get username. if ($event->eventname === '\core\event\user_created') { $this->assertContains($username, $createdusers); unset($events[$index]); // Remove matching event. } else if ($event->eventname === '\core\event\role_assigned') { $this->assertContains($username, $assignedroles); unset($events[$index]); // Remove matching event. } else { $this->fail('Unexpected event found: ' . $event->eventname); } } // If all the user_created and role_assigned events have matched // then the $events array should be now empty. $this->assertCount(0, $events); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); for ($i=1; $i<=5; $i++) { $this->assertTrue($DB->record_exists('user', array('username'=>'username'.$i, 'email'=>'user'.$i.'@example.com', 'firstname'=>'Firstname'.$i, 'lastname'=>'Lastname'.$i))); } $this->delete_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, no new event. $this->assertCount(0, $events); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_ldap'); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 1 user got updated. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('auth'=>'nologin', 'username'=>'username1'))); $this->assertEquals(1, $DB->count_records('user', array('auth'=>'ldap', 'suspended'=>'1', 'username'=>'username1'))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $this->create_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 1 user got updated. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $DB->set_field('user', 'auth', 'nologin', array('username'=>'username1')); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 1 user got updated. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_ldap'); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $this->delete_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 2 events role_unassigned and user_deleted. $this->assertCount(2, $events); $event = array_pop($events); $this->assertInstanceOf('\core\event\user_deleted', $event); $event = array_pop($events); $this->assertInstanceOf('\core\event\role_unassigned', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('username'=>'username1'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(1, $DB->count_records('role_assignments')); $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $this->create_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 2 events role_assigned and user_created. $this->assertCount(2, $events); $event = array_pop($events); $this->assertInstanceOf('\core\event\role_assigned', $event); $event = array_pop($events); $this->assertInstanceOf('\core\event\user_created', $event); $this->assertEquals(6, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(1, $DB->count_records('user', array('username'=>'username1'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); ldap_close($connection); } /** * Test logging in via LDAP calls a user_loggedin event. */ public function test_ldap_user_loggedin_event() { global $CFG, $DB, $USER; require_once($CFG->dirroot . '/auth/ldap/auth.php'); $this->resetAfterTest(); $this->assertFalse(isloggedin()); $user = $DB->get_record('user', array('username'=>'admin')); // Note: we are just going to trigger the function that calls the event, // not actually perform a LDAP login, for the sake of sanity. $ldap = new \auth_plugin_ldap(); // Set the key for the cache flag we want to set which is used by LDAP. set_cache_flag($ldap->pluginconfig . '/ntlmsess', sesskey(), $user->username, AUTH_NTLMTIMEOUT); // We are going to need to set the sesskey as the user's password in order for the LDAP log in to work. update_internal_user_password($user, sesskey()); // The function ntlmsso_finish is responsible for triggering the event, so call it directly and catch the event. $sink = $this->redirectEvents(); // We need to supress this function call, or else we will get the message "session_regenerate_id(): Cannot // regenerate session id - headers already sent" as the ntlmsso_finish function calls complete_user_login @$ldap->ntlmsso_finish(); $events = $sink->get_events(); $sink->close(); // Check that the event is valid. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_loggedin', $event); $this->assertEquals('user', $event->objecttable); $this->assertEquals('2', $event->objectid); $this->assertEquals(\context_system::instance()->id, $event->contextid); $expectedlog = array(SITEID, 'user', 'login', 'view.php?id=' . $USER->id . '&course=' . SITEID, $user->id, 0, $user->id); $this->assertEventLegacyLogData($expectedlog, $event); } /** * Test logging in via LDAP calls a user_loggedin event. */ public function test_ldap_user_signup() { global $CFG, $DB; // User to create. $user = array( 'username' => 'usersignuptest1', 'password' => 'Moodle2014!', 'idnumber' => 'idsignuptest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'middlename' => 'Middle Name User Test 1', 'lastnamephonetic' => '最後のお名前のテスト一号', 'firstnamephonetic' => 'お名前のテスト一号', 'alternatename' => 'Alternate Name User Test 1', 'email' => 'usersignuptest1@example.com', 'description' => 'This is a description for user 1', 'city' => 'Perth', 'country' => 'AU', 'mnethostid' => $CFG->mnet_localhost_id, 'auth' => 'ldap' ); if (!extension_loaded('ldap')) { $this->markTestSkipped('LDAP extension is not loaded.'); } $this->resetAfterTest(); require_once($CFG->dirroot.'/auth/ldap/auth.php'); require_once($CFG->libdir.'/ldaplib.php'); if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) { $this->markTestSkipped('External LDAP test server not configured.'); } // Make sure we can connect the server. $debuginfo = ''; if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) { $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo); } $this->enable_plugin(); // Create new empty test container. $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN; $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); $o = array(); $o['objectClass'] = array('dcObject', 'organizationalUnit'); $o['dc'] = 'moodletest'; $o['ou'] = 'MOODLETEST'; if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) { $this->markTestSkipped('Can not create test LDAP container.'); } // Create a few users. $o = array(); $o['objectClass'] = array('organizationalUnit'); $o['ou'] = 'users'; ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o); // Configure the plugin a bit. set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap'); set_config('start_tls', 0, 'auth_ldap'); set_config('ldap_version', 3, 'auth_ldap'); set_config('ldapencoding', 'utf-8', 'auth_ldap'); set_config('pagesize', '2', 'auth_ldap'); set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap'); set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap'); set_config('user_type', 'rfc2307', 'auth_ldap'); set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap'); set_config('search_sub', 0, 'auth_ldap'); set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap'); set_config('user_attribute', 'cn', 'auth_ldap'); set_config('memberattribute', 'memberuid', 'auth_ldap'); set_config('memberattribute_isdn', 0, 'auth_ldap'); set_config('creators', 'cn=creators,'.$topdn, 'auth_ldap'); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap'); set_config('field_map_email', 'mail', 'auth_ldap'); set_config('field_updatelocal_email', 'oncreate', 'auth_ldap'); set_config('field_updateremote_email', '0', 'auth_ldap'); set_config('field_lock_email', 'unlocked', 'auth_ldap'); set_config('field_map_firstname', 'givenName', 'auth_ldap'); set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_firstname', '0', 'auth_ldap'); set_config('field_lock_firstname', 'unlocked', 'auth_ldap'); set_config('field_map_lastname', 'sn', 'auth_ldap'); set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_lastname', '0', 'auth_ldap'); set_config('field_lock_lastname', 'unlocked', 'auth_ldap'); set_config('passtype', 'md5', 'auth_ldap'); set_config('create_context', 'ou=users,'.$topdn, 'auth_ldap'); $this->assertEquals(2, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('role_assignments')); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $sink = $this->redirectEvents(); $mailsink = $this->redirectEmails(); $auth->user_signup((object)$user, false); $this->assertEquals(1, $mailsink->count()); $events = $sink->get_events(); $sink->close(); // Verify 2 events get generated. $this->assertCount(2, $events); // Get record from db. $dbuser = $DB->get_record('user', array('username' => $user['username'])); $user['id'] = $dbuser->id; // Last event is user_created. $event = array_pop($events); $this->assertInstanceOf('\core\event\user_created', $event); $this->assertEquals($user['id'], $event->objectid); $this->assertEquals('user_created', $event->get_legacy_eventname()); $this->assertEquals(\context_user::instance($user['id']), $event->get_context()); $expectedlogdata = array(SITEID, 'user', 'add', '/view.php?id='.$event->objectid, fullname($dbuser)); $this->assertEventLegacyLogData($expectedlogdata, $event); // First event is user_password_updated. $event = array_pop($events); $this->assertInstanceOf('\core\event\user_password_updated', $event); $this->assertEventContextNotUsed($event); // Delete user which we just created. ldap_delete($connection, 'cn='.$user['username'].',ou=users,'.$topdn); } protected function create_ldap_user($connection, $topdn, $i) { $o = array(); $o['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'posixAccount'); $o['cn'] = 'username'.$i; $o['sn'] = 'Lastname'.$i; $o['givenName'] = 'Firstname'.$i; $o['uid'] = $o['cn']; $o['uidnumber'] = 2000+$i; $o['gidNumber'] = 1000+$i; $o['homeDirectory'] = '/'; $o['mail'] = 'user'.$i.'@example.com'; $o['userPassword'] = 'pass'.$i; ldap_add($connection, 'cn='.$o['cn'].',ou=users,'.$topdn, $o); } protected function delete_ldap_user($connection, $topdn, $i) { ldap_delete($connection, 'cn=username'.$i.',ou=users,'.$topdn); } protected function enable_plugin() { $auths = get_enabled_auth_plugins(); if (!in_array('ldap', $auths)) { $auths[] = 'ldap'; } set_config('auth', implode(',', $auths)); } protected function recursive_delete($connection, $dn, $filter) { if ($res = ldap_list($connection, $dn, $filter, array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); if ($info['count'] > 0) { if ($res = ldap_search($connection, "$filter,$dn", 'cn=*', array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); foreach ($info as $i) { if (isset($i['dn'])) { ldap_delete($connection, $i['dn']); } } } if ($res = ldap_search($connection, "$filter,$dn", 'ou=*', array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); foreach ($info as $i) { if (isset($i['dn']) and $info[0]['dn'] != $i['dn']) { ldap_delete($connection, $i['dn']); } } } ldap_delete($connection, "$filter,$dn"); } } } } ldap/auth.php 0000644 00000273632 15152311435 0007153 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: LDAP Authentication * Authentication using LDAP (Lightweight Directory Access Protocol). * * @package auth_ldap * @author Martin Dougiamas * @author Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); // See http://support.microsoft.com/kb/305144 to interprete these values. if (!defined('AUTH_AD_ACCOUNTDISABLE')) { define('AUTH_AD_ACCOUNTDISABLE', 0x0002); } if (!defined('AUTH_AD_NORMAL_ACCOUNT')) { define('AUTH_AD_NORMAL_ACCOUNT', 0x0200); } if (!defined('AUTH_NTLMTIMEOUT')) { // timewindow for the NTLM SSO process, in secs... define('AUTH_NTLMTIMEOUT', 10); } // UF_DONT_EXPIRE_PASSWD value taken from MSDN directly if (!defined('UF_DONT_EXPIRE_PASSWD')) { define ('UF_DONT_EXPIRE_PASSWD', 0x00010000); } // The Posix uid and gid of the 'nobody' account and 'nogroup' group. if (!defined('AUTH_UID_NOBODY')) { define('AUTH_UID_NOBODY', -2); } if (!defined('AUTH_GID_NOGROUP')) { define('AUTH_GID_NOGROUP', -2); } // Regular expressions for a valid NTLM username and domain name. if (!defined('AUTH_NTLM_VALID_USERNAME')) { define('AUTH_NTLM_VALID_USERNAME', '[^/\\\\\\\\\[\]:;|=,+*?<>@"]+'); } if (!defined('AUTH_NTLM_VALID_DOMAINNAME')) { define('AUTH_NTLM_VALID_DOMAINNAME', '[^\\\\\\\\\/:*?"<>|]+'); } // Default format for remote users if using NTLM SSO if (!defined('AUTH_NTLM_DEFAULT_FORMAT')) { define('AUTH_NTLM_DEFAULT_FORMAT', '%domain%\\%username%'); } if (!defined('AUTH_NTLM_FASTPATH_ATTEMPT')) { define('AUTH_NTLM_FASTPATH_ATTEMPT', 0); } if (!defined('AUTH_NTLM_FASTPATH_YESFORM')) { define('AUTH_NTLM_FASTPATH_YESFORM', 1); } if (!defined('AUTH_NTLM_FASTPATH_YESATTEMPT')) { define('AUTH_NTLM_FASTPATH_YESATTEMPT', 2); } // Allows us to retrieve a diagnostic message in case of LDAP operation error if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) { define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032); } require_once($CFG->libdir.'/authlib.php'); require_once($CFG->libdir.'/ldaplib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->dirroot.'/auth/ldap/locallib.php'); /** * LDAP authentication plugin. */ class auth_plugin_ldap extends auth_plugin_base { /** * Init plugin config from database settings depending on the plugin auth type. */ function init_plugin($authtype) { $this->pluginconfig = 'auth_'.$authtype; $this->config = get_config($this->pluginconfig); if (empty($this->config->ldapencoding)) { $this->config->ldapencoding = 'utf-8'; } if (empty($this->config->user_type)) { $this->config->user_type = 'default'; } $ldap_usertypes = ldap_supported_usertypes(); $this->config->user_type_name = $ldap_usertypes[$this->config->user_type]; unset($ldap_usertypes); $default = ldap_getdefaults(); // Use defaults if values not given foreach ($default as $key => $value) { // watch out - 0, false are correct values too if (!isset($this->config->{$key}) or $this->config->{$key} == '') { $this->config->{$key} = $value[$this->config->user_type]; } } // Hack prefix to objectclass $this->config->objectclass = ldap_normalise_objectclass($this->config->objectclass); } /** * Constructor with initialisation. */ public function __construct() { $this->authtype = 'ldap'; $this->roleauth = 'auth_ldap'; $this->errorlogtag = '[AUTH LDAP] '; $this->init_plugin($this->authtype); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_ldap() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username (without system magic quotes) * @param string $password The password (without system magic quotes) * * @return bool Authentication success or failure. */ function user_login($username, $password) { if (! function_exists('ldap_bind')) { throw new \moodle_exception('auth_ldapnotinstalled', 'auth_ldap'); return false; } if (!$username or !$password) { // Don't allow blank usernames or passwords return false; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $extpassword = core_text::convert($password, 'utf-8', $this->config->ldapencoding); // Before we connect to LDAP, check if this is an AD SSO login // if we succeed in this block, we'll return success early. // $key = sesskey(); if (!empty($this->config->ntlmsso_enabled) && $key === $password) { $sessusername = get_cache_flag($this->pluginconfig.'/ntlmsess', $key); // We only get the cache flag if we retrieve it before // it expires (AUTH_NTLMTIMEOUT seconds). if (empty($sessusername)) { return false; } if ($username === $sessusername) { unset($sessusername); // Check that the user is inside one of the configured LDAP contexts $validuser = false; $ldapconnection = $this->ldap_connect(); // if the user is not inside the configured contexts, // ldap_find_userdn returns false. if ($this->ldap_find_userdn($ldapconnection, $extusername)) { $validuser = true; } $this->ldap_close(); // Shortcut here - SSO confirmed return $validuser; } } // End SSO processing unset($key); $ldapconnection = $this->ldap_connect(); $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); // If ldap_user_dn is empty, user does not exist if (!$ldap_user_dn) { $this->ldap_close(); return false; } // Try to bind with current username and password $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword); // If login fails and we are using MS Active Directory, retrieve the diagnostic // message to see if this is due to an expired password, or that the user is forced to // change the password on first login. If it is, only proceed if we can change // password from Moodle (otherwise we'll get stuck later in the login process). if (!$ldap_login && ($this->config->user_type == 'ad') && $this->can_change_password() && (!empty($this->config->expiration) and ($this->config->expiration == 1))) { // We need to get the diagnostic message right after the call to ldap_bind(), // before any other LDAP operation. ldap_get_option($ldapconnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagmsg); if ($this->ldap_ad_pwdexpired_from_diagmsg($diagmsg)) { // If login failed because user must change the password now or the // password has expired, let the user in. We'll catch this later in the // login process when we explicitly check for expired passwords. $ldap_login = true; } } $this->ldap_close(); return $ldap_login; } /** * Reads user information from ldap and returns it in array() * * Function should return all information available. If you are saving * this information to moodle user-table you should honor syncronization flags * * @param string $username username * * @return mixed array with no magic quotes or false on error */ function get_userinfo($username) { $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extusername))) { $this->ldap_close(); return false; } $search_attribs = array(); $attrmap = $this->ldap_attributes(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!in_array($value, $search_attribs)) { array_push($search_attribs, $value); } } } if (!$user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs)) { $this->ldap_close(); return false; // error! } $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); if (empty($user_entry)) { $this->ldap_close(); return false; // entry not found } $result = array(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } $ldapval = NULL; foreach ($values as $value) { $entry = $user_entry[0]; if (($value == 'dn') || ($value == 'distinguishedname')) { $result[$key] = $user_dn; continue; } if (!array_key_exists($value, $entry)) { continue; // wrong data mapping! } if (is_array($entry[$value])) { $newval = core_text::convert($entry[$value][0], $this->config->ldapencoding, 'utf-8'); } else { $newval = core_text::convert($entry[$value], $this->config->ldapencoding, 'utf-8'); } if (!empty($newval)) { // favour ldap entries that are set $ldapval = $newval; } } if (!is_null($ldapval)) { $result[$key] = $ldapval; } } $this->ldap_close(); return $result; } /** * Reads user information from ldap and returns it in an object * * @param string $username username (with system magic quotes) * @return mixed object or false on error */ function get_userinfo_asobj($username) { $user_array = $this->get_userinfo($username); if ($user_array == false) { return false; //error or not found } $user_array = truncate_userinfo($user_array); $user = new stdClass(); foreach ($user_array as $key=>$value) { $user->{$key} = $value; } return $user; } /** * Returns all usernames from LDAP * * get_userlist returns all usernames from LDAP * * @return array */ function get_userlist() { return $this->ldap_get_userlist("({$this->config->user_attribute}=*)"); } /** * Checks if user exists on LDAP * * @param string $username */ function user_exists($username) { $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); // Returns true if given username exists on ldap $users = $this->ldap_get_userlist('('.$this->config->user_attribute.'='.ldap_filter_addslashes($extusername).')'); return count($users); } /** * Creates a new user on LDAP. * By using information in userobject * Use user_exists to prevent duplicate usernames * * @param mixed $userobject Moodle userobject * @param mixed $plainpass Plaintext password */ function user_create($userobject, $plainpass) { $extusername = core_text::convert($userobject->username, 'utf-8', $this->config->ldapencoding); $extpassword = core_text::convert($plainpass, 'utf-8', $this->config->ldapencoding); switch ($this->config->passtype) { case 'md5': $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); break; case 'sha1': $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); break; case 'plaintext': default: break; // plaintext } $ldapconnection = $this->ldap_connect(); $attrmap = $this->ldap_attributes(); $newuser = array(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!empty($userobject->$key) ) { $newuser[$value] = core_text::convert($userobject->$key, 'utf-8', $this->config->ldapencoding); } } } //Following sets all mandatory and other forced attribute values //User should be creted as login disabled untill email confirmation is processed //Feel free to add your user type and send patches to paca@sci.fi to add them //Moodle distribution switch ($this->config->user_type) { case 'edir': $newuser['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'top'); $newuser['uniqueId'] = $extusername; $newuser['logindisabled'] = 'TRUE'; $newuser['userpassword'] = $extpassword; $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); break; case 'rfc2307': case 'rfc2307bis': // posixAccount object class forces us to specify a uidNumber // and a gidNumber. That is quite complicated to generate from // Moodle without colliding with existing numbers and without // race conditions. As this user is supposed to be only used // with Moodle (otherwise the user would exist beforehand) and // doesn't need to login into a operating system, we assign the // user the uid of user 'nobody' and gid of group 'nogroup'. In // addition to that, we need to specify a home directory. We // use the root directory ('/') as the home directory, as this // is the only one can always be sure exists. Finally, even if // it's not mandatory, we specify '/bin/false' as the login // shell, to prevent the user from login in at the operating // system level (Moodle ignores this). $newuser['objectClass'] = array('posixAccount', 'inetOrgPerson', 'organizationalPerson', 'person', 'top'); $newuser['cn'] = $extusername; $newuser['uid'] = $extusername; $newuser['uidNumber'] = AUTH_UID_NOBODY; $newuser['gidNumber'] = AUTH_GID_NOGROUP; $newuser['homeDirectory'] = '/'; $newuser['loginShell'] = '/bin/false'; // IMPORTANT: // We have to create the account locked, but posixAccount has // no attribute to achive this reliably. So we are going to // modify the password in a reversable way that we can later // revert in user_activate(). // // Beware that this can be defeated by the user if we are not // using MD5 or SHA-1 passwords. After all, the source code of // Moodle is available, and the user can see the kind of // modification we are doing and 'undo' it by hand (but only // if we are using plain text passwords). // // Also bear in mind that you need to use a binding user that // can create accounts and has read/write privileges on the // 'userPassword' attribute for this to work. $newuser['userPassword'] = '*'.$extpassword; $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); break; case 'ad': // User account creation is a two step process with AD. First you // create the user object, then you set the password. If you try // to set the password while creating the user, the operation // fails. // Passwords in Active Directory must be encoded as Unicode // strings (UCS-2 Little Endian format) and surrounded with // double quotes. See http://support.microsoft.com/?kbid=269190 if (!function_exists('mb_convert_encoding')) { throw new \moodle_exception('auth_ldap_no_mbstring', 'auth_ldap'); } // Check for invalid sAMAccountName characters. if (preg_match('#[/\\[\]:;|=,+*?<>@"]#', $extusername)) { throw new \moodle_exception ('auth_ldap_ad_invalidchars', 'auth_ldap'); } // First create the user account, and mark it as disabled. $newuser['objectClass'] = array('top', 'person', 'user', 'organizationalPerson'); $newuser['sAMAccountName'] = $extusername; $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT | AUTH_AD_ACCOUNTDISABLE; $userdn = 'cn='.ldap_addslashes($extusername).','.$this->config->create_context; if (!ldap_add($ldapconnection, $userdn, $newuser)) { throw new \moodle_exception('auth_ldap_ad_create_req', 'auth_ldap'); } // Now set the password unset($newuser); $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"', 'UCS-2LE', 'UTF-8'); if(!ldap_modify($ldapconnection, $userdn, $newuser)) { // Something went wrong: delete the user account and error out ldap_delete ($ldapconnection, $userdn); throw new \moodle_exception('auth_ldap_ad_create_req', 'auth_ldap'); } $uadd = true; break; default: throw new \moodle_exception('auth_ldap_unsupportedusertype', 'auth_ldap', '', $this->config->user_type_name); } $this->ldap_close(); return $uadd; } /** * Returns true if plugin allows resetting of password from moodle. * * @return bool */ function can_reset_password() { return !empty($this->config->stdchangepassword); } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } /** * Returns true if plugin allows signup and user creation. * * @return bool */ function can_signup() { return (!empty($this->config->auth_user_create) and !empty($this->config->create_context)); } /** * Sign up a new user ready for confirmation. * Password is passed in plaintext. * * @param object $user new user object * @param boolean $notify print notice with link and terminate * @return boolean success */ function user_signup($user, $notify=true) { global $CFG, $DB, $PAGE, $OUTPUT; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); if ($this->user_exists($user->username)) { throw new \moodle_exception('auth_ldap_user_exists', 'auth_ldap'); } $plainslashedpassword = $user->password; unset($user->password); if (! $this->user_create($user, $plainslashedpassword)) { throw new \moodle_exception('auth_ldap_create_error', 'auth_ldap'); } $user->id = user_create_user($user, false, false); user_add_password_history($user->id, $plainslashedpassword); // Save any custom profile field information profile_save_data($user); $userinfo = $this->get_userinfo($user->username); $this->update_user_record($user->username, false, false, $this->is_user_suspended((object) $userinfo)); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). update_internal_user_password($user, $plainslashedpassword); $user = $DB->get_record('user', array('id'=>$user->id)); \core\event\user_created::create_from_userid($user->id)->trigger(); if (! send_confirmation_email($user)) { throw new \moodle_exception('noemail', 'auth_ldap'); } if ($notify) { $emailconfirm = get_string('emailconfirm'); $PAGE->set_url('/auth/ldap/auth.php'); $PAGE->navbar->add($emailconfirm); $PAGE->set_title($emailconfirm); $PAGE->set_heading($emailconfirm); echo $OUTPUT->header(); notice(get_string('emailconfirmsent', '', $user->email), "{$CFG->wwwroot}/index.php"); } else { return true; } } /** * Returns true if plugin allows confirming of new users. * * @return bool */ function can_confirm() { return $this->can_signup(); } /** * Confirm the new user as registered. * * @param string $username * @param string $confirmsecret */ function user_confirm($username, $confirmsecret) { global $DB; $user = get_complete_user_data('username', $username); if (!empty($user)) { if ($user->auth != $this->authtype) { return AUTH_CONFIRM_ERROR; } else if ($user->secret === $confirmsecret && $user->confirmed) { return AUTH_CONFIRM_ALREADY; } else if ($user->secret === $confirmsecret) { // They have provided the secret key to get in if (!$this->user_activate($username)) { return AUTH_CONFIRM_FAIL; } $user->confirmed = 1; user_update_user($user, false); return AUTH_CONFIRM_OK; } } else { return AUTH_CONFIRM_ERROR; } } /** * Return number of days to user password expires * * If userpassword does not expire it should return 0. If password is already expired * it should return negative value. * * @param mixed $username username * @return integer */ function password_expire($username) { $result = 0; $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); $search_attribs = array($this->config->expireattr); $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); if ($sr) { $info = ldap_get_entries_moodle($ldapconnection, $sr); if (!empty ($info)) { $info = $info[0]; if (isset($info[$this->config->expireattr][0])) { $expiretime = $this->ldap_expirationtime2unix($info[$this->config->expireattr][0], $ldapconnection, $user_dn); if ($expiretime != 0) { $now = time(); if ($expiretime > $now) { $result = ceil(($expiretime - $now) / DAYSECS); } else { $result = floor(($expiretime - $now) / DAYSECS); } } } } } else { error_log($this->errorlogtag.get_string('didtfindexpiretime', 'auth_ldap')); } return $result; } /** * Syncronizes user fron external LDAP server to moodle user table * * Sync is now using username attribute. * * Syncing users removes or suspends users that dont exists anymore in external LDAP. * Creates new users and updates coursecreator status of users. * * @param bool $do_updates will do pull in data updates from LDAP if relevant */ function sync_users($do_updates=true) { global $CFG, $DB; require_once($CFG->dirroot . '/user/profile/lib.php'); print_string('connectingldap', 'auth_ldap'); $ldapconnection = $this->ldap_connect(); $dbman = $DB->get_manager(); /// Define table user to be created $table = new xmldb_table('tmp_extuser'); $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); $table->add_field('username', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); $table->add_field('mnethostid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); $table->add_index('username', XMLDB_INDEX_UNIQUE, array('mnethostid', 'username')); print_string('creatingtemptable', 'auth_ldap', 'tmp_extuser'); $dbman->create_temp_table($table); //// //// get user's list from ldap to sql in a scalable fashion //// // prepare some data we'll need $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; $servercontrols = array(); $contexts = explode(';', $this->config->contexts); if (!empty($this->config->create_context)) { array_push($contexts, $this->config->create_context); } $ldappagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection); $ldapcookie = ''; foreach ($contexts as $context) { $context = trim($context); if (empty($context)) { continue; } do { if ($ldappagedresults) { $servercontrols = array(array( 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 'size' => $this->config->pagesize, 'cookie' => $ldapcookie))); } if ($this->config->search_sub) { // Use ldap_search to find first user from subtree. $ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } else { // Search only in this context. $ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } if (!$ldapresult) { continue; } if ($ldappagedresults) { // Get next server cookie to know if we'll need to continue searching. $ldapcookie = ''; // Get next cookie from controls. ldap_parse_result($ldapconnection, $ldapresult, $errcode, $matcheddn, $errmsg, $referrals, $controls); if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { $ldapcookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } } if ($entry = @ldap_first_entry($ldapconnection, $ldapresult)) { do { $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute); $value = core_text::convert($value[0], $this->config->ldapencoding, 'utf-8'); $value = trim($value); $this->ldap_bulk_insert($value); } while ($entry = ldap_next_entry($ldapconnection, $entry)); } unset($ldapresult); // Free mem. } while ($ldappagedresults && $ldapcookie !== null && $ldapcookie != ''); } // If LDAP paged results were used, the current connection must be completely // closed and a new one created, to work without paged results from here on. if ($ldappagedresults) { $this->ldap_close(true); $ldapconnection = $this->ldap_connect(); } /// preserve our user database /// if the temp table is empty, it probably means that something went wrong, exit /// so as to avoid mass deletion of users; which is hard to undo $count = $DB->count_records_sql('SELECT COUNT(username) AS count, 1 FROM {tmp_extuser}'); if ($count < 1) { print_string('didntgetusersfromldap', 'auth_ldap'); $dbman->drop_table($table); $this->ldap_close(); return false; } else { print_string('gotcountrecordsfromldap', 'auth_ldap', $count); } /// User removal // Find users in DB that aren't in ldap -- to be removed! // this is still not as scalable (but how often do we mass delete?) if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) { $sql = "SELECT u.* FROM {user} u LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) WHERE u.auth = :auth AND u.deleted = 0 AND e.username IS NULL"; $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); if (!empty($remove_users)) { print_string('userentriestoremove', 'auth_ldap', count($remove_users)); foreach ($remove_users as $user) { if (delete_user($user)) { echo "\t"; print_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; } else { echo "\t"; print_string('auth_dbdeleteusererror', 'auth_db', $user->username); echo "\n"; } } } else { print_string('nouserentriestoremove', 'auth_ldap'); } unset($remove_users); // Free mem! } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $sql = "SELECT u.* FROM {user} u LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) WHERE u.auth = :auth AND u.deleted = 0 AND u.suspended = 0 AND e.username IS NULL"; $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); if (!empty($remove_users)) { print_string('userentriestoremove', 'auth_ldap', count($remove_users)); foreach ($remove_users as $user) { $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->suspended = 1; user_update_user($updateuser, false); echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; \core\session\manager::kill_user_sessions($user->id); } } else { print_string('nouserentriestoremove', 'auth_ldap'); } unset($remove_users); // Free mem! } /// Revive suspended users if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $sql = "SELECT u.id, u.username FROM {user} u JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) WHERE (u.auth = 'nologin' OR (u.auth = ? AND u.suspended = 1)) AND u.deleted = 0"; // Note: 'nologin' is there for backwards compatibility. $revive_users = $DB->get_records_sql($sql, array($this->authtype)); if (!empty($revive_users)) { print_string('userentriestorevive', 'auth_ldap', count($revive_users)); foreach ($revive_users as $user) { $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->auth = $this->authtype; $updateuser->suspended = 0; user_update_user($updateuser, false); echo "\t"; print_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; } } else { print_string('nouserentriestorevive', 'auth_ldap'); } unset($revive_users); } /// User Updates - time-consuming (optional) if ($do_updates) { // Narrow down what fields we need to update $updatekeys = $this->get_profile_keys(); } else { print_string('noupdatestobedone', 'auth_ldap'); } if ($do_updates and !empty($updatekeys)) { // run updates only if relevant $users = $DB->get_records_sql('SELECT u.username, u.id FROM {user} u WHERE u.deleted = 0 AND u.auth = ? AND u.mnethostid = ?', array($this->authtype, $CFG->mnet_localhost_id)); if (!empty($users)) { print_string('userentriestoupdate', 'auth_ldap', count($users)); foreach ($users as $user) { $transaction = $DB->start_delegated_transaction(); echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); $userinfo = $this->get_userinfo($user->username); if (!$this->update_user_record($user->username, $updatekeys, true, $this->is_user_suspended((object) $userinfo))) { echo ' - '.get_string('skipped'); } echo "\n"; // Update system roles, if needed. $this->sync_roles($user); $transaction->allow_commit(); } unset($users); // free mem } } else { // end do updates print_string('noupdatestobedone', 'auth_ldap'); } /// User Additions // Find users missing in DB that are in LDAP // and gives me a nifty object I don't want. // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin $sql = 'SELECT e.id, e.username FROM {tmp_extuser} e LEFT JOIN {user} u ON (e.username = u.username AND e.mnethostid = u.mnethostid) WHERE u.id IS NULL'; $add_users = $DB->get_records_sql($sql); if (!empty($add_users)) { print_string('userentriestoadd', 'auth_ldap', count($add_users)); $errors = 0; foreach ($add_users as $user) { $transaction = $DB->start_delegated_transaction(); $user = $this->get_userinfo_asobj($user->username); // Prep a few params $user->modified = time(); $user->confirmed = 1; $user->auth = $this->authtype; $user->mnethostid = $CFG->mnet_localhost_id; // get_userinfo_asobj() might have replaced $user->username with the value // from the LDAP server (which can be mixed-case). Make sure it's lowercase $user->username = trim(core_text::strtolower($user->username)); // It isn't possible to just rely on the configured suspension attribute since // things like active directory use bit masks, other things using LDAP might // do different stuff as well. // // The cast to int is a workaround for MDL-53959. $user->suspended = (int)$this->is_user_suspended($user); if (empty($user->calendartype)) { $user->calendartype = $CFG->calendartype; } // $id = user_create_user($user, false); try { $id = user_create_user($user, false); } catch (Exception $e) { print_string('invaliduserexception', 'auth_ldap', print_r($user, true) . $e->getMessage()); $errors++; continue; } echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n"; $euser = $DB->get_record('user', array('id' => $id)); if (!empty($this->config->forcechangepassword)) { set_user_preference('auth_forcepasswordchange', 1, $id); } // Save custom profile fields. $this->update_user_record($user->username, $this->get_profile_keys(true), false); // Add roles if needed. $this->sync_roles($euser); $transaction->allow_commit(); } // Display number of user creation errors, if any. if ($errors) { print_string('invalidusererrors', 'auth_ldap', $errors); } unset($add_users); // free mem } else { print_string('nouserstobeadded', 'auth_ldap'); } $dbman->drop_table($table); $this->ldap_close(); return true; } /** * Bulk insert in SQL's temp table */ function ldap_bulk_insert($username) { global $DB, $CFG; $username = core_text::strtolower($username); // usernames are __always__ lowercase. $DB->insert_record_raw('tmp_extuser', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id), false, true); echo '.'; } /** * Activates (enables) user in external LDAP so user can login * * @param mixed $username * @return boolean result */ function user_activate($username) { $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); $userdn = $this->ldap_find_userdn($ldapconnection, $extusername); switch ($this->config->user_type) { case 'edir': $newinfo['loginDisabled'] = 'FALSE'; break; case 'rfc2307': case 'rfc2307bis': // Remember that we add a '*' character in front of the // external password string to 'disable' the account. We just // need to remove it. $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', array('userPassword')); $info = ldap_get_entries($ldapconnection, $sr); $info[0] = array_change_key_case($info[0], CASE_LOWER); $newinfo['userPassword'] = ltrim($info[0]['userpassword'][0], '*'); break; case 'ad': // We need to unset the ACCOUNTDISABLE bit in the // userAccountControl attribute ( see // http://support.microsoft.com/kb/305144 ) $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', array('userAccountControl')); $info = ldap_get_entries($ldapconnection, $sr); $info[0] = array_change_key_case($info[0], CASE_LOWER); $newinfo['userAccountControl'] = $info[0]['useraccountcontrol'][0] & (~AUTH_AD_ACCOUNTDISABLE); break; default: throw new \moodle_exception('user_activatenotsupportusertype', 'auth_ldap', '', $this->config->user_type_name); } $result = ldap_modify($ldapconnection, $userdn, $newinfo); $this->ldap_close(); return $result; } /** * Returns true if user should be coursecreator. * * @param mixed $username username (without system magic quotes) * @return mixed result null if course creators is not configured, boolean otherwise. * * @deprecated since Moodle 3.4 MDL-30634 - please do not use this function any more. */ function iscreator($username) { debugging('iscreator() is deprecated. Please use auth_plugin_ldap::is_role() instead.', DEBUG_DEVELOPER); if (empty($this->config->creators) or empty($this->config->memberattribute)) { return null; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); if ($this->config->memberattribute_isdn) { if(!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { return false; } } else { $userid = $extusername; } $group_dns = explode(';', $this->config->creators); $creator = ldap_isgroupmember($ldapconnection, $userid, $group_dns, $this->config->memberattribute); $this->ldap_close(); return $creator; } /** * Check if user has LDAP group membership. * * Returns true if user should be assigned role. * * @param mixed $username username (without system magic quotes). * @param array $role Array of role's shortname, localname, and settingname for the config value. * @return mixed result null if role/LDAP context is not configured, boolean otherwise. */ private function is_role($username, $role) { if (empty($this->config->{$role['settingname']}) or empty($this->config->memberattribute)) { return null; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); if ($this->config->memberattribute_isdn) { if (!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { return false; } } else { $userid = $extusername; } $groupdns = explode(';', $this->config->{$role['settingname']}); $isrole = ldap_isgroupmember($ldapconnection, $userid, $groupdns, $this->config->memberattribute); $this->ldap_close(); return $isrole; } /** * Called when the user record is updated. * * Modifies user in external LDAP server. It takes olduser (before * changes) and newuser (after changes) compares information and * saves modified information to external LDAP server. * * @param mixed $olduser Userobject before modifications (without system magic quotes) * @param mixed $newuser Userobject new modified userobject (without system magic quotes) * @return boolean result * */ function user_update($olduser, $newuser) { global $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) { error_log($this->errorlogtag.get_string('renamingnotallowed', 'auth_ldap')); return false; } if (isset($olduser->auth) and $olduser->auth != $this->authtype) { return true; // just change auth and skip update } $attrmap = $this->ldap_attributes(); // Before doing anything else, make sure we really need to update anything // in the external LDAP server. $update_external = false; foreach ($attrmap as $key => $ldapkeys) { if (!empty($this->config->{'field_updateremote_'.$key})) { $update_external = true; break; } } if (!$update_external) { return true; } $extoldusername = core_text::convert($olduser->username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); $search_attribs = array(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!in_array($value, $search_attribs)) { array_push($search_attribs, $value); } } } if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername))) { return false; } // Load old custom fields. $olduserprofilefields = (array) profile_user_record($olduser->id, false); $fields = array(); foreach (profile_get_custom_fields(false) as $field) { $fields[$field->shortname] = $field; } $success = true; $user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); if ($user_info_result) { $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); if (empty($user_entry)) { $attribs = join (', ', $search_attribs); error_log($this->errorlogtag.get_string('updateusernotfound', 'auth_ldap', array('userdn'=>$user_dn, 'attribs'=>$attribs))); return false; // old user not found! } else if (count($user_entry) > 1) { error_log($this->errorlogtag.get_string('morethanoneuser', 'auth_ldap')); return false; } $user_entry = $user_entry[0]; foreach ($attrmap as $key => $ldapkeys) { if (preg_match('/^profile_field_(.*)$/', $key, $match)) { // Custom field. $fieldname = $match[1]; if (isset($fields[$fieldname])) { $class = 'profile_field_' . $fields[$fieldname]->datatype; $formfield = new $class($fields[$fieldname]->id, $olduser->id); $oldvalue = isset($olduserprofilefields[$fieldname]) ? $olduserprofilefields[$fieldname] : null; } else { $oldvalue = null; } $newvalue = $formfield->edit_save_data_preprocess($newuser->{$formfield->inputname}, new stdClass); } else { // Standard field. $oldvalue = isset($olduser->$key) ? $olduser->$key : null; $newvalue = isset($newuser->$key) ? $newuser->$key : null; } if ($newvalue !== null and $newvalue !== $oldvalue and !empty($this->config->{'field_updateremote_' . $key})) { // For ldap values that could be in more than one // ldap key, we will do our best to match // where they came from $ambiguous = true; $changed = false; if (!is_array($ldapkeys)) { $ldapkeys = array($ldapkeys); } if (count($ldapkeys) < 2) { $ambiguous = false; } $nuvalue = core_text::convert($newvalue, 'utf-8', $this->config->ldapencoding); empty($nuvalue) ? $nuvalue = array() : $nuvalue; $ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding); foreach ($ldapkeys as $ldapkey) { // If the field is empty in LDAP there are two options: // 1. We get the LDAP field using ldap_first_attribute. // 2. LDAP don't send the field using ldap_first_attribute. // So, for option 1 we check the if the field is retrieve it. // And get the original value of field in LDAP if the field. // Otherwise, let value in blank and delegate the check in ldap_modify. if (isset($user_entry[$ldapkey][0])) { $ldapvalue = $user_entry[$ldapkey][0]; } else { $ldapvalue = ''; } if (!$ambiguous) { // Skip update if the values already match if ($nuvalue !== $ldapvalue) { // This might fail due to schema validation if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { $changed = true; continue; } else { $success = false; error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 'key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); continue; } } } else { // Ambiguous. Value empty before in Moodle (and LDAP) - use // 1st ldap candidate field, no need to guess if ($ouvalue === '') { // value empty before - use 1st ldap candidate // This might fail due to schema validation if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { $changed = true; continue; } else { $success = false; error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 'key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); continue; } } // We found which ldap key to update! if ($ouvalue !== '' and $ouvalue === $ldapvalue ) { // This might fail due to schema validation if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { $changed = true; continue; } else { $success = false; error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 'key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); continue; } } } } if ($ambiguous and !$changed) { $success = false; error_log($this->errorlogtag.get_string ('updateremfailamb', 'auth_ldap', array('key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); } } } } else { error_log($this->errorlogtag.get_string ('usernotfound', 'auth_ldap')); $success = false; } $this->ldap_close(); return $success; } /** * Changes userpassword in LDAP * * Called when the user password is updated. It assumes it is * called by an admin or that you've otherwise checked the user's * credentials * * @param object $user User table object * @param string $newpassword Plaintext password (not crypted/md5'ed) * @return boolean result * */ function user_update_password($user, $newpassword) { global $USER; $result = false; $username = $user->username; $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $extpassword = core_text::convert($newpassword, 'utf-8', $this->config->ldapencoding); switch ($this->config->passtype) { case 'md5': $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); break; case 'sha1': $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); break; case 'plaintext': default: break; // plaintext } $ldapconnection = $this->ldap_connect(); $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); if (!$user_dn) { error_log($this->errorlogtag.get_string ('nodnforusername', 'auth_ldap', $user->username)); return false; } switch ($this->config->user_type) { case 'edir': // Change password $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } // Update password expiration time, grace logins count $search_attribs = array($this->config->expireattr, 'passwordExpirationInterval', 'loginGraceLimit'); $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); if ($sr) { $entry = ldap_get_entries_moodle($ldapconnection, $sr); $info = $entry[0]; $newattrs = array(); if (!empty($info[$this->config->expireattr][0])) { // Set expiration time only if passwordExpirationInterval is defined if (!empty($info['passwordexpirationinterval'][0])) { $expirationtime = time() + $info['passwordexpirationinterval'][0]; $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime); $newattrs['passwordExpirationTime'] = $ldapexpirationtime; } // Set gracelogin count if (!empty($info['logingracelimit'][0])) { $newattrs['loginGraceRemaining']= $info['logingracelimit'][0]; } // Store attribute changes in LDAP $result = ldap_modify($ldapconnection, $user_dn, $newattrs); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserrorexpiregrace', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } } } else { error_log($this->errorlogtag.get_string ('updatepasserrorexpire', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } break; case 'ad': // Passwords in Active Directory must be encoded as Unicode // strings (UCS-2 Little Endian format) and surrounded with // double quotes. See http://support.microsoft.com/?kbid=269190 if (!function_exists('mb_convert_encoding')) { error_log($this->errorlogtag.get_string ('needmbstring', 'auth_ldap')); return false; } $extpassword = mb_convert_encoding('"'.$extpassword.'"', "UCS-2LE", $this->config->ldapencoding); $result = ldap_modify($ldapconnection, $user_dn, array('unicodePwd' => $extpassword)); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } break; default: // Send LDAP the password in cleartext, it will md5 it itself $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } } $this->ldap_close(); return $result; } /** * Take expirationtime and return it as unix timestamp in seconds * * Takes expiration timestamp as read from LDAP and returns it as unix timestamp in seconds * Depends on $this->config->user_type variable * * @param mixed time Time stamp read from LDAP as it is. * @param string $ldapconnection Only needed for Active Directory. * @param string $user_dn User distinguished name for the user we are checking password expiration (only needed for Active Directory). * @return timestamp */ function ldap_expirationtime2unix ($time, $ldapconnection, $user_dn) { $result = false; switch ($this->config->user_type) { case 'edir': $yr=substr($time, 0, 4); $mo=substr($time, 4, 2); $dt=substr($time, 6, 2); $hr=substr($time, 8, 2); $min=substr($time, 10, 2); $sec=substr($time, 12, 2); $result = mktime($hr, $min, $sec, $mo, $dt, $yr); break; case 'rfc2307': case 'rfc2307bis': $result = $time * DAYSECS; // The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date break; case 'ad': $result = $this->ldap_get_ad_pwdexpire($time, $ldapconnection, $user_dn); break; default: throw new \moodle_exception('auth_ldap_usertypeundefined', 'auth_ldap'); } return $result; } /** * Takes unix timestamp and returns it formated for storing in LDAP * * @param integer unix time stamp */ function ldap_unix2expirationtime($time) { $result = false; switch ($this->config->user_type) { case 'edir': $result=date('YmdHis', $time).'Z'; break; case 'rfc2307': case 'rfc2307bis': $result = $time ; // Already in correct format break; default: throw new \moodle_exception('auth_ldap_usertypeundefined2', 'auth_ldap'); } return $result; } /** * Returns user attribute mappings between moodle and LDAP * * @return array */ function ldap_attributes () { $moodleattributes = array(); // If we have custom fields then merge them with user fields. $customfields = $this->get_custom_user_profile_fields(); if (!empty($customfields) && !empty($this->userfields)) { $userfields = array_merge($this->userfields, $customfields); } else { $userfields = $this->userfields; } foreach ($userfields as $field) { if (!empty($this->config->{"field_map_$field"})) { $moodleattributes[$field] = core_text::strtolower(trim($this->config->{"field_map_$field"})); if (preg_match('/,/', $moodleattributes[$field])) { $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ? } } } $moodleattributes['username'] = core_text::strtolower(trim($this->config->user_attribute)); $moodleattributes['suspended'] = core_text::strtolower(trim($this->config->suspended_attribute)); return $moodleattributes; } /** * Returns all usernames from LDAP * * @param $filter An LDAP search filter to select desired users * @return array of LDAP user names converted to UTF-8 */ function ldap_get_userlist($filter='*') { $fresult = array(); $ldapconnection = $this->ldap_connect(); if ($filter == '*') { $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; } $servercontrols = array(); $contexts = explode(';', $this->config->contexts); if (!empty($this->config->create_context)) { array_push($contexts, $this->config->create_context); } $ldap_cookie = ''; $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection); foreach ($contexts as $context) { $context = trim($context); if (empty($context)) { continue; } do { if ($ldap_pagedresults) { $servercontrols = array(array( 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 'size' => $this->config->pagesize, 'cookie' => $ldap_cookie))); } if ($this->config->search_sub) { // Use ldap_search to find first user from subtree. $ldap_result = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } else { // Search only in this context. $ldap_result = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } if(!$ldap_result) { continue; } if ($ldap_pagedresults) { // Get next server cookie to know if we'll need to continue searching. $ldap_cookie = ''; // Get next cookie from controls. ldap_parse_result($ldapconnection, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $controls); if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } } $users = ldap_get_entries_moodle($ldapconnection, $ldap_result); // Add found users to list. for ($i = 0; $i < count($users); $i++) { $extuser = core_text::convert($users[$i][$this->config->user_attribute][0], $this->config->ldapencoding, 'utf-8'); array_push($fresult, $extuser); } unset($ldap_result); // Free mem. } while ($ldap_pagedresults && !empty($ldap_cookie)); } // If paged results were used, make sure the current connection is completely closed $this->ldap_close($ldap_pagedresults); return $fresult; } /** * Indicates if password hashes should be stored in local moodle database. * * @return bool true means flag 'not_cached' stored instead of password hash */ function prevent_local_passwords() { return !empty($this->config->preventpassindb); } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return false; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return !empty($this->config->stdchangepassword) or !empty($this->config->changepasswordurl); } /** * Returns the URL for changing the user's password, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { if (empty($this->config->stdchangepassword)) { if (!empty($this->config->changepasswordurl)) { return new moodle_url($this->config->changepasswordurl); } else { return null; } } else { return null; } } /** * Will get called before the login page is shownr. Ff NTLM SSO * is enabled, and the user is in the right network, we'll redirect * to the magic NTLM page for SSO... * */ function loginpage_hook() { global $CFG, $SESSION; // HTTPS is potentially required //httpsrequired(); - this must be used before setting the URL, it is already done on the login/index.php if (($_SERVER['REQUEST_METHOD'] === 'GET' // Only on initial GET of loginpage || ($_SERVER['REQUEST_METHOD'] === 'POST' && (get_local_referer() != strip_querystring(qualified_me())))) // Or when POSTed from another place // See MDL-14071 && !empty($this->config->ntlmsso_enabled) // SSO enabled && !empty($this->config->ntlmsso_subnet) // have a subnet to test for && empty($_GET['authldap_skipntlmsso']) // haven't failed it yet && (isguestuser() || !isloggedin()) // guestuser or not-logged-in users && address_in_subnet(getremoteaddr(), $this->config->ntlmsso_subnet)) { // First, let's remember where we were trying to get to before we got here if (empty($SESSION->wantsurl)) { $SESSION->wantsurl = null; $referer = get_local_referer(false); if ($referer && $referer != $CFG->wwwroot && $referer != $CFG->wwwroot . '/' && $referer != $CFG->wwwroot . '/login/' && $referer != $CFG->wwwroot . '/login/index.php') { $SESSION->wantsurl = $referer; } } // Now start the whole NTLM machinery. if($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESATTEMPT || $this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) { if (core_useragent::is_ie()) { $sesskey = sesskey(); redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey); } else if ($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) { redirect($CFG->wwwroot.'/login/index.php?authldap_skipntlmsso=1'); } } redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_attempt.php'); } // No NTLM SSO, Use the normal login page instead. // If $SESSION->wantsurl is empty and we have a 'Referer:' header, the login // page insists on redirecting us to that page after user validation. If // we clicked on the redirect link at the ntlmsso_finish.php page (instead // of waiting for the redirection to happen) then we have a 'Referer:' header // we don't want to use at all. As we can't get rid of it, just point // $SESSION->wantsurl to $CFG->wwwroot (after all, we came from there). if (empty($SESSION->wantsurl) && (get_local_referer() == $CFG->wwwroot.'/auth/ldap/ntlmsso_finish.php')) { $SESSION->wantsurl = $CFG->wwwroot; } } /** * To be called from a page running under NTLM's * "Integrated Windows Authentication". * * If successful, it will set a special "cookie" (not an HTTP cookie!) * in cache_flags under the $this->pluginconfig/ntlmsess "plugin" and return true. * The "cookie" will be picked up by ntlmsso_finish() to complete the * process. * * On failure it will return false for the caller to display an appropriate * error message (probably saying that Integrated Windows Auth isn't enabled!) * * NOTE that this code will execute under the OS user credentials, * so we MUST avoid dealing with files -- such as session files. * (The caller should define('NO_MOODLE_COOKIES', true) before including config.php) * */ function ntlmsso_magic($sesskey) { if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER'])) { // HTTP __headers__ seem to be sent in ISO-8859-1 encoding // (according to my reading of RFC-1945, RFC-2616 and RFC-2617 and // my local tests), so we need to convert the REMOTE_USER value // (i.e., what we got from the HTTP WWW-Authenticate header) into UTF-8 $username = core_text::convert($_SERVER['REMOTE_USER'], 'iso-8859-1', 'utf-8'); switch ($this->config->ntlmsso_type) { case 'ntlm': // The format is now configurable, so try to extract the username $username = $this->get_ntlm_remote_user($username); if (empty($username)) { return false; } break; case 'kerberos': // Format is username@DOMAIN $username = substr($username, 0, strpos($username, '@')); break; default: error_log($this->errorlogtag.get_string ('ntlmsso_unknowntype', 'auth_ldap')); return false; // Should never happen! } $username = core_text::strtolower($username); // Compatibility hack set_cache_flag($this->pluginconfig.'/ntlmsess', $sesskey, $username, AUTH_NTLMTIMEOUT); return true; } return false; } /** * Find the session set by ntlmsso_magic(), validate it and * call authenticate_user_login() to authenticate the user through * the auth machinery. * * It is complemented by a similar check in user_login(). * * If it succeeds, it never returns. * */ function ntlmsso_finish() { global $CFG, $USER, $SESSION; $key = sesskey(); $username = get_cache_flag($this->pluginconfig.'/ntlmsess', $key); if (empty($username)) { return false; } // Here we want to trigger the whole authentication machinery // to make sure no step is bypassed... $reason = null; $user = authenticate_user_login($username, $key, false, $reason, false); if ($user) { complete_user_login($user); // Cleanup the key to prevent reuse... // and to allow re-logins with normal credentials unset_cache_flag($this->pluginconfig.'/ntlmsess', $key); // Redirection if (user_not_fully_set_up($USER, true)) { $urltogo = $CFG->wwwroot.'/user/edit.php'; // We don't delete $SESSION->wantsurl yet, so we get there later } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { $urltogo = $SESSION->wantsurl; // Because it's an address in this site unset($SESSION->wantsurl); } else { // No wantsurl stored or external - go to homepage $urltogo = $CFG->wwwroot.'/'; unset($SESSION->wantsurl); } // We do not want to redirect if we are in a PHPUnit test. if (!PHPUNIT_TEST) { redirect($urltogo); } } // Should never reach here. return false; } /** * Sync roles for this user. * * @param object $user The user to sync (without system magic quotes). */ function sync_roles($user) { global $DB; $roles = get_ldap_assignable_role_names(2); // Admin user. foreach ($roles as $role) { $isrole = $this->is_role($user->username, $role); if ($isrole === null) { continue; // Nothing to sync - role/LDAP contexts not configured. } // Sync user. $systemcontext = context_system::instance(); if ($isrole) { // Following calls will not create duplicates. role_assign($role['id'], $user->id, $systemcontext->id, $this->roleauth); } else { // Unassign only if previously assigned by this plugin. role_unassign($role['id'], $user->id, $systemcontext->id, $this->roleauth); } } } /** * Get password expiration time for a given user from Active Directory * * @param string $pwdlastset The time last time we changed the password. * @param resource $lcapconn The open LDAP connection. * @param string $user_dn The distinguished name of the user we are checking. * * @return string $unixtime */ function ldap_get_ad_pwdexpire($pwdlastset, $ldapconn, $user_dn){ global $CFG; if (!function_exists('bcsub')) { error_log($this->errorlogtag.get_string ('needbcmath', 'auth_ldap')); return 0; } // If UF_DONT_EXPIRE_PASSWD flag is set in user's // userAccountControl attribute, the password doesn't expire. $sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)', array('userAccountControl')); if (!$sr) { error_log($this->errorlogtag.get_string ('useracctctrlerror', 'auth_ldap', $user_dn)); // Don't expire password, as we are not sure if it has to be // expired or not. return 0; } $entry = ldap_get_entries_moodle($ldapconn, $sr); $info = $entry[0]; $useraccountcontrol = $info['useraccountcontrol'][0]; if ($useraccountcontrol & UF_DONT_EXPIRE_PASSWD) { // Password doesn't expire. return 0; } // If pwdLastSet is zero, the user must change his/her password now // (unless UF_DONT_EXPIRE_PASSWD flag is set, but we already // tested this above) if ($pwdlastset === '0') { // Password has expired return -1; } // ---------------------------------------------------------------- // Password expiration time in Active Directory is the composition of // two values: // // - User's pwdLastSet attribute, that stores the last time // the password was changed. // // - Domain's maxPwdAge attribute, that sets how long // passwords last in this domain. // // We already have the first value (passed in as a parameter). We // need to get the second one. As we don't know the domain DN, we // have to query rootDSE's defaultNamingContext attribute to get // it. Then we have to query that DN's maxPwdAge attribute to get // the real value. // // Once we have both values, we just need to combine them. But MS // chose to use a different base and unit for time measurements. // So we need to convert the values to Unix timestamps (see // details below). // ---------------------------------------------------------------- $sr = ldap_read($ldapconn, ROOTDSE, '(objectClass=*)', array('defaultNamingContext')); if (!$sr) { error_log($this->errorlogtag.get_string ('rootdseerror', 'auth_ldap')); return 0; } $entry = ldap_get_entries_moodle($ldapconn, $sr); $info = $entry[0]; $domaindn = $info['defaultnamingcontext'][0]; $sr = ldap_read ($ldapconn, $domaindn, '(objectClass=*)', array('maxPwdAge')); $entry = ldap_get_entries_moodle($ldapconn, $sr); $info = $entry[0]; $maxpwdage = $info['maxpwdage'][0]; if ($sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)', array('msDS-ResultantPSO'))) { if ($entry = ldap_get_entries_moodle($ldapconn, $sr)) { $info = $entry[0]; $userpso = $info['msds-resultantpso'][0]; // If a PSO exists, FGPP is being utilized. // Grab the new maxpwdage from the msDS-MaximumPasswordAge attribute of the PSO. if (!empty($userpso)) { $sr = ldap_read($ldapconn, $userpso, '(objectClass=*)', array('msDS-MaximumPasswordAge')); if ($entry = ldap_get_entries_moodle($ldapconn, $sr)) { $info = $entry[0]; // Default value of msds-maximumpasswordage is 42 and is always set. $maxpwdage = $info['msds-maximumpasswordage'][0]; } } } } // ---------------------------------------------------------------- // MSDN says that "pwdLastSet contains the number of 100 nanosecond // intervals since January 1, 1601 (UTC), stored in a 64 bit integer". // // According to Perl's Date::Manip, the number of seconds between // this date and Unix epoch is 11644473600. So we have to // substract this value to calculate a Unix time, once we have // scaled pwdLastSet to seconds. This is the script used to // calculate the value shown above: // // #!/usr/bin/perl -w // // use Date::Manip; // // $date1 = ParseDate ("160101010000 UTC"); // $date2 = ParseDate ("197001010000 UTC"); // $delta = DateCalc($date1, $date2, \$err); // $secs = Delta_Format($delta, 0, "%st"); // print "$secs \n"; // // MSDN also says that "maxPwdAge is stored as a large integer that // represents the number of 100 nanosecond intervals from the time // the password was set before the password expires." We also need // to scale this to seconds. Bear in mind that this value is stored // as a _negative_ quantity (at least in my AD domain). // // As a last remark, if the low 32 bits of maxPwdAge are equal to 0, // the maximum password age in the domain is set to 0, which means // passwords do not expire (see // http://msdn2.microsoft.com/en-us/library/ms974598.aspx) // // As the quantities involved are too big for PHP integers, we // need to use BCMath functions to work with arbitrary precision // numbers. // ---------------------------------------------------------------- // If the low order 32 bits are 0, then passwords do not expire in // the domain. Just do '$maxpwdage mod 2^32' and check the result // (2^32 = 4294967296) if (bcmod ($maxpwdage, 4294967296) === '0') { return 0; } // Add up pwdLastSet and maxPwdAge to get password expiration // time, in MS time units. Remember maxPwdAge is stored as a // _negative_ quantity, so we need to substract it in fact. $pwdexpire = bcsub ($pwdlastset, $maxpwdage); // Scale the result to convert it to Unix time units and return // that value. return bcsub( bcdiv($pwdexpire, '10000000'), '11644473600'); } /** * Connect to the LDAP server, using the plugin configured * settings. It's actually a wrapper around ldap_connect_moodle() * * @return resource A valid LDAP connection (or dies if it can't connect) */ function ldap_connect() { // Cache ldap connections. They are expensive to set up // and can drain the TCP/IP ressources on the server if we // are syncing a lot of users (as we try to open a new connection // to get the user details). This is the least invasive way // to reuse existing connections without greater code surgery. if(!empty($this->ldapconnection)) { $this->ldapconns++; return $this->ldapconnection; } if($ldapconnection = ldap_connect_moodle($this->config->host_url, $this->config->ldap_version, $this->config->user_type, $this->config->bind_dn, $this->config->bind_pw, $this->config->opt_deref, $debuginfo, $this->config->start_tls)) { $this->ldapconns = 1; $this->ldapconnection = $ldapconnection; return $ldapconnection; } throw new \moodle_exception('auth_ldap_noconnect_all', 'auth_ldap', '', $debuginfo); } /** * Disconnects from a LDAP server * * @param force boolean Forces closing the real connection to the LDAP server, ignoring any * cached connections. This is needed when we've used paged results * and want to use normal results again. */ function ldap_close($force=false) { $this->ldapconns--; if (($this->ldapconns == 0) || ($force)) { $this->ldapconns = 0; @ldap_close($this->ldapconnection); unset($this->ldapconnection); } } /** * Search specified contexts for username and return the user dn * like: cn=username,ou=suborg,o=org. It's actually a wrapper * around ldap_find_userdn(). * * @param resource $ldapconnection a valid LDAP connection * @param string $extusername the username to search (in external LDAP encoding, no db slashes) * @return mixed the user dn (external LDAP encoding) or false */ function ldap_find_userdn($ldapconnection, $extusername) { $ldap_contexts = explode(';', $this->config->contexts); if (!empty($this->config->create_context)) { array_push($ldap_contexts, $this->config->create_context); } return ldap_find_userdn($ldapconnection, $extusername, $ldap_contexts, $this->config->objectclass, $this->config->user_attribute, $this->config->search_sub); } /** * When using NTLM SSO, the format of the remote username we get in * $_SERVER['REMOTE_USER'] may vary, depending on where from and how the web * server gets the data. So we let the admin configure the format using two * place holders (%domain% and %username%). This function tries to extract * the username (stripping the domain part and any separators if they are * present) from the value present in $_SERVER['REMOTE_USER'], using the * configured format. * * @param string $remoteuser The value from $_SERVER['REMOTE_USER'] (converted to UTF-8) * * @return string The remote username (without domain part or * separators). Empty string if we can't extract the username. */ protected function get_ntlm_remote_user($remoteuser) { if (empty($this->config->ntlmsso_remoteuserformat)) { $format = AUTH_NTLM_DEFAULT_FORMAT; } else { $format = $this->config->ntlmsso_remoteuserformat; } $format = preg_quote($format); $formatregex = preg_replace(array('#%domain%#', '#%username%#'), array('('.AUTH_NTLM_VALID_DOMAINNAME.')', '('.AUTH_NTLM_VALID_USERNAME.')'), $format); if (preg_match('#^'.$formatregex.'$#', $remoteuser, $matches)) { $user = end($matches); return $user; } /* We are unable to extract the username with the configured format. Probably * the format specified is wrong, so log a warning for the admin and return * an empty username. */ error_log($this->errorlogtag.get_string ('auth_ntlmsso_maybeinvalidformat', 'auth_ldap')); return ''; } /** * Check if the diagnostic message for the LDAP login error tells us that the * login is denied because the user password has expired or the password needs * to be changed on first login (using interactive SMB/Windows logins, not * LDAP logins). * * @param string the diagnostic message for the LDAP login error * @return bool true if the password has expired or the password must be changed on first login */ protected function ldap_ad_pwdexpired_from_diagmsg($diagmsg) { // The format of the diagnostic message is (actual examples from W2003 and W2008): // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 52e, vece" (W2003) // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 773, vece" (W2003) // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 52e, v1771" (W2008) // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 773, v1771" (W2008) // We are interested in the 'data nnn' part. // if nnn == 773 then user must change password on first login // if nnn == 532 then user password has expired $diagmsg = explode(',', $diagmsg); if (preg_match('/data (773|532)/i', trim($diagmsg[2]))) { return true; } return false; } /** * Check if a user is suspended. This function is intended to be used after calling * get_userinfo_asobj. This is needed because LDAP doesn't have a notion of disabled * users, however things like MS Active Directory support it and expose information * through a field. * * @param object $user the user object returned by get_userinfo_asobj * @return boolean */ protected function is_user_suspended($user) { if (!$this->config->suspended_attribute || !isset($user->suspended)) { return false; } if ($this->config->suspended_attribute == 'useraccountcontrol' && $this->config->user_type == 'ad') { return (bool)($user->suspended & AUTH_AD_ACCOUNTDISABLE); } return (bool)$user->suspended; } /** * Test a DN * * @param resource $ldapconn * @param string $dn The DN to check for existence * @param string $message The identifier of a string as in get_string() * @param string|object|array $a An object, string or number that can be used * within translation strings as in get_string() * @return true or a message in case of error */ private function test_dn($ldapconn, $dn, $message, $a = null) { $ldapresult = @ldap_read($ldapconn, $dn, '(objectClass=*)', array()); if (!$ldapresult) { if (ldap_errno($ldapconn) == 32) { // No such object. return get_string($message, 'auth_ldap', $a); } $a = array('code' => ldap_errno($ldapconn), 'subject' => $a, 'message' => ldap_error($ldapconn)); return get_string('diag_genericerror', 'auth_ldap', $a); } return true; } /** * Test if settings are correct, print info to output. */ public function test_settings() { global $OUTPUT; if (!function_exists('ldap_connect')) { // Is php-ldap really there? echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR); return; } // Check to see if this is actually configured. if (empty($this->config->host_url)) { // LDAP is not even configured. echo $OUTPUT->notification(get_string('ldapnotconfigured', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR); return; } if ($this->config->ldap_version != 3) { echo $OUTPUT->notification(get_string('diag_toooldversion', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); } try { $ldapconn = $this->ldap_connect(); } catch (Exception $e) { echo $OUTPUT->notification($e->getMessage(), \core\output\notification::NOTIFY_ERROR); return; } // Display paged file results. if (!ldap_paged_results_supported($this->config->ldap_version, $ldapconn)) { echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO); } // Check contexts. foreach (explode(';', $this->config->contexts) as $context) { $context = trim($context); if (empty($context)) { echo $OUTPUT->notification(get_string('diag_emptycontext', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); continue; } $message = $this->test_dn($ldapconn, $context, 'diag_contextnotfound', $context); if ($message !== true) { echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); } } // Create system role mapping field for each assignable system role. $roles = get_ldap_assignable_role_names(); foreach ($roles as $role) { foreach (explode(';', $this->config->{$role['settingname']}) as $groupdn) { if (empty($groupdn)) { continue; } $role['group'] = $groupdn; $message = $this->test_dn($ldapconn, $groupdn, 'diag_rolegroupnotfound', $role); if ($message !== true) { echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); } } } $this->ldap_close(true); // We were able to connect successfuly. echo $OUTPUT->notification(get_string('connectingldapsuccess', 'auth_ldap'), \core\output\notification::NOTIFY_SUCCESS); } /** * Get the list of profile fields. * * @param bool $fetchall Fetch all, not just those for update. * @return array */ protected function get_profile_keys($fetchall = false) { $keys = array_keys(get_object_vars($this->config)); $updatekeys = []; foreach ($keys as $key) { if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) { // If we have a field to update it from and it must be updated 'onlogin' we update it on cron. if (!empty($this->config->{'field_map_'.$match[1]})) { if ($fetchall || $this->config->{$match[0]} === 'onlogin') { array_push($updatekeys, $match[1]); // the actual key name } } } } if ($this->config->suspended_attribute && $this->config->sync_suspended) { $updatekeys[] = 'suspended'; } return $updatekeys; } } ldap/settings.php 0000644 00000042726 15152311435 0010050 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin settings and defaults. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { if (!function_exists('ldap_connect')) { $notify = new \core\output\notification(get_string('auth_ldap_noextension', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); $settings->add(new admin_setting_heading('auth_ldap_noextension', '', $OUTPUT->render($notify))); } else { // We use a couple of custom admin settings since we need to massage the data before it is inserted into the DB. require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_lowercase_configtext.php'); require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_contexts_configtext.php'); require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_ntlm_configtext.php'); // We need to use some of the Moodle LDAP constants / functions to create the list of options. require_once($CFG->dirroot.'/auth/ldap/auth.php'); // Introductory explanation. $settings->add(new admin_setting_heading('auth_ldap/pluginname', '', new lang_string('auth_ldapdescription', 'auth_ldap'))); // LDAP server settings. $settings->add(new admin_setting_heading('auth_ldap/ldapserversettings', new lang_string('auth_ldap_server_settings', 'auth_ldap'), '')); // Host. $settings->add(new admin_setting_configtext('auth_ldap/host_url', get_string('auth_ldap_host_url_key', 'auth_ldap'), get_string('auth_ldap_host_url', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Version. $versions = array(); $versions[2] = '2'; $versions[3] = '3'; $settings->add(new admin_setting_configselect('auth_ldap/ldap_version', new lang_string('auth_ldap_version_key', 'auth_ldap'), new lang_string('auth_ldap_version', 'auth_ldap'), 3, $versions)); // Start TLS. $yesno = array( new lang_string('no'), new lang_string('yes'), ); $settings->add(new admin_setting_configselect('auth_ldap/start_tls', new lang_string('start_tls_key', 'auth_ldap'), new lang_string('start_tls', 'auth_ldap'), 0 , $yesno)); // Encoding. $settings->add(new admin_setting_configtext('auth_ldap/ldapencoding', get_string('auth_ldap_ldap_encoding_key', 'auth_ldap'), get_string('auth_ldap_ldap_encoding', 'auth_ldap'), 'utf-8', PARAM_RAW_TRIMMED)); // Page Size. (Hide if not available). $settings->add(new admin_setting_configtext('auth_ldap/pagesize', get_string('pagesize_key', 'auth_ldap'), get_string('pagesize', 'auth_ldap'), '250', PARAM_INT)); // Bind settings. $settings->add(new admin_setting_heading('auth_ldap/ldapbindsettings', new lang_string('auth_ldap_bind_settings', 'auth_ldap'), '')); // Store Password in DB. $settings->add(new admin_setting_configselect('auth_ldap/preventpassindb', new lang_string('auth_ldap_preventpassindb_key', 'auth_ldap'), new lang_string('auth_ldap_preventpassindb', 'auth_ldap'), 0 , $yesno)); // User ID. $settings->add(new admin_setting_configtext('auth_ldap/bind_dn', get_string('auth_ldap_bind_dn_key', 'auth_ldap'), get_string('auth_ldap_bind_dn', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Password. $settings->add(new admin_setting_configpasswordunmask('auth_ldap/bind_pw', get_string('auth_ldap_bind_pw_key', 'auth_ldap'), get_string('auth_ldap_bind_pw', 'auth_ldap'), '')); // User Lookup settings. $settings->add(new admin_setting_heading('auth_ldap/ldapuserlookup', new lang_string('auth_ldap_user_settings', 'auth_ldap'), '')); // User Type. $settings->add(new admin_setting_configselect('auth_ldap/user_type', new lang_string('auth_ldap_user_type_key', 'auth_ldap'), new lang_string('auth_ldap_user_type', 'auth_ldap'), 'default', ldap_supported_usertypes())); // Contexts. $settings->add(new auth_ldap_admin_setting_special_contexts_configtext('auth_ldap/contexts', get_string('auth_ldap_contexts_key', 'auth_ldap'), get_string('auth_ldap_contexts', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Search subcontexts. $settings->add(new admin_setting_configselect('auth_ldap/search_sub', new lang_string('auth_ldap_search_sub_key', 'auth_ldap'), new lang_string('auth_ldap_search_sub', 'auth_ldap'), 0 , $yesno)); // Dereference aliases. $optderef = array(); $optderef[LDAP_DEREF_NEVER] = get_string('no'); $optderef[LDAP_DEREF_ALWAYS] = get_string('yes'); $settings->add(new admin_setting_configselect('auth_ldap/opt_deref', new lang_string('auth_ldap_opt_deref_key', 'auth_ldap'), new lang_string('auth_ldap_opt_deref', 'auth_ldap'), LDAP_DEREF_NEVER , $optderef)); // User attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/user_attribute', get_string('auth_ldap_user_attribute_key', 'auth_ldap'), get_string('auth_ldap_user_attribute', 'auth_ldap'), '', PARAM_RAW)); // Suspended attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/suspended_attribute', get_string('auth_ldap_suspended_attribute_key', 'auth_ldap'), get_string('auth_ldap_suspended_attribute', 'auth_ldap'), '', PARAM_RAW)); // Member attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/memberattribute', get_string('auth_ldap_memberattribute_key', 'auth_ldap'), get_string('auth_ldap_memberattribute', 'auth_ldap'), '', PARAM_RAW)); // Member attribute uses dn. $settings->add(new admin_setting_configselect('auth_ldap/memberattribute_isdn', get_string('auth_ldap_memberattribute_isdn_key', 'auth_ldap'), get_string('auth_ldap_memberattribute_isdn', 'auth_ldap'), 0, $yesno)); // Object class. $settings->add(new admin_setting_configtext('auth_ldap/objectclass', get_string('auth_ldap_objectclass_key', 'auth_ldap'), get_string('auth_ldap_objectclass', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Force Password change Header. $settings->add(new admin_setting_heading('auth_ldap/ldapforcepasswordchange', new lang_string('forcechangepassword', 'auth'), '')); // Force Password change. $settings->add(new admin_setting_configselect('auth_ldap/forcechangepassword', new lang_string('forcechangepassword', 'auth'), new lang_string('forcechangepasswordfirst_help', 'auth'), 0 , $yesno)); // Standard Password Change. $settings->add(new admin_setting_configselect('auth_ldap/stdchangepassword', new lang_string('stdchangepassword', 'auth'), new lang_string('stdchangepassword_expl', 'auth') .' '. get_string('stdchangepassword_explldap', 'auth'), 0 , $yesno)); // Password Type. $passtype = array(); $passtype['plaintext'] = get_string('plaintext', 'auth'); $passtype['md5'] = get_string('md5', 'auth'); $passtype['sha1'] = get_string('sha1', 'auth'); $settings->add(new admin_setting_configselect('auth_ldap/passtype', new lang_string('auth_ldap_passtype_key', 'auth_ldap'), new lang_string('auth_ldap_passtype', 'auth_ldap'), 'plaintext', $passtype)); // Password change URL. $settings->add(new admin_setting_configtext('auth_ldap/changepasswordurl', get_string('auth_ldap_changepasswordurl_key', 'auth_ldap'), get_string('changepasswordhelp', 'auth'), '', PARAM_URL)); // Password Expiration Header. $settings->add(new admin_setting_heading('auth_ldap/passwordexpire', new lang_string('auth_ldap_passwdexpire_settings', 'auth_ldap'), '')); // Password Expiration. // Create the description lang_string object. $strno = get_string('no'); $strldapserver = get_string('pluginname', 'auth_ldap'); $langobject = new stdClass(); $langobject->no = $strno; $langobject->ldapserver = $strldapserver; $description = new lang_string('auth_ldap_expiration_desc', 'auth_ldap', $langobject); // Now create the options. $expiration = array(); $expiration['0'] = $strno; $expiration['1'] = $strldapserver; // Add the setting. $settings->add(new admin_setting_configselect('auth_ldap/expiration', new lang_string('auth_ldap_expiration_key', 'auth_ldap'), $description, 0 , $expiration)); // Password Expiration warning. $settings->add(new admin_setting_configtext('auth_ldap/expiration_warning', get_string('auth_ldap_expiration_warning_key', 'auth_ldap'), get_string('auth_ldap_expiration_warning_desc', 'auth_ldap'), '', PARAM_RAW)); // Password Expiration attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/expireattr', get_string('auth_ldap_expireattr_key', 'auth_ldap'), get_string('auth_ldap_expireattr_desc', 'auth_ldap'), '', PARAM_RAW)); // Grace Logins. $settings->add(new admin_setting_configselect('auth_ldap/gracelogins', new lang_string('auth_ldap_gracelogins_key', 'auth_ldap'), new lang_string('auth_ldap_gracelogins_desc', 'auth_ldap'), 0 , $yesno)); // Grace logins attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/graceattr', get_string('auth_ldap_gracelogin_key', 'auth_ldap'), get_string('auth_ldap_graceattr_desc', 'auth_ldap'), '', PARAM_RAW)); // User Creation. $settings->add(new admin_setting_heading('auth_ldap/usercreation', new lang_string('auth_user_create', 'auth'), '')); // Create users externally. $settings->add(new admin_setting_configselect('auth_ldap/auth_user_create', new lang_string('auth_ldap_auth_user_create_key', 'auth_ldap'), new lang_string('auth_user_creation', 'auth'), 0 , $yesno)); // Context for new users. $settings->add(new admin_setting_configtext('auth_ldap/create_context', get_string('auth_ldap_create_context_key', 'auth_ldap'), get_string('auth_ldap_create_context', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // System roles mapping header. $settings->add(new admin_setting_heading('auth_ldap/systemrolemapping', new lang_string('systemrolemapping', 'auth_ldap'), '')); // Create system role mapping field for each assignable system role. $roles = get_ldap_assignable_role_names(); foreach ($roles as $role) { // Before we can add this setting we need to check a few things. // A) It does not exceed 100 characters otherwise it will break the DB as the 'name' field // in the 'config_plugins' table is a varchar(100). // B) The setting name does not contain hyphens. If it does then it will fail the check // in parse_setting_name() and everything will explode. Role short names are validated // against PARAM_ALPHANUMEXT which is similar to the regex used in parse_setting_name() // except it also allows hyphens. // Instead of shortening the name and removing/replacing the hyphens we are showing a warning. // If we were to manipulate the setting name by removing the hyphens we may get conflicts, eg // 'thisisashortname' and 'this-is-a-short-name'. The same applies for shortening the setting name. if (core_text::strlen($role['settingname']) > 100 || !preg_match('/^[a-zA-Z0-9_]+$/', $role['settingname'])) { $url = new moodle_url('/admin/roles/define.php', array('action' => 'edit', 'roleid' => $role['id'])); $a = (object)['rolename' => $role['localname'], 'shortname' => $role['shortname'], 'charlimit' => 93, 'link' => $url->out()]; $settings->add(new admin_setting_heading('auth_ldap/role_not_mapped_' . sha1($role['settingname']), '', get_string('cannotmaprole', 'auth_ldap', $a))); } else { $settings->add(new admin_setting_configtext('auth_ldap/' . $role['settingname'], get_string('auth_ldap_rolecontext', 'auth_ldap', $role), get_string('auth_ldap_rolecontext_help', 'auth_ldap', $role), '', PARAM_RAW_TRIMMED)); } } // User Account Sync. $settings->add(new admin_setting_heading('auth_ldap/syncusers', new lang_string('auth_sync_script', 'auth'), '')); // Remove external user. $deleteopt = array(); $deleteopt[AUTH_REMOVEUSER_KEEP] = get_string('auth_remove_keep', 'auth'); $deleteopt[AUTH_REMOVEUSER_SUSPEND] = get_string('auth_remove_suspend', 'auth'); $deleteopt[AUTH_REMOVEUSER_FULLDELETE] = get_string('auth_remove_delete', 'auth'); $settings->add(new admin_setting_configselect('auth_ldap/removeuser', new lang_string('auth_remove_user_key', 'auth'), new lang_string('auth_remove_user', 'auth'), AUTH_REMOVEUSER_KEEP, $deleteopt)); // Sync Suspension. $settings->add(new admin_setting_configselect('auth_ldap/sync_suspended', new lang_string('auth_sync_suspended_key', 'auth'), new lang_string('auth_sync_suspended', 'auth'), 0 , $yesno)); // NTLM SSO Header. $settings->add(new admin_setting_heading('auth_ldap/ntlm', new lang_string('auth_ntlmsso', 'auth_ldap'), '')); // Enable NTLM. $settings->add(new admin_setting_configselect('auth_ldap/ntlmsso_enabled', new lang_string('auth_ntlmsso_enabled_key', 'auth_ldap'), new lang_string('auth_ntlmsso_enabled', 'auth_ldap'), 0 , $yesno)); // Subnet. $settings->add(new admin_setting_configtext('auth_ldap/ntlmsso_subnet', get_string('auth_ntlmsso_subnet_key', 'auth_ldap'), get_string('auth_ntlmsso_subnet', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // NTLM Fast Path. $fastpathoptions = array(); $fastpathoptions[AUTH_NTLM_FASTPATH_YESFORM] = get_string('auth_ntlmsso_ie_fastpath_yesform', 'auth_ldap'); $fastpathoptions[AUTH_NTLM_FASTPATH_YESATTEMPT] = get_string('auth_ntlmsso_ie_fastpath_yesattempt', 'auth_ldap'); $fastpathoptions[AUTH_NTLM_FASTPATH_ATTEMPT] = get_string('auth_ntlmsso_ie_fastpath_attempt', 'auth_ldap'); $settings->add(new admin_setting_configselect('auth_ldap/ntlmsso_ie_fastpath', new lang_string('auth_ntlmsso_ie_fastpath_key', 'auth_ldap'), new lang_string('auth_ntlmsso_ie_fastpath', 'auth_ldap'), AUTH_NTLM_FASTPATH_ATTEMPT, $fastpathoptions)); // Authentication type. $types = array(); $types['ntlm'] = 'NTLM'; $types['kerberos'] = 'Kerberos'; $settings->add(new admin_setting_configselect('auth_ldap/ntlmsso_type', new lang_string('auth_ntlmsso_type_key', 'auth_ldap'), new lang_string('auth_ntlmsso_type', 'auth_ldap'), 'ntlm', $types)); // Remote Username format. $settings->add(new auth_ldap_admin_setting_special_ntlm_configtext('auth_ldap/ntlmsso_remoteuserformat', get_string('auth_ntlmsso_remoteuserformat_key', 'auth_ldap'), get_string('auth_ntlmsso_remoteuserformat', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); } // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('ldap'); $help = get_string('auth_ldapextrafields', 'auth_ldap'); $help .= get_string('auth_updatelocal_expl', 'auth'); $help .= get_string('auth_fieldlock_expl', 'auth'); $help .= get_string('auth_updateremote_expl', 'auth'); $help .= '<hr />'; $help .= get_string('auth_updateremote_ldap', 'auth'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, $help, true, true, $authplugin->get_custom_user_profile_fields()); } ldap/upgrade.txt 0000644 00000001073 15152311435 0007655 0 ustar 00 This files describes API changes in the auth_ldap code. === 3.4 === * The "auth_ldap/coursecreators" setting was replaced with dynamically generated "auth_ldap/<role>context" settings, migrating any existing value to a new setting in this style. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/ldap' to 'auth_ldap'. === 2.9.1 === * auth_plugin_ldap::update_user_record() accepts an additional (optional) param to trigger update event. ldap/version.php 0000644 00000002223 15152311435 0007661 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version details * * @package auth_ldap * @author Martin Dougiamas * @author Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_ldap'; // Full name of the plugin (used for diagnostics) ldap/ntlmsso_attempt.php 0000644 00000002423 15152311435 0011433 0 ustar 00 <?php require(__DIR__.'/../../config.php'); $PAGE->set_url('/auth/ldap/ntlmsso_attempt.php'); $PAGE->set_context(context_system::instance()); // Define variables used in page $site = get_site(); $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. if (!in_array('ldap', $authsequence, true)) { throw new \moodle_exception('ldap_isdisabled', 'auth'); } $authplugin = get_auth_plugin('ldap'); if (empty($authplugin->config->ntlmsso_enabled)) { throw new \moodle_exception('ntlmsso_isdisabled', 'auth_ldap'); } $sesskey = sesskey(); // Display the page header. This makes redirect respect the timeout we specify // here (and not add 3 more secs) which in turn prevents a bug in both IE 6.x // and FF 3.x (Windows version at least) where javascript timers fire up even // when we've already left the page that set the timer. $loginsite = get_string("loginsite"); $PAGE->navbar->add($loginsite); $PAGE->set_title("$site->fullname: $loginsite"); $PAGE->set_heading($site->fullname); echo $OUTPUT->header(); $msg = '<p>'.get_string('ntlmsso_attempting', 'auth_ldap').'</p>' . '<img width="1", height="1" ' . ' src="' . $CFG->wwwroot . '/auth/ldap/ntlmsso_magic.php?sesskey=' . $sesskey . '" />'; redirect($CFG->wwwroot . '/auth/ldap/ntlmsso_finish.php', $msg, 3); ldap/classes/privacy/provider.php 0000644 00000002745 15152311435 0013151 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_ldap. * * @package auth_ldap * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_ldap implementing null_provider. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } ldap/classes/adminpresets/adminpresets_auth_ldap_admin_setting_special_contexts_configtext.php 0000644 00000004210 15152311435 0027632 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_ldap\adminpresets; use core_adminpresets\local\setting\adminpresets_setting; /** * Basic text setting, cleans the param using the admin_setting paramtext attribute. * * @package auth_ldap * @copyright 2021 Pimenko <support@pimenko.com><pimenko.com> * @author Jordan Kesraoui | Sylvain Revenu | Pimenko based on David Monllaó <david.monllao@urv.cat> code * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class adminpresets_auth_ldap_admin_setting_special_contexts_configtext extends adminpresets_setting { /** * Validates the value using paramtype attribute * * @param string $value * @return boolean Returned value is always true, whenever the value has been successfully cleaned or not. */ protected function set_value($value): bool { $this->value = $value; if (empty($this->settingdata->paramtype)) { // For configfile, configpasswordunmask.... $this->settingdata->paramtype = 'RAW'; } $paramtype = 'PARAM_' . strtoupper($this->settingdata->paramtype); // Regexp. if (!defined($paramtype)) { $this->value = preg_replace($this->settingdata->paramtype, '', $this->value); // Standard moodle param type. } else { $this->value = clean_param($this->value, constant($paramtype)); } $this->set_visiblevalue(); return true; } } ldap/classes/admin_setting_special_contexts_configtext.php 0000644 00000003467 15152311435 0020352 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Special setting for auth_ldap that cleans up context values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special setting for auth_ldap that cleans up context values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_ldap_admin_setting_special_contexts_configtext extends admin_setting_configtext { /** * We need to remove duplicates on save to prevent issues in other areas of Moodle. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { // Try to remove duplicates before storing the contexts (to avoid problems in sync_users()). $data = explode(';', $data); $data = array_map(function($x) { return core_text::strtolower(trim($x)); }, $data); $data = implode(';', array_unique($data)); return parent::write_setting($data); } } ldap/classes/admin_setting_special_ntlm_configtext.php 0000644 00000003372 15152311435 0017450 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Special admin setting for auth_ldap that validates ntlm usernames. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special admin setting for auth_ldap that validates ntlm usernames. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_ldap_admin_setting_special_ntlm_configtext extends admin_setting_configtext { /** * We need to validate the username format when using NTLM. * * @param string $data Form data. * @return string Empty when no errors. */ public function validate($data) { if (get_config('auth_ldap', 'ntlmsso_type') === 'ntlm') { $format = trim($data); if (!empty($format) && !preg_match('/%username%/i', $format)) { return get_string('auth_ntlmsso_missing_username', 'auth_ldap'); } } return parent::validate($data); } } ldap/classes/task/sync_roles.php 0000644 00000003747 15152311435 0012767 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A scheduled task for LDAP roles sync. * * @package auth_ldap * @author David Balch <david.balch@conted.ox.ac.uk> * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; defined('MOODLE_INTERNAL') || die(); /** * A scheduled task class for LDAP roles sync. * * @author David Balch <david.balch@conted.ox.ac.uk> * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_roles extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('syncroles', 'auth_ldap'); } /** * Synchronise role assignments from LDAP. */ public function execute() { global $DB; if (is_enabled_auth('ldap')) { $auth = get_auth_plugin('ldap'); $users = $DB->get_records('user', array('auth' => 'ldap')); foreach ($users as $user) { $auth->sync_roles($user); } } } } ldap/classes/task/sync_task.php 0000644 00000003065 15152311435 0012576 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A scheduled task for LDAP user sync. * * @package auth_ldap * @copyright 2015 Vadim Dvorovenko <Vadimon@mail.ru> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; /** * A scheduled task class for LDAP user sync. * * @copyright 2015 Vadim Dvorovenko <Vadimon@mail.ru> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_task extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('synctask', 'auth_ldap'); } /** * Run users sync. */ public function execute() { global $CFG; if (is_enabled_auth('ldap')) { $auth = get_auth_plugin('ldap'); $auth->sync_users(true); } } } ldap/classes/admin_setting_special_lowercase_configtext.php 0000644 00000002773 15152311435 0020466 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Special setting for auth_ldap that lowercases values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special setting for auth_ldap that lowercases values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_ldap_admin_setting_special_lowercase_configtext extends admin_setting_configtext { /** * We need to convert the data to lowercase prior to save. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { return parent::write_setting(core_text::strtolower($data)); } } ldap/lang/en/auth_ldap.php 0000644 00000040437 15152311435 0011471 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_ldap', language 'en'. * * @package auth_ldap * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_ldap_ad_create_req'] = 'Cannot create the new account in Active Directory. Make sure you meet all the requirements for this to work (LDAPS connection, bind user with adequate rights, etc.)'; $string['auth_ldap_attrcreators'] = 'List of groups or contexts whose members are allowed to create attributes. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\''; $string['auth_ldap_attrcreators_key'] = 'Attribute creators'; $string['auth_ldap_auth_user_create_key'] = 'Create users externally'; $string['auth_ldap_bind_dn'] = 'If you want to use bind-user to search users, specify it here. Something like \'cn=ldapuser,ou=public,o=org\''; $string['auth_ldap_bind_dn_key'] = 'Distinguished name'; $string['auth_ldap_bind_pw'] = 'Password for bind-user.'; $string['auth_ldap_bind_pw_key'] = 'Password'; $string['auth_ldap_bind_settings'] = 'Bind settings'; $string['auth_ldap_contexts'] = 'List of contexts where users are located. Separate different contexts with \';\'. For example: \'ou=users,o=org; ou=others,o=org\''; $string['auth_ldap_contexts_key'] = 'Contexts'; $string['auth_ldap_create_context'] = 'If you enable user creation with email confirmation, specify the context where users are created. This context should be different from other users to prevent security issues. You don\'t need to add this context to ldap_context-variable, Moodle will search for users from this context automatically.<br /><b>Note!</b> You have to modify the method user_create() in file auth/ldap/auth.php to make user creation work'; $string['auth_ldap_create_context_key'] = 'Context for new users'; $string['auth_ldap_create_error'] = 'Error creating user in LDAP.'; $string['auth_ldapdescription'] = 'This method provides authentication against an external LDAP server. If the given username and password are valid, Moodle creates a new user entry in its database. This plugin can read user attributes from LDAP and prefill wanted fields in Moodle. For following logins only the username and password are checked.'; $string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiry time directly from the LDAP server.'; $string['auth_ldap_expiration_key'] = 'Expiry'; $string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiry warning is issued.'; $string['auth_ldap_expiration_warning_key'] = 'Expiry warning'; $string['auth_ldap_expireattr_desc'] = 'Optional: Overrides the LDAP attribute that stores password expiry time.'; $string['auth_ldap_expireattr_key'] = 'Expiry attribute'; $string['auth_ldapextrafields'] = 'These fields are optional. You can choose to pre-fill some Moodle user fields with information from the <b>LDAP fields</b> that you specify here. <p>If you leave these fields blank, then nothing will be transferred from LDAP and Moodle defaults will be used instead.</p><p>In either case, the user will be able to edit all of these fields after they log in.</p>'; $string['auth_ldap_graceattr_desc'] = 'Optional: Overrides grace login attribute'; $string['auth_ldap_gracelogin_key'] = 'Grace login attribute'; $string['auth_ldap_gracelogins_desc'] = 'Enable LDAP grace login support. After password has expired, user can log in until grace login count is 0. Enabling this setting displays grace login message if password has expired.'; $string['auth_ldap_gracelogins_key'] = 'Grace logins'; $string['auth_ldap_groupecreators'] = 'List of groups or contexts whose members are allowed to create groups. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\''; $string['auth_ldap_groupecreators_key'] = 'Group creators'; $string['auth_ldap_host_url'] = 'Specify LDAP host in URL-form like \'ldap://ldap.myorg.com/\' or \'ldaps://ldap.myorg.com/\'. Separate multiple servers with \';\' to get failover support.'; $string['auth_ldap_host_url_key'] = 'Host URL'; $string['auth_ldap_changepasswordurl_key'] = 'Password-change URL'; $string['auth_ldap_ldap_encoding'] = 'Encoding used by the LDAP server, most likely utf-8. If LDAP v2 is selected, Active Directory uses its configured encoding, such as cp1252 or cp1250.'; $string['auth_ldap_ldap_encoding_key'] = 'LDAP encoding'; $string['auth_ldap_login_settings'] = 'Login settings'; $string['auth_ldap_memberattribute'] = 'Optional: Overrides user member attribute, when users belongs to a group. Usually \'member\''; $string['auth_ldap_memberattribute_isdn'] = 'Overrides handling of member attribute values'; $string['auth_ldap_memberattribute_isdn_key'] = 'Member attribute uses dn'; $string['auth_ldap_memberattribute_key'] = 'Member attribute'; $string['auth_ldap_noconnect'] = 'LDAP-module cannot connect to server: {$a}'; $string['auth_ldap_noconnect_all'] = 'LDAP-module cannot connect to any servers: {$a}'; $string['auth_ldap_noextension'] = 'The PHP LDAP module does not seem to be present. Please ensure it is installed and enabled if you want to use this authentication plugin.'; $string['auth_ldap_no_mbstring'] = 'You need the mbstring extension to create users in Active Directory.'; $string['auth_ldapnotinstalled'] = 'Cannot use LDAP authentication. The PHP LDAP module is not installed.'; $string['auth_ldap_objectclass'] = 'Optional: Overrides objectClass used to name/search users on ldap_user_type. Usually you don\'t need to change this.'; $string['auth_ldap_objectclass_key'] = 'Object class'; $string['auth_ldap_opt_deref'] = 'Determines how aliases are handled during search. Select one of the following values: "No" (LDAP_DEREF_NEVER) or "Yes" (LDAP_DEREF_ALWAYS)'; $string['auth_ldap_opt_deref_key'] = 'Dereference aliases'; $string['auth_ldap_passtype'] = 'Specify the format of new or changed passwords in LDAP server.'; $string['auth_ldap_passtype_key'] = 'Password format'; $string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiry settings'; $string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.'; $string['auth_ldap_preventpassindb_key'] = 'Prevent password caching'; $string['auth_ldap_rolecontext'] = '{$a->localname} context'; $string['auth_ldap_rolecontext_help'] = 'LDAP context used to select for <i>{$a->localname}</i> mapping. Separate multiple groups with \';\'. Usually something like "cn={$a->shortname},ou=first-ou-with-role-groups,o=myorg; cn={$a->shortname},ou=second-ou-with-role-groups,o=myorg".'; $string['auth_ldap_search_sub'] = 'Search users from subcontexts.'; $string['auth_ldap_search_sub_key'] = 'Search subcontexts'; $string['auth_ldap_server_settings'] = 'LDAP server settings'; $string['auth_ldap_unsupportedusertype'] = 'auth: ldap user_create() does not support selected usertype: {$a}'; $string['auth_ldap_update_userinfo'] = 'Update user information (firstname, lastname, address..) from LDAP to Moodle. Specify "Data mapping" settings as you need.'; $string['auth_ldap_user_attribute'] = 'Optional: Overrides the attribute used to name/search users. Usually \'cn\'.'; $string['auth_ldap_user_attribute_key'] = 'User attribute'; $string['auth_ldap_suspended_attribute'] = 'Optional: When provided this attribute will be used to enable/suspend the locally created user account.'; $string['auth_ldap_suspended_attribute_key'] = 'Suspended attribute'; $string['auth_ldap_user_exists'] = 'LDAP username already exists.'; $string['auth_ldap_user_settings'] = 'User lookup settings'; $string['auth_ldap_user_type'] = 'Select how users are stored in LDAP. This setting also specifies how login expiry, grace logins and user creation will work.'; $string['auth_ldap_user_type_key'] = 'User type'; $string['auth_ldap_usertypeundefined'] = 'config.user_type not defined or function ldap_expirationtime2unix does not support selected type!'; $string['auth_ldap_usertypeundefined2'] = 'config.user_type not defined or function ldap_unixi2expirationtime does not support selected type!'; $string['auth_ldap_version'] = 'The version of the LDAP protocol your server is using.'; $string['auth_ldap_version_key'] = 'Version'; $string['auth_ntlmsso'] = 'NTLM SSO'; $string['auth_ntlmsso_enabled'] = 'Set to yes to attempt Single Sign On with the NTLM domain. Note that this requires additional setup on the server to work. For further details, see the documentation <a href="https://docs.moodle.org/en/NTLM_authentication">NTLM authentication</a>.'; $string['auth_ntlmsso_enabled_key'] = 'Enable'; $string['auth_ntlmsso_ie_fastpath'] = 'Set to enable the NTLM SSO fast path (bypasses certain steps if the client\'s browser is MS Internet Explorer).'; $string['auth_ntlmsso_ie_fastpath_key'] = 'MS IE fast path?'; $string['auth_ntlmsso_ie_fastpath_yesform'] = 'Yes, all other browsers use standard login form'; $string['auth_ntlmsso_ie_fastpath_yesattempt'] = 'Yes, attempt NTLM other browsers'; $string['auth_ntlmsso_ie_fastpath_attempt'] = 'Attempt NTLM with all browsers'; $string['auth_ntlmsso_maybeinvalidformat'] = 'Unable to extract the username from the REMOTE_USER header. Is the configured format right?'; $string['auth_ntlmsso_missing_username'] = 'You need to specify at least %username% in the remote username format'; $string['auth_ntlmsso_remoteuserformat_key'] = 'Remote username format'; $string['auth_ntlmsso_remoteuserformat'] = 'If you have chosen \'NTLM\' in \'Authentication type\', you can specify the remote username format here. If you leave this empty, the default DOMAIN\\username format will be used. You can use the optional <b>%domain%</b> placeholder to specify where the domain name appears, and the mandatory <b>%username%</b> placeholder to specify where the username appears. <br /><br />Some widely used formats are <tt>%domain%\\%username%</tt> (MS Windows default), <tt>%domain%/%username%</tt>, <tt>%domain%+%username%</tt> and just <tt>%username%</tt> (if there is no domain part).'; $string['auth_ntlmsso_subnet'] = 'If set, it will only attempt SSO with clients in this subnet. Format: xxx.xxx.xxx.xxx/bitmask. Separate multiple subnets with \',\' (comma).'; $string['auth_ntlmsso_subnet_key'] = 'Subnet'; $string['auth_ntlmsso_type_key'] = 'Authentication type'; $string['auth_ntlmsso_type'] = 'The authentication method configured in the web server to authenticate the users (if in doubt, choose NTLM)'; $string['cannotmaprole'] = 'The role "{$a->rolename}" cannot be mapped because its short name "{$a->shortname}" is too long and/or contains hyphens. To allow it to be mapped, the short name needs to be reduced to a maximum of {$a->charlimit} characters and any hyphens removed. <a href="{$a->link}">Edit the role</a>'; $string['connectingldap'] = "Connecting to LDAP server...\n"; $string['connectingldapsuccess'] = "Connecting to your LDAP server was successful"; $string['creatingtemptable'] = "Creating temporary table {\$a}\n"; $string['didntfindexpiretime'] = 'password_expire() didn\'t find expiration time.'; $string['didntgetusersfromldap'] = "Did not get any users from LDAP -- error? -- exiting\n"; $string['gotcountrecordsfromldap'] = "Got {\$a} records from LDAP\n"; $string['invalidusererrors'] = "Warning: Skipped creation of {\$a} user accounts.\n\n"; $string['invaliduserexception'] = "\nError: Cannot create new user account. Details and reason:\n{\$a}\nSkipping this user.\n\n"; $string['ldapnotconfigured'] = 'The LDAP host url is currently not configured'; $string['morethanoneuser'] = 'More than one user record found in LDAP. Using only the first one.'; $string['needbcmath'] = 'You need the BCMath extension to use expired password checking with Active Directory.'; $string['needmbstring'] = 'You need the mbstring extension to change passwords in Active Directory'; $string['nodnforusername'] = 'Error in user_update_password(). No DN for: {$a->username}'; $string['noemail'] = 'Tried to send you an email but failed!'; $string['notcalledfromserver'] = 'Should not be called from the web server!'; $string['noupdatestobedone'] = "No updates to be done\n"; $string['nouserentriestoremove'] = "No user entries to be removed\n"; $string['nouserentriestorevive'] = "No user entries to be revived\n"; $string['nouserstobeadded'] = 'No user entries to be added'; $string['ntlmsso_attempting'] = 'Attempting Single Sign On via NTLM...'; $string['ntlmsso_failed'] = 'Auto-login failed, try the normal login page...'; $string['ntlmsso_isdisabled'] = 'NTLM SSO is disabled.'; $string['ntlmsso_unknowntype'] = 'Unknown ntlmsso type!'; $string['pagedresultsnotsupp'] = 'LDAP paged results not supported (either your PHP version lacks support, you have configured Moodle to use LDAP protocol version 2 or Moodle cannot contact your LDAP server to see if paged support is available.)'; $string['pagesize'] = 'Make sure this value is smaller than your LDAP server result set size limit (the maximum number of entries that can be returned in a single query)'; $string['pagesize_key'] = 'Page size'; $string['pluginname'] = 'LDAP server'; $string['pluginnotenabled'] = 'Plugin not enabled!'; $string['renamingnotallowed'] = 'User renaming not allowed in LDAP'; $string['rootdseerror'] = 'Error querying rootDSE for Active Directory'; $string['syncroles'] = 'Synchronise system roles from LDAP'; $string['synctask'] = 'LDAP users sync job'; $string['systemrolemapping'] = 'System role mapping'; $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'; $string['start_tls_key'] = 'Use TLS'; $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\''; $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\''; $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}'; $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}'; $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expiry time and/or grace logins. Error code: {$a->errno}; Error string: {$a->errstring}'; $string['updateusernotfound'] = 'Could not find user while updating externally. Details follow: search base: \'{$a->userdn}\'; search filter: \'(objectClass=*)\'; search attributes: {$a->attribs}'; $string['user_activatenotsupportusertype'] = 'auth: ldap user_activate() does not support selected usertype: {$a}'; $string['user_disablenotsupportusertype'] = 'auth: ldap user_disable() does not support selected usertype: {$a}'; $string['userentriestoadd'] = "User entries to be added: {\$a}\n"; $string['userentriestoremove'] = "User entries to be removed: {\$a}\n"; $string['userentriestorevive'] = "User entries to be revived: {\$a}\n"; $string['userentriestoupdate'] = "User entries to be updated: {\$a}\n"; $string['usernotfound'] = 'User not found in LDAP'; $string['useracctctrlerror'] = 'Error getting userAccountControl for {$a}'; $string['diag_genericerror'] = 'LDAP error {$a->code} reading {$a->subject}: {$a->message}.'; $string['diag_toooldversion'] = 'It is very unlikely a modern LDAP server uses LDAPv2 protocol. Wrong settings can corrupt values in user fields. Check with your LDAP administrator.'; $string['diag_emptycontext'] = 'Empty context found.'; $string['diag_contextnotfound'] = 'Context {$a} doesn\'t exist or can\'t be read by bind DN.'; $string['diag_rolegroupnotfound'] = 'Group {$a->group} for role {$a->localname} doesn\'t exist or can\'t be read by bind DN.'; $string['privacy:metadata'] = 'The LDAP server authentication plugin does not store any personal data.'; email/db/upgrade.php 0000644 00000002674 15152311435 0010371 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * No authentication plugin upgrade code * * @package auth_email * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Function to upgrade auth_email. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_email_upgrade($oldversion) { global $CFG, $DB; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } email/db/services.php 0000644 00000003054 15152311435 0010556 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Auth email webservice definitions. * * @package auth_email * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $functions = array( 'auth_email_get_signup_settings' => array( 'classname' => 'auth_email_external', 'methodname' => 'get_signup_settings', 'description' => 'Get the signup required settings and profile fields.', 'type' => 'read', 'ajax' => true, 'loginrequired' => false, ), 'auth_email_signup_user' => array( 'classname' => 'auth_email_external', 'methodname' => 'signup_user', 'description' => 'Adds a new user (pendingto be confirmed) in the site.', 'type' => 'write', 'ajax' => true, 'loginrequired' => false, ), ); email/tests/behat/behat_auth_email.php 0000644 00000003444 15152311435 0014051 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Step definition for auth_email * * @package auth_email * @category test * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); /** * Step definition for auth_email. * * @package auth_email * @category test * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_auth_email extends behat_base { /** * Emulate clicking on confirmation link from the email * * @When /^I confirm email for "(?P<username>(?:[^"]|\\")*)"$/ * * @param string $username */ public function i_confirm_email_for($username) { global $DB; $secret = $DB->get_field('user', 'secret', ['username' => $username], MUST_EXIST); $confirmationurl = new moodle_url('/login/confirm.php'); $confirmationpath = $confirmationurl->out_as_local_url(false); $url = $confirmationpath . '?' . 'data='. $secret .'/'. $username; $this->execute('behat_general::i_visit', [$url]); } } email/tests/behat/signup.feature 0000644 00000011073 15152311435 0012744 0 ustar 00 @auth @auth_email Feature: User must accept policy when logging in and signing up In order to record user agreement to use the site As a user I need to be able to accept site policy during sign up Scenario: Accept policy on sign up, no site policy Given the following config values are set as admin: | registerauth | email | | passwordpolicy | 0 | And I am on site homepage And I follow "Log in" When I click on "Create new account" "link" Then I should not see "I understand and agree" And I set the following fields to these values: | Username | user1 | | Password | user1 | | Email address | user1@address.invalid | | Email (again) | user1@address.invalid | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" And I should see "An email should have been sent to your address at user1@address.invalid" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" And I open my profile in edit mode And the field "First name" matches value "User1" And I log out # Confirm that user can login and browse the site (edit their profile). And I log in as "user1" And I open my profile in edit mode And the field "First name" matches value "User1" Scenario: Accept policy on sign up, with site policy Given the following config values are set as admin: | registerauth | email | | passwordpolicy | 0 | | sitepolicy | https://moodle.org | And I am on site homepage And I follow "Log in" When I click on "Create new account" "link" Then the field "I understand and agree" matches value "0" And I set the following fields to these values: | Username | user1 | | Password | user1 | | Email address | user1@address.invalid | | Email (again) | user1@address.invalid | | First name | User1 | | Last name | L1 | | I understand and agree | 1 | And I press "Create my new account" And I should see "Confirm your account" And I should see "An email should have been sent to your address at user1@address.invalid" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" And I open my profile in edit mode And the field "First name" matches value "User1" And I log out # Confirm that user is not asked to agree to site policy again after the next login. And I log in as "user1" And I open my profile in edit mode And the field "First name" matches value "User1" Scenario Outline: Email validation during email registration Given the following config values are set as admin: | allowaccountssameemail | <allowsameemail> | | registerauth | email | | passwordpolicy | 0 | And the following "users" exist: | username | firstname | lastname | email | | s1 | John | Doe | s1@example.com | And I am on site homepage And I follow "Log in" When I click on "Create new account" "link" And I set the following fields to these values: | Username | s2 | | Password | test | | Email address | <email1> | | Email (again) | <email2> | | First name | Jane | | Last name | Doe | And I press "Create my new account" Then I should <expect> "This email address is already registered. Perhaps you created an account in the past?" And I should <expect2> "Invalid email address" Examples: | allowsameemail | email1 | email2 | expect | expect2 | | 0 | s1@example.com | s1@example.com | see | not see | | 0 | S1@EXAMPLE.COM | S1@EXAMPLE.COM | see | not see | | 0 | s1@example.com | S1@EXAMPLE.COM | see | not see | | 0 | s2@example.com | s1@example.com | not see | see | | 1 | s1@example.com | s1@example.com | not see | not see | | 1 | S1@EXAMPLE.COM | S1@EXAMPLE.COM | not see | not see | | 1 | s1@example.com | S1@EXAMPLE.COM | not see | not see | | 1 | s1@example.com | s2@example.com | not see | see | email/tests/external/external_test.php 0000644 00000021713 15152311435 0014215 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Auth email external functions tests. * * @package auth_email * @category external * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ namespace auth_email\external; use auth_email_external; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External auth email API tests. * * @package auth_email * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ class external_test extends externallib_advanced_testcase { /** * Set up for every test */ public function setUp(): void { global $CFG; $this->resetAfterTest(true); $CFG->registerauth = 'email'; $this->field1 = $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'frogname', 'name' => 'Name of frog', 'datatype' => 'text', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 1))->id; $this->field2 = $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'sometext', 'name' => 'Some text in textarea', 'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2))->id; } public function test_get_signup_settings() { global $CFG; $CFG->defaultcity = 'Bcn'; $CFG->country = 'ES'; $CFG->sitepolicy = 'https://moodle.org'; $result = auth_email_external::get_signup_settings(); $result = \external_api::clean_returnvalue(auth_email_external::get_signup_settings_returns(), $result); // Check expected data. $this->assertEquals(array('firstname', 'lastname'), $result['namefields']); $this->assertEquals($CFG->defaultcity, $result['defaultcity']); $this->assertEquals($CFG->country, $result['country']); $this->assertEquals($CFG->sitepolicy, $result['sitepolicy']); $this->assertEquals(print_password_policy(), $result['passwordpolicy']); $this->assertNotContains('recaptchachallengehash', $result); $this->assertNotContains('recaptchachallengeimage', $result); // Whip up a array with named entries to easily check against. $namedarray = array(); foreach ($result['profilefields'] as $key => $value) { $namedarray[$value['shortname']] = array( 'datatype' => $value['datatype'] ); } // Just check if we have the fields from this test. If a plugin adds fields we'll let it slide. $this->assertArrayHasKey('frogname', $namedarray); $this->assertArrayHasKey('sometext', $namedarray); $this->assertEquals('text', $namedarray['frogname']['datatype']); $this->assertEquals('textarea', $namedarray['sometext']['datatype']); } /** * Test get_signup_settings with mathjax in a profile field. */ public function test_get_signup_settings_with_mathjax_in_profile_fields() { global $CFG, $DB; require_once($CFG->dirroot . '/lib/externallib.php'); // Enable MathJax filter in content and headings. $this->configure_filters([ ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true], ]); // Create category with MathJax and a new field with MathJax. $categoryname = 'Cat $$(a+b)=2$$'; $fieldname = 'Some text $$(a+b)=2$$'; $categoryid = $this->getDataGenerator()->create_custom_profile_field_category(['name' => $categoryname])->id; $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'mathjaxname', 'name' => $fieldname, 'categoryid' => $categoryid, 'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2)); $result = auth_email_external::get_signup_settings(); $result = \external_api::clean_returnvalue(auth_email_external::get_signup_settings_returns(), $result); // Format the original data. $sitecontext = \context_system::instance(); $categoryname = external_format_string($categoryname, $sitecontext->id); $fieldname = external_format_string($fieldname, $sitecontext->id); // Whip up a array with named entries to easily check against. $namedarray = array(); foreach ($result['profilefields'] as $key => $value) { $namedarray[$value['shortname']] = $value; } // Check the new profile field. $this->assertArrayHasKey('mathjaxname', $namedarray); $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $namedarray['mathjaxname']['categoryname']); $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $namedarray['mathjaxname']['name']); $this->assertEquals($categoryname, $namedarray['mathjaxname']['categoryname']); $this->assertEquals($fieldname, $namedarray['mathjaxname']['name']); } public function test_signup_user() { global $DB; $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; $city = 'Bcn'; $country = 'ES'; $customprofilefields = array( array( 'type' => 'text', 'name' => 'profile_field_frogname', 'value' => 'random text', ), array( 'type' => 'textarea', 'name' => 'profile_field_sometext', 'value' => json_encode( array( 'text' => 'blah blah', 'format' => FORMAT_HTML ) ), ) ); // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email, $city, $country, '', '', $customprofilefields); $result = \external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $user = $DB->get_record('user', array('username' => $username)); $this->assertEquals($firstname, $user->firstname); $this->assertEquals($lastname, $user->lastname); $this->assertEquals($email, $user->email); $this->assertEquals($city, $user->city); $this->assertEquals($country, $user->country); $this->assertEquals(0, $user->confirmed); $this->assertEquals(current_language(), $user->lang); $this->assertEquals('email', $user->auth); $infofield = $DB->get_record('user_info_data', array('userid' => $user->id, 'fieldid' => $this->field1)); $this->assertEquals($customprofilefields[0]['value'], $infofield->data); $infofield = $DB->get_record('user_info_data', array('userid' => $user->id, 'fieldid' => $this->field2)); $this->assertEquals(json_decode($customprofilefields[1]['value'])->text, $infofield->data); // Try to create a user with the same username, email and password. We ommit also the profile fields. $password = 'abc'; $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email, $city, $country, '', '', $customprofilefields); $result = \external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertFalse($result['success']); $this->assertCount(3, $result['warnings']); $expectederrors = array('username', 'email', 'password'); $finalerrors = []; foreach ($result['warnings'] as $warning) { $finalerrors[] = $warning['item']; } $this->assertEquals($expectederrors, $finalerrors); // Do not pass the required profile fields. $this->expectException('invalid_parameter_exception'); $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email, $city, $country); } } email/auth.php 0000644 00000017301 15152311435 0007307 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: Email Authentication * * @author Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package auth_email */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Email authentication plugin. */ class auth_plugin_email extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'email'; $this->config = get_config('auth_email'); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_email() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ function user_login ($username, $password) { global $CFG, $DB; if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) { return validate_internal_user_password($user, $password); } return false; } /** * Updates the user's password. * * called when the user password is updated. * * @param object $user User table object (with system magic quotes) * @param string $newpassword Plaintext password (with system magic quotes) * @return boolean result * */ function user_update_password($user, $newpassword) { $user = get_complete_user_data('id', $user->id); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). return update_internal_user_password($user, $newpassword); } function can_signup() { return true; } /** * Sign up a new user ready for confirmation. * Password is passed in plaintext. * * @param object $user new user object * @param boolean $notify print notice with link and terminate */ function user_signup($user, $notify=true) { // Standard signup, without custom confirmatinurl. return $this->user_signup_with_confirmation($user, $notify); } /** * Sign up a new user ready for confirmation. * * Password is passed in plaintext. * A custom confirmationurl could be used. * * @param object $user new user object * @param boolean $notify print notice with link and terminate * @param string $confirmationurl user confirmation URL * @return boolean true if everything well ok and $notify is set to true * @throws moodle_exception * @since Moodle 3.2 */ public function user_signup_with_confirmation($user, $notify=true, $confirmationurl = null) { global $CFG, $DB, $SESSION; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); $plainpassword = $user->password; $user->password = hash_internal_user_password($user->password); if (empty($user->calendartype)) { $user->calendartype = $CFG->calendartype; } $user->id = user_create_user($user, false, false); user_add_password_history($user->id, $plainpassword); // Save any custom profile field information. profile_save_data($user); // Save wantsurl against user's profile, so we can return them there upon confirmation. if (!empty($SESSION->wantsurl)) { set_user_preference('auth_email_wantsurl', $SESSION->wantsurl, $user); } // Trigger event. \core\event\user_created::create_from_userid($user->id)->trigger(); if (! send_confirmation_email($user, $confirmationurl)) { throw new \moodle_exception('auth_emailnoemail', 'auth_email'); } if ($notify) { global $CFG, $PAGE, $OUTPUT; $emailconfirm = get_string('emailconfirm'); $PAGE->navbar->add($emailconfirm); $PAGE->set_title($emailconfirm); $PAGE->set_heading($PAGE->course->fullname); echo $OUTPUT->header(); notice(get_string('emailconfirmsent', '', $user->email), "$CFG->wwwroot/index.php"); } else { return true; } } /** * Returns true if plugin allows confirming of new users. * * @return bool */ function can_confirm() { return true; } /** * Confirm the new user as registered. * * @param string $username * @param string $confirmsecret */ function user_confirm($username, $confirmsecret) { global $DB, $SESSION; $user = get_complete_user_data('username', $username); if (!empty($user)) { if ($user->auth != $this->authtype) { return AUTH_CONFIRM_ERROR; } else if ($user->secret === $confirmsecret && $user->confirmed) { return AUTH_CONFIRM_ALREADY; } else if ($user->secret === $confirmsecret) { // They have provided the secret key to get in $DB->set_field("user", "confirmed", 1, array("id"=>$user->id)); if ($wantsurl = get_user_preferences('auth_email_wantsurl', false, $user)) { // Ensure user gets returned to page they were trying to access before signing up. $SESSION->wantsurl = $wantsurl; unset_user_preference('auth_email_wantsurl', $user); } return AUTH_CONFIRM_OK; } } else { return AUTH_CONFIRM_ERROR; } } function prevent_local_passwords() { return false; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return true; } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { return null; // use default internal method } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return true; } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } /** * Returns whether or not the captcha element is enabled. * @return bool */ function is_captcha_enabled() { return get_config("auth_{$this->authtype}", 'recaptcha'); } } email/settings.php 0000644 00000003205 15152311435 0010204 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin settings and defaults. * * @package auth_email * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // Introductory explanation. $settings->add(new admin_setting_heading('auth_email/pluginname', '', new lang_string('auth_emaildescription', 'auth_email'))); $options = array( new lang_string('no'), new lang_string('yes'), ); $settings->add(new admin_setting_configselect('auth_email/recaptcha', new lang_string('auth_emailrecaptcha_key', 'auth_email'), new lang_string('auth_emailrecaptcha', 'auth_email'), 0, $options)); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('email'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); } email/upgrade.txt 0000644 00000000476 15152311435 0010032 0 ustar 00 This files describes API changes in /auth/email/*, information provided here is intended especially for developers. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/email' to 'auth_email'. email/version.php 0000644 00000002230 15152311435 0010026 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version details * * @package auth_email * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_email'; // Full name of the plugin (used for diagnostics) email/classes/privacy/provider.php 0000644 00000002751 15152311435 0013315 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_email. * * @package auth_email * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_email\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_email implementing null_provider. * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } email/classes/external.php 0000644 00000042077 15152311435 0011635 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Auth e-mail external API * * @package auth_email * @category external * @copyright 2016 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir . '/externallib.php'); require_once($CFG->libdir . '/authlib.php'); require_once($CFG->dirroot . '/user/editlib.php'); require_once($CFG->dirroot . '/user/profile/lib.php'); /** * Auth e-mail external functions * * @package auth_email * @category external * @copyright 2016 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ class auth_email_external extends external_api { /** * Check if registration is enabled in this site. * * @throws moodle_exception * @since Moodle 3.2 */ protected static function check_signup_enabled() { global $CFG; if (empty($CFG->registerauth) or $CFG->registerauth != 'email') { throw new moodle_exception('registrationdisabled', 'error'); } } /** * Describes the parameters for get_signup_settings. * * @return external_function_parameters * @since Moodle 3.2 */ public static function get_signup_settings_parameters() { return new external_function_parameters(array()); } /** * Get the signup required settings and profile fields. * * @return array settings and possible warnings * @since Moodle 3.2 * @throws moodle_exception */ public static function get_signup_settings() { global $CFG, $PAGE; $context = context_system::instance(); // We need this to make work the format text functions. $PAGE->set_context($context); self::check_signup_enabled(); $result = array(); $result['namefields'] = useredit_get_required_name_fields(); if (!empty($CFG->passwordpolicy)) { $result['passwordpolicy'] = print_password_policy(); } $manager = new \core_privacy\local\sitepolicy\manager(); if ($sitepolicy = $manager->get_embed_url()) { $result['sitepolicy'] = $sitepolicy->out(false); } if (!empty($CFG->sitepolicyhandler)) { $result['sitepolicyhandler'] = $CFG->sitepolicyhandler; } if (!empty($CFG->defaultcity)) { $result['defaultcity'] = $CFG->defaultcity; } if (!empty($CFG->country)) { $result['country'] = $CFG->country; } if ($fields = profile_get_signup_fields()) { $result['profilefields'] = array(); foreach ($fields as $field) { $fielddata = $field->object->get_field_config_for_external(); $fielddata['categoryname'] = external_format_string($field->categoryname, $context->id); $fielddata['name'] = external_format_string($fielddata['name'], $context->id); list($fielddata['defaultdata'], $fielddata['defaultdataformat']) = external_format_text($fielddata['defaultdata'], $fielddata['defaultdataformat'], $context->id); $result['profilefields'][] = $fielddata; } } if (signup_captcha_enabled()) { // With reCAPTCHA v2 the captcha will be rendered by the mobile client using just the publickey. $result['recaptchapublickey'] = $CFG->recaptchapublickey; } $result['warnings'] = array(); return $result; } /** * Describes the get_signup_settings return value. * * @return external_single_structure * @since Moodle 3.2 */ public static function get_signup_settings_returns() { return new external_single_structure( array( 'namefields' => new external_multiple_structure( new external_value(PARAM_NOTAGS, 'The order of the name fields') ), 'passwordpolicy' => new external_value(PARAM_RAW, 'Password policy', VALUE_OPTIONAL), 'sitepolicy' => new external_value(PARAM_RAW, 'Site policy', VALUE_OPTIONAL), 'sitepolicyhandler' => new external_value(PARAM_PLUGIN, 'Site policy handler', VALUE_OPTIONAL), 'defaultcity' => new external_value(PARAM_NOTAGS, 'Default city', VALUE_OPTIONAL), 'country' => new external_value(PARAM_ALPHA, 'Default country', VALUE_OPTIONAL), 'profilefields' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Profile field id', VALUE_OPTIONAL), 'shortname' => new external_value(PARAM_ALPHANUMEXT, 'Profile field shortname', VALUE_OPTIONAL), 'name' => new external_value(PARAM_RAW, 'Profield field name', VALUE_OPTIONAL), 'datatype' => new external_value(PARAM_ALPHANUMEXT, 'Profield field datatype', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'Profield field description', VALUE_OPTIONAL), 'descriptionformat' => new external_format_value('description'), 'categoryid' => new external_value(PARAM_INT, 'Profield field category id', VALUE_OPTIONAL), 'categoryname' => new external_value(PARAM_RAW, 'Profield field category name', VALUE_OPTIONAL), 'sortorder' => new external_value(PARAM_INT, 'Profield field sort order', VALUE_OPTIONAL), 'required' => new external_value(PARAM_INT, 'Profield field required', VALUE_OPTIONAL), 'locked' => new external_value(PARAM_INT, 'Profield field locked', VALUE_OPTIONAL), 'visible' => new external_value(PARAM_INT, 'Profield field visible', VALUE_OPTIONAL), 'forceunique' => new external_value(PARAM_INT, 'Profield field unique', VALUE_OPTIONAL), 'signup' => new external_value(PARAM_INT, 'Profield field in signup form', VALUE_OPTIONAL), 'defaultdata' => new external_value(PARAM_RAW, 'Profield field default data', VALUE_OPTIONAL), 'defaultdataformat' => new external_format_value('defaultdata'), 'param1' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), 'param2' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), 'param3' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), 'param4' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), 'param5' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), ) ), 'Required profile fields', VALUE_OPTIONAL ), 'recaptchapublickey' => new external_value(PARAM_RAW, 'Recaptcha public key', VALUE_OPTIONAL), 'recaptchachallengehash' => new external_value(PARAM_RAW, 'Recaptcha challenge hash', VALUE_OPTIONAL), 'recaptchachallengeimage' => new external_value(PARAM_URL, 'Recaptcha challenge noscript image', VALUE_OPTIONAL), 'recaptchachallengejs' => new external_value(PARAM_URL, 'Recaptcha challenge js url', VALUE_OPTIONAL), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for signup_user. * * @return external_function_parameters * @since Moodle 3.2 */ public static function signup_user_parameters() { return new external_function_parameters( array( 'username' => new external_value(core_user::get_property_type('username'), 'Username'), 'password' => new external_value(core_user::get_property_type('password'), 'Plain text password'), 'firstname' => new external_value(core_user::get_property_type('firstname'), 'The first name(s) of the user'), 'lastname' => new external_value(core_user::get_property_type('lastname'), 'The family name of the user'), 'email' => new external_value(core_user::get_property_type('email'), 'A valid and unique email address'), 'city' => new external_value(core_user::get_property_type('city'), 'Home city of the user', VALUE_DEFAULT, ''), 'country' => new external_value(core_user::get_property_type('country'), 'Home country code', VALUE_DEFAULT, ''), 'recaptchachallengehash' => new external_value(PARAM_RAW, 'Recaptcha challenge hash', VALUE_DEFAULT, ''), 'recaptcharesponse' => new external_value(PARAM_NOTAGS, 'Recaptcha response', VALUE_DEFAULT, ''), 'customprofilefields' => new external_multiple_structure( new external_single_structure( array( 'type' => new external_value(PARAM_ALPHANUMEXT, 'The type of the custom field'), 'name' => new external_value(PARAM_ALPHANUMEXT, 'The name of the custom field'), 'value' => new external_value(PARAM_RAW, 'Custom field value, can be an encoded json if required') ) ), 'User custom fields (also known as user profile fields)', VALUE_DEFAULT, array() ), 'redirect' => new external_value(PARAM_LOCALURL, 'Redirect the user to this site url after confirmation.', VALUE_DEFAULT, ''), ) ); } /** * Get the signup required settings and profile fields. * * @param string $username username * @param string $password plain text password * @param string $firstname the first name(s) of the user * @param string $lastname the family name of the user * @param string $email a valid and unique email address * @param string $city home city of the user * @param string $country home country code * @param string $recaptchachallengehash recaptcha challenge hash * @param string $recaptcharesponse recaptcha response * @param array $customprofilefields user custom fields (also known as user profile fields) * @param string $redirect Site url to redirect the user after confirmation * @return array settings and possible warnings * @since Moodle 3.2 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function signup_user($username, $password, $firstname, $lastname, $email, $city = '', $country = '', $recaptchachallengehash = '', $recaptcharesponse = '', $customprofilefields = array(), $redirect = '') { global $CFG, $PAGE; $warnings = array(); $params = self::validate_parameters( self::signup_user_parameters(), array( 'username' => $username, 'password' => $password, 'firstname' => $firstname, 'lastname' => $lastname, 'email' => $email, 'city' => $city, 'country' => $country, 'recaptchachallengehash' => $recaptchachallengehash, 'recaptcharesponse' => $recaptcharesponse, 'customprofilefields' => $customprofilefields, 'redirect' => $redirect, ) ); // We need this to make work the format text functions. $context = context_system::instance(); $PAGE->set_context($context); self::check_signup_enabled(); // Validate profile fields param types. $allowedfields = profile_get_signup_fields(); $fieldproperties = array(); $fieldsrequired = array(); foreach ($allowedfields as $field) { $fieldproperties[$field->object->inputname] = $field->object->get_field_properties(); if ($field->object->is_required()) { $fieldsrequired[$field->object->inputname] = true; } } foreach ($params['customprofilefields'] as $profilefield) { if (!array_key_exists($profilefield['name'], $fieldproperties)) { throw new invalid_parameter_exception('Invalid field' . $profilefield['name']); } list($type, $allownull) = $fieldproperties[$profilefield['name']]; validate_param($profilefield['value'], $type, $allownull); // Remove from the potential required list. if (isset($fieldsrequired[$profilefield['name']])) { unset($fieldsrequired[$profilefield['name']]); } } if (!empty($fieldsrequired)) { throw new invalid_parameter_exception('Missing required parameters: ' . implode(',', array_keys($fieldsrequired))); } // Validate the data sent. $data = $params; $data['email2'] = $data['email']; // Force policy agreed if a site policy is set. The client is responsible of implementing the interface check. $manager = new \core_privacy\local\sitepolicy\manager(); if ($manager->is_defined()) { $data['policyagreed'] = 1; } unset($data['recaptcharesponse']); unset($data['customprofilefields']); // Add profile fields data. foreach ($params['customprofilefields'] as $profilefield) { // First, check if the value is a json (some profile fields like text area uses an array for sending data). $datadecoded = json_decode($profilefield['value'], true); if (is_array($datadecoded) && (json_last_error() == JSON_ERROR_NONE)) { $data[$profilefield['name']] = $datadecoded; } else { $data[$profilefield['name']] = $profilefield['value']; } } $errors = signup_validate_data($data, array()); // Validate recaptcha. if (signup_captcha_enabled()) { require_once($CFG->libdir . '/recaptchalib_v2.php'); $response = recaptcha_check_response(RECAPTCHA_VERIFY_URL, $CFG->recaptchaprivatekey, getremoteaddr(), $params['recaptcharesponse']); if (!$response['isvalid']) { $errors['recaptcharesponse'] = $response['error']; } } if (!empty($errors)) { foreach ($errors as $itemname => $message) { $warnings[] = array( 'item' => $itemname, 'itemid' => 0, 'warningcode' => 'fielderror', 'message' => s($message) ); } $result = array( 'success' => false, 'warnings' => $warnings, ); } else { // Save the user. $user = signup_setup_new_user((object) $data); $authplugin = get_auth_plugin('email'); // Check if we should redirect the user once the user is confirmed. $confirmationurl = null; if (!empty($params['redirect'])) { // Pass via moodle_url to fix thinks like admin links. $redirect = new moodle_url($params['redirect']); $confirmationurl = new moodle_url('/login/confirm.php', array('redirect' => $redirect->out())); } $authplugin->user_signup_with_confirmation($user, false, $confirmationurl); $result = array( 'success' => true, 'warnings' => array(), ); } return $result; } /** * Describes the signup_user return value. * * @return external_single_structure * @since Moodle 3.2 */ public static function signup_user_returns() { return new external_single_structure( array( 'success' => new external_value(PARAM_BOOL, 'True if the user was created false otherwise'), 'warnings' => new external_warnings(), ) ); } } email/lang/en/auth_email.php 0000644 00000004104 15152311435 0011776 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_email', language 'en'. * * @package auth_email * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_emaildescription'] = '<p>Email-based self-registration enables a user to create their own account via a \'Create new account\' button on the login page. The user then receives an email containing a secure link to a page where they can confirm their account. Future logins just check the username and password against the stored values in the Moodle database.</p><p>Note: In addition to enabling the plugin, email-based self-registration must also be selected from the self registration drop-down menu on the \'Manage authentication\' page.</p>'; $string['auth_emailnoemail'] = 'Tried to send you an email but failed!'; $string['auth_emailrecaptcha'] = 'Adds a visual/audio confirmation form element to the sign-up page for email self-registering users. This protects your site against spammers and contributes to a worthwhile cause. See https://www.google.com/recaptcha for more details.'; $string['auth_emailrecaptcha_key'] = 'Enable reCAPTCHA element'; $string['auth_emailsettings'] = 'Settings'; $string['pluginname'] = 'Email-based self-registration'; $string['privacy:metadata'] = 'The Email-based self-registration authentication plugin does not store any personal data.'; mnet/db/upgrade.php 0000644 00000002716 15152311435 0010242 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Keeps track of upgrades to the auth_mnet plugin * * @package auth_mnet * @copyright 2010 David Mudrak <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Function to upgrade auth_mnet. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_mnet_upgrade($oldversion) { global $CFG; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } mnet/db/mnet.php 0000644 00000004340 15152311435 0007551 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the mnet services for the mnet authentication plugin * * @since Moodle 2.0 * @package auth_mnet * @copyright 2010 Penny Leach * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $publishes = array( 'sso_idp' => array( 'apiversion' => 1, 'classname' => 'auth_plugin_mnet', 'filename' => 'auth.php', 'methods' => array( 'user_authorise', 'keepalive_server', 'kill_children', 'refresh_log', 'fetch_user_image', 'fetch_theme_info', 'update_enrolments', ), ), 'sso_sp' => array( 'apiversion' => 1, 'classname' => 'auth_plugin_mnet', 'filename' => 'auth.php', 'methods' => array( 'keepalive_client', 'kill_child' ) ) ); $subscribes = array( 'sso_idp' => array( 'user_authorise' => 'auth/mnet/auth.php/user_authorise', 'keepalive_server' => 'auth/mnet/auth.php/keepalive_server', 'kill_children' => 'auth/mnet/auth.php/kill_children', 'refresh_log' => 'auth/mnet/auth.php/refresh_log', 'fetch_user_image' => 'auth/mnet/auth.php/fetch_user_image', 'fetch_theme_info' => 'auth/mnet/auth.php/fetch_theme_info', 'update_enrolments' => 'auth/mnet/auth.php/update_enrolments', ), 'sso_sp' => array( 'keepalive_client' => 'auth/mnet/auth.php/keepalive_client', 'kill_child' => 'auth/mnet/auth.php/kill_child', ), ); mnet/db/install.php 0000644 00000000105 15152311435 0010247 0 ustar 00 <?php function xmldb_auth_mnet_install() { global $CFG, $DB; } mnet/db/tasks.php 0000644 00000002207 15152311435 0007733 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Definition of chat scheduled tasks. * * @package auth_mnet * @copyright 2019 Simey Lameze <simey@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => '\auth_mnet\task\cron_task', 'blocking' => 0, 'minute' => '*/10', 'hour' => '*', 'day' => '*', 'month' => '*', 'dayofweek' => '*' ) ); mnet/jump.php 0000644 00000004576 15152311435 0007207 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: Moodle Network Authentication * Multiple host authentication support for Moodle Network. * * @package auth_mnet * @author Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ require_once __DIR__ . '/../../config.php'; // grab the GET params - wantsurl could be anything - take it // with PARAM_RAW $hostid = optional_param('hostid', '0', PARAM_INT); $hostwwwroot = optional_param('hostwwwroot', '', PARAM_URL); $wantsurl = optional_param('wantsurl', '', PARAM_RAW); $url = new moodle_url('/auth/mnet/jump.php'); if ($hostid !== '0') $url->param('hostid', $hostid); if ($hostwwwroot !== '') $url->param('hostwwwroot', $hostwwwroot); if ($wantsurl !== '') $url->param('wantsurl', $wantsurl); $PAGE->set_url($url); if (!isloggedin() or isguestuser()) { $SESSION->wantsurl = $PAGE->url->out(false); redirect(get_login_url()); } if (!is_enabled_auth('mnet')) { throw new \moodle_exception('mnetdisable'); } // If hostid hasn't been specified, try getting it using wwwroot if (!$hostid) { $hostwwwroot = trim($hostwwwroot); $hostwwwroot = rtrim($hostwwwroot, '/'); // ensure the wwwroot starts with a http or https prefix if (strtolower(substr($hostwwwroot, 0, 4)) != 'http') { $hostwwwroot = 'http://'.$hostwwwroot; } $hostid = $DB->get_field('mnet_host', 'id', array('wwwroot' => $hostwwwroot)); } // start the mnet session and redirect browser to remote URL $mnetauth = get_auth_plugin('mnet'); $url = $mnetauth->start_jump_session($hostid, $wantsurl); if (empty($url)) { throw new \moodle_exception('DEBUG: Jump session was not started correctly or blank URL returned.'); // TODO: errors. } redirect($url); mnet/tests/privacy/provider_test.php 0000644 00000030032 15152311435 0013726 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy test for the authentication mnet * * @package auth_mnet * @category test * @copyright 2018 Victor Deniz <victor@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_mnet\privacy; defined('MOODLE_INTERNAL') || die(); use auth_mnet\privacy\provider; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; use core_privacy\local\request\transform; use core_privacy\local\request\approved_userlist; /** * Privacy test for the authentication mnet * * @package auth_mnet * @category test * @copyright 2018 Victor Deniz <victor@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Set up method. */ public function setUp(): void { $this->resetAfterTest(); $this->setAdminUser(); } /** * Check that a user context is returned if there is any user data for this user. */ public function test_get_contexts_for_userid() { global $DB; $user = $this->getDataGenerator()->create_user(['auth' => 'mnet']); $this->assertEmpty(provider::get_contexts_for_userid($user->id)); // Insert mnet_log record. $logrecord = new \stdClass(); $logrecord->hostid = ''; $logrecord->remoteid = 65; $logrecord->time = time(); $logrecord->userid = $user->id; $DB->insert_record('mnet_log', $logrecord); $contextlist = provider::get_contexts_for_userid($user->id); // Check that we only get back one context. $this->assertCount(1, $contextlist); // Check that a context is returned is the expected. $usercontext = \context_user::instance($user->id); $this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]); } /** * Test that user data is exported correctly. */ public function test_export_user_data() { global $DB; $user = $this->getDataGenerator()->create_user(['auth' => 'mnet']); // Insert mnet_host record. $hostrecord = new \stdClass(); $hostrecord->wwwroot = 'https://external.moodle.com'; $hostrecord->name = 'External Moodle'; $hostrecord->public_key = '-----BEGIN CERTIFICATE-----'; $hostid = $DB->insert_record('mnet_host', $hostrecord); // Insert mnet_log record. $logrecord = new \stdClass(); $logrecord->hostid = $hostid; $logrecord->remoteid = 65; $logrecord->time = time(); $logrecord->userid = $user->id; $logrecord->course = 3; $logrecord->coursename = 'test course'; $DB->insert_record('mnet_log', $logrecord); $usercontext = \context_user::instance($user->id); $writer = writer::with_context($usercontext); $this->assertFalse($writer->has_any_data()); $approvedlist = new approved_contextlist($user, 'auth_mnet', [$usercontext->id]); provider::export_user_data($approvedlist); $data = (array)$writer->get_data([get_string('pluginname', 'auth_mnet'), $hostrecord->name, $logrecord->coursename]); $this->assertEquals($logrecord->remoteid, reset($data)->remoteid); $this->assertEquals(transform::datetime($logrecord->time), reset($data)->time); } /** * Test deleting all user data for a specific context. */ public function test_delete_data_for_all_users_in_context() { global $DB; $user1 = $this->getDataGenerator()->create_user(['auth' => 'mnet']); // Insert mnet_log record. $logrecord1 = new \stdClass(); $logrecord1->hostid = ''; $logrecord1->remoteid = 65; $logrecord1->time = time(); $logrecord1->userid = $user1->id; $DB->insert_record('mnet_log', $logrecord1); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(['auth' => 'mnet']); // Insert mnet_log record. $logrecord2 = new \stdClass(); $logrecord2->hostid = ''; $logrecord2->remoteid = 65; $logrecord2->time = time(); $logrecord2->userid = $user2->id; $DB->insert_record('mnet_log', $logrecord2); // Get all mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', array()); // There should be two. $this->assertCount(2, $mnetlogrecords); // Delete everything for the first user context. provider::delete_data_for_all_users_in_context($user1context); // Get all user1 mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', ['userid' => $user1->id]); $this->assertCount(0, $mnetlogrecords); // Get all mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', array()); // There should be one (user2). $this->assertCount(1, $mnetlogrecords); } /** * This should work identical to the above test. */ public function test_delete_data_for_user() { global $DB; $user1 = $this->getDataGenerator()->create_user(['auth' => 'mnet']); // Insert mnet_log record. $logrecord1 = new \stdClass(); $logrecord1->hostid = ''; $logrecord1->remoteid = 65; $logrecord1->time = time(); $logrecord1->userid = $user1->id; $DB->insert_record('mnet_log', $logrecord1); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(['auth' => 'mnet']); // Insert mnet_log record. $logrecord2 = new \stdClass(); $logrecord2->hostid = ''; $logrecord2->remoteid = 65; $logrecord2->time = time(); $logrecord2->userid = $user2->id; $DB->insert_record('mnet_log', $logrecord2); // Get all mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', array()); // There should be two. $this->assertCount(2, $mnetlogrecords); // Delete everything for the first user. $approvedlist = new approved_contextlist($user1, 'auth_mnet', [$user1context->id]); provider::delete_data_for_user($approvedlist); // Get all user1 mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', ['userid' => $user1->id]); $this->assertCount(0, $mnetlogrecords); // Get all mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', array()); // There should be one (user2). $this->assertCount(1, $mnetlogrecords); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context() { global $DB; $this->resetAfterTest(); $component = 'auth_mnet'; // Create a user. $user = $this->getDataGenerator()->create_user(['auth' => 'mnet']); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); // Insert mnet_log record. $logrecord = new \stdClass(); $logrecord->hostid = ''; $logrecord->remoteid = 65; $logrecord->time = time(); $logrecord->userid = $user->id; $DB->insert_record('mnet_log', $logrecord); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users() { global $DB; $this->resetAfterTest(); $component = 'auth_mnet'; // Create user1. $user1 = $this->getDataGenerator()->create_user(['auth' => 'mnet']); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(['auth' => 'mnet']); $usercontext2 = \context_user::instance($user2->id); // Insert mnet_log record. $logrecord1 = new \stdClass(); $logrecord1->hostid = ''; $logrecord1->remoteid = 65; $logrecord1->time = time(); $logrecord1->userid = $user1->id; $DB->insert_record('mnet_log', $logrecord1); // Insert mnet_log record. $logrecord2 = new \stdClass(); $logrecord2->hostid = ''; $logrecord2->remoteid = 65; $logrecord2->time = time(); $logrecord2->userid = $user2->id; $DB->insert_record('mnet_log', $logrecord2); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); } } mnet/auth.php 0000644 00000140077 15152311435 0007172 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: Moodle Network Authentication * Multiple host authentication support for Moodle Network. * * @package auth_mnet * @author Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Moodle Network authentication plugin. */ class auth_plugin_mnet extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'mnet'; $this->config = get_config('auth_mnet'); $this->mnet = get_mnet_environment(); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_mnet() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * This function is normally used to determine if the username and password * are correct for local logins. Always returns false, as local users do not * need to login over mnet xmlrpc. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ function user_login($username, $password) { return false; // Throw moodle_exception("mnetlocal"). } /** * Return user data for the provided token, compare with user_agent string. * * @param string $token The unique ID provided by remotehost. * @param string $useragent User Agent string. * @return array $userdata Array of user info for remote host */ function user_authorise($token, $useragent) { global $CFG, $SITE, $DB; $remoteclient = get_mnet_remote_client(); require_once $CFG->dirroot . '/mnet/xmlrpc/serverlib.php'; $mnet_session = $DB->get_record('mnet_session', array('token'=>$token, 'useragent'=>$useragent)); if (empty($mnet_session)) { throw new mnet_server_exception(1, 'authfail_nosessionexists'); } // check session confirm timeout if ($mnet_session->confirm_timeout < time()) { throw new mnet_server_exception(2, 'authfail_sessiontimedout'); } // session okay, try getting the user if (!$user = $DB->get_record('user', array('id'=>$mnet_session->userid))) { throw new mnet_server_exception(3, 'authfail_usermismatch'); } $userdata = mnet_strip_user((array)$user, mnet_fields_to_send($remoteclient)); // extra special ones $userdata['auth'] = 'mnet'; $userdata['wwwroot'] = $this->mnet->wwwroot; $userdata['session.gc_maxlifetime'] = ini_get('session.gc_maxlifetime'); if (array_key_exists('picture', $userdata) && !empty($user->picture)) { $fs = get_file_storage(); $usercontext = context_user::instance($user->id, MUST_EXIST); if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) { $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified(); $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype(); } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) { $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified(); $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype(); } } $userdata['myhosts'] = array(); if ($courses = enrol_get_users_courses($user->id, false)) { $userdata['myhosts'][] = array('name'=> $SITE->shortname, 'url' => $CFG->wwwroot, 'count' => count($courses)); } $sql = "SELECT h.name AS hostname, h.wwwroot, h.id AS hostid, COUNT(c.id) AS count FROM {mnetservice_enrol_courses} c JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid) JOIN {mnet_host} h ON h.id = c.hostid WHERE e.userid = ? AND c.hostid = ? GROUP BY h.name, h.wwwroot, h.id"; if ($courses = $DB->get_records_sql($sql, array($user->id, $remoteclient->id))) { foreach($courses as $course) { $userdata['myhosts'][] = array('name'=> $course->hostname, 'url' => $CFG->wwwroot.'/auth/mnet/jump.php?hostid='.$course->hostid, 'count' => $course->count); } } return $userdata; } /** * Generate a random string for use as an RPC session token. */ function generate_token() { return sha1(str_shuffle('' . mt_rand() . time())); } /** * Starts an RPC jump session and returns the jump redirect URL. * * @param int $mnethostid id of the mnet host to jump to * @param string $wantsurl url to redirect to after the jump (usually on remote system) * @param boolean $wantsurlbackhere defaults to false, means that the remote system should bounce us back here * rather than somewhere inside *its* wwwroot */ function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) { global $CFG, $USER, $DB; require_once $CFG->dirroot . '/mnet/xmlrpc/client.php'; if (\core\session\manager::is_loggedinas()) { throw new \moodle_exception('notpermittedtojumpas', 'mnet'); } // check remote login permissions if (! has_capability('moodle/site:mnetlogintoremote', context_system::instance()) or is_mnet_remote_user($USER) or isguestuser() or !isloggedin()) { throw new \moodle_exception('notpermittedtojump', 'mnet'); } // check for SSO publish permission first if ($this->has_service($mnethostid, 'sso_sp') == false) { throw new \moodle_exception('hostnotconfiguredforsso', 'mnet'); } // set RPC timeout to 30 seconds if not configured if (empty($this->config->rpc_negotiation_timeout)) { $this->config->rpc_negotiation_timeout = 30; set_config('rpc_negotiation_timeout', '30', 'auth_mnet'); } // get the host info $mnet_peer = new mnet_peer(); $mnet_peer->set_id($mnethostid); // set up the session $mnet_session = $DB->get_record('mnet_session', array('userid'=>$USER->id, 'mnethostid'=>$mnethostid, 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT']))); if ($mnet_session == false) { $mnet_session = new stdClass(); $mnet_session->mnethostid = $mnethostid; $mnet_session->userid = $USER->id; $mnet_session->username = $USER->username; $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']); $mnet_session->token = $this->generate_token(); $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout; $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime'); $mnet_session->session_id = session_id(); $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session); } else { $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']); $mnet_session->token = $this->generate_token(); $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout; $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime'); $mnet_session->session_id = session_id(); $DB->update_record('mnet_session', $mnet_session); } // construct the redirection URL //$transport = mnet_get_protocol($mnet_peer->transport); $wantsurl = urlencode($wantsurl); $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$this->mnet->wwwroot}&wantsurl={$wantsurl}"; if ($wantsurlbackhere) { $url .= '&remoteurl=1'; } return $url; } /** * This function confirms the remote (ID provider) host's mnet session * by communicating the token and UA over the XMLRPC transport layer, and * returns the local user record on success. * * @param string $token The random session token. * @param mnet_peer $remotepeer The ID provider mnet_peer object. * @return array The local user record. */ function confirm_mnet_session($token, $remotepeer) { global $CFG, $DB; require_once $CFG->dirroot . '/mnet/xmlrpc/client.php'; require_once $CFG->libdir . '/gdlib.php'; require_once($CFG->dirroot.'/user/lib.php'); // verify the remote host is configured locally before attempting RPC call if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot, 'deleted' => 0))) { throw new \moodle_exception('notpermittedtoland', 'mnet'); } // set up the RPC request $mnetrequest = new mnet_xmlrpc_client(); $mnetrequest->set_method('auth/mnet/auth.php/user_authorise'); // set $token and $useragent parameters $mnetrequest->add_param($token); $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT'])); // Thunderbirds are go! Do RPC call and store response if ($mnetrequest->send($remotepeer) === true) { $remoteuser = (object) $mnetrequest->response; } else { foreach ($mnetrequest->error as $errormessage) { list($code, $message) = array_map('trim',explode(':', $errormessage, 2)); if($code == 702) { $site = get_site(); throw new \moodle_exception('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot, format_string($site->fullname)); exit; } $message .= "ERROR $code:<br/>$errormessage<br/>"; } throw new \moodle_exception("rpcerror", '', '', $message); } unset($mnetrequest); if (empty($remoteuser) or empty($remoteuser->username)) { throw new \moodle_exception('unknownerror', 'mnet'); exit; } if (user_not_fully_set_up($remoteuser, false)) { throw new \moodle_exception('notenoughidpinfo', 'mnet'); exit; } $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer)); $remoteuser->auth = 'mnet'; $remoteuser->wwwroot = $remotepeer->wwwroot; // the user may roam from Moodle 1.x where lang has _utf8 suffix // also, make sure that the lang is actually installed, otherwise set site default if (isset($remoteuser->lang)) { $remoteuser->lang = clean_param(str_replace('_utf8', '', $remoteuser->lang), PARAM_LANG); } $firsttime = false; // get the local record for the remote user $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id)); // add the remote user to the database if necessary, and if allowed // TODO: refactor into a separate function if (empty($localuser) || ! $localuser->id) { /* if (empty($this->config->auto_add_remote_users)) { throw new \moodle_exception('nolocaluser', 'mnet'); } See MDL-21327 for why this is commented out */ $remoteuser->mnethostid = $remotehost->id; $remoteuser->firstaccess = 0; $remoteuser->confirmed = 1; $remoteuser->id = user_create_user($remoteuser, false); $firsttime = true; $localuser = $remoteuser; } // check sso access control list for permission first if (!$this->can_login_remotely($localuser->username, $remotehost->id)) { throw new \moodle_exception('sso_mnet_login_refused', 'mnet', '', array('user' => $localuser->username, 'host' => $remotehost->name)); } $fs = get_file_storage(); // update the local user record with remote user data foreach ((array) $remoteuser as $key => $val) { if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages) and isset($remoteuser->picture)) { // update the user picture if there is a newer verion at the identity provider $usercontext = context_user::instance($localuser->id, MUST_EXIST); if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) { $localtimemodified = $usericonfile->get_timemodified(); } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) { $localtimemodified = $usericonfile->get_timemodified(); } else { $localtimemodified = 0; } if (!empty($val) and $localtimemodified < $val) { mnet_debug('refetching the user picture from the identity provider host'); $fetchrequest = new mnet_xmlrpc_client(); $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image'); $fetchrequest->add_param($localuser->username); if ($fetchrequest->send($remotepeer) === true) { if (strlen($fetchrequest->response['f1']) > 0) { $imagefilename = $CFG->tempdir . '/mnet-usericon-' . $localuser->id; $imagecontents = base64_decode($fetchrequest->response['f1']); file_put_contents($imagefilename, $imagecontents); if ($newrev = process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) { $localuser->picture = $newrev; } unlink($imagefilename); } // note that since Moodle 2.0 we ignore $fetchrequest->response['f2'] // the mimetype information provided is ignored and the type of the file is detected // by process_new_icon() } } } if($key == 'myhosts') { $localuser->mnet_foreign_host_array = array(); foreach($val as $rhost) { $name = clean_param($rhost['name'], PARAM_ALPHANUM); $url = clean_param($rhost['url'], PARAM_URL); $count = clean_param($rhost['count'], PARAM_INT); $url_is_local = stristr($url , $CFG->wwwroot); if (!empty($name) && !empty($count) && empty($url_is_local)) { $localuser->mnet_foreign_host_array[] = array('name' => $name, 'url' => $url, 'count' => $count); } } } $localuser->{$key} = $val; } $localuser->mnethostid = $remotepeer->id; user_update_user($localuser, false); if (!$firsttime) { // repeat customer! let the IDP know about enrolments // we have for this user. // set up the RPC request $mnetrequest = new mnet_xmlrpc_client(); $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments'); // pass username and an assoc array of "my courses" // with info so that the IDP can maintain mnetservice_enrol_enrolments $mnetrequest->add_param($remoteuser->username); $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible'; $courses = enrol_get_users_courses($localuser->id, false, $fields); if (is_array($courses) && !empty($courses)) { // Second request to do the JOINs that we'd have done // inside enrol_get_users_courses() if we had been allowed $sql = "SELECT c.id, cc.name AS cat_name, cc.description AS cat_description FROM {course} c JOIN {course_categories} cc ON c.category = cc.id WHERE c.id IN (" . join(',',array_keys($courses)) . ')'; $extra = $DB->get_records_sql($sql); $keys = array_keys($courses); $studentroles = get_archetype_roles('student'); if (!empty($studentroles)) { $defaultrole = reset($studentroles); //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!! foreach ($keys AS $id) { if ($courses[$id]->visible == 0) { unset($courses[$id]); continue; } $courses[$id]->cat_id = $courses[$id]->category; $courses[$id]->defaultroleid = $defaultrole->id; unset($courses[$id]->category); unset($courses[$id]->visible); $courses[$id]->cat_name = $extra[$id]->cat_name; $courses[$id]->cat_description = $extra[$id]->cat_description; $courses[$id]->defaultrolename = $defaultrole->name; // coerce to array $courses[$id] = (array)$courses[$id]; } } else { throw new moodle_exception('unknownrole', 'error', '', 'student'); } } else { // if the array is empty, send it anyway // we may be clearing out stale entries $courses = array(); } $mnetrequest->add_param($courses, 'array'); // Call 0800-RPC Now! -- we don't care too much if it fails // as it's just informational. if ($mnetrequest->send($remotepeer) === false) { // error_log(print_r($mnetrequest->error,1)); } } return $localuser; } /** * creates (or updates) the mnet session once * {@see confirm_mnet_session} and {@see complete_user_login} have both been called * * @param stdclass $user the local user (must exist already * @param string $token the jump/land token * @param mnet_peer $remotepeer the mnet_peer object of this users's idp */ public function update_mnet_session($user, $token, $remotepeer) { global $DB; $session_gc_maxlifetime = 1440; if (isset($user->session_gc_maxlifetime)) { $session_gc_maxlifetime = $user->session_gc_maxlifetime; } if (!$mnet_session = $DB->get_record('mnet_session', array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id, 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) { $mnet_session = new stdClass(); $mnet_session->mnethostid = $remotepeer->id; $mnet_session->userid = $user->id; $mnet_session->username = $user->username; $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']); $mnet_session->token = $token; // Needed to support simultaneous sessions // and preserving DB rec uniqueness $mnet_session->confirm_timeout = time(); $mnet_session->expires = time() + (integer)$session_gc_maxlifetime; $mnet_session->session_id = session_id(); $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session); } else { $mnet_session->expires = time() + (integer)$session_gc_maxlifetime; $DB->update_record('mnet_session', $mnet_session); } } /** * Invoke this function _on_ the IDP to update it with enrolment info local to * the SP right after calling user_authorise() * * Normally called by the SP after calling user_authorise() * * @param string $username The username * @param array $courses Assoc array of courses following the structure of mnetservice_enrol_courses * @return bool */ function update_enrolments($username, $courses) { global $CFG, $DB; $remoteclient = get_mnet_remote_client(); if (empty($username) || !is_array($courses)) { return false; } // make sure it is a user we have an in active session // with that host... $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id), '', 'id, userid'); $userid = null; foreach ($mnetsessions as $mnetsession) { if (is_null($userid)) { $userid = $mnetsession->userid; continue; } if ($userid != $mnetsession->userid) { throw new mnet_server_exception(3, 'authfail_usermismatch'); } } if (empty($courses)) { // no courses? clear out quickly $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid)); return true; } // IMPORTANT: Ask for remoteid as the first element in the query, so // that the array that comes back is indexed on the same field as the // array that we have received from the remote client $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder, c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate, e.id AS enrolmentid FROM {mnetservice_enrol_courses} c LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid AND e.userid = ?) WHERE c.hostid = ?"; $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id)); $keepenrolments = array(); foreach($courses as $ix => $course) { $course['remoteid'] = $course['id']; $course['hostid'] = (int)$remoteclient->id; $userisregd = false; // if we do not have the the information about the remote course, it is not available // to us for remote enrolment - skip if (array_key_exists($course['remoteid'], $currentcourses)) { // We are going to keep this enrolment, it will be updated or inserted, but will keep it. $keepenrolments[] = $course['id']; // Pointer to current course: $currentcourse =& $currentcourses[$course['remoteid']]; $saveflag = false; foreach($course as $key => $value) { // Only compare what is available locally, data coming from enrolment tables have // way more information that tables used to keep the track of mnet enrolments. if (!property_exists($currentcourse, $key)) { continue; } // Don't compare ids either, they come from different databases. if ($key === 'id') { continue; } if ($currentcourse->$key != $value) { $saveflag = true; $currentcourse->$key = $value; } } if ($saveflag) { $DB->update_record('mnetservice_enrol_courses', $currentcourse); } if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) { $userisregd = true; } } else { unset ($courses[$ix]); continue; } // Do we have a record for this assignment? if ($userisregd) { // Yes - we know about this one already // We don't want to do updates because the new data is probably // 'less complete' than the data we have. } else { // No - create a record $newenrol = new stdClass(); $newenrol->userid = $userid; $newenrol->hostid = (int)$remoteclient->id; $newenrol->remotecourseid = $course['remoteid']; $newenrol->rolename = $course['defaultrolename']; $newenrol->enroltype = 'mnet'; $newenrol->id = $DB->insert_record('mnetservice_enrol_enrolments', $newenrol); } } // Clean up courses that the user is no longer enrolled in. list($insql, $inparams) = $DB->get_in_or_equal($keepenrolments, SQL_PARAMS_NAMED, 'param', false, null); $whereclause = ' userid = :userid AND hostid = :hostid AND remotecourseid ' . $insql; $params = array_merge(['userid' => $userid, 'hostid' => $remoteclient->id], $inparams); $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, $params); } function prevent_local_passwords() { return true; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return false; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { //TODO: it should be able to redirect, right? return false; } /** * Returns the URL for changing the user's pw, or false if the default can * be used. * * @return moodle_url */ function change_password_url() { return null; } /** * Poll the IdP server to let it know that a user it has authenticated is still * online * * @return void */ function keepalive_client() { global $CFG, $DB; $cutoff = time() - 300; // TODO - find out what the remote server's session // cutoff is, and preempt that $sql = " select id, username, mnethostid from {user} where lastaccess > ? AND mnethostid != ? order by mnethostid"; $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id)); if ($immigrants == false) { return true; } $usersArray = array(); foreach($immigrants as $immigrant) { $usersArray[$immigrant->mnethostid][] = $immigrant->username; } require_once $CFG->dirroot . '/mnet/xmlrpc/client.php'; foreach($usersArray as $mnethostid => $users) { $mnet_peer = new mnet_peer(); $mnet_peer->set_id($mnethostid); $mnet_request = new mnet_xmlrpc_client(); $mnet_request->set_method('auth/mnet/auth.php/keepalive_server'); // set $token and $useragent parameters $mnet_request->add_param($users); if ($mnet_request->send($mnet_peer) === true) { if (!isset($mnet_request->response['code'])) { debugging("Server side error has occured on host $mnethostid"); continue; } elseif ($mnet_request->response['code'] > 0) { debugging($mnet_request->response['message']); } if (!isset($mnet_request->response['last log id'])) { debugging("Server side error has occured on host $mnethostid\nNo log ID was received."); continue; } } else { debugging("Server side error has occured on host $mnethostid: " . join("\n", $mnet_request->error)); break; } } } /** * Receives an array of log entries from an SP and adds them to the mnet_log * table * * @deprecated since Moodle 2.8 Please don't use this function for recording mnet logs. * @param array $array An array of usernames * @return string "All ok" or an error message */ function refresh_log($array) { debugging('refresh_log() is deprecated, The transfer of logs through mnet are no longer recorded.', DEBUG_DEVELOPER); return array('code' => 0, 'message' => 'All ok'); } /** * Receives an array of usernames from a remote machine and prods their * sessions to keep them alive * * @param array $array An array of usernames * @return string "All ok" or an error message */ function keepalive_server($array) { global $CFG, $DB; $remoteclient = get_mnet_remote_client(); // We don't want to output anything to the client machine $start = ob_start(); // We'll get session records in batches of 30 $superArray = array_chunk($array, 30); $returnString = ''; foreach($superArray as $subArray) { $subArray = array_values($subArray); $results = $DB->get_records_list('mnet_session', 'username', $subArray, '', 'id, session_id, username'); if ($results == false) { // We seem to have a username that breaks our query: // TODO: Handle this error appropriately $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n"; } else { foreach($results as $emigrant) { \core\session\manager::touch_session($emigrant->session_id); } } } $end = ob_end_clean(); if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id); return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id); } /** * Cleanup any remote mnet_sessions, kill the local mnet_session data * * This is called by require_logout in moodlelib * * @return void */ function prelogout_hook() { global $CFG, $USER; if (!is_enabled_auth('mnet')) { return; } // If the user is local to this Moodle: if ($USER->mnethostid == $this->mnet->id) { $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT'])); // Else the user has hit 'logout' at a Service Provider Moodle: } else { $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT'])); } } /** * The SP uses this function to kill the session on the parent IdP * * @param string $username Username for session to kill * @param string $useragent SHA1 hash of user agent to look for * @return string A plaintext report of what has happened */ function kill_parent($username, $useragent) { global $CFG, $USER, $DB; require_once $CFG->dirroot.'/mnet/xmlrpc/client.php'; $sql = " select * from {mnet_session} s where s.username = ? AND s.useragent = ? AND s.mnethostid = ?"; $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid)); $ignore = $DB->delete_records('mnet_session', array('username'=>$username, 'useragent'=>$useragent, 'mnethostid'=>$USER->mnethostid)); if (false != $mnetsessions) { $mnet_peer = new mnet_peer(); $mnet_peer->set_id($USER->mnethostid); $mnet_request = new mnet_xmlrpc_client(); $mnet_request->set_method('auth/mnet/auth.php/kill_children'); // set $token and $useragent parameters $mnet_request->add_param($username); $mnet_request->add_param($useragent); if ($mnet_request->send($mnet_peer) === false) { debugging(join("\n", $mnet_request->error)); return false; } } return true; } /** * The IdP uses this function to kill child sessions on other hosts * * @param string $username Username for session to kill * @param string $useragent SHA1 hash of user agent to look for * @return string A plaintext report of what has happened */ function kill_children($username, $useragent) { global $CFG, $USER, $DB; $remoteclient = null; if (defined('MNET_SERVER')) { $remoteclient = get_mnet_remote_client(); } require_once $CFG->dirroot.'/mnet/xmlrpc/client.php'; $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username)); $returnstring = ''; $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent)); if (false == $mnetsessions) { $returnstring .= "Could find no remote sessions\n"; $mnetsessions = array(); } foreach($mnetsessions as $mnetsession) { // If this script is being executed by a remote peer, that means the user has clicked // logout on that peer, and the session on that peer can be deleted natively. // Skip over it. if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) { continue; } $returnstring .= "Deleting session\n"; $mnet_peer = new mnet_peer(); $mnet_peer->set_id($mnetsession->mnethostid); $mnet_request = new mnet_xmlrpc_client(); $mnet_request->set_method('auth/mnet/auth.php/kill_child'); // set $token and $useragent parameters $mnet_request->add_param($username); $mnet_request->add_param($useragent); if ($mnet_request->send($mnet_peer) === false) { debugging("Server side error has occured on host $mnetsession->mnethostid: " . join("\n", $mnet_request->error)); } } $ignore = $DB->delete_records('mnet_session', array('useragent'=>$useragent, 'userid'=>$userid)); if (isset($remoteclient) && isset($remoteclient->id)) { \core\session\manager::kill_user_sessions($userid); } return $returnstring; } /** * When the IdP requests that child sessions are terminated, * this function will be called on each of the child hosts. The machine that * calls the function (over xmlrpc) provides us with the mnethostid we need. * * @param string $username Username for session to kill * @param string $useragent SHA1 hash of user agent to look for * @return bool True on success */ function kill_child($username, $useragent) { global $CFG, $DB; $remoteclient = get_mnet_remote_client(); $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent)); $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent)); if (false != $session) { \core\session\manager::kill_session($session->session_id); return true; } return false; } /** * To delete a host, we must delete all current sessions that users from * that host are currently engaged in. * * @param string $sessionidarray An array of session hashes * @return bool True on success */ function end_local_sessions(&$sessionArray) { global $CFG; if (is_array($sessionArray)) { while($session = array_pop($sessionArray)) { \core\session\manager::kill_session($session->session_id); } return true; } return false; } /** * Returns the user's profile image info * * If the user exists and has a profile picture, the returned array will contain keys: * f1 - the content of the default 100x100px image * f1_mimetype - the mimetype of the f1 file * f2 - the content of the 35x35px variant of the image * f2_mimetype - the mimetype of the f2 file * * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs. * * @see process_new_icon() * @uses mnet_remote_client callable via MNet XML-RPC * @param int $username The id of the user * @return false|array false if user not found, empty array if no picture exists, array with data otherwise */ function fetch_user_image($username) { global $CFG, $DB; if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id))) { $fs = get_file_storage(); $usercontext = context_user::instance($user->id, MUST_EXIST); $return = array(); if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) { $return['f1'] = base64_encode($f1->get_content()); $return['f1_mimetype'] = $f1->get_mimetype(); } else if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) { $return['f1'] = base64_encode($f1->get_content()); $return['f1_mimetype'] = $f1->get_mimetype(); } if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.png')) { $return['f2'] = base64_encode($f2->get_content()); $return['f2_mimetype'] = $f2->get_mimetype(); } else if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.jpg')) { $return['f2'] = base64_encode($f2->get_content()); $return['f2_mimetype'] = $f2->get_mimetype(); } return $return; } return false; } /** * Returns the theme information and logo url as strings. * * @return string The theme info */ function fetch_theme_info() { global $CFG; $themename = "$CFG->theme"; $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg"; $return['themename'] = $themename; $return['logourl'] = $logourl; return $return; } /** * Determines if an MNET host is providing the nominated service. * * @param int $mnethostid The id of the remote host * @param string $servicename The name of the service * @return bool Whether the service is available on the remote host */ function has_service($mnethostid, $servicename) { global $CFG, $DB; $sql = " SELECT svc.id as serviceid, svc.name, svc.description, svc.offer, svc.apiversion, h2s.id as h2s_id FROM {mnet_host} h, {mnet_service} svc, {mnet_host2service} h2s WHERE h.deleted = '0' AND h.id = h2s.hostid AND h2s.hostid = ? AND h2s.serviceid = svc.id AND svc.name = ? AND h2s.subscribe = '1'"; return $DB->get_records_sql($sql, array($mnethostid, $servicename)); } /** * Checks the MNET access control table to see if the username/mnethost * is permitted to login to this moodle. * * @param string $username The username * @param int $mnethostid The id of the remote mnethost * @return bool Whether the user can login from the remote host */ function can_login_remotely($username, $mnethostid) { global $DB; $accessctrl = 'allow'; $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid)); if (!empty($aclrecord)) { $accessctrl = $aclrecord->accessctrl; } return $accessctrl == 'allow'; } function logoutpage_hook() { global $USER, $CFG, $redirect, $DB; if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) { $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid)); $redirect = $host->wwwroot.'/'; } } /** * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB * * @param object $logline The log information to be trimmed * @return object The passed logline object trimmed to not exceed storable limits */ function trim_logline ($logline) { $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40, 'url' => 255); foreach ($limits as $property => $limit) { if (isset($logline->$property)) { $logline->$property = substr($logline->$property, 0, $limit); } } return $logline; } /** * Returns a list of MNet IdPs that the user can roam from. * * @param string $wantsurl The relative url fragment the user wants to get to. * @return array List of arrays with keys url, icon and name. */ function loginpage_idp_list($wantsurl) { global $DB, $CFG; // strip off wwwroot, since the remote site will prefix it's return url with this $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . ')/', '', $wantsurl); $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application FROM {mnet_host} h JOIN {mnet_host2service} m ON h.id = m.hostid JOIN {mnet_service} s ON s.id = m.serviceid JOIN {mnet_application} a ON h.applicationid = a.id WHERE s.name = ? AND h.deleted = ? AND m.publish = ?"; $params = array('sso_sp', 0, 1); if (!empty($CFG->mnet_all_hosts_id)) { $sql .= " AND h.id <> ?"; $params[] = $CFG->mnet_all_hosts_id; } if (!$hosts = $DB->get_records_sql($sql, $params)) { return array(); } $idps = array(); foreach ($hosts as $host) { $idps[] = array( 'url' => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)), 'icon' => new pix_icon('i/' . $host->application . '_host', $host->name), 'name' => $host->name, ); } return $idps; } /** * Test if settings are correct, print info to output. */ public function test_settings() { global $CFG, $OUTPUT, $DB; // Generate warning if MNET is disabled. if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') { echo $OUTPUT->notification(get_string('mnetdisabled', 'mnet'), 'notifyproblem'); return; } // Generate full list of ID and service providers. $query = " SELECT h.id, h.name as hostname, h.wwwroot, h2idp.publish as idppublish, h2idp.subscribe as idpsubscribe, idp.name as idpname, h2sp.publish as sppublish, h2sp.subscribe as spsubscribe, sp.name as spname FROM {mnet_host} h LEFT JOIN {mnet_host2service} h2idp ON (h.id = h2idp.hostid AND (h2idp.publish = 1 OR h2idp.subscribe = 1)) INNER JOIN {mnet_service} idp ON (h2idp.serviceid = idp.id AND idp.name = 'sso_idp') LEFT JOIN {mnet_host2service} h2sp ON (h.id = h2sp.hostid AND (h2sp.publish = 1 OR h2sp.subscribe = 1)) INNER JOIN {mnet_service} sp ON (h2sp.serviceid = sp.id AND sp.name = 'sso_sp') WHERE ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND h.id != ? ORDER BY h.name ASC"; $idproviders = array(); $serviceproviders = array(); if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) { foreach ($resultset as $hostservice) { if (!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) { $serviceproviders[] = array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot); } if (!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) { $idproviders[] = array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot); } } } // ID Providers. $table = html_writer::start_tag('table', array('class' => 'generaltable')); $count = 0; foreach ($idproviders as $host) { $table .= html_writer::start_tag('tr'); $table .= html_writer::start_tag('td'); $table .= $host['name']; $table .= html_writer::end_tag('td'); $table .= html_writer::start_tag('td'); $table .= $host['wwwroot']; $table .= html_writer::end_tag('td'); $table .= html_writer::end_tag('tr'); $count++; } $table .= html_writer::end_tag('table'); if ($count > 0) { echo html_writer::tag('h3', get_string('auth_mnet_roamin', 'auth_mnet')); echo $table; } // Service Providers. unset($table); $table = html_writer::start_tag('table', array('class' => 'generaltable')); $count = 0; foreach ($serviceproviders as $host) { $table .= html_writer::start_tag('tr'); $table .= html_writer::start_tag('td'); $table .= $host['name']; $table .= html_writer::end_tag('td'); $table .= html_writer::start_tag('td'); $table .= $host['wwwroot']; $table .= html_writer::end_tag('td'); $table .= html_writer::end_tag('tr'); $count++; } $table .= html_writer::end_tag('table'); if ($count > 0) { echo html_writer::tag('h3', get_string('auth_mnet_roamout', 'auth_mnet')); echo $table; } } } mnet/land.php 0000644 00000004617 15152311435 0007146 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Authentication Plugin: Moodle Network Authentication * Multiple host authentication support for Moodle Network. * * @package auth_mnet * @author Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ require_once __DIR__ . '/../../config.php'; require_once $CFG->dirroot . '/mnet/xmlrpc/client.php'; // grab the GET params $token = required_param('token', PARAM_BASE64); $remotewwwroot = required_param('idp', PARAM_URL); $wantsurl = required_param('wantsurl', PARAM_LOCALURL); $wantsremoteurl = optional_param('remoteurl', false, PARAM_BOOL); $url = new moodle_url('/auth/mnet/jump.php', array('token'=>$token, 'idp'=>$remotewwwroot, 'wantsurl'=>$wantsurl)); if ($wantsremoteurl !== false) $url->param('remoteurl', $wantsremoteurl); $PAGE->set_url($url); $PAGE->set_context(context_system::instance()); $site = get_site(); if (!is_enabled_auth('mnet')) { throw new \moodle_exception('mnetdisable'); } // confirm the MNET session $mnetauth = get_auth_plugin('mnet'); $remotepeer = new mnet_peer(); $remotepeer->set_wwwroot($remotewwwroot); // this creates the local user account if necessary, or updates it if it already exists $localuser = $mnetauth->confirm_mnet_session($token, $remotepeer); // log in $user = get_complete_user_data('id', $localuser->id, $localuser->mnethostid); complete_user_login($user); // now that we've logged in, set up the mnet session properly $mnetauth->update_mnet_session($user, $token, $remotepeer); if (!empty($localuser->mnet_foreign_host_array)) { $USER->mnet_foreign_host_array = $localuser->mnet_foreign_host_array; } // redirect if ($wantsremoteurl) { redirect($remotewwwroot . $wantsurl); } redirect($CFG->wwwroot . $wantsurl); mnet/settings.php 0000644 00000002616 15152311435 0010065 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin settings and defaults. * * @package auth_mnet * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { require_once($CFG->dirroot.'/lib/outputlib.php'); // Introductory explanation. $settings->add(new admin_setting_heading('auth_mnet/pluginname', '', new lang_string('auth_mnetdescription', 'auth_mnet'))); // RPC Timeout. $settings->add(new admin_setting_configtext('auth_mnet/rpc_negotiation_timeout', get_string('rpc_negotiation_timeout', 'auth_mnet'), get_string('auth_mnet_rpc_negotiation_timeout', 'auth_mnet'), '30', PARAM_INT)); } mnet/upgrade.txt 0000644 00000000735 15152311435 0007704 0 ustar 00 This files describes API changes in auth_mnet code. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/mnet' to 'auth_mnet'. === 3.1 === * Users now are created through user_create_user function which, apart from inserting the user in the database and generating a user_created event, is applying the site default preferences if they are not set. mnet/version.php 0000644 00000002272 15152311435 0007710 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Manual authentication plugin version information * * @package auth_mnet * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'auth_mnet'; // Full name of the plugin (used for diagnostics) mnet/classes/privacy/provider.php 0000644 00000027230 15152311435 0013170 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for auth_mnet. * * @package auth_mnet * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_mnet\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\metadata\collection; use core_privacy\local\request\contextlist; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy provider for the mnet authentication * * @copyright 2018 Carlos Escobedo <carlos@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised item collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { $sessionfields = [ 'userid' => 'privacy:metadata:mnet_session:userid', 'username' => 'privacy:metadata:mnet_session:username', 'token' => 'privacy:metadata:mnet_session:token', 'mnethostid' => 'privacy:metadata:mnet_session:mnethostid', 'useragent' => 'privacy:metadata:mnet_session:useragent', 'expires' => 'privacy:metadata:mnet_session:expires' ]; $collection->add_database_table('mnet_session', $sessionfields, 'privacy:metadata:mnet_session'); $logfields = [ 'hostid' => 'privacy:metadata:mnet_log:hostid', 'remoteid' => 'privacy:metadata:mnet_log:remoteid', 'time' => 'privacy:metadata:mnet_log:time', 'userid' => 'privacy:metadata:mnet_log:userid', 'ip' => 'privacy:metadata:mnet_log:ip', 'course' => 'privacy:metadata:mnet_log:course', 'coursename' => 'privacy:metadata:mnet_log:coursename', 'module' => 'privacy:metadata:mnet_log:module', 'cmid' => 'privacy:metadata:mnet_log:cmid', 'action' => 'privacy:metadata:mnet_log:action', 'url' => 'privacy:metadata:mnet_log:url', 'info' => 'privacy:metadata:mnet_log:info' ]; $collection->add_database_table('mnet_log', $logfields, 'privacy:metadata:mnet_log'); $externalfields = [ 'address' => 'privacy:metadata:mnet_external:address', 'alternatename' => 'privacy:metadata:mnet_external:alternatename', 'autosubscribe' => 'privacy:metadata:mnet_external:autosubscribe', 'calendartype' => 'privacy:metadata:mnet_external:calendartype', 'city' => 'privacy:metadata:mnet_external:city', 'country' => 'privacy:metadata:mnet_external:country', 'currentlogin' => 'privacy:metadata:mnet_external:currentlogin', 'department' => 'privacy:metadata:mnet_external:department', 'description' => 'privacy:metadata:mnet_external:description', 'email' => 'privacy:metadata:mnet_external:email', 'emailstop' => 'privacy:metadata:mnet_external:emailstop', 'firstaccess' => 'privacy:metadata:mnet_external:firstaccess', 'firstname' => 'privacy:metadata:mnet_external:firstname', 'firstnamephonetic' => 'privacy:metadata:mnet_external:firstnamephonetic', 'id' => 'privacy:metadata:mnet_external:id', 'idnumber' => 'privacy:metadata:mnet_external:idnumber', 'imagealt' => 'privacy:metadata:mnet_external:imagealt', 'institution' => 'privacy:metadata:mnet_external:institution', 'lang' => 'privacy:metadata:mnet_external:lang', 'lastaccess' => 'privacy:metadata:mnet_external:lastaccess', 'lastlogin' => 'privacy:metadata:mnet_external:lastlogin', 'lastname' => 'privacy:metadata:mnet_external:lastname', 'lastnamephonetic' => 'privacy:metadata:mnet_external:lastnamephonetic', 'maildigest' => 'privacy:metadata:mnet_external:maildigest', 'maildisplay' => 'privacy:metadata:mnet_external:maildisplay', 'middlename' => 'privacy:metadata:mnet_external:middlename', 'phone1' => 'privacy:metadata:mnet_external:phone1', 'pnone2' => 'privacy:metadata:mnet_external:phone2', 'picture' => 'privacy:metadata:mnet_external:picture', 'policyagreed' => 'privacy:metadata:mnet_external:policyagreed', 'suspended' => 'privacy:metadata:mnet_external:suspended', 'timezone' => 'privacy:metadata:mnet_external:timezone', 'trackforums' => 'privacy:metadata:mnet_external:trackforums', 'trustbitmask' => 'privacy:metadata:mnet_external:trustbitmask', 'username' => 'privacy:metadata:mnet_external:username', ]; $collection->add_external_location_link('moodle', $externalfields, 'privacy:metadata:external:moodle'); $collection->add_external_location_link('mahara', $externalfields, 'privacy:metadata:external:mahara'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { $sql = "SELECT ctx.id FROM {mnet_log} ml JOIN {context} ctx ON ctx.instanceid = ml.userid AND ctx.contextlevel = :contextlevel WHERE ml.userid = :userid"; $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT userid FROM {mnet_log} WHERE userid = ?"; $params = [$context->instanceid]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts, using the supplied exporter instance. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; $context = \context_user::instance($contextlist->get_user()->id); $sql = "SELECT ml.id, mh.wwwroot, mh.name, ml.remoteid, ml.time, ml.userid, ml.ip, ml.course, ml.coursename, ml.module, ml.cmid, ml.action, ml.url, ml.info FROM {mnet_log} ml JOIN {mnet_host} mh ON mh.id = ml.hostid WHERE ml.userid = :userid ORDER BY mh.name, ml.coursename"; $params = ['userid' => $contextlist->get_user()->id]; $data = []; $lastcourseid = null; $logentries = $DB->get_recordset_sql($sql, $params); foreach ($logentries as $logentry) { $item = (object) [ 'time' => transform::datetime($logentry->time), 'remoteid' => $logentry->remoteid, 'ip' => $logentry->ip, 'course' => $logentry->course, 'coursename' => format_string($logentry->coursename), 'module' => $logentry->module, 'cmid' => $logentry->cmid, 'action' => $logentry->action, 'url' => $logentry->url, 'info' => format_string($logentry->info) ]; $item->externalhost = ($logentry->name == '') ? preg_replace('#^https?://#', '', $logentry->wwwroot) : preg_replace('#^https?://#', '', $logentry->name); if ($lastcourseid && $lastcourseid != $logentry->course) { $path = [get_string('pluginname', 'auth_mnet'), $data[0]->externalhost, $data[0]->coursename]; writer::with_context($context)->export_data($path, (object) $data); $data = []; } $data[] = $item; $lastcourseid = $logentry->course; } $logentries->close(); $path = [get_string('pluginname', 'auth_mnet'), $item->externalhost, $item->coursename]; writer::with_context($context)->export_data($path, (object) $data); } /** * Delete all personal data for all users in the specified context. * * @param context $context Context to delete data from. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; if ($context->contextlevel != CONTEXT_USER) { return; } $DB->delete_records('mnet_log', ['userid' => $context->instanceid]); } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); if ($context instanceof \context_user) { $DB->delete_records('mnet_log', ['userid' => $context->instanceid]); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel != CONTEXT_USER) { continue; } if ($context->instanceid == $userid) { // Because we only use user contexts the instance ID is the user ID. $DB->delete_records('mnet_log', ['userid' => $context->instanceid]); } } } } mnet/classes/task/cron_task.php 0000644 00000003006 15152311435 0012601 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace auth_mnet\task; defined('MOODLE_INTERNAL') || die(); /** * A schedule task for mnet cron. * * @package auth_mnet * @copyright 2019 Simey Lameze <simey@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cron_task extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('crontask', 'auth_mnet'); } /** * Run auth mnet cron. */ public function execute() { global $DB, $CFG; require_once($CFG->dirroot . '/auth/mnet/auth.php'); $mnetplugin = new \auth_plugin_mnet(); $mnetplugin->keepalive_client(); $DB->delete_records_select('mnet_session', "expires < ?", [time() - DAYSECS]); } } mnet/lang/en/deprecated.txt 0000644 00000000422 15152311435 0011671 0 ustar 00 privacy:metadata:mnet_external:aim,auth_mnet privacy:metadata:mnet_external:icq,auth_mnet privacy:metadata:mnet_external:msn,auth_mnet privacy:metadata:mnet_external:skype,auth_mnet privacy:metadata:mnet_external:url,auth_mnet privacy:metadata:mnet_external:yahoo,auth_mnet mnet/lang/en/auth_mnet.php 0000644 00000021620 15152311435 0011530 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'auth_mnet', language 'en'. * * @package auth_mnet * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_mnet_auto_add_remote_users'] = 'When set to Yes, a local user record is auto-created when a remote user logs in for the first time.'; $string['auth_mnetdescription'] = 'Users are authenticated according to the web of trust defined in your Moodle Network settings.'; $string['auth_mnet_roamin'] = 'These host\'s users can roam in to your site'; $string['auth_mnet_roamout'] = 'Your users can roam out to these hosts'; $string['auth_mnet_rpc_negotiation_timeout'] = 'The timeout in seconds for authentication over the XMLRPC transport.'; $string['auto_add_remote_users'] = 'Auto add remote users'; $string['crontask'] = 'Background processing for MNET authentication'; $string['rpc_negotiation_timeout'] = 'RPC negotiation timeout'; $string['sso_idp_description'] = 'Publish this service to allow your users to roam to the {$a} site without having to re-login there. <ul><li><em>Dependency</em>: You must also <strong>subscribe</strong> to the SSO (Service Provider) service on {$a}.</li></ul><br />Subscribe to this service to allow authenticated users from {$a} to access your site without having to re-login. <ul><li><em>Dependency</em>: You must also <strong>publish</strong> the SSO (Service Provider) service to {$a}.</li></ul><br />'; $string['sso_idp_name'] = 'SSO (Identity Provider)'; $string['sso_mnet_login_refused'] = 'Username {$a->user} is not permitted to login from {$a->host}.'; $string['sso_sp_description'] = 'Publish this service to allow authenticated users from {$a} to access your site without having to re-login. <ul><li><em>Dependency</em>: You must also <strong>subscribe</strong> to the SSO (Identity Provider) service on {$a}.</li></ul><br />Subscribe to this service to allow your users to roam to the {$a} site without having to re-login there. <ul><li><em>Dependency</em>: You must also <strong>publish</strong> the SSO (Identity Provider) service to {$a}.</li></ul><br />'; $string['sso_sp_name'] = 'SSO (Service Provider)'; $string['pluginname'] = 'MNet authentication'; $string['privacy:metadata:external:mahara'] = 'This plugin can send data externally to a linked Mahara application.'; $string['privacy:metadata:external:moodle'] = 'This plugin can send data externally to a linked Moodle application.'; $string['privacy:metadata:mnet_external:address'] = 'The address of the user.'; $string['privacy:metadata:mnet_external:alternatename'] = 'An alternative name for the user.'; $string['privacy:metadata:mnet_external:autosubscribe'] = 'A preference as to if the user should be auto-subscribed to forums the user posts in.'; $string['privacy:metadata:mnet_external:calendartype'] = 'A user preference for the type of calendar to use.'; $string['privacy:metadata:mnet_external:city'] = 'The city of the user.'; $string['privacy:metadata:mnet_external:country'] = 'The country that the user is in.'; $string['privacy:metadata:mnet_external:currentlogin'] = 'The current login for this user.'; $string['privacy:metadata:mnet_external:department'] = 'The department that this user can be found in.'; $string['privacy:metadata:mnet_external:description'] = 'General details about this user.'; $string['privacy:metadata:mnet_external:email'] = 'An email address for contact.'; $string['privacy:metadata:mnet_external:emailstop'] = 'A preference to stop email being sent to the user.'; $string['privacy:metadata:mnet_external:firstaccess'] = 'The time that this user first accessed the site.'; $string['privacy:metadata:mnet_external:firstname'] = 'The first name of the user.'; $string['privacy:metadata:mnet_external:firstnamephonetic'] = 'The phonetic details about the user\'s first name.'; $string['privacy:metadata:mnet_external:id'] = 'The user ID'; $string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution'; $string['privacy:metadata:mnet_external:imagealt'] = 'Alternative text for the user\'s image.'; $string['privacy:metadata:mnet_external:institution'] = 'The institution that this user is a member of.'; $string['privacy:metadata:mnet_external:lang'] = 'A user preference for the language shown.'; $string['privacy:metadata:mnet_external:lastaccess'] = 'The time that the user last accessed the site.'; $string['privacy:metadata:mnet_external:lastlogin'] = 'The last login of this user.'; $string['privacy:metadata:mnet_external:lastname'] = 'The last name of the user.'; $string['privacy:metadata:mnet_external:lastnamephonetic'] = 'The phonetic details of the user\'s last name.'; $string['privacy:metadata:mnet_external:maildigest'] = 'A setting for the mail digest for this user.'; $string['privacy:metadata:mnet_external:maildisplay'] = 'A preference for the user about displaying their email address to other users.'; $string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user'; $string['privacy:metadata:mnet_external:phone1'] = 'A phone number for the user.'; $string['privacy:metadata:mnet_external:phone2'] = 'An additional phone number for the user.'; $string['privacy:metadata:mnet_external:picture'] = 'The picture details associated with this user.'; $string['privacy:metadata:mnet_external:policyagreed'] = 'A flag to determine if the user has agreed to the site policy.'; $string['privacy:metadata:mnet_external:suspended'] = 'A flag to show if the user has been suspended on this system.'; $string['privacy:metadata:mnet_external:timezone'] = 'The timezone of the user'; $string['privacy:metadata:mnet_external:trackforums'] = 'A preference for forums and tracking them.'; $string['privacy:metadata:mnet_external:trustbitmask'] = 'The trust bit mask'; $string['privacy:metadata:mnet_external:username'] = 'The username for this user.'; $string['privacy:metadata:mnet_log'] = 'Details of remote actions carried out by a local user logged in a remote system.'; $string['privacy:metadata:mnet_log:action'] = 'Action carried out by the user.'; $string['privacy:metadata:mnet_log:cmid'] = 'ID of the course module.'; $string['privacy:metadata:mnet_log:course'] = 'Remote system course ID where the action occurred.'; $string['privacy:metadata:mnet_log:coursename'] = 'Remote system course full name where the action occurred.'; $string['privacy:metadata:mnet_log:hostid'] = 'Remote system MNet ID.'; $string['privacy:metadata:mnet_log:info'] = 'Additional information about the action.'; $string['privacy:metadata:mnet_log:ip'] = 'The IP address used at the time of the action occurred.'; $string['privacy:metadata:mnet_log:module'] = 'Remote system module where the action occurred.'; $string['privacy:metadata:mnet_log:remoteid'] = 'Remote ID of the user who carried out the action in the remote system.'; $string['privacy:metadata:mnet_log:time'] = 'Time when the action occurred.'; $string['privacy:metadata:mnet_log:url'] = 'Remote system URL where the action occurred.'; $string['privacy:metadata:mnet_log:userid'] = 'Local ID of the user who carried out the action in the remote system.'; $string['privacy:metadata:mnet_session'] = 'The details of each MNet user session in a remote system. The data is stored temporarily.'; $string['privacy:metadata:mnet_session:expires'] = 'Time when the session expires.'; $string['privacy:metadata:mnet_session:mnethostid'] = 'Remote system MNet ID.'; $string['privacy:metadata:mnet_session:token'] = 'Unique session identifier'; $string['privacy:metadata:mnet_session:useragent'] = 'User agent used to access the remote system'; $string['privacy:metadata:mnet_session:userid'] = 'ID of the user jumping to remote system.'; $string['privacy:metadata:mnet_session:username'] = 'Username of the user jumping to remote system.'; $string['unknownhost'] = 'Unknown host'; // Deprecated since Moodle 3.11. $string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user'; $string['privacy:metadata:mnet_external:icq'] = 'The ICQ number of the user.'; $string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user'; $string['privacy:metadata:mnet_external:skype'] = 'The Skype identifier of the user'; $string['privacy:metadata:mnet_external:url'] = 'A URL related to this user.'; $string['privacy:metadata:mnet_external:yahoo'] = 'The Yahoo identifier of the user'; classes/output/verify_age_location_page.php 0000644 00000004414 15152311435 0015261 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Age and location verification renderable. * * @package core_auth * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use templatable; require_once($CFG->libdir.'/formslib.php'); /** * Age and location verification renderable class. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class verify_age_location_page implements renderable, templatable { /** @var mform The form object */ protected $form; /** @var string Error message */ protected $errormessage; /** * Constructor * * @param mform $form The form object * @param string $errormessage The error message. */ public function __construct($form, $errormessage = null) { $this->form = $form; $this->errormessage = $errormessage; } /** * Export the page data for the mustache template. * * @param renderer_base $output renderer to be used to render the page elements. * @return stdClass */ public function export_for_template(renderer_base $output) { global $SITE; $sitename = format_string($SITE->fullname); $formhtml = $this->form->render(); $error = $this->errormessage; $context = [ 'sitename' => $sitename, 'formhtml' => $formhtml, 'error' => $error ]; return $context; } } classes/output/login.php 0000644 00000014131 15152311435 0011362 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Login renderable. * * @package core_auth * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\output; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/externallib.php'); use context_system; use help_icon; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; /** * Login renderable class. * * @package core_auth * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class login implements renderable, templatable { /** @var bool Whether to auto focus the form fields. */ public $autofocusform; /** @var bool Whether we can login as guest. */ public $canloginasguest; /** @var bool Whether we can login by e-mail. */ public $canloginbyemail; /** @var bool Whether we can sign-up. */ public $cansignup; /** @var help_icon The cookies help icon. */ public $cookieshelpicon; /** @var string The error message, if any. */ public $error; /** @var moodle_url Forgot password URL. */ public $forgotpasswordurl; /** @var array Additional identify providers, contains the keys 'url', 'name' and 'icon'. */ public $identityproviders; /** @var string Login instructions, if any. */ public $instructions; /** @var moodle_url The form action login URL. */ public $loginurl; /** @var moodle_url The sign-up URL. */ public $signupurl; /** @var string The user name to pre-fill the form with. */ public $username; /** @var string The language selector menu. */ public $languagemenu; /** @var string The csrf token to limit login to requests that come from the login form. */ public $logintoken; /** @var string Maintenance message, if Maintenance is enabled. */ public $maintenance; /** * Constructor. * * @param array $authsequence The enabled sequence of authentication plugins. * @param string $username The username to display. */ public function __construct(array $authsequence, $username = '') { global $CFG, $OUTPUT, $PAGE; $this->username = $username; $languagedata = new \core\output\language_menu($PAGE); $this->languagemenu = $languagedata->export_for_action_menu($OUTPUT); $this->canloginasguest = $CFG->guestloginbutton and !isguestuser(); $this->canloginbyemail = !empty($CFG->authloginviaemail); $this->cansignup = $CFG->registerauth == 'email' || !empty($CFG->registerauth); if ($CFG->rememberusername == 0) { $this->cookieshelpicon = new help_icon('cookiesenabledonlysession', 'core'); } else { $this->cookieshelpicon = new help_icon('cookiesenabled', 'core'); } $this->autofocusform = !empty($CFG->loginpageautofocus); $this->forgotpasswordurl = new moodle_url('/login/forgot_password.php'); $this->loginurl = new moodle_url('/login/index.php'); $this->signupurl = new moodle_url('/login/signup.php'); // Authentication instructions. $this->instructions = $CFG->auth_instructions; if (is_enabled_auth('none')) { $this->instructions = get_string('loginstepsnone'); } else if ($CFG->registerauth == 'email' && empty($this->instructions)) { $this->instructions = get_string('loginsteps', 'core', 'signup.php'); } if ($CFG->maintenance_enabled == true) { if (!empty($CFG->maintenance_message)) { $this->maintenance = $CFG->maintenance_message; } else { $this->maintenance = get_string('sitemaintenance', 'admin'); } } // Identity providers. $this->identityproviders = \auth_plugin_base::get_identity_providers($authsequence); $this->logintoken = \core\session\manager::get_login_token(); } /** * Set the error message. * * @param string $error The error message. */ public function set_error($error) { $this->error = $error; } public function export_for_template(renderer_base $output) { $identityproviders = \auth_plugin_base::prepare_identity_providers_for_output($this->identityproviders, $output); $data = new stdClass(); $data->autofocusform = $this->autofocusform; $data->canloginasguest = $this->canloginasguest; $data->canloginbyemail = $this->canloginbyemail; $data->cansignup = $this->cansignup; $data->cookieshelpicon = $this->cookieshelpicon->export_for_template($output); $data->error = $this->error; $data->forgotpasswordurl = $this->forgotpasswordurl->out(false); $data->hasidentityproviders = !empty($this->identityproviders); $data->hasinstructions = !empty($this->instructions) || $this->cansignup; $data->identityproviders = $identityproviders; list($data->instructions, $data->instructionsformat) = external_format_text($this->instructions, FORMAT_MOODLE, context_system::instance()->id); $data->loginurl = $this->loginurl->out(false); $data->signupurl = $this->signupurl->out(false); $data->username = $this->username; $data->logintoken = $this->logintoken; $data->maintenance = format_text($this->maintenance, FORMAT_MOODLE); $data->languagemenu = $this->languagemenu; return $data; } } classes/output/digital_minor_page.php 0000644 00000003564 15152311435 0014077 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Digital minor renderable. * * @package core_auth * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use templatable; /** * Digital minor renderable class. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class digital_minor_page implements renderable, templatable { /** * Export the page data for the mustache template. * * @param renderer_base $output renderer to be used to render the page elements. * @return stdClass */ public function export_for_template(renderer_base $output) { global $SITE, $CFG; $sitename = format_string($SITE->fullname); $supportname = $CFG->supportname; $supportemail = $CFG->supportemail ?? null; $context = [ 'sitename' => $sitename, 'supportname' => $supportname, 'supportemail' => $supportemail, 'homelink' => new \moodle_url('/') ]; return $context; } } classes/privacy/provider.php 0000644 00000010075 15152311435 0012224 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Data provider. * * @package core_auth * @copyright 2018 Frédéric Massart * @author Frédéric Massart <fred@branchup.tech> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\privacy; defined('MOODLE_INTERNAL') || die(); use context; use core_privacy\local\metadata\collection; use core_privacy\local\request\transform; use core_privacy\local\request\writer; /** * Data provider class. * * @package core_auth * @copyright 2018 Frédéric Massart * @author Frédéric Massart <fred@branchup.tech> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\user_preference_provider { /** * Returns metadata. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { $collection->add_user_preference('auth_forcepasswordchange', 'privacy:metadata:userpref:forcepasswordchange'); $collection->add_user_preference('create_password', 'privacy:metadata:userpref:createpassword'); $collection->add_user_preference('login_failed_count', 'privacy:metadata:userpref:loginfailedcount'); $collection->add_user_preference('login_failed_count_since_success', 'privacy:metadata:userpref:loginfailedcountsincesuccess'); $collection->add_user_preference('login_failed_last', 'privacy:metadata:userpref:loginfailedlast'); $collection->add_user_preference('login_lockout', 'privacy:metadata:userpref:loginlockout'); $collection->add_user_preference('login_lockout_ignored', 'privacy:metadata:userpref:loginlockoutignored'); $collection->add_user_preference('login_lockout_secret', 'privacy:metadata:userpref:loginlockoutsecret'); return $collection; } /** * Export all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { $yesno = function($v) { return transform::yesno($v); }; $datetime = function($v) { return $v ? transform::datetime($v) : null; }; $prefs = [ ['auth_forcepasswordchange', 'forcepasswordchange', $yesno], ['create_password', 'createpassword', $yesno], ['login_failed_count', 'loginfailedcount', null], ['login_failed_count_since_success', 'loginfailedcountsincesuccess', null], ['login_failed_last', 'loginfailedlast', $datetime], ['login_lockout', 'loginlockout', $datetime], ['login_lockout_ignored', 'loginlockoutignored', $yesno], ['login_lockout_secret', 'loginlockoutsecret', null], ]; foreach ($prefs as $prefdata) { list($prefname, $langkey, $transformer) = $prefdata; $value = get_user_preferences($prefname, null, $userid); if ($value === null) { continue; } writer::export_user_preference('core_auth', $prefname, $transformer ? $transformer($value) : $value, get_string("privacy:metadata:userpref:{$langkey}", 'core_auth')); } } } classes/form/verify_age_location_form.php 0000644 00000004150 15152311435 0014710 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Age and location verification mform. * * @package core_auth * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\form; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); use moodleform; /** * Age and location verification mform class. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class verify_age_location_form extends moodleform { /** * Defines the form fields. */ public function definition() { global $CFG; $mform = $this->_form; $mform->addElement('text', 'age', get_string('whatisyourage'), array('optional' => false)); $mform->setType('age', PARAM_RAW); $mform->addRule('age', null, 'required', null, 'client'); $mform->addRule('age', null, 'numeric', null, 'client'); $countries = get_string_manager()->get_list_of_countries(); $defaultcountry[''] = get_string('selectacountry'); $countries = array_merge($defaultcountry, $countries); $mform->addElement('select', 'country', get_string('wheredoyoulive'), $countries); $mform->addRule('country', null, 'required', null, 'client'); $mform->setDefault('country', $CFG->country); $this->add_action_buttons(true, get_string('proceed')); } } classes/external.php 0000644 00000034170 15152311435 0010541 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Auth external API * * @package core_auth * @category external * @copyright 2016 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir . '/externallib.php'); require_once($CFG->libdir . '/authlib.php'); /** * Auth external functions * * @package core_auth * @category external * @copyright 2016 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ class core_auth_external extends external_api { /** * Describes the parameters for confirm_user. * * @return external_function_parameters * @since Moodle 3.2 */ public static function confirm_user_parameters() { return new external_function_parameters( array( 'username' => new external_value(core_user::get_property_type('username'), 'User name'), 'secret' => new external_value(core_user::get_property_type('secret'), 'Confirmation secret'), ) ); } /** * Confirm a user account. * * @param string $username user name * @param string $secret confirmation secret (random string) used for validating the confirm request * @return array warnings and success status (true if the user was confirmed, false if he was already confirmed) * @since Moodle 3.2 * @throws moodle_exception */ public static function confirm_user($username, $secret) { global $PAGE; $warnings = array(); $params = self::validate_parameters( self::confirm_user_parameters(), array( 'username' => $username, 'secret' => $secret, ) ); $context = context_system::instance(); $PAGE->set_context($context); if (!$authplugin = signup_get_user_confirmation_authplugin()) { throw new moodle_exception('confirmationnotenabled'); } $confirmed = $authplugin->user_confirm($username, $secret); if ($confirmed == AUTH_CONFIRM_ALREADY) { $success = false; $warnings[] = array( 'item' => 'user', 'itemid' => 0, 'warningcode' => 'alreadyconfirmed', 'message' => s(get_string('alreadyconfirmed')) ); } else if ($confirmed == AUTH_CONFIRM_OK) { $success = true; } else { throw new moodle_exception('invalidconfirmdata'); } $result = array( 'success' => $success, 'warnings' => $warnings, ); return $result; } /** * Describes the confirm_user return value. * * @return external_single_structure * @since Moodle 3.2 */ public static function confirm_user_returns() { return new external_single_structure( array( 'success' => new external_value(PARAM_BOOL, 'True if the user was confirmed, false if he was already confirmed'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for request_password_reset. * * @return external_function_parameters * @since Moodle 3.4 */ public static function request_password_reset_parameters() { return new external_function_parameters( array( 'username' => new external_value(core_user::get_property_type('username'), 'User name', VALUE_DEFAULT, ''), 'email' => new external_value(core_user::get_property_type('email'), 'User email', VALUE_DEFAULT, ''), ) ); } /** * Requests a password reset. * * @param string $username user name * @param string $email user email * @return array warnings and success status (including notices and errors while processing) * @since Moodle 3.4 * @throws moodle_exception */ public static function request_password_reset($username = '', $email = '') { global $CFG, $PAGE; require_once($CFG->dirroot . '/login/lib.php'); $warnings = array(); $params = self::validate_parameters( self::request_password_reset_parameters(), array( 'username' => $username, 'email' => $email, ) ); $context = context_system::instance(); $PAGE->set_context($context); // Needed by format_string calls. // Check if an alternate forgotten password method is set. if (!empty($CFG->forgottenpasswordurl)) { throw new moodle_exception('cannotmailconfirm'); } $errors = core_login_validate_forgot_password_data($params); if (!empty($errors)) { $status = 'dataerror'; $notice = ''; foreach ($errors as $itemname => $message) { $warnings[] = array( 'item' => $itemname, 'itemid' => 0, 'warningcode' => 'fielderror', 'message' => s($message) ); } } else { list($status, $notice, $url) = core_login_process_password_reset($params['username'], $params['email']); } return array( 'status' => $status, 'notice' => $notice, 'warnings' => $warnings, ); } /** * Describes the request_password_reset return value. * * @return external_single_structure * @since Moodle 3.4 */ public static function request_password_reset_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_ALPHANUMEXT, 'The returned status of the process: dataerror: Error in the sent data (username or email). More information in warnings field. emailpasswordconfirmmaybesent: Email sent or not (depends on user found in database). emailpasswordconfirmnotsent: Failure, user not found. emailpasswordconfirmnoemail: Failure, email not found. emailalreadysent: Email already sent. emailpasswordconfirmsent: User pending confirmation. emailresetconfirmsent: Email sent. '), 'notice' => new external_value(PARAM_RAW, 'Important information for the user about the process.'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for the digital minor check. * * @return external_function_parameters * @since Moodle 3.4 */ public static function is_minor_parameters() { return new external_function_parameters( array( 'age' => new external_value(PARAM_INT, 'Age'), 'country' => new external_value(PARAM_ALPHA, 'Country of residence'), ) ); } /** * Requests a check if a user is digital minor. * * @param int $age User age * @param string $country Country of residence * @return array status (true if the user is a minor, false otherwise) * @since Moodle 3.4 * @throws moodle_exception */ public static function is_minor($age, $country) { global $CFG, $PAGE; require_once($CFG->dirroot . '/login/lib.php'); $params = self::validate_parameters( self::is_minor_parameters(), array( 'age' => $age, 'country' => $country, ) ); if (!array_key_exists($params['country'], get_string_manager()->get_list_of_countries())) { throw new invalid_parameter_exception('Invalid value for country parameter (value: '. $params['country'] .')'); } $context = context_system::instance(); $PAGE->set_context($context); // Check if verification of age and location (minor check) is enabled. if (!\core_auth\digital_consent::is_age_digital_consent_verification_enabled()) { throw new moodle_exception('nopermissions', 'error', '', get_string('agelocationverificationdisabled', 'error')); } $status = \core_auth\digital_consent::is_minor($params['age'], $params['country']); return array( 'status' => $status ); } /** * Describes the is_minor return value. * * @return external_single_structure * @since Moodle 3.4 */ public static function is_minor_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'True if the user is considered to be a digital minor, false if not') ) ); } /** * Describes the parameters for is_age_digital_consent_verification_enabled. * * @return external_function_parameters * @since Moodle 3.3 */ public static function is_age_digital_consent_verification_enabled_parameters() { return new external_function_parameters(array()); } /** * Checks if age digital consent verification is enabled. * * @return array status (true if digital consent verification is enabled, false otherwise.) * @since Moodle 3.3 * @throws moodle_exception */ public static function is_age_digital_consent_verification_enabled() { global $PAGE; $context = context_system::instance(); $PAGE->set_context($context); $status = false; // Check if verification is enabled. if (\core_auth\digital_consent::is_age_digital_consent_verification_enabled()) { $status = true; } return array( 'status' => $status ); } /** * Describes the is_age_digital_consent_verification_enabled return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function is_age_digital_consent_verification_enabled_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'True if digital consent verification is enabled, false otherwise.') ) ); } /** * Describes the parameters for resend_confirmation_email. * * @return external_function_parameters * @since Moodle 3.6 */ public static function resend_confirmation_email_parameters() { return new external_function_parameters( array( 'username' => new external_value(core_user::get_property_type('username'), 'Username.'), 'password' => new external_value(core_user::get_property_type('password'), 'Plain text password.'), 'redirect' => new external_value(PARAM_LOCALURL, 'Redirect the user to this site url after confirmation.', VALUE_DEFAULT, ''), ) ); } /** * Requests resend the confirmation email. * * @param string $username user name * @param string $password plain text password * @param string $redirect redirect the user to this site url after confirmation * @return array warnings and success status * @since Moodle 3.6 * @throws moodle_exception */ public static function resend_confirmation_email($username, $password, $redirect = '') { global $PAGE; $warnings = array(); $params = self::validate_parameters( self::resend_confirmation_email_parameters(), array( 'username' => $username, 'password' => $password, 'redirect' => $redirect, ) ); $context = context_system::instance(); $PAGE->set_context($context); // Need by internal APIs. $username = trim(core_text::strtolower($params['username'])); $password = $params['password']; if (is_restored_user($username)) { throw new moodle_exception('restoredaccountresetpassword', 'webservice'); } $user = authenticate_user_login($username, $password); if (empty($user)) { throw new moodle_exception('invalidlogin'); } if ($user->confirmed) { throw new moodle_exception('alreadyconfirmed'); } // Check if we should redirect the user once the user is confirmed. $confirmationurl = null; if (!empty($params['redirect'])) { // Pass via moodle_url to fix thinks like admin links. $redirect = new moodle_url($params['redirect']); $confirmationurl = new moodle_url('/login/confirm.php', array('redirect' => $redirect->out())); } $status = send_confirmation_email($user, $confirmationurl); return array( 'status' => $status, 'warnings' => $warnings, ); } /** * Describes the resend_confirmation_email return value. * * @return external_single_structure * @since Moodle 3.6 */ public static function resend_confirmation_email_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'True if the confirmation email was sent, false otherwise.'), 'warnings' => new external_warnings(), ) ); } } classes/digital_consent.php 0000644 00000007313 15152311435 0012064 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains helper class for digital consent. * * @package core_auth * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth; defined('MOODLE_INTERNAL') || die(); /** * Helper class for digital consent. * * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class digital_consent { /** * Returns true if age and location verification is enabled in the site. * * @return bool */ public static function is_age_digital_consent_verification_enabled() { global $CFG; return !empty($CFG->agedigitalconsentverification); } /** * Checks if a user is a digital minor. * * @param int $age * @param string $country The country code (ISO 3166-2) * @return bool */ public static function is_minor($age, $country) { global $CFG; $ageconsentmap = $CFG->agedigitalconsentmap; $agedigitalconsentmap = self::parse_age_digital_consent_map($ageconsentmap); return array_key_exists($country, $agedigitalconsentmap) ? $age < $agedigitalconsentmap[$country] : $age < $agedigitalconsentmap['*']; } /** * Parse the agedigitalconsentmap setting into an array. * * @param string $ageconsentmap The value of the agedigitalconsentmap setting * @return array $ageconsentmapparsed */ public static function parse_age_digital_consent_map($ageconsentmap) { $ageconsentmapparsed = array(); $countries = get_string_manager()->get_list_of_countries(true); $isdefaultvaluepresent = false; $lines = preg_split('/\r|\n/', $ageconsentmap, -1, PREG_SPLIT_NO_EMPTY); foreach ($lines as $line) { $arr = explode(",", $line); // Handle if there is more or less than one comma separator. if (count($arr) != 2) { throw new \moodle_exception('agedigitalconsentmapinvalidcomma', 'error', '', $line); } $country = trim($arr[0]); $age = trim($arr[1]); // Check if default. if ($country == "*") { $isdefaultvaluepresent = true; } // Handle if the presented value for country is not valid. if ($country !== "*" && !array_key_exists($country, $countries)) { throw new \moodle_exception('agedigitalconsentmapinvalidcountry', 'error', '', $country); } // Handle if the presented value for age is not valid. if (!is_numeric($age)) { throw new \moodle_exception('agedigitalconsentmapinvalidage', 'error', '', $age); } $ageconsentmapparsed[$country] = $age; } // Handle if a default value does not exist. if (!$isdefaultvaluepresent) { throw new \moodle_exception('agedigitalconsentmapinvaliddefault'); } return $ageconsentmapparsed; } } yui/passwordunmask/passwordunmask.js 0000644 00000003504 15152311435 0014054 0 ustar 00 YUI.add('moodle-auth-passwordunmask', function(Y) { var PASSWORDUNMASK = function() { PASSWORDUNMASK.superclass.constructor.apply(this, arguments); } Y.extend(PASSWORDUNMASK, Y.Base, { // Initialize checkboxes. initializer : function(params) { this.add_checkboxes(); }, // Create checkboxes for all unmasking passwords. add_checkboxes : function() { Y.all('#authmenu input[type=password]').each(function(node) { var checkboxlabel = M.util.get_string('unmaskpassword', 'core_form'); var elementid = node.get('id'); var elementname = node.get('name'); // Retain unmask div from previous implementation. var unmaskdiv = Y.Node.create('<div id="'+elementid+'unmaskdiv" class="unmask"></div>'); // Add checkbox for unmasking to unmaskdiv. var unmaskchb = Y.Node.create('<input id="'+elementid+'unmask" type="checkbox" name="'+elementname+'unmask">'); unmaskdiv.appendChild(unmaskchb); //Attach event using static javascript function for unmasking password. unmaskchb.on('click', function() {unmaskPassword(elementid);}); // Add label for checkbox to unmaskdiv. var unmasklabel = Y.Node.create('<label for="'+elementid+'unmask">'+checkboxlabel+'</label>'); unmaskdiv.appendChild(unmasklabel); // Insert unmask div in the same div as password input. node.get('parentNode').insert(unmaskdiv, node.get('lastNode')); }); return; } }); M.auth = M.auth || {}; M.auth.passwordunmask = function(params) { return new PASSWORDUNMASK(params); } }, '@VERSION@', {requires:['base', 'node']});
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 1.28 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�