Passed
Push — master ( 4426f1...9211cf )
by Tim
02:20
created

Aggregator::isCacheValid()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 20
c 0
b 0
f 0
nc 8
nop 2
dl 0
loc 34
rs 8.6666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\aggregator2;
6
7
use DateTimeImmutable;
8
use DateTimeZone;
9
use Exception;
10
use SimpleSAML\Configuration;
11
use SimpleSAML\Logger;
12
use SimpleSAML\SAML2\Constants as C;
13
use SimpleSAML\SAML2\XML\md\EntitiesDescriptor;
14
use SimpleSAML\SAML2\XML\md\EntityDescriptor;
15
use SimpleSAML\SAML2\XML\md\Extensions;
16
use SimpleSAML\SAML2\XML\mdrpi\PublicationInfo;
17
use SimpleSAML\SAML2\XML\mdrpi\RegistrationInfo;
18
use SimpleSAML\Utils;
19
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
20
use SimpleSAML\XMLSecurity\CryptoEncoding\PEM;
21
use SimpleSAML\XMLSecurity\Key\PrivateKey;
22
use SimpleSAML\XMLSecurity\XML\ds\KeyInfo;
23
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
24
use SimpleSAML\XMLSecurity\XML\ds\X509Data;
25
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
26
27
use function array_intersect;
28
use function array_keys;
29
use function array_merge;
30
use function array_unique;
31
use function explode;
32
use function file_exists;
33
use function file_get_contents;
34
use function get_class;
35
use function in_array;
36
use function intval;
37
use function serialize;
38
use function sha1;
39
use function strval;
40
use function time;
41
use function var_export;
42
43
/**
44
 * Class which implements a basic metadata aggregator.
45
 *
46
 * @package SimpleSAMLphp
47
 */
