Passed
Push — master ( 911977...6c0652 )
by Tim
02:06
created

Aggregator::filter()   B

Complexity

Conditions 9
Paths 12

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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

286
        return new Aggregator($id, /** @scrutinizer ignore-type */ $config->getConfigItem($id, []));
Loading history...
287
    }
288
289
290
    /**
291
     * Retrieve the ID of the aggregator.
292
     *
293
     * @return string  The ID of this aggregator.
294
     */
295
    public function getId(): string
296
    {
297
        return $this->id;
298
    }
299
300
301
    /**
302
     * Add an item to the cache.
303
     *
304
     * @param string $id  The identifier of this data.
305
     * @param string $data  The data.
306
     * @param int $expires  The timestamp the data expires.
307
     * @param string|null $tag  An extra tag that can be used to verify the validity of the cached data.
308
     */
309
    public function addCacheItem(string $id, string $data, int $expires, string $tag = null): void
310
    {
311
        $sysUtils = new Utils\System();
312
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
313
        try {
314
            $sysUtils->writeFile($cacheFile, $data);
315
        } catch (Exception $e) {
316
            Logger::warning($this->logLoc . 'Unable to write to cache file ' . var_export($cacheFile, true));
317
            return;
318
        }
319
320
        $expireInfo = (string)$expires;
321
        if ($tag !== null) {
322
            $expireInfo .= ':' . $tag;
323
        }
324
325
        $expireFile = $cacheFile . '.expire';
326
        try {
327
            $sysUtils->writeFile($expireFile, $expireInfo);
328
        } catch (Exception $e) {
329
            Logger::warning($this->logLoc . 'Unable to write expiration info to ' . var_export($expireFile, true));
330
        }
331
    }
332
333
334
    /**
335
     * Check validity of cached data.
336
     *
337
     * @param string $id  The identifier of this data.
338
     * @param string|null $tag  The tag that was passed to addCacheItem.
339
     * @return bool  TRUE if the data is valid, FALSE if not.
340
     */
341
    public function isCacheValid(string $id, string $tag = null): bool
342
    {
343
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
344
        if (!file_exists($cacheFile)) {
345
            return false;
346
        }
347
348
        $expireFile = $cacheFile . '.expire';
349
        if (!file_exists($expireFile)) {
350
            return false;
351
        }
352
353
        $expireData = @file_get_contents($expireFile);
354
        if ($expireData === false) {
355
            return false;
356
        }
357
358
        $expireData = explode(':', $expireData, 2);
359
360
        $expireTime = intval($expireData[0]);
361
        if ($expireTime <= time()) {
362
            return false;
363
        }
364
365
        if (count($expireData) === 1) {
366
            $expireTag = null;
367
        } else {
368
            $expireTag = $expireData[1];
369
        }
370
        if ($expireTag !== $tag) {
371
            return false;
372
        }
373
374
        return true;
375
    }
376
377
378
    /**
379
     * Get the cache item.
380
     *
381
     * @param string $id  The identifier of this data.
382
     * @param string|null $tag  The tag that was passed to addCacheItem.
383
     * @return string|null  The cache item, or NULL if it isn't cached or if it is expired.
384
     */
385
    public function getCacheItem(string $id, string $tag = null): ?string
386
    {
387
        if (!$this->isCacheValid($id, $tag)) {
388
            return null;
389
        }
390
391
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
392
        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...
393
    }
394
395
396
    /**
397
     * Get the cache filename for the specific id.
398
     *
399
     * @param string $id  The identifier of the cached data.
400
     * @return string|null  The filename, or NULL if the cache file doesn't exist.
401
     */
402
    public function getCacheFile(string $id): ?string
403
    {
404
        $cacheFile = strval($this->cacheDirectory) . '/' . $id;
405
        if (!file_exists($cacheFile)) {
406
            return null;
407
        }
408
409
        return $cacheFile;
410
    }
411
412
413
    /**
414
     * Retrieve the SSL CA file path, if it is set.
415
     *
416
     * @return string|null  The SSL CA file path.
417
     */
