Passed
Pull Request — master (#5)
by Tim
01:46
created

Aggregator::updateCachedMetadata()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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

259
        $this->initSources(/** @scrutinizer ignore-type */ $config->getConfigList('sources'));
Loading history...
260
    }
261
262
263
    /**
264
     * Populate the sources array.
265
     *
266
     * This is called from the constructor, and can be overridden in subclasses.
267
     *
268
     * @param array $sources  The sources as an array of \SimpleSAML\Configuration objects.
269
     * @return void
270
     */
271
    protected function initSources(array $sources)
272
    {
273
        foreach ($sources as $source) {
274
            $this->sources[] = new EntitySource($this, $source);
275
        }
276
    }
277
278
279
    /**
280
     * Return an instance of the aggregator with the given id.
281
     *
282
     * @param string $id  The id of the aggregator.
283
     * @return Aggregator
284
     */
285
    public static function getAggregator($id)
286
    {
287
        assert('is_string($id)');
288
289
        $config = Configuration::getConfig('module_aggregator2.php');
290
        return new Aggregator($id, $config->getConfigItem($id));
0 ignored issues
show
Bug introduced by
It seems like $config->getConfigItem($id) can also be of type string; 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

290
        return new Aggregator($id, /** @scrutinizer ignore-type */ $config->getConfigItem($id));
Loading history...
291
    }
292
293
294
    /**
295
     * Retrieve the ID of the aggregator.
296
     *
297
     * @return string  The ID of this aggregator.
298
     */
299
    public function getId()
300
    {
301
        return $this->id;
302
    }
303
304
305
    /**
306
     * Add an item to the cache.
307
     *
308
     * @param string $id  The identifier of this data.
309
     * @param string $data  The data.
310
     * @param int $expires  The timestamp the data expires.
311
     * @param string|null $tag  An extra tag that can be used to verify the validity of the cached data.
312
     * @return void
313
     */
314
    public function addCacheItem($id, $data, $expires, $tag = null)
315
    {
316
        assert('is_string($id)');
317
        assert('is_string($data)');
318
        assert('is_int($expires)');
319
        assert('is_null($tag) || is_string($tag)');
320
321
        $cacheFile = strval($this->cacheDirectory).'/'.$id;
322
        try {
323
            System::writeFile($cacheFile, $data);
324
        } catch (\Exception $e) {
325
            Logger::warning($this->logLoc.'Unable to write to cache file '.var_export($cacheFile, true));
326
            return;
327
        }
328
329
        $expireInfo = (string)$expires;
330
        if ($tag !== null) {
331
            $expireInfo .= ':'.$tag;
332
        }
333
334
        $expireFile = $cacheFile.'.expire';
335
        try {
336
            System::writeFile($expireFile, $expireInfo);
337
        } catch (\Exception $e) {
338
            Logger::warning($this->logLoc.'Unable to write expiration info to '.var_export($expireFile, true));
339
        }
340
    }
341
342
343
    /**
344
     * Check validity of cached data.
345
     *
346
     * @param string $id  The identifier of this data.
347
     * @param string $tag  The tag that was passed to addCacheItem.
348
     * @return bool  TRUE if the data is valid, FALSE if not.
349
     */
350
    public function isCacheValid($id, $tag = null)