48
class Aggregator
49
{
50
    /**
51
     * The ID of this aggregator.
52
     *
53
     * @var string
54
     */
55
    protected string $id;
56
57
    /**
58
     * Our log "location".
59
     *
60
     * @var string
61
     */
62
    protected string $logLoc;
63
64
    /**
65
     * Which cron-tag this should be updated in.
66
     *
67
     * @var string|null
68
     */
69
    protected ?string $cronTag;
70
71
    /**
72
     * Absolute path to a cache directory.
73
     *
74
     * @var string|null
75
     */
76
    protected ?string $cacheDirectory;
77
78
    /**
79
     * The entity sources.
80
     *
81
     * Array of sspmod_aggregator2_EntitySource objects.
82
     *
83
     * @var array
84
     */
85
    protected array $sources = [];
86
87
    /**
88
     * How long the generated metadata should be valid, as a number of seconds.
89
     *
90
     * This is used to set the validUntil attribute on the generated EntityDescriptor.
91
     *
92
     * @var int
93
     */
94
    protected int $validLength;
95
96
    /**
97
     * Duration we should cache generated metadata.
98
     *
99
     * @var int|null
100
     */
101
    protected ?int $cacheGenerated;
102
103
    /**
104
     * An array of entity IDs to exclude from the aggregate.
105
     *
106
     * @var string[]
107
     */
108
    protected array $excluded = [];
109
110
    /**
111
     * An indexed array of protocols to filter the aggregate by. keys can be any of:
112
     *
113
     * - urn:oasis:names:tc:SAML:1.1:protocol
114
     * - urn:oasis:names:tc:SAML:2.0:protocol
115
     *
116
     * Values will be true if enabled, false otherwise.
117
     *
118
     * @var array
119
     */
120
    protected array $protocols = [];
121
122
    /**
123
     * An array of roles to filter the aggregate by. Keys can be any of:
124
     *
125
     * - SimpleSAML\SAML2\XML\md\IDPSSODescriptor
126
     * - SimpleSAML\SAML2\XML\md\SPSSODescriptor
127
     * - SimpleSAML\SAML2\XML\md\AttributeAuthorityDescriptor
128
     *
129
     * Values will be true if enabled, false otherwise.
130
     *
131
     * @var array
132
     */
133
    protected array $roles;
134
135
    /**
136
     * The key we should use to sign the metadata.
137
     *
138
     * @var string|null
139
     */
140
    protected ?string $signKey = null;
141
142
    /**
143
     * The password for the private key.
144
     *
145
     * @var string|null
146
     */
147
    protected ?string $signKeyPass;
148
149
    /**
150
     * The certificate of the key we sign the metadata with.
151
     *
152
     * @var string|null
153
     */
154
    protected ?string $signCert;
155
156
    /**
157
     * The algorithm to use for metadata signing.
158
     *
159
     * @var string|null
160
     */
161
    protected ?string $signAlg;
162
163
    /**
164
     * The CA certificate file that should be used to validate https-connections.
165
     *
166
     * @var string|null
167
     */
168
    protected ?string $sslCAFile;
169
170
    /**
171
     * The cache ID for our generated metadata.
172
     *
173
     * @var string
174
     */
175
    protected string $cacheId = 'dummy';
176
177
    /**
178
     * The cache tag for our generated metadata.
179
     *
180
     * This tag is used to make sure that a config change
181
     * invalidates our cached metadata.
182
     *
183
     * @var string
184
     */
185
    protected string $cacheTag = 'dummy';
186
187
    /**
188
     * The registration information for our generated metadata.
189
     *
190
     * @var array
191
     */
192
    protected array $regInfo;
193
194
    /**
195
     * The publication information for our generated metadata.
196
     *
197
     * @var array
198
     */
199
    protected array $pubInfo;
200
201
    /**
202
     * The name for the EntitiesDescriptor
203
     *
204
     * @var string|null
205
     */
206
    protected ?string $name;
207
208
209
    /**
210
     * Initialize this aggregator.
211
     *
212
     * @param string $id  The id of this aggregator.
213
     * @param \SimpleSAML\Configuration $config  The configuration for this aggregator.
214
     */
215
    protected function __construct(string $id, Configuration $config)
216
    {
217
        $sysUtils = new Utils\System();
218
        $this->id = $id;
219
        $this->name = $config->getOptionalString('name', null);
220
        $this->logLoc = 'aggregator2:' . $this->id . ': ';
221
        $this->cronTag = $config->getOptionalString('cron.tag', null);
222
223
        $this->cacheDirectory = $config->getOptionalString('cache.directory', $sysUtils->getTempDir());
224
        if ($this->cacheDirectory !== null) {
225
            $this->cacheDirectory = $sysUtils->resolvePath($this->cacheDirectory);
226
        }
227
228
        $this->cacheGenerated = $config->getOptionalInteger('cache.generated', null);
229
        if ($this->cacheGenerated !== null) {
230
            $this->cacheId = sha1($this->id);
231
            $this->cacheTag = sha1(serialize($config));
232
        }
233
234
        // configure entity IDs excluded by default
235
        $this->excludeEntities($config->getOptionalArrayize('exclude', []));
236
237
        // configure filters
238
        $this->setFilters($config->getOptionalArrayize('filter', []));
239
240
        $this->validLength = $config->getOptionalInteger('valid.length', 7 * 24 * 60 * 60);
241
242
        $globalConfig = Configuration::getInstance();
243
        $certDir = $globalConfig->getPathValue('certdir', 'cert/');
244
245
        $signKey = $config->getOptionalString('sign.privatekey', null);
246
        if ($signKey !== null) {
247
            $signKey = $sysUtils->resolvePath($signKey, $certDir);
248
            $this->signKey = PEM::fromFile($signKey);
249
        }
250
251
        $this->signKeyPass = $config->getOptionalString('sign.privatekey_pass', null);
252
253
        $signCert = $config->getOptionalString('sign.certificate', null);
254
        if ($signCert !== null) {
255
            $signCert = $sysUtils->resolvePath($signCert, $certDir);
256
            $this->signCert = PEM::fromFile($signCert);
257
        }
258
259
        $this->signAlg = $config->getOptionalString('sign.algorithm', C::SIG_RSA_SHA256);
260
        if (!in_array($this->signAlg, array_keys(C::$RSA_DIGESTS))) {
261
            throw new Exception('Unsupported signature algorithm ' . var_export($this->signAlg, true));
262
        }
263
264
        $this->sslCAFile = $config->getOptionalString('ssl.cafile', null);
265
266
        $this->regInfo = $config->getOptionalArray('RegistrationInfo', []);
267
        $this->pubInfo = $config->getOptionalArray('PublicationInfo', []);
268
269
        $this->initSources($config->getOptionalArray('sources', []));
0 ignored issues
show
Bug introduced by
It seems like $config->getOptionalArray('sources', array()) can also be of type null; however, parameter $sources of SimpleSAML\Module\aggreg...gregator::initSources() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

269
        $this->initSources(/** @scrutinizer ignore-type */ $config->getOptionalArray('sources', []));
Loading history...
270
    }
271
272
273
    /**
274
     * Populate the sources array.
275
     *
276
     * This is called from the constructor, and can be overridden in subclasses.
277
     *
278
     * @param array $sources  The sources as an array of \SimpleSAML\Configuration objects.
279
     */
280
    protected function initSources(array $sources): void
281
    {
282
        foreach ($sources as $source) {
283
            $this->sources[] = new EntitySource($this, Configuration::loadFromArray($source));
284
        }
285
    }
286
287
288
    /**
289
     * Return an instance of the aggregator with the given id.
290
     *
291
     * @param string $id  The id of the aggregator.
292
     * @return Aggregator
293
     */
294
    public static function getAggregator(string $id): Aggregator
295
    {
296
        $config = Configuration::getConfig('module_aggregator2.php');
297
        /** @psalm-suppress PossiblyNullArgument */
298
        return new Aggregator($id, $config->getOptionalConfigItem($id, []));
0 ignored issues
show
Bug introduced by
It seems like $config->getOptionalConfigItem($id, array()) can also be of type null; however, parameter $config of SimpleSAML\Module\aggreg...gregator::__construct() does only seem to accept SimpleSAML\Configuration, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

298
        return new Aggregator($id, /** @scrutinizer ignore-type */ $config->getOptionalConfigItem($id, []));
Loading history...
299
    }
300
301
302
    /**
303
     * Retrieve the ID of the aggregator.
304
     *
305
     * @return string  The ID of this aggregator.
306
     */
307
    public function getId(): string
308
    {
309
        return $this->id;
310
    }
311
312
313
    /**
314
     * Add an item to the cache.
315
     *
316
     * @param string $id  The identifier of this data.
317
     * @param string $data  The data.
318
     * @param int $expires  The timestamp the data expires.
319
     * @param string|null $tag  An extra tag that can be used to verify the validity of the cached data.
320
     */
321
    public function addCacheItem(string $id, string $data, int $expires, string $tag = null): void
322
    {
323
        $sysUtils = new Utils\System();
324
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
325
        try {
326
            $sysUtils->writeFile($cacheFile, $data);
327
        } catch (Exception $e) {
328
            Logger::warning($this->logLoc . 'Unable to write to cache file ' . var_export($cacheFile, true));
329
            return;
330
        }
331
332
        $expireInfo = strval($expires);
333
        if ($tag !== null) {
334
            $expireInfo .= ':' . $tag;
335
        }
336
337
        $expireFile = $cacheFile . '.expire';
338
        try {
339
            $sysUtils->writeFile($expireFile, $expireInfo);
340
        } catch (Exception $e) {
341
            Logger::warning($this->logLoc . 'Unable to write expiration info to ' . var_export($expireFile, true));
342
        }
343
    }
344
345
346
    /**
347
     * Check validity of cached data.
348
     *
349
     * @param string $id  The identifier of this data.
350
     * @param string|null $tag  The tag that was passed to addCacheItem.
351
     * @return bool  TRUE if the data is valid, FALSE if not.
352
     */
353
    public function isCacheValid(string $id, string $tag = null): bool
354
    {
355
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
356
        if (!file_exists($cacheFile)) {
357
            return false;
358
        }
359
360
        $expireFile = $cacheFile . '.expire';
361
        if (!file_exists($expireFile)) {
362
            return false;
363
        }
364
365
        $expireData = @file_get_contents($expireFile);
366
        if ($expireData === false) {
367
            return false;
368
        }
369
370
        $expireData = explode(':', $expireData, 2);
371
372
        $expireTime = intval($expireData[0]);
373
        if ($expireTime <= time()) {
374
            return false;
375
        }
376
377
        if (count($expireData) === 1) {
378
            $expireTag = null;
379
        } else {
380
            $expireTag = $expireData[1];
381
        }
382
        if ($expireTag !== $tag) {
383
            return false;
384
        }
385
386
        return true;
387
    }
388
389
390
    /**
391
     * Get the cache item.
392
     *
393
     * @param string $id  The identifier of this data.
394
     * @param string|null $tag  The tag that was passed to addCacheItem.
395
     * @return string|null  The cache item, or NULL if it isn't cached or if it is expired.
396
     */
397
    public function getCacheItem(string $id, string $tag = null): ?string
398
    {
399
        if (!$this->isCacheValid($id, $tag)) {
400
            return null;
401
        }
402
403
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
404
        return @file_get_contents($cacheFile);
0 ignored issues
show
Bug Best Practice introduced by
The expression return @file_get_contents($cacheFile) could return the type false which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
405
    }
406
407
408
    /**
409
     * Get the cache filename for the specific id.
410
     *
411
     * @param string $id  The identifier of the cached data.
412
     * @return string|null  The filename, or NULL if the cache file doesn't exist.
413
     */
414
    public function getCacheFile(string $id): ?string
415
    {
416
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
417
        if (!file_exists($cacheFile)) {
418
            return null;
419
        }
420
421
        return $cacheFile;
422
    }
423
424
425
    /**
426
     * Retrieve the SSL CA file path, if it is set.
427
     *
428
     * @return string|null  The SSL CA file path.
429
     */
430
    public function getCAFile(): ?string
431
    {
432
        return $this->sslCAFile;
433
    }
434
435
436
    /**
437
     * Sign the generated EntitiesDescriptor.
438
     */
439
    protected function addSignature(SignableElementInterface $element): void
440
    {
441
        if ($this->signKey === null) {
442
            return;
443
        }
444
445
        $keyInfo = null;
446
        if ($this->signCert !== null) {
447
            $keyInfo = new KeyInfo(
448
                [
449
                    new X509Data(
450
                        [
451
                            new X509Certificate($this->signCert->getMaterial()),
452
                        ],
453
                    ),
454
                ],
455
            );
456
        }
457
458
        /** @var string $this->signAlg */
459
        $key = PrivateKey::fromFile($this->signKey, $this->signKeyPass);
0 ignored issues
show
Bug introduced by
It seems like $this->signKeyPass can also be of type null; however, parameter $passphrase of SimpleSAML\XMLSecurity\Key\PrivateKey::fromFile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

459
        $key = PrivateKey::fromFile($this->signKey, /** @scrutinizer ignore-type */ $this->signKeyPass);
Loading history...
460
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(
461
            $this->signAlg,
0 ignored issues
show
Bug introduced by
It seems like $this->signAlg can also be of type null; however, parameter $algId of SimpleSAML\XMLSecurity\A...Factory::getAlgorithm() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

461
            /** @scrutinizer ignore-type */ $this->signAlg,
Loading history...
462
            $key
463
        );
464
465
        $element->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);
466
    }
