Skip to content

Commit cf65ee8

Browse files
committed
feat: add local caching for remote IdP logos
1 parent f5a33af commit cf65ee8

6 files changed

Lines changed: 203 additions & 7 deletions

File tree

classes/admin/setting_idpmetadata.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use admin_setting_configtextarea;
2020
use auth_saml2\idp_data;
2121
use auth_saml2\idp_parser;
22+
use auth_saml2\local\idp_logo_cache;
2223
use DOMDocument;
2324
use DOMElement;
2425
use DOMNodeList;
@@ -170,7 +171,8 @@ private function process_idp_xml(
170171
}
171172

172173
if (!empty($logo) && $oldidp->logo !== $logo) {
173-
$DB->set_field('auth_saml2_idps', 'logo', $logo, ['id' => $oldidp->id]);
174+
idp_logo_cache::delete_cached_logo($oldidp->id);
175+
idp_logo_cache::cache_logo($logo, $oldidp->id);
174176
}
175177

176178
// Remove the idp from the current array so that we don't delete it later.
@@ -185,7 +187,11 @@ private function process_idp_xml(
185187
$newidp->defaultname = $idpname;
186188
$newidp->logo = $logo;
187189

188-
$DB->insert_record('auth_saml2_idps', $newidp);
190+
$idpid = $DB->insert_record('auth_saml2_idps', $newidp);
191+
192+
if ($idpid) {
193+
idp_logo_cache::cache_logo($logo, $idpid);
194+
}
189195
}
190196
}
191197

@@ -200,6 +206,7 @@ private function remove_old_idps($oldidps) {
200206
foreach ($oldidps as $metadataidps) {
201207
foreach ($metadataidps as $oldidp) {
202208
$DB->delete_records('auth_saml2_idps', ['id' => $oldidp->id]);
209+
idp_logo_cache::delete_cached_logo($oldidp->id);
203210
}
204211
}
205212
}

classes/auth.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use moodle_url;
3030
use pix_icon;
3131
use auth_saml2\admin\saml2_settings;
32+
use auth_saml2\local\idp_logo_cache;
3233
use coding_exception;
3334
use core\output\notification;
3435
use dml_exception;
@@ -293,11 +294,9 @@ class_exists(\tool_tenant\local\auth\saml2\manager::class) && !component_class_c
293294
$idpurl->param('passive', 'off');
294295