418
    public function getCAFile(): ?string
419
    {
420
        return $this->sslCAFile;
421
    }
422
423
424
    /**
425
     * Sign the generated EntitiesDescriptor.
426
     */
427
    protected function addSignature(SignedElement $element): void
428
    {
429
        if ($this->signKey === null) {
430
            return;
431
        }
432
433
        /** @var string $this->signAlg */
434
        $privateKey = new XMLSecurityKey($this->signAlg, ['type' => 'private']);
435
        if ($this->signKeyPass !== null) {
436
            $privateKey->passphrase = $this->signKeyPass;
437
        }
438
        $privateKey->loadKey($this->signKey, false);
439
440
        $element->setSignatureKey($privateKey);
441
442
        if ($this->signCert !== null) {
443
            $element->setCertificates([$this->signCert]);
444
        }
445
    }
446
447
448
    /**
449
     * Recursively browse the children of an EntitiesDescriptor element looking for EntityDescriptor elements, and
450
     * return an array containing all of them.
451
     *
452
     * @param \SAML2\XML\md\EntitiesDescriptor $entity The source EntitiesDescriptor that holds the entities to extract.
453
     *
454
     * @return array An array containing all the EntityDescriptors found.
455
     */
456
    private static function extractEntityDescriptors(EntitiesDescriptor $entity): array
457
    {
458
        $results = [];
459
        foreach ($entity->getChildren() as $child) {
460
            if ($child instanceof EntityDescriptor) {
461
                $results[] = $child;
462
                continue;
463
            }
464
465
            $results = array_merge($results, self::extractEntityDescriptors($child));
466
        }
467
        return $results;
468
    }
469
470
471
    /**
472
     * Retrieve all entities as an EntitiesDescriptor.
473
     *
474
     * @return \SAML2\XML\md\EntitiesDescriptor  The entities.
475
     */
476
    protected function getEntitiesDescriptor(): EntitiesDescriptor
477
    {
478
        $ret = new EntitiesDescriptor();
479
        $now = time();
480
        $extensions = [];
481
482
        // add RegistrationInfo extension if enabled
483
        if (!empty($this->regInfo)) {
484
            $ri = new RegistrationInfo();
485
            $ri->setRegistrationInstant($now);
486
            foreach ($this->regInfo as $riName => $riValues) {
487
                switch ($riName) {
488
                    case 'authority':
489
                        $ri->setRegistrationAuthority($riValues);
490
                        break;
491
                    case 'instant':
492
                        $ri->setRegistrationInstant(SAML2_Utils::xsDateTimeToTimestamp($riValues));
493
                        break;
494
                    case 'policies':
495
                        $ri->setRegistrationPolicy($riValues);
496
                        break;
497
                    default:
498
                        Logger::warning(
499
                            "Unable to apply unknown configuration setting \$config['RegistrationInfo']['"
500
                            . strval($riValues) . "'; skipping."
501
                        );
502
                        break;
503
                }
504
            }
505
            $extensions[] = $ri;
506
        }
507
508
        // add PublicationInfo extension if enabled
509
        if (!empty($this->pubInfo)) {
510
            $pi = new PublicationInfo();
511
            $pi->setCreationInstant($now);
512
            foreach ($this->pubInfo as $piName => $piValues) {
513
                switch ($piName) {
514
                    case 'publisher':
515
                        $pi->setPublisher($piValues);
516
                        break;
517
                    case 'publicationId':
518
                        $pi->setPublicationId($piValues);
519
                        break;
520
                    case 'instant':
521
                        $pi->setCreationInstant(SAML2_Utils::xsDateTimeToTimestamp($piValues));
522
                        break;
523
                    case 'policies':
524
                        $pi->setUsagePolicy($piValues);
525
                        break;
526
                    default:
527
                        Logger::warning(
528
                            "Unable to apply unknown configuration setting \$config['PublicationInfo']['"
529
                            . strval($piValues) . "'; skipping."
530
                        );
531
                        break;
532
                }
533
            }
534
            $extensions[] = $pi;
535
        }
536
        $ret->setExtensions($extensions);
537
538
        foreach ($this->sources as $source) {
539
            $m = $source->getMetadata();
540
            if ($m === null) {
541
                continue;
542
            }
543
            if ($m instanceof EntityDescriptor) {
544
                $ret->addChildren($m);
545
            } elseif ($m instanceof EntitiesDescriptor) {
546
                $ret->setChildren(array_merge($ret->getChildren(), self::extractEntityDescriptors($m)));
547
            }
548
        }
549
550
        $ret->setChildren(array_unique($ret->getChildren(), SORT_REGULAR));
551
        $ret->validUntil = $now + $this->validLength;
552
553
        return $ret;
554
    }