467
468
469
    /**
470
     * Recursively browse the children of an EntitiesDescriptor element looking for EntityDescriptor elements, and
471
     * return an array containing all of them.
472
     *
473
     * @param \SAML2\XML\md\EntitiesDescriptor $entity The source EntitiesDescriptor that holds the entities to extract.
474
     *
475
     * @return array An array containing all the EntityDescriptors found.
476
     */
477
    private static function extractEntityDescriptors(EntitiesDescriptor $entity): array
478
    {
479
        $results = [];
480
        $descriptors = array_merge($entity->getEntityDescriptors(), $entity->getEntitiesDescriptors());
481
        foreach ($descriptors as $child) {
482
            if ($child instanceof EntityDescriptor) {
483
                $results[] = $child;
484
                continue;
485
            }
486
487
            $results = array_merge($results, self::extractEntityDescriptors($child));
488
        }
489
        return $results;
490
    }
491
492
493
    /**
494
     * Retrieve all entities as an EntitiesDescriptor.
495
     *
496
     * @return \SimpleSAML\SAML2\XML\md\EntitiesDescriptor  The entities.
497
     */
498
    protected function getEntitiesDescriptor(): EntitiesDescriptor
499
    {
500
        $extensions = [];
501
502
        // add RegistrationInfo extension if enabled
503
        if (!empty($this->regInfo)) {
504
            $extensions[] = RegistrationInfo::fromArray($this->regInfo);
505
        }
506
507
        // add PublicationInfo extension if enabled
508
        if (!empty($this->pubInfo)) {
509
            $extensions[] = PublicationInfo::fromArray($this->pubInfo);
510
        }
511
512
        $children = [];
513
        foreach ($this->sources as $source) {
514
            $m = $source->getMetadata();
515
            if ($m === null) {
516
                continue;
517
            }
518
519
            if ($m instanceof EntityDescriptor) {
520
                $children[] = $m;
521
            } elseif ($m instanceof EntitiesDescriptor) {
522
                $children = array_merge($children, self::extractEntityDescriptors($m));
523
            }
524
        }
525
        $children = array_unique($children, SORT_REGULAR);
526
527
        $now = new DateTimeImmutable('@' . strval(time() + $this->validLength));
528
        $now = $now->setTimeZone(new DateTimeZone('Z'));
529
530
        $ret = new EntitiesDescriptor(
531
            entityDescriptors: $children,
532
            validUntil: $now,
533
            extensions: empty($extensions) ? null : new Extensions($extensions),
534
            Name: $this->name,
535
        );
536
537
        return $ret;
538
    }