351
    {
352
        assert('is_string($id)');
353
        assert('is_null($tag) || is_string($tag)');
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 $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($id, $tag = null)
398
    {
399
        assert('is_string($id)');
400
        assert('is_null($tag) || is_string($tag)');
401
402
        if (!$this->isCacheValid($id, $tag)) {
403
            return null;
404
        }
405
406
        $cacheFile = strval($this->cacheDirectory).'/'.$id;
407
        return @file_get_contents($cacheFile);
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($id)
418
    {
419
        assert('is_string($id)');
420
421
        $cacheFile = strval($this->cacheDirectory).'/'.$id;
422
        if (!file_exists($cacheFile)) {
423
            return null;
424
        }
425
426
        return $cacheFile;
427
    }
428
429
430
    /**
431
     * Retrieve the SSL CA file path, if it is set.
432
     *
433
     * @return string|null  The SSL CA file path.
434
     */
435
    public function getCAFile()
436
    {
437
        return $this->sslCAFile;
438
    }
439
440
441
    /**
442
     * Sign the generated EntitiesDescriptor.
443
     * @return void
444
     */
445
    protected function addSignature(SignedElement $element)
446
    {
447
        if ($this->signKey === null) {
448
            return;
449
        }
450
451
        /** @var string $this->signAlg */
452
        $privateKey = new XMLSecurityKey($this->signAlg, ['type' => 'private']);
453
        if ($this->signKeyPass !== null) {
454
            $privateKey->passphrase = $this->signKeyPass;
455
        }
456
        $privateKey->loadKey($this->signKey, false);
457
458
        $element->setSignatureKey($privateKey);
459
460
        if ($this->signCert !== null) {
461
            $element->setCertificates([$this->signCert]);
462
        }
463
    }
464
465
466
    /**
467
     * Recursively browse the children of an EntitiesDescriptor element looking for EntityDescriptor elements, and
468
     * return an array containing all of them.
469
     *
470
     * @param \SAML2\XML\md\EntitiesDescriptor $entity The source EntitiesDescriptor that holds the entities to extract.
471
     *
472
     * @return array An array containing all the EntityDescriptors found.
473
     */
474
    private static function extractEntityDescriptors(EntitiesDescriptor $entity)
475
    {
476
        $results = [];
477
        foreach ($entity->children as $child) {
478
            if ($child instanceof EntityDescriptor) {
479
                $results[] = $child;
480
                continue;
481
            }
482
483
            $results = array_merge($results, self::extractEntityDescriptors($child));
484
        }
485
        return $results;
486
    }
487
488
489
    /**
490
     * Retrieve all entities as an EntitiesDescriptor.
491
     *
492
     * @return \SAML2\XML\md\EntitiesDescriptor  The entities.
493
     */
494
    protected function getEntitiesDescriptor()
495
    {
496
        $ret = new EntitiesDescriptor();
497
        $now = time();
498
499
        // add RegistrationInfo extension if enabled
500
        if (!empty($this->regInfo)) {
501
            $ri = new RegistrationInfo();
502
            $ri->setRegistrationInstant($now);
503
            foreach ($this->regInfo as $riName => $riValues) {
504
                switch ($riName) {
505
                    case 'authority':
506
                        $ri->setRegistrationAuthority($riValues);
507
                        break;
508
                    case 'instant':
509
                        $ri->setRegistrationInstant(Utils::xsDateTimeToTimestamp($riValues));
510
                        break;
511
                    case 'policies':
512
                        $ri->setRegistrationPolicy($riValues);
513
                        break;
514
                }
515
            }
516
            $ret->Extensions[] = $ri;
517
        }
518
519
        // add PublicationInfo extension if enabled
520
        if (!empty($this->pubInfo)) {
521
            $pi = new PublicationInfo();
0 ignored issues
show
Bug introduced by
The type SimpleSAML\Module\aggregator2\PublicationInfo was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
522
            $pi->setCreationInstant($now);
523
            foreach ($this->pubInfo as $piName => $piValues) {
524
                switch ($piName) {
525
                    case 'publisher':
526
                        $pi->setPublisher($piValues);
527
                        break;
528
                    case 'publicationId':
529
                        $pi->setPublicationId($piValues);
530
                        break;
531
                    case 'instant':
532
                        $pi->setCreationInstant(Utils::xsDateTimeToTimestamp($piValues));
533
                        break;
534
                    case 'policies':
535
                        $pi->setUsagePolicy($piValues);
536
                        break;
537
                }
538
            }
539
            $ret->Extensions[] = $pi;
540
        }
541
542
        foreach ($this->sources as $source) {
543
            $m = $source->getMetadata();
544
            if ($m === null) {
545
                continue;
546
            }
547
            if ($m instanceof EntityDescriptor) {
548
                $ret->children[] = $m;
549
            } elseif ($m instanceof EntitiesDescriptor) {
550
                $ret->children = array_merge($ret->children, self::extractEntityDescriptors($m));
551
            }
552
        }
553
554
        $ret->children = array_unique($ret->children, SORT_REGULAR);
555
        $ret->validUntil = $now + $this->validLength;
556
557
        return $ret;
558
    }
559
560
561
    /**
562
     * Recursively traverse the children of an EntitiesDescriptor, removing those entities listed in the $entities
563
     * property. Returns the EntitiesDescriptor with the entities filtered out.
564
     *
565
     * @param \SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor from where to exclude entities.
566
     *
567
     * @return \SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with excluded entities filtered out.
568
     */
569
    protected function exclude(EntitiesDescriptor $descriptor)
570
    {
571
        if (empty($this->excluded)) {
572
            return $descriptor;
573
        }
574
575
        $filtered = [];
576
        foreach ($descriptor->children as $child) {
577
            if ($child instanceof EntityDescriptor) {
578
                if (in_array($child->entityID, $this->excluded)) {
579
                    continue;
580
                }
581
                $filtered[] = $child;
582
            }
583
584
            if ($child instanceof EntitiesDescriptor) {
585
                $filtered[] = $this->exclude($child);
586
            }
587
        }
588
589
        $descriptor->children = $filtered;
590
        return $descriptor;
591
    }
592
593
594
    /**
595
     * Recursively traverse the children of an EntitiesDescriptor, keeping only those entities with the roles listed in
596
     * the $roles property, and support for the protocols listed in the $protocols property. Returns the
597
     * EntitiesDescriptor containing only those entities.
598
     *
599
     * @param \SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor to filter.
600
     *
601
     * @return \SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with only the entities filtered.
602
     */
603
    protected function filter(EntitiesDescriptor $descriptor)
604
    {
605
        if ($this->roles === null || $this->protocols === null) {
606
            return $descriptor;
607
        }
608
609
        $enabled_roles = array_keys($this->roles, true);
610
        $enabled_protos = array_keys($this->protocols, true);
611
612
        $filtered = [];
613
        foreach ($descriptor->children as $child) {
614
            if ($child instanceof EntityDescriptor) {
615
                foreach ($child->RoleDescriptor as $role) {
616
                    if (in_array(get_class($role), $enabled_roles)) {
617
                        // we found a role descriptor that is enabled by our filters, check protocols
618
                        if (array_intersect($enabled_protos, $role->protocolSupportEnumeration) !== []) {
619
                            // it supports some protocol we have enabled, add it
620
                            $filtered[] = $child;
621
                            break;
622
                        }
623
                    }
624
                }
625
626
            }
627
628
            if ($child instanceof EntitiesDescriptor) {
629
                $filtered[] = $this->filter($child);
630
            }
631
        }
632
633
        $descriptor->children = $filtered;
634
        return $descriptor;
635
    }
636
637
638
    /**
639
     * Set this aggregator to exclude a set of entities from the resulting aggregate.
640
     *
641
     * @param array|null $entities The entity IDs of the entities to exclude.
642
     * @return void
643
     */
644
    public function excludeEntities($entities)
645
    {
646
        assert('is_array($entities) || is_null($entities)');
647
648
        if ($entities === null) {
649
            return;
650
        }
651
        $this->excluded = $entities;
652
        sort($this->excluded);
653
        $this->cacheId = sha1($this->cacheId.serialize($this->excluded));
654
    }
655
656
657
    /**
658
     * Set the internal filters according to one or more options:
659
     *
660
     * - 'saml2': all SAML2.0-capable entities.
661
     * - 'shib13': all SHIB1.3-capable entities.
662
     * - 'saml20-idp': all SAML2.0-capable identity providers.
663
     * - 'saml20-sp': all SAML2.0-capable service providers.
664
     * - 'saml20-aa': all SAML2.0-capable attribute authorities.
665
     * - 'shib13-idp': all SHIB1.3-capable identity providers.
666
     * - 'shib13-sp': all SHIB1.3-capable service providers.
667
     * - 'shib13-aa': all SHIB1.3-capable attribute authorities.
668
     *
669
     * @param array|null $set An array of the different roles and protocols to filter by.
670
     * @return void
671
     */
672
    public function setFilters($set)
673
    {
674
        assert('is_array($set) || is_null($set)');
675
676
        if ($set === null) {
677
            return;
678
        }
679
680
        // configure filters
681
        $this->protocols = [
682
            Constants::NS_SAMLP                    => true,
683
            'urn:oasis:names:tc:SAML:1.1:protocol' => true,
684
        ];
685
        $this->roles = [
686
            'SAML2_XML_md_IDPSSODescriptor'             => true,
687
            'SAML2_XML_md_SPSSODescriptor'              => true,
688
            'SAML2_XML_md_AttributeAuthorityDescriptor' => true,
689
        ];
690
691
        // now translate from the options we have, to specific protocols and roles
692
693
        // check SAML 2.0 protocol
694
        $options = ['saml2', 'saml20-idp', 'saml20-sp', 'saml20-aa'];
695
        $this->protocols[Constants::NS_SAMLP] = (array_intersect($set, $options) !== []);
696
697
        // check SHIB 1.3 protocol
698
        $options = ['shib13', 'shib13-idp', 'shib13-sp', 'shib13-aa'];
699
        $this->protocols['urn:oasis:names:tc:SAML:1.1:protocol'] = (array_intersect($set, $options) !== []);
700
701
        // check IdP
702
        $options = ['saml2', 'shib13', 'saml20-idp', 'shib13-idp'];
703
        $this->roles['SAML2_XML_md_IDPSSODescriptor'] = (array_intersect($set, $options) !== []);
704
705
        // check SP
706
        $options = ['saml2', 'shib13', 'saml20-sp', 'shib13-sp'];
707
        $this->roles['SAML2_XML_md_SPSSODescriptor'] = (array_intersect($set, $options) !== []);
708
709
        // check AA
710
        $options = ['saml2', 'shib13', 'saml20-aa', 'shib13-aa'];
711
        $this->roles['SAML2_XML_md_AttributeAuthorityDescriptor'] = (array_intersect($set, $options) !== []);
712
713
        $this->cacheId = sha1($this->cacheId.serialize($this->protocols).serialize($this->roles));
714
    }
715
716
717
    /**
718
     * Retrieve the complete, signed metadata as text.
719
     *
720
     * This function will write the new metadata to the cache file, but will not return
721
     * the cached metadata.
722
     *
723
     * @return string  The metadata, as text.
724
     */
725
    public function updateCachedMetadata()
726
    {
727
        $ed = $this->getEntitiesDescriptor();
728
        $ed = $this->exclude($ed);
729
        $ed = $this->filter($ed);
730
        $this->addSignature($ed);
731
732
        $xml = $ed->toXML();
733
        $xml = $xml->ownerDocument->saveXML($xml);
734
735
        if ($this->cacheGenerated !== null) {
736
            Logger::debug($this->logLoc.'Saving generated metadata to cache.');
737
            $this->addCacheItem($this->cacheId, $xml, time() + $this->cacheGenerated, $this->cacheTag);
738
        }
739
740
        return $xml;
741
    }
742
743
744
    /**
745
     * Retrieve the complete, signed metadata as text.
746
     *
747
     * @return string  The metadata, as text.
748
     */
749
    public function getMetadata()
750
    {
751
        if ($this->cacheGenerated !== null) {
752
            $xml = $this->getCacheItem($this->cacheId, $this->cacheTag);
753
            if ($xml !== null) {
754
                Logger::debug($this->logLoc.'Loaded generated metadata from cache.');
755
                return $xml;
756
            }
757
        }
758
759
        return $this->updateCachedMetadata();
760
    }
761
762
763
    /**
764
     * Update the cached copy of our metadata.
765
     * @return void
766
     */
767
    public function updateCache()
768
    {
769
        foreach ($this->sources as $source) {
770
            $source->updateCache();
771
        }
772
773
        $this->updateCachedMetadata();
774
    }
775
}
776