555
556
557
    /**
558
     * Recursively traverse the children of an EntitiesDescriptor, removing those entities listed in the $entities
559
     * property. Returns the EntitiesDescriptor with the entities filtered out.
560
     *
561
     * @param \SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor from where to exclude entities.
562
     *
563
     * @return \SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with excluded entities filtered out.
564
     */
565
    protected function exclude(EntitiesDescriptor $descriptor): EntitiesDescriptor
566
    {
567
        if (empty($this->excluded)) {
568
            return $descriptor;
569
        }
570
571
        $filtered = [];
572
        foreach ($descriptor->getChildren() as $child) {
573
            if ($child instanceof EntityDescriptor) {
574
                if (in_array($child->getEntityID(), $this->excluded)) {
575
                    continue;
576
                }
577
                $filtered[] = $child;
578
            }
579
580
            if ($child instanceof EntitiesDescriptor) {
581
                $filtered[] = $this->exclude($child);
582
            }
583
        }
584
585
        $descriptor->setChildren($filtered);
586
        return $descriptor;
587
    }
588
589
590
    /**
591
     * Recursively traverse the children of an EntitiesDescriptor, keeping only those entities with the roles listed in
592
     * the $roles property, and support for the protocols listed in the $protocols property. Returns the
593
     * EntitiesDescriptor containing only those entities.
594
     *
595
     * @param \SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor to filter.
596
     *
597
     * @return \SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with only the entities filtered.
598
     */
599
    protected function filter(EntitiesDescriptor $descriptor): EntitiesDescriptor
600
    {
601
        if ($this->roles === null || $this->protocols === null) {
602
            return $descriptor;
603
        }
604
605
        $enabled_roles = array_keys($this->roles, true);
606
        $enabled_protos = array_keys($this->protocols, true);
607
608
        $filtered = [];
609
        foreach ($descriptor->getChildren() as $child) {
610
            if ($child instanceof EntityDescriptor) {
611
                foreach ($child->getRoleDescriptor() as $role) {
612
                    if (in_array(get_class($role), $enabled_roles)) {
613
                        // we found a role descriptor that is enabled by our filters, check protocols
614
                        if (array_intersect($enabled_protos, $role->getProtocolSupportEnumeration()) !== []) {
615
                            // it supports some protocol we have enabled, add it
616
                            $filtered[] = $child;
617
                            break;
618
                        }
619
                    }
620
                }
621
            }
622
623
            if ($child instanceof EntitiesDescriptor) {
624
                $filtered[] = $this->filter($child);
625
            }
626
        }
627
628
        $descriptor->setChildren($filtered);
629
        return $descriptor;
630
    }
631
632
633
    /**
634
     * Set this aggregator to exclude a set of entities from the resulting aggregate.
635
     *
636
     * @param array|null $entities The entity IDs of the entities to exclude.
637
     */
638
    public function excludeEntities(?array $entities): void
639
    {
640
        if ($entities === null) {
0 ignored issues
show
introduced by
The condition $entities === null is always false.
Loading history...
641
            return;
642
        }
643
        $this->excluded = $entities;
644
        sort($this->excluded);
645
        $this->cacheId = sha1($this->cacheId . serialize($this->excluded));
646
    }
