Aggregator::addSignature()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 28
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

249
            $this->signKey = PrivateKey::fromFile($signKey, /** @scrutinizer ignore-type */ $signKeyPass);
Loading history...
250
        }
251
252
        $signCert = $config->getOptionalString('sign.certificate', null);
253
        if ($signCert !== null) {
254
            $signCert = $sysUtils->resolvePath($signCert, $certDir);
255
            $this->signCert = PublicKey::fromFile($signCert);
256
        }
257
258
        $this->signAlg = $config->getOptionalString('sign.algorithm', C::SIG_RSA_SHA256);
259
        if (!in_array($this->signAlg, array_keys(C::$RSA_DIGESTS))) {
260
            throw new Exception('Unsupported signature algorithm ' . var_export($this->signAlg, true));
261
        }
262
263
        $this->sslCAFile = $config->getOptionalString('ssl.cafile', null);
264
265
        $this->regInfo = $config->getOptionalArray('RegistrationInfo', []);
266
        $this->pubInfo = $config->getOptionalArray('PublicationInfo', []);
267
268
        $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

268
        $this->initSources(/** @scrutinizer ignore-type */ $config->getOptionalArray('sources', []));
Loading history...
269
    }
270
271
272
    /**
273
     * Populate the sources array.
274
     *
275
     * This is called from the constructor, and can be overridden in subclasses.
276
     *
277
     * @param array<mixed> $sources  The sources as an array of \SimpleSAML\Configuration objects.
278
     */
279
    protected function initSources(array $sources): void
280
    {
281
        foreach ($sources as $source) {
282
            $this->sources[] = new EntitySource($this, Configuration::loadFromArray($source));
283
        }
284
    }
285
286
287
    /**
288
     * Return an instance of the aggregator with the given id.
289
     *
290
     * @param string $id  The id of the aggregator.
291
     * @return Aggregator
292
     */
293
    public static function getAggregator(string $id): Aggregator
294
    {
295
        $config = Configuration::getConfig('module_aggregator2.php');
296
        /** @psalm-suppress PossiblyNullArgument */
297
        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

297
        return new Aggregator($id, /** @scrutinizer ignore-type */ $config->getOptionalConfigItem($id, []));
Loading history...
298
    }
299
300
301
    /**
302
     * Retrieve the ID of the aggregator.
303
     *
304
     * @return string  The ID of this aggregator.
305
     */
306
    public function getId(): string
307
    {
308
        return $this->id;
309
    }
310
311
312
    /**
313
     * Add an item to the cache.
314
     *
315
     * @param string $id  The identifier of this data.
316
     * @param string $data  The data.
317
     * @param \DateTimeImmutable $expires  The timestamp the data expires.
318
     * @param string|null $tag  An extra tag that can be used to verify the validity of the cached data.
319
     */
320
    public function addCacheItem(string $id, string $data, DateTimeImmutable $expires, ?string $tag = null): void
321
    {
322
        $sysUtils = new Utils\System();
323
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
324
        try {
325
            $sysUtils->writeFile($cacheFile, $data);
326
        } catch (Exception $e) {
327
            Logger::warning($this->logLoc . 'Unable to write to cache file ' . var_export($cacheFile, true));
328
            return;
329
        }
330
331
        $expireInfo = strval($expires->getTimestamp());
332
        if ($tag !== null) {
333
            $expireInfo .= ':' . $tag;
334
        }
335
336
        $expireFile = $cacheFile . '.expire';
337
        try {
338
            $sysUtils->writeFile($expireFile, $expireInfo);
339
        } catch (Exception $e) {
340
            Logger::warning($this->logLoc . 'Unable to write expiration info to ' . var_export($expireFile, true));
341
        }
342
    }
343
344
345
    /**
346
     * Check validity of cached data.
347
     *
348
     * @param string $id  The identifier of this data.
349
     * @param string|null $tag  The tag that was passed to addCacheItem.
350
     * @return bool  TRUE if the data is valid, FALSE if not.
351
     */
352
    public function isCacheValid(string $id, ?string $tag = null): bool
353
    {
354
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
355
        if (!file_exists($cacheFile)) {
356
            return false;
357
        }
358
359
        $expireFile = $cacheFile . '.expire';
360
        if (!file_exists($expireFile)) {
361
            return false;
362
        }
363
364
        $expireData = @file_get_contents($expireFile);
365
        if ($expireData === false) {
366
            return false;
367
        }
368
369
        $expireData = explode(':', $expireData, 2);
370
371
        $expireTime = intval($expireData[0]);
372
        if ($expireTime <= time()) {
373
            return false;
374
        }
375
376
        if (count($expireData) === 1) {
377
            $expireTag = null;
378
        } else {
379
            $expireTag = $expireData[1];
380
        }
381
        if ($expireTag !== $tag) {
382
            return false;
383
        }
384
385
        return true;
386
    }
387
388
389
    /**
390
     * Get the cache item.
391
     *
392
     * @param string $id  The identifier of this data.
393
     * @param string|null $tag  The tag that was passed to addCacheItem.
394
     * @return string|null  The cache item, or NULL if it isn't cached or if it is expired.
395
     */
396
    public function getCacheItem(string $id, ?string $tag = null): ?string
397
    {
398
        if (!$this->isCacheValid($id, $tag)) {
399
            return null;
400
        }
401
402
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
403
        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...
404
    }
405
406
407
    /**
408
     * Get the cache filename for the specific id.
409
     *
410
     * @param string $id  The identifier of the cached data.
411
     * @return string|null  The filename, or NULL if the cache file doesn't exist.
412
     */
413
    public function getCacheFile(string $id): ?string
414
    {
415
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
416
        if (!file_exists($cacheFile)) {
417
            return null;
418
        }
419
420
        return $cacheFile;
421
    }
422
423
424
    /**
425
     * Retrieve the SSL CA file path, if it is set.
426
     *
427
     * @return string|null  The SSL CA file path.
428
     */
429
    public function getCAFile(): ?string
430
    {
431
        return $this->sslCAFile;
432
    }
433
434
435
    /**
436
     * Sign the generated EntitiesDescriptor.
437
     */
438
    protected function addSignature(SignableElementInterface $element): void