539
540
541
    /**
542
     * Recursively traverse the children of an EntitiesDescriptor, removing those entities listed in the $entities
543
     * property. Returns the EntitiesDescriptor with the entities filtered out.
544
     *
545
     * @param \SimpleSAML\SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor from where to exclude entities.
546
     *
547
     * @return \SimpleSAML\SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with excluded entities filtered out.
548
     */
549
    protected function exclude(EntitiesDescriptor $descriptor): EntitiesDescriptor
550
    {
551
        if (empty($this->excluded)) {
552
            return $descriptor;
553
        }
554
555
        $descriptors = array_merge($descriptor->getEntityDescriptors(), $descriptor->getEntitiesDescriptors());
556
        $filtered = [];
557
        foreach ($descriptors as $child) {
558
            if ($child instanceof EntityDescriptor) {
559
                if (in_array($child->getEntityID(), $this->excluded)) {
560
                    continue;
561
                }
562
                $filtered[] = $child;
563
            }
564
565
            if ($child instanceof EntitiesDescriptor) {
566
                $filtered[] = $this->exclude($child);
567
            }
568
        }
569
570
571
        return new EntitiesDescriptor($filtered);
572
    }
573
574
575
    /**
576
     * Recursively traverse the children of an EntitiesDescriptor, keeping only those entities with the roles listed in
577
     * the $roles property, and support for the protocols listed in the $protocols property. Returns the
578
     * EntitiesDescriptor containing only those entities.
579
     *
580
     * @param \SimpleSAML\SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor to filter.
581
     *
582
     * @return \SimpleSAML\SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with only the entities filtered.
583
     */