647
648
649
    /**
650
     * Set the internal filters according to one or more options:
651
     *
652
     * - 'saml2': all SAML2.0-capable entities.
653
     * - 'saml20-idp': all SAML2.0-capable identity providers.
654
     * - 'saml20-sp': all SAML2.0-capable service providers.
655
     * - 'saml20-aa': all SAML2.0-capable attribute authorities.
656
     *
657
     * @param array|null $set An array of the different roles and protocols to filter by.
658
     */
659
    public function setFilters(?array $set): void
660
    {
661
        if ($set === null) {
0 ignored issues
show
introduced by
The condition $set === null is always false.
Loading history...
662
            return;
663
        }
664
665
        // configure filters
666
        $this->protocols = [
667
            Constants::NS_SAMLP                    => true,
668
        ];
669
        $this->roles = [
670
            'SAML2_XML_md_IDPSSODescriptor'             => true,
671
            'SAML2_XML_md_SPSSODescriptor'              => true,
672
            'SAML2_XML_md_AttributeAuthorityDescriptor' => true,
673
        ];
674
675
        // now translate from the options we have, to specific protocols and roles
676
677
        // check SAML 2.0 protocol
678
        $options = ['saml2', 'saml20-idp', 'saml20-sp', 'saml20-aa'];
679
        $this->protocols[Constants::NS_SAMLP] = (array_intersect($set, $options) !== []);
680
681
        // check IdP
682
        $options = ['saml2', 'saml20-idp'];
683
        $this->roles['SAML2_XML_md_IDPSSODescriptor'] = (array_intersect($set, $options) !== []);
684
685
        // check SP
686
        $options = ['saml2', 'saml20-sp'];
687
        $this->roles['SAML2_XML_md_SPSSODescriptor'] = (array_intersect($set, $options) !== []);
688
689
        // check AA
690
        $options = ['saml2', 'saml20-aa'];
691
        $this->roles['SAML2_XML_md_AttributeAuthorityDescriptor'] = (array_intersect($set, $options) !== []);
692
693
        $this->cacheId = sha1($this->cacheId . serialize($this->protocols) . serialize($this->roles));
694
    }
695
696
697
    /**
698
     * Retrieve the complete, signed metadata as text.
699
     *
700
     * This function will write the new metadata to the cache file, but will not return
701
     * the cached metadata.
702
     *
703
     * @return string  The metadata, as text.
704
     */
705
    public function updateCachedMetadata(): string
706
    {
707
        $ed = $this->getEntitiesDescriptor();
708
        $ed = $this->exclude($ed);
709
        $ed = $this->filter($ed);
710
        $this->addSignature($ed);
711
712
        $xml = $ed->toXML();
713
        $xml = $xml->ownerDocument->saveXML($xml);
0 ignored issues
show
Bug introduced by
The method saveXML() does not exist on null. ( Ignorable by Annotation )

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

713
        /** @scrutinizer ignore-call */ 
714
        $xml = $xml->ownerDocument->saveXML($xml);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
714
715
        if ($this->cacheGenerated !== null) {
716
            Logger::debug($this->logLoc . 'Saving generated metadata to cache.');
717
            $this->addCacheItem($this->cacheId, $xml, time() + $this->cacheGenerated, $this->cacheTag);
718
        }
719
720
        return $xml;
721
    }
722
723
724
    /**
725
     * Retrieve the complete, signed metadata as text.
726
     *
727
     * @return string  The metadata, as text.
728
     */
729
    public function getMetadata(): string
730
    {
731
        if ($this->cacheGenerated !== null) {
732
            $xml = $this->getCacheItem($this->cacheId, $this->cacheTag);
733
            if ($xml !== null) {
734
                Logger::debug($this->logLoc . 'Loaded generated metadata from cache.');
735
                return $xml;
736
            }
737
        }
738
739
        return $this->updateCachedMetadata();
740
    }
741
742
743
    /**
744
     * Update the cached copy of our metadata.
745
     */
746
    public function updateCache(): void
747
    {
748
        foreach ($this->sources as $source) {
749
            $source->updateCache();
750
        }
751
752
        $this->updateCachedMetadata();
753
    }
754
}
755