diff --git a/admin/tool/mobile/classes/event_handler.php b/admin/tool/mobile/classes/event_handler.php new file mode 100644 index 00000000000..5b9bd3476e4 --- /dev/null +++ b/admin/tool/mobile/classes/event_handler.php @@ -0,0 +1,45 @@ +. + +namespace tool_mobile; + +use core\session\utility\cookie_helper; +use core\event\user_loggedin; + +/** + * Event handler for tool_mobile. + * + * @package tool_mobile + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_handler { + + /** + * Allows the plugin to augment Set-Cookie headers when the user_loggedin event is fired as part of complete_user_login() calls. + * + * @param user_loggedin $event the event + * @return void + */ + public static function handle_user_loggedin(user_loggedin $event): void { + global $CFG; + + // Set Partitioned and Secure attributes to the MoodleSession cookie if the user is using the Moodle app. + if (\core_useragent::is_moodle_app()) { + cookie_helper::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, ['Secure', 'Partitioned']); + } + } +} diff --git a/admin/tool/mobile/db/events.php b/admin/tool/mobile/db/events.php new file mode 100644 index 00000000000..f0d2ab02c1e --- /dev/null +++ b/admin/tool/mobile/db/events.php @@ -0,0 +1,33 @@ +. + +/** + * tool_mobile plugin event handler definition. + * + * @package tool_mobile + * @category event + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$observers = [ + [ + 'eventname' => '\core\event\user_loggedin', + 'callback' => '\tool_mobile\event_handler::handle_user_loggedin', + ], +]; diff --git a/admin/tool/mobile/lib.php b/admin/tool/mobile/lib.php index 3014c9dbc27..9f7a67e1ff0 100644 --- a/admin/tool/mobile/lib.php +++ b/admin/tool/mobile/lib.php @@ -265,3 +265,16 @@ function tool_mobile_pre_processor_message_send($procname, $data) { $data->fullmessagehtml .= html_writer::tag('p', get_string('readingthisemailgettheapp', 'tool_mobile', $url->out())); } } + +/** + * Callback to add headers before the HTTP headers are sent. + * + */ +function tool_mobile_before_http_headers() { + global $CFG; + + // Set Partitioned and Secure attributes to the MoodleSession cookie if the user is using the Moodle app. + if (\core_useragent::is_moodle_app()) { + \core\session\utility\cookie_helper::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, ['Secure', 'Partitioned']); + } +} diff --git a/admin/tool/mobile/version.php b/admin/tool/mobile/version.php index f950b8677ac..d77b4f048a9 100644 --- a/admin/tool/mobile/version.php +++ b/admin/tool/mobile/version.php @@ -23,7 +23,7 @@ */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2022112801; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics). $plugin->dependencies = array( diff --git a/auth/lti/classes/local/ltiadvantage/utility/cookie_helper.php b/auth/lti/classes/local/ltiadvantage/utility/cookie_helper.php index aaaccfd944b..5ed92534450 100644 --- a/auth/lti/classes/local/ltiadvantage/utility/cookie_helper.php +++ b/auth/lti/classes/local/ltiadvantage/utility/cookie_helper.php @@ -15,13 +15,11 @@ // along with Moodle. If not, see . namespace auth_lti\local\ltiadvantage\utility; +use core\session\utility\cookie_helper as core_cookie_helper; /** * Helper class providing utils dealing with cookies in LTI, particularly 3rd party cookies. * - * This class primarily provides a means to augment outbound cookie headers, in order to satisfy browser-specific - * requirements for setting 3rd party cookies. - * * @package auth_lti * @copyright 2024 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -37,62 +35,6 @@ final class cookie_helper { /** @var int Cookies are supported via explicit partitioning. */ public const COOKIE_METHOD_EXPLICIT_PARTITIONING = 2; - /** - * Make sure the given attributes are set on the Set-Cookie response header identified by name=$cookiename. - * - * This function only affects Set-Cookie headers and modifies the headers directly with the required changes, if any. - * - * @param string $cookiename the cookie name. - * @param array $attributes the attributes to set/ensure are set. - * @return void - */ - public static function add_attributes_to_cookie_response_header(string $cookiename, array $attributes): void { - - $setcookieheaders = array_filter(headers_list(), function($val) { - return preg_match("/Set-Cookie:/i", $val); - }); - if (empty($setcookieheaders)) { - return; - } - - $updatedheaders = self::cookie_response_headers_add_attributes($setcookieheaders, [$cookiename], $attributes); - - // Note: The header_remove() method is quite crude and removes all headers of that header name. - header_remove('Set-Cookie'); - foreach ($updatedheaders as $header) { - header($header, false); - } - } - - /** - * Given a list of HTTP header strings, return a list of HTTP header strings where the matched 'Set-Cookie' headers - * have been updated with the attributes defined in $attribute - an array of strings. - * - * This method does not verify whether a given attribute is valid or not. It blindly sets it and returns the header - * strings. It's up to calling code to determine whether an attribute makes sense or not. - * - * @param array $headerstrings the array of header strings. - * @param array $cookiestomatch the array of cookie names to match. - * @param array $attributes the attributes to set on each matched 'Set-Cookie' header. - * @param bool $casesensitive whether to match the attribute in a case-sensitive way. - * @return array the updated array of header strings. - */ - public static function cookie_response_headers_add_attributes(array $headerstrings, array $cookiestomatch, array $attributes, - bool $casesensitive = false): array { - - return array_map(function($headerstring) use ($attributes, $casesensitive, $cookiestomatch) { - if (!self::cookie_response_header_matches_names($headerstring, $cookiestomatch)) { - return $headerstring; - } - foreach ($attributes as $attribute) { - if (!self::cookie_response_header_contains_attribute($headerstring, $attribute, $casesensitive)) { - $headerstring = self::cookie_response_header_append_attribute($headerstring, $attribute); - } - } - return $headerstring; - }, $headerstrings); - } - /** * Check whether cookies can be used with the current user agent and, if so, via what method they are set. * @@ -144,7 +86,7 @@ final class cookie_helper { // LTI needs to guarantee the 'SameSite=None', 'Secure' (and sometimes 'Partitioned') attributes are set on the // MoodleSession cookie. This is done via manipulation of the outgoing headers after the cookie check redirect. To // guarantee these outgoing Set-Cookie headers will be created after the redirect, expire the current cookie. - self::expire_moodlesession(); + core_cookie_helper::expire_moodlesession(); redirect($pageurl); } else { @@ -187,27 +129,6 @@ final class cookie_helper { return $SESSION->auth_lti_cookie_method ?? self::COOKIE_METHOD_NOT_SUPPORTED; } - /** - * Forces the expiry of the MoodleSession cookie. - * - * This is useful to force a new Set-Cookie header on the next redirect. - * - * @return void - */ - private static function expire_moodlesession(): void { - global $CFG; - - $setcookieheader = array_filter(headers_list(), function($val) use ($CFG) { - return self::cookie_response_header_matches_name($val, 'MoodleSession'.$CFG->sessioncookie); - }); - if (!empty($setcookieheader)) { - $expirestr = 'Expires='.gmdate(DATE_RFC7231, time() - 60); - self::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, [$expirestr]); - } else { - setcookie('MoodleSession'.$CFG->sessioncookie, '', time() - 60); - } - } - /** * Sets up the session cookie according to the method used in the cookie check, and with SameSite=None; Secure attributes. * @@ -222,7 +143,7 @@ final class cookie_helper { if (self::get_cookies_supported_method() == self::COOKIE_METHOD_EXPLICIT_PARTITIONING) { $atts[] = 'Partitioned'; } - self::add_attributes_to_cookie_response_header('MoodleSession' . $CFG->sessioncookie, $atts); + core_cookie_helper::add_attributes_to_cookie_response_header('MoodleSession' . $CFG->sessioncookie, $atts); } } @@ -246,66 +167,7 @@ final class cookie_helper { setcookie($name, $value, $atts); if (is_moodle_cookie_secure() && $partitioned) { - self::add_attributes_to_cookie_response_header($name, ['Partitioned']); + core_cookie_helper::add_attributes_to_cookie_response_header($name, ['Partitioned']); } } - - /** - * Check whether the header string is a 'Set-Cookie' header for the cookie identified by $cookiename. - * - * @param string $headerstring the header string to check. - * @param string $cookiename the name of the cookie to match. - * @return bool true if the header string is a Set-Cookie header for the named cookie, false otherwise. - */ - private static function cookie_response_header_matches_name(string $headerstring, string $cookiename): bool { - // Generally match the format, but in a case-insensitive way so that 'set-cookie' and "SET-COOKIE" are both valid. - return preg_match("/Set-Cookie: *$cookiename=/i", $headerstring) - // Case-sensitive match on cookiename, which is case-sensitive. - && preg_match("/: *$cookiename=/", $headerstring); - } - - /** - * Check whether the header string is a 'Set-Cookie' header for the cookies identified in the $cookienames array. - * - * @param string $headerstring the header string to check. - * @param array $cookienames the array of cookie names to match. - * @return bool true if the header string is a Set-Cookie header for one of the named cookies, false otherwise. - */ - private static function cookie_response_header_matches_names(string $headerstring, array $cookienames): bool { - foreach ($cookienames as $cookiename) { - if (self::cookie_response_header_matches_name($headerstring, $cookiename)) { - return true; - } - } - return false; - } - - /** - * Check whether the header string contains the given attribute. - * - * @param string $headerstring the header string to check. - * @param string $attribute the attribute to check for. - * @param bool $casesensitive whether to perform a case-sensitive check. - * @return bool true if the header contains the attribute, false otherwise. - */ - private static function cookie_response_header_contains_attribute(string $headerstring, string $attribute, - bool $casesensitive): bool { - - if ($casesensitive) { - return strpos($headerstring, $attribute) !== false; - } - return strpos(strtolower($headerstring), strtolower($attribute)) !== false; - } - - /** - * Append the given attribute to the header string. - * - * @param string $headerstring the header string to append to. - * @param string $attribute the attribute to append. - * @return string the updated header string. - */ - private static function cookie_response_header_append_attribute(string $headerstring, string $attribute): string { - $headerstring = rtrim($headerstring, ';'); // Sometimes included. - return "$headerstring; $attribute;"; - } } diff --git a/lib/classes/session/utility/cookie_helper.php b/lib/classes/session/utility/cookie_helper.php new file mode 100644 index 00000000000..75f2eb6433b --- /dev/null +++ b/lib/classes/session/utility/cookie_helper.php @@ -0,0 +1,166 @@ +. + +namespace core\session\utility; + +/** + * Helper class providing utils dealing with cookies, particularly 3rd party cookies. + * + * This class primarily provides a means to augment outbound cookie headers, in order to satisfy browser-specific + * requirements for setting 3rd party cookies. + * + * @package core + * @copyright 2024 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class cookie_helper { + + /** + * Make sure the given attributes are set on the Set-Cookie response header identified by name=$cookiename. + * + * This function only affects Set-Cookie headers and modifies the headers directly with the required changes, if any. + * + * @param string $cookiename the cookie name. + * @param array $attributes the attributes to set/ensure are set. + * @return void + */ + public static function add_attributes_to_cookie_response_header(string $cookiename, array $attributes): void { + + $setcookieheaders = array_filter(headers_list(), function($val) { + return preg_match("/Set-Cookie:/i", $val); + }); + if (empty($setcookieheaders)) { + return; + } + + $updatedheaders = self::cookie_response_headers_add_attributes($setcookieheaders, [$cookiename], $attributes); + + // Note: The header_remove() method is quite crude and removes all headers of that header name. + header_remove('Set-Cookie'); + foreach ($updatedheaders as $header) { + header($header, false); + } + } + + /** + * Given a list of HTTP header strings, return a list of HTTP header strings where the matched 'Set-Cookie' headers + * have been updated with the attributes defined in $attribute - an array of strings. + * + * This method does not verify whether a given attribute is valid or not. It blindly sets it and returns the header + * strings. It's up to calling code to determine whether an attribute makes sense or not. + * + * @param array $headerstrings the array of header strings. + * @param array $cookiestomatch the array of cookie names to match. + * @param array $attributes the attributes to set on each matched 'Set-Cookie' header. + * @param bool $casesensitive whether to match the attribute in a case-sensitive way. + * @return array the updated array of header strings. + */ + public static function cookie_response_headers_add_attributes(array $headerstrings, array $cookiestomatch, array $attributes, + bool $casesensitive = false): array { + + return array_map(function($headerstring) use ($attributes, $casesensitive, $cookiestomatch) { + if (!self::cookie_response_header_matches_names($headerstring, $cookiestomatch)) { + return $headerstring; + } + foreach ($attributes as $attribute) { + if (!self::cookie_response_header_contains_attribute($headerstring, $attribute, $casesensitive)) { + $headerstring = self::cookie_response_header_append_attribute($headerstring, $attribute); + } + } + return $headerstring; + }, $headerstrings); + } + + /** + * Forces the expiry of the MoodleSession cookie. + * + * This is useful to force a new Set-Cookie header on the next redirect. + * + * @return void + */ + public static function expire_moodlesession(): void { + global $CFG; + + $setcookieheader = array_filter(headers_list(), function($val) use ($CFG) { + return self::cookie_response_header_matches_name($val, 'MoodleSession'.$CFG->sessioncookie); + }); + if (!empty($setcookieheader)) { + $expirestr = 'Expires='.gmdate(DATE_RFC7231, time() - 60); + self::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, [$expirestr]); + } else { + setcookie('MoodleSession'.$CFG->sessioncookie, '', time() - 60); + } + } + + /** + * Check whether the header string is a 'Set-Cookie' header for the cookie identified by $cookiename. + * + * @param string $headerstring the header string to check. + * @param string $cookiename the name of the cookie to match. + * @return bool true if the header string is a Set-Cookie header for the named cookie, false otherwise. + */ + private static function cookie_response_header_matches_name(string $headerstring, string $cookiename): bool { + // Generally match the format, but in a case-insensitive way so that 'set-cookie' and "SET-COOKIE" are both valid. + return preg_match("/Set-Cookie: *$cookiename=/i", $headerstring) + // Case-sensitive match on cookiename, which is case-sensitive. + && preg_match("/: *$cookiename=/", $headerstring); + } + + /** + * Check whether the header string is a 'Set-Cookie' header for the cookies identified in the $cookienames array. + * + * @param string $headerstring the header string to check. + * @param array $cookienames the array of cookie names to match. + * @return bool true if the header string is a Set-Cookie header for one of the named cookies, false otherwise. + */ + private static function cookie_response_header_matches_names(string $headerstring, array $cookienames): bool { + foreach ($cookienames as $cookiename) { + if (self::cookie_response_header_matches_name($headerstring, $cookiename)) { + return true; + } + } + return false; + } + + /** + * Check whether the header string contains the given attribute. + * + * @param string $headerstring the header string to check. + * @param string $attribute the attribute to check for. + * @param bool $casesensitive whether to perform a case-sensitive check. + * @return bool true if the header contains the attribute, false otherwise. + */ + private static function cookie_response_header_contains_attribute(string $headerstring, string $attribute, + bool $casesensitive): bool { + + if ($casesensitive) { + return str_contains($headerstring, $attribute); + } + return str_contains(strtolower($headerstring), strtolower($attribute)); + } + + /** + * Append the given attribute to the header string. + * + * @param string $headerstring the header string to append to. + * @param string $attribute the attribute to append. + * @return string the updated header string. + */ + private static function cookie_response_header_append_attribute(string $headerstring, string $attribute): string { + $headerstring = rtrim($headerstring, ';'); // Sometimes included. + return "$headerstring; $attribute;"; + } +} diff --git a/lib/lti1p3/src/ImsStorage/ImsCookie.php b/lib/lti1p3/src/ImsStorage/ImsCookie.php index 6c6b4c479e7..aa69b4c88a2 100644 --- a/lib/lti1p3/src/ImsStorage/ImsCookie.php +++ b/lib/lti1p3/src/ImsStorage/ImsCookie.php @@ -2,7 +2,7 @@ namespace Packback\Lti1p3\ImsStorage; -use auth_lti\local\ltiadvantage\utility\cookie_helper; +use core\session\utility\cookie_helper; use Packback\Lti1p3\Interfaces\ICookie; class ImsCookie implements ICookie diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index ad9ae0b8139..46d396158ce 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -523,7 +523,7 @@ class component_test extends advanced_testcase { $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', 'output\\myprofile')); // Without namespace it returns classes/ classes. - $this->assertCount(5, core_component::get_component_classes_in_namespace('tool_mobile', '')); + $this->assertCount(6, core_component::get_component_classes_in_namespace('tool_mobile', '')); $this->assertCount(2, core_component::get_component_classes_in_namespace('tool_filetypes')); // When no component is specified, classes are returned for the namespace in all components. diff --git a/auth/lti/tests/local/ltiadvantage/utility/cookie_helper_test.php b/lib/tests/session/utility/cookie_helper_test.php similarity index 98% rename from auth/lti/tests/local/ltiadvantage/utility/cookie_helper_test.php rename to lib/tests/session/utility/cookie_helper_test.php index b7d79d79b39..03d5455b38e 100644 --- a/auth/lti/tests/local/ltiadvantage/utility/cookie_helper_test.php +++ b/lib/tests/session/utility/cookie_helper_test.php @@ -14,15 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace auth_lti\local\ltiadvantage\utility; +namespace core\session\utility; /** * Tests for the cookie_helper utility class. * - * @package auth_lti + * @package core * @copyright 2024 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @covers \auth_lti\local\ltiadvantage\utility\cookie_helper + * @covers \core\session\utility\cookie_helper */ class cookie_helper_test extends \advanced_testcase {