295296
// A default icon.
296-
$idpiconurl = null;
297297
$idpicon = null;
298-
if (!empty($idp->logo)) {
299-
$idpiconurl = new moodle_url($idp->logo);
300-
} else {
298+
$idpiconurl = idp_logo_cache::get_cached_logo($idp);
299+
if (!$idpiconurl) {
301300
$idpicon = new pix_icon('i/user', 'Login');
302301
}
303302

classes/local/idp_logo_cache.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace auth_saml2\local;
18+
19+
use core\url as moodle_url;
20+
use core\context\system;
21+
use file_exception;
22+
23+
/**
24+
* Class idp_logo_cache
25+
*
26+
* @package auth_saml2
27+
* @copyright 2026 University of Graz
28+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29+
*/
30+
class idp_logo_cache {
31+
const COMPONENT = 'auth_saml2';
32+
const FILEAREA = 'idplogo';
33+
const FILEPATH = '/';
34+
35+
/**
36+
* Cache remote idp icon to local plugin filearea.
37+
*
38+
* @param string $remoteurl The remote origin URL of the icon/logo to cache.
39+
* @param int $itemid The id of the idp in the auth_saml2_idps table.
40+
* @return \stored_file|false
41+
*/
42+
public static function cache_logo($remoteurl, $itemid) {
43+
$fs = get_file_storage();
44+
45+
$fileinfo = [
46+
'contextid' => system::instance()->id,
47+
'component' => self::COMPONENT,
48+
'filearea' => self::FILEAREA,
49+
'itemid' => $itemid,
50+
'filepath' => self::FILEPATH,
51+
'filename' => self::get_filename($remoteurl),
52+
];
53+
54+
try {
55+
$file = $fs->create_file_from_url($fileinfo, url: $remoteurl);
56+
return $file;
57+
} catch (file_exception $e) {
58+
return false;
59+
}
60+
}
61+
62+
/**
63+
* Delete a cached logo.
64+
*
65+
* @param int $itemid The id of the idp. `itemid` of the file is the `auth_saml2_idps.id`.
66+
* @return void
67+
*/
68+
public static function delete_cached_logo($itemid) {
69+
$fs = get_file_storage();
70+
$fs->delete_area_files(system::instance()->id, self::COMPONENT, self::FILEAREA, $itemid);
71+
}
72+
73+
/**
74+
* Get the URL of a cached logo, only if the file exists.
75+
*
76+
* @param object $idp
77+
* @return null|moodle_url
78+
*/
79+
public static function get_cached_logo($idp) {
80+
if (empty($idp->logo)) {
81+
return null;
82+
}
83+
84+
$context = system::instance();
85+
$component = self::COMPONENT;
86+
$filearea = self::FILEAREA;
87+
$itemid = $idp->id;
88+
$filepath = self::FILEPATH;
89+
$filename = self::get_filename($idp->logo);
90+
91+
if ($filename === '' || $filename === null) {
92+
return null;
93+
}
94+
95+
$fs = get_file_storage();
96+
$file = $fs->get_file(
97+
$context->id,
98+
$component,
99+
$filearea,
100+
$itemid,
101+
$filepath,
102+
$filename
103+
);
104+
105+
if (!$file) {
106+
return null;
107+
}
108+
109+
return \moodle_url::make_pluginfile_url(
110+
$file->get_contextid(),
111+
$file->get_component(),
112+
$file->get_filearea(),
113+
$file->get_itemid(),
114+
$file->get_filepath(),
115+
$file->get_filename(),
116+
false
117+
);
118+
}
119+
120+
121+
/**
122+
* Generate a unique filename via hashing the remote source URL and taking the original file extension.
123+
*
124+
* @param mixed $remoteurl
125+
* @return string
126+
*/
127+
private static function get_filename($remoteurl): string {
128+
$hash = md5($remoteurl);
129+
// Try to preserve extension from URL, default to png.
130+
$basename = basename(parse_url($remoteurl, PHP_URL_PATH) ?? '');
131+
$ext = pathinfo($basename, PATHINFO_EXTENSION) ?: 'png';
132+
return "{$hash}.{$ext}";
133+
}
134+
}

db/upgrade.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2323
*/
2424

25+
use auth_saml2\local\idp_logo_cache;
2526
use auth_saml2\task\metadata_refresh;
2627
use auth_saml2\ssl_algorithms;
2728

@@ -408,5 +409,17 @@ function xmldb_auth_saml2_upgrade($oldversion) {
408409
upgrade_plugin_savepoint(true, 2023100300, 'auth', 'saml2');
409410
}
410411

412+
if ($oldversion < 2026021300) {
413+
// Cache the idps logos.
414+
$idps = $DB->get_records('auth_saml2_idps');
415+
foreach ($idps as $idp) {
416+
if (!empty($idp->logo)) {
417+
idp_logo_cache::cache_logo($idp->logo, $idp->id);
418+
}
419+
}
420+
421+
upgrade_plugin_savepoint(true, 2026021300, 'auth', 'saml2');
422+
}
423+
411424
return true;
412425
}

lib.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,46 @@ function auth_saml2_status_checks(): array {
4848
}
4949
return [];
5050
}
51+
52+
/**
53+
* Serve the files from the auth_saml2 file areas.
54+
*
55+
* @param stdClass $course the course object
56+
* @param stdClass $cm the course module object
57+
* @param stdClass $context the context
58+
* @param string $filearea the name of the file area
59+
* @param array $args extra arguments (itemid, path)
60+
* @param bool $forcedownload whether or not force download
61+
* @param array $options additional options affecting the file serving
62+
* @return bool|void false if the file not found, just send the file otherwise and do not return anything
63+
*/
64+
function auth_saml2_pluginfile(
65+
$course,
66+
$cm,
67+
$context,
68+
string $filearea,
69+
array $args,
70+
bool $forcedownload,
71+
array $options = []
72+
) {
73+
global $DB;
74+
75+
if ($context->contextlevel != CONTEXT_SYSTEM) {
76+
return false;
77+
}
78+
if ($filearea !== 'idplogo') {
79+
return false;
80+
}
81+
82+
$itemid = array_shift($args);
83+
$filename = array_pop($args);
84+
$filepath = '/';
85+
86+
$fs = get_file_storage();
87+
$file = $fs->get_file($context->id, 'auth_saml2', $filearea, $itemid, $filepath, $filename);
88+
if (!$file) {
89+
return false;
90+
}
91+
92+
send_stored_file($file, DAYSECS, 0, $forcedownload, $options);
93+
}

version.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
defined('MOODLE_INTERNAL') || die();
2626

27-
$plugin->version = 2025121500; // The current plugin version (Date: YYYYMMDDXX).
27+
$plugin->version = 2026021300; // The current plugin version (Date: YYYYMMDDXX).
2828
$plugin->release = 2025121500; // Match release exactly to version.
2929
$plugin->requires = 2025040400; // Requires Moodle 5.0
3030
$plugin->component = 'auth_saml2'; // Full name of the plugin (used for diagnostics).

0 commit comments

Comments
 (0)