584
    protected function filter(EntitiesDescriptor $descriptor): EntitiesDescriptor
585
    {
586
        if (empty($this->roles) || empty($this->protocols)) {
587
            return $descriptor;
588
        }
589
590
        $enabled_roles = array_keys($this->roles, true);
591
        $enabled_protos = array_keys($this->protocols, true);
592
593
        $descriptors = array_merge($descriptor->getEntityDescriptors(), $descriptor->getEntitiesDescriptors());
594
        $filtered = [];
595
        foreach ($descriptors as $child) {
596
            if ($child instanceof EntityDescriptor) {
597
                foreach ($child->getRoleDescriptor() as $role) {
598
                    if (in_array(get_class($role), $enabled_roles)) {
599
                        // we found a role descriptor that is enabled by our filters, check protocols
600
                        if (array_intersect($enabled_protos, $role->getProtocolSupportEnumeration()) !== []) {
601
                            // it supports some protocol we have enabled, add it
602
                            $filtered[] = $child;
603
                            break;
604
                        }
605
                    }
606
                }
607
            }
608
609
            if ($child instanceof EntitiesDescriptor) {
610
                $filtered[] = $this->filter($child);
611
            }
612
        }
613
614
        return new EntitiesDescriptor($filtered);
615
    }
616
617
618
    /**
619
     * Set this aggregator to exclude a set of entities from the resulting aggregate.
620
     *
621
     * @param array $entities The entity IDs of the entities to exclude.
622
     */
623
    public function excludeEntities(array $entities): void
624
    {
625
        if (empty($entities)) {
626
            return;
627
        }
628
        $this->excluded = $entities;
629
        sort($this->excluded);
630
        $this->cacheId = sha1($this->cacheId . serialize($this->excluded));
631
    }
632
633
634
    /**
635
     * Set the internal filters according to one or more options:
636
     *
637
     * - 'saml2': all SAML2.0-capable entities.
638
     * - 'saml20-idp': all SAML2.0-capable identity providers.
639
     * - 'saml20-sp': all SAML2.0-capable service providers.
640
     * - 'saml20-aa': all SAML2.0-capable attribute authorities.
641
     *
642
     * @param array $set An array of the different roles and protocols to filter by.
643
     */
644
    public function setFilters(array $set): void
645
    {
646
        if (empty($set)) {
647
            return;
648
        }
649
650
        // configure filters
651
        $this->protocols = [
652
            C::NS_SAMLP => true,
653
        ];
654
        $this->roles = [
655
            'SimpleSAML\SAML2\XML\md\IDPSSODescriptor'             => true,
656
            'SimpleSAML\SAML2\XML\md\SPSSODescriptor'              => true,
657
            'SimpleSAML\SAML2\XML\md\AttributeAuthorityDescriptor' => true,
658
        ];
659
660
        // now translate from the options we have, to specific protocols and roles
661
662
        // check SAML 2.0 protocol
663
        $options = ['saml2', 'saml20-idp', 'saml20-sp', 'saml20-aa'];
664
        $this->protocols[C::NS_SAMLP] = (array_intersect($set, $options) !== []);
665
666
        // check IdP
667
        $options = ['saml2', 'saml20-idp'];
668
        $this->roles['SimpleSAML\SAML2\XML\md\IDPSSODescriptor'] = (array_intersect($set, $options) !== []);
669
670
        // check SP
671
        $options = ['saml2', 'saml20-sp'];
672
        $this->roles['SimpleSAML\SAML2\XML\md\SPSSODescriptor'] = (array_intersect($set, $options) !== []);
673
674
        // check AA
675
        $options = ['saml2', 'saml20-aa'];
676
        $this->roles['SimpleSAML\SAML2\XML\md\AttributeAuthorityDescriptor'] = (array_intersect($set, $options) !== []);
677
678
        $this->cacheId = sha1($this->cacheId . serialize($this->protocols) . serialize($this->roles));
679
    }
680
681
682
    /**
683
     * Retrieve the complete, signed metadata as text.
684
     *
685
     * This function will write the new metadata to the cache file, but will not return
686
     * the cached metadata.
687
     *
688
     * @return string  The metadata, as text.
689
     */
690
    public function updateCachedMetadata(): string
691
    {
692
        $ed = $this->getEntitiesDescriptor();
693
        $ed = $this->exclude($ed);
694
        $ed = $this->filter($ed);
695
        $this->addSignature($ed);
696
697
        $xml = $ed->toXML();
698
        $xml = $xml->ownerDocument?->saveXML($xml);
699
700
        if ($this->cacheGenerated !== null) {
701
            Logger::debug($this->logLoc . 'Saving generated metadata to cache.');
702
            $this->addCacheItem($this->cacheId, $xml, time() + $this->cacheGenerated, $this->cacheTag);
703
        }
704
705
        return $xml;
706
    }
707
708
709
    /**
710
     * Retrieve the complete, signed metadata as text.
711
     *
712
     * @return string  The metadata, as text.
713
     */
714
    public function getMetadata(): string
715
    {
716
        if ($this->cacheGenerated !== null) {
717
            $xml = $this->getCacheItem($this->cacheId, $this->cacheTag);
718
            if ($xml !== null) {
719
                Logger::debug($this->logLoc . 'Loaded generated metadata from cache.');
720
                return $xml;
721
            }
722
        }
723
724
        return $this->updateCachedMetadata();
725
    }
726
727
728
    /**
729
     * Update the cached copy of our metadata.
730
     */
731
    public function updateCache(): void
732
    {
733
        foreach ($this->sources as $source) {
734
            $source->updateCache();
735
        }
736
737
        $this->updateCachedMetadata();
738
    }
739
}
740