Passed
Push — master ( af692e...7c370b )
by Tim
02:34
created

Aggregator::excludeEntities()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

462
        $key = PrivateKey::fromFile($this->signKey, /** @scrutinizer ignore-type */ $this->signKeyPass);
Loading history...
463
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(
464
            $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

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