439
    {
440
        if ($this->signKey === null) {
441
            return;
442
        }
443
444
        $keyInfo = null;
445
        if ($this->signCert !== null) {
446
            $keyInfo = new KeyInfo(
447
                [
448
                    new X509Data(
449
                        [
450
                            new X509Certificate(
451
                                trim(chunk_split(base64_encode($this->signCert->getPEM()->data()), 64, "\n")),
452
                            ),
453
                        ],
454
                    ),
455
                ],
456
            );
457
        }
458
459
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(
460
            /** @var string $this->signAlg */
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
            $this->signKey,
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 \SimpleSAML\SAML2\XML\md\EntitiesDescriptor $entity The source EntitiesDescriptor that holds the entities to extract.
474
     *
475
     * @return array<\SimpleSAML\SAML2\XML\md\EntityDescriptor> 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
546
     *   The EntitiesDescriptor from where to exclude entities.
547
     *
548
     * @return \SimpleSAML\SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with excluded entities filtered out.
549
     */
550
    protected function exclude(EntitiesDescriptor $descriptor): EntitiesDescriptor
551
    {
552
        if (empty($this->excluded)) {
553
            return $descriptor;
554
        }
555
556
        $descriptors = array_merge($descriptor->getEntityDescriptors(), $descriptor->getEntitiesDescriptors());
557
        $filtered = [];
558
        foreach ($descriptors as $child) {
559
            if ($child instanceof EntityDescriptor) {
560
                if (in_array($child->getEntityID(), $this->excluded)) {
561
                    continue;
562
                }
563
                $filtered[] = $child;
564
            }
565
566
            if ($child instanceof EntitiesDescriptor) {
567
                $filtered[] = $this->exclude($child);
568
            }
569
        }
570
571
572
        return new EntitiesDescriptor($filtered);
573
    }
574
575
576
    /**
577
     * Recursively traverse the children of an EntitiesDescriptor, keeping only those entities with the roles listed in
578
     * the $roles property, and support for the protocols listed in the $protocols property. Returns the
579
     * EntitiesDescriptor containing only those entities.
580
     *
581
     * @param \SimpleSAML\SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor to filter.
582
     *
583
     * @return \SimpleSAML\SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with only the entities filtered.
584
     */
585
    protected function filter(EntitiesDescriptor $descriptor): EntitiesDescriptor
586
    {
587
        if (empty($this->roles) || empty($this->protocols)) {
588
            return $descriptor;
589
        }
590
591
        $enabled_roles = array_keys($this->roles, true);
592
        $enabled_protos = array_keys($this->protocols, true);
593
594
        $descriptors = array_merge($descriptor->getEntityDescriptors(), $descriptor->getEntitiesDescriptors());
595
        $filtered = [];
596
        foreach ($descriptors as $child) {
597
            if ($child instanceof EntityDescriptor) {
598
                foreach ($child->getRoleDescriptor() as $role) {
599
                    if (in_array(get_class($role), $enabled_roles)) {
600
                        // we found a role descriptor that is enabled by our filters, check protocols
601
                        if (array_intersect($enabled_protos, $role->getProtocolSupportEnumeration()) !== []) {
602
                            // it supports some protocol we have enabled, add it
603
                            $filtered[] = $child;
604
                            break;
605
                        }
606
                    }
607
                }
608
            }
609
610
            if ($child instanceof EntitiesDescriptor) {
611
                $filtered[] = $this->filter($child);
612
            }
613
        }
614
615
        return new EntitiesDescriptor($filtered);
616
    }
617
618
619
    /**
620
     * Set this aggregator to exclude a set of entities from the resulting aggregate.
621
     *
622
     * @param string[] $entities The entity IDs of the entities to exclude.
623
     */
624
    public function excludeEntities(array $entities): void
625
    {
626
        if (empty($entities)) {
627
            return;
628
        }
629
        $this->excluded = $entities;
630
        sort($this->excluded);
631
        $this->cacheId = sha1($this->cacheId . serialize($this->excluded));
632
    }
633
634
635
    /**
636
     * Set the internal filters according to one or more options:
637
     *
638
     * - 'saml2': all SAML2.0-capable entities.
639
     * - 'saml20-idp': all SAML2.0-capable identity providers.
640
     * - 'saml20-sp': all SAML2.0-capable service providers.
641
     * - 'saml20-aa': all SAML2.0-capable attribute authorities.
642
     *
643
     * @param string[] $set An array of the different roles and protocols to filter by.
644
     */
645
    public function setFilters(array $set): void
646
    {
647
        if (empty($set)) {
648
            return;
649
        }
650
651
        // configure filters
652
        $this->protocols = [
653
            C::NS_SAMLP => true,
654
        ];
655
        $this->roles = [
656
            'SimpleSAML\SAML2\XML\md\IDPSSODescriptor'             => true,
657
            'SimpleSAML\SAML2\XML\md\SPSSODescriptor'              => true,
658
            'SimpleSAML\SAML2\XML\md\AttributeAuthorityDescriptor' => true,
659
        ];
660
661
        // now translate from the options we have, to specific protocols and roles
662
663
        // check SAML 2.0 protocol
664
        $options = ['saml2', 'saml20-idp', 'saml20-sp', 'saml20-aa'];
665
        $this->protocols[C::NS_SAMLP] = (array_intersect($set, $options) !== []);
666
667
        // check IdP
668
        $options = ['saml2', 'saml20-idp'];
669
        $this->roles['SimpleSAML\SAML2\XML\md\IDPSSODescriptor'] = (array_intersect($set, $options) !== []);
670
671
        // check SP
672
        $options = ['saml2', 'saml20-sp'];
673
        $this->roles['SimpleSAML\SAML2\XML\md\SPSSODescriptor'] = (array_intersect($set, $options) !== []);
674
675
        // check AA
676
        $options = ['saml2', 'saml20-aa'];
677
        $this->roles['SimpleSAML\SAML2\XML\md\AttributeAuthorityDescriptor'] = (array_intersect($set, $options) !== []);
678
679
        $this->cacheId = sha1($this->cacheId . serialize($this->protocols) . serialize($this->roles));
680
    }
681
682
683
    /**
684
     * Retrieve the complete, signed metadata as text.
685
     *
686
     * This function will write the new metadata to the cache file, but will not return
687
     * the cached metadata.
688
     *
689
     * @return string  The metadata, as text.
690
     */
691
    public function updateCachedMetadata(): string
692
    {
693
        $ed = $this->getEntitiesDescriptor();
694
        $ed = $this->exclude($ed);
695
        $ed = $this->filter($ed);
696
        $this->addSignature($ed);
697
698
        $xml = $ed->toXML();
699
        $xml = $xml->ownerDocument?->saveXML($xml);
700
701
        if ($this->cacheGenerated !== null) {
702
            Logger::debug($this->logLoc . 'Saving generated metadata to cache.');
703
            $now = new DateTimeImmutable('now');
704
            $now = $now->setTimeZone(new DateTimezone('Z'));
705
            $this->addCacheItem($this->cacheId, $xml, $now->add($this->cacheGenerated), $this->cacheTag);
706
        }
707
708
        return $xml;
709
    }
710
711
712
    /**
713
     * Retrieve the complete, signed metadata as text.
714
     *
715
     * @return string  The metadata, as text.
716
     */
717
    public function getMetadata(): string
718
    {
719
        if ($this->cacheGenerated !== null) {
720
            $xml = $this->getCacheItem($this->cacheId, $this->cacheTag);
721
            if ($xml !== null) {
722
                Logger::debug($this->logLoc . 'Loaded generated metadata from cache.');
723
                return $xml;
724
            }
725
        }
726
727
        return $this->updateCachedMetadata();
728
    }
729
730
731
    /**
732
     * Update the cached copy of our metadata.
733
     */
734
    public function updateCache(): void
735
    {
736
        foreach ($this->sources as $source) {
737
            $source->updateCache();
738
        }
739
740
        $this->updateCachedMetadata();
741
    }
742
}
743