Completed
Push — master ( 4e360d...80bc96 )
by Daan van
07:27
created

Message::setId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4286
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace SAML2;
4
5
use RobRichards\XMLSecLibs\XMLSecurityKey;
6
use SAML2\Utilities\Temporal;
7
use SAML2\XML\samlp\Extensions;
8
9
/**
10
 * Base class for all SAML 2 messages.
11
 *
12
 * Implements what is common between the samlp:RequestAbstractType and
13
 * samlp:StatusResponseType element types.
14
 *
15
 * @package SimpleSAMLphp
16
 *
17
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
18
 */
19
abstract class Message implements SignedElement
20
{
21
    /**
22
     * Request extensions.
23
     *
24
     * @var array
25
     */
26
    protected $extensions;
27
28
    /**
29
     * The name of the root element of the DOM tree for the message.
30
     *
31
     * Used when creating a DOM tree from the message.
32
     *
33
     * @var string
34
     */
35
    private $tagName;
36
37
    /**
38
     * The identifier of this message.
39
     *
40
     * @var string
41
     */
42
    private $id;
43
44
    /**
45
     * The issue timestamp of this message, as an UNIX timestamp.
46
     *
47
     * @var int
48
     */
49
    private $issueInstant;
50
51
    /**
52
     * The destination URL of this message if it is known.
53
     *
54
     * @var string|null
55
     */
56
    private $destination;
57
58
    /**
59
     * The destination URL of this message if it is known.
60
     *
61
     * @var string|null
62
     */
63
    private $consent = Constants::CONSENT_UNSPECIFIED;
64
65
    /**
66
     * The entity id of the issuer of this message, or null if unknown.
67
     *
68
     * @var string|null
69
     */
70
    private $issuer;
71
72
    /**
73
     * The RelayState associated with this message.
74
     *
75
     * @var string|null
76
     */
77
    private $relayState;
78
79
    /**
80
     * The \DOMDocument we are currently building.
81
     *
82
     * This variable is used while generating XML from this message. It holds the
83
     * \DOMDocument of the XML we are generating.
84
     *
85
     * @var \DOMDocument
86
     */
87
    protected $document;
88
89
    /**
90
     * The private key we should use to sign the message.
91
     *
92
     * The private key can be null, in which case the message is sent unsigned.
93
     *
94
     * @var XMLSecurityKey|null
95
     */
96
    private $signatureKey;
97
98
    /**
99
     * @var bool
100
     */
101
    protected $messageContainedSignatureUponConstruction = false;
102
103
    /**
104
     * List of certificates that should be included in the message.
105
     *
106
     * @var array
107
     */
108
    private $certificates;
109
110
    /**
111
     * Available methods for validating this message.
112
     *
113
     * @var array
114
     */
115
    private $validators;
116
117
    /**
118
     * Initialize a message.
119
     *
120
     * This constructor takes an optional parameter with a \DOMElement. If this
121
     * parameter is given, the message will be initialized with data from that
122
     * XML element.
123
     *
124
     * If no XML element is given, the message is initialized with suitable
125
     * default values.
126
     *
127
     * @param string          $tagName The tag name of the root element.
128
     * @param \DOMElement|null $xml     The input message.
129
     * @throws \Exception
130
     */
131
    protected function __construct($tagName, \DOMElement $xml = null)
132
    {
133
        assert('is_string($tagName)');
134
        $this->tagName = $tagName;
135
136
        $this->id = Utils::getContainer()->generateId();
137
        $this->issueInstant = Temporal::getTime();
138
        $this->certificates = array();
139
        $this->validators = array();
140
141
        if ($xml === null) {
142
            return;
143
        }
144
145
        if (!$xml->hasAttribute('ID')) {
146
            throw new \Exception('Missing ID attribute on SAML message.');
147
        }
148
        $this->id = $xml->getAttribute('ID');
149
150
        if ($xml->getAttribute('Version') !== '2.0') {
151
            /* Currently a very strict check. */
152
            throw new \Exception('Unsupported version: ' . $xml->getAttribute('Version'));
153
        }
154
155
        $this->issueInstant = Utils::xsDateTimeToTimestamp($xml->getAttribute('IssueInstant'));
156
157
        if ($xml->hasAttribute('Destination')) {
158
            $this->destination = $xml->getAttribute('Destination');
159
        }
160
161
        if ($xml->hasAttribute('Consent')) {
162
            $this->consent = $xml->getAttribute('Consent');
163
        }
164
165
        $issuer = Utils::xpQuery($xml, './saml_assertion:Issuer');
166
        if (!empty($issuer)) {
167
            $this->issuer = trim($issuer[0]->textContent);
168
        }
169
170
        /* Validate the signature element of the message. */
171
        try {
172
            $sig = Utils::validateElement($xml);
173
174 View Code Duplication
            if ($sig !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
175
                $this->messageContainedSignatureUponConstruction = true;
176
                $this->certificates = $sig['Certificates'];
177
                $this->validators[] = array(
178
                    'Function' => array('\SAML2\Utils', 'validateSignature'),
179
                    'Data' => $sig,
180
                    );
181
            }
182
        } catch (\Exception $e) {
183
            /* Ignore signature validation errors. */
184
        }
185
186
        $this->extensions = Extensions::getList($xml);
187
    }
188
189
    /**
190
     * Add a method for validating this message.
191
     *
192
     * This function is used by the HTTP-Redirect binding, to make it possible to
193
     * check the signature against the one included in the query string.
194
     *
195
     * @param callback $function The function which should be called.
196
     * @param mixed    $data     The data that should be included as the first parameter to the function.
197
     */
198
    public function addValidator($function, $data)
199
    {
200
        assert('is_callable($function)');
201
202
        $this->validators[] = array(
203
            'Function' => $function,
204
            'Data' => $data,
205
            );
206
    }
207
208
    /**
209
     * Validate this message against a public key.
210
     *
211
     * true is returned on success, false is returned if we don't have any
212
     * signature we can validate. An exception is thrown if the signature
213
     * validation fails.
214
     *
215
     * @param  XMLSecurityKey $key The key we should check against.
216
     * @return boolean        true on success, false when we don't have a signature.
217
     * @throws \Exception
218
     */
219 View Code Duplication
    public function validate(XMLSecurityKey $key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
220
    {
221
        if (count($this->validators) === 0) {
222
            return false;
223
        }
224
225
        $exceptions = array();
226
227
        foreach ($this->validators as $validator) {
228
            $function = $validator['Function'];
229
            $data = $validator['Data'];
230
231
            try {
232
                call_user_func($function, $data, $key);
233
                /* We were able to validate the message with this validator. */
234
235
                return true;
236
            } catch (\Exception $e) {
237
                $exceptions[] = $e;
238
            }
239
        }
240
241
        /* No validators were able to validate the message. */
242
        throw $exceptions[0];
243
    }
244
245
    /**
246
     * Retrieve the identifier of this message.
247
     *
248
     * @return string The identifier of this message.
249
     */
250
    public function getId()
251
    {
252
        return $this->id;
253
    }
254
255
    /**
256
     * Set the identifier of this message.
257
     *
258
     * @param string $id The new identifier of this message.
259
     */
260
    public function setId($id)
261
    {
262
        assert('is_string($id)');
263
264
        $this->id = $id;
265
    }
266
267
    /**
268
     * Retrieve the issue timestamp of this message.
269
     *
270
     * @return int The issue timestamp of this message, as an UNIX timestamp.
271
     */
272
    public function getIssueInstant()
273
    {
274
        return $this->issueInstant;
275
    }
276
277
    /**
278
     * Set the issue timestamp of this message.
279
     *
280
     * @param int $issueInstant The new issue timestamp of this message, as an UNIX timestamp.
281
     */
282
    public function setIssueInstant($issueInstant)
283
    {
284
        assert('is_int($issueInstant)');
285
286
        $this->issueInstant = $issueInstant;
287
    }
288
289
    /**
290
     * Retrieve the destination of this message.
291
     *
292
     * @return string|null The destination of this message, or NULL if no destination is given.
293
     */
294
    public function getDestination()
295
    {
296
        return $this->destination;
297
    }
298
299
    /**
300
     * Set the destination of this message.
301
     *
302
     * @param string|null $destination The new destination of this message.
303
     */
304
    public function setDestination($destination)
305
    {
306
        assert('is_string($destination) || is_null($destination)');
307
308
        $this->destination = $destination;
309
    }
310
311
    /**
312
     * Set the given consent for this message.
313
     *
314
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
315
     * @see \SAML2\Constants
316
     *
317
     * @param string $consent
318
     */
319
    public function setConsent($consent)
320
    {
321
        assert('is_string($consent)');
322
323
        $this->consent = $consent;
324
    }
325
326
    /**
327
     * Set the given consent for this message.
328
     *
329
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
330
     * @see \SAML2\Constants
331
     *
332
     * @return string Consent
333
     */
334
    public function getConsent()
335
    {
336
        return $this->consent;
337
    }
338
339
    /**
340
     * Retrieve the issuer if this message.
341
     *
342
     * @return string|null The issuer of this message, or NULL if no issuer is given.
343
     */
344
    public function getIssuer()
345
    {
346
        return $this->issuer;
347
    }
348
349
    /**
350
     * Set the issuer of this message.
351
     *
352
     * @param string|null $issuer The new issuer of this message.
353
     */
354
    public function setIssuer($issuer)
355
    {
356
        assert('is_string($issuer) || is_null($issuer)');
357
358
        $this->issuer = $issuer;
359
    }
360
361
    /**
362
     * Query whether or not the message contained a signature at the root level when the object was constructed.
363
     *
364
     * @return bool
365
     */
366
    public function isMessageConstructedWithSignature()
367
    {
368
        return $this->messageContainedSignatureUponConstruction;
369
    }
370
371
    /**
372
     * Retrieve the RelayState associated with this message.
373
     *
374
     * @return string|null The RelayState, or NULL if no RelayState is given.
375
     */
376
    public function getRelayState()
377
    {
378
        return $this->relayState;
379
    }
380
381
    /**
382
     * Set the RelayState associated with this message.
383
     *
384
     * @param string|null $relayState The new RelayState.
385
     */
386
    public function setRelayState($relayState)
387
    {
388
        assert('is_string($relayState) || is_null($relayState)');
389
390
        $this->relayState = $relayState;
391
    }
392
393
    /**
394
     * Convert this message to an unsigned XML document.
395
     *
396
     * This method does not sign the resulting XML document.
397
     *
398
     * @return \DOMElement The root element of the DOM tree.
399
     */
400
    public function toUnsignedXML()
401
    {
402
        $this->document = DOMDocumentFactory::create();
403
404
        $root = $this->document->createElementNS(Constants::NS_SAMLP, 'samlp:' . $this->tagName);
405
        $this->document->appendChild($root);
406
407
        /* Ugly hack to add another namespace declaration to the root element. */
408
        $root->setAttributeNS(Constants::NS_SAML, 'saml:tmp', 'tmp');
409
        $root->removeAttributeNS(Constants::NS_SAML, 'tmp');
410
411
        $root->setAttribute('ID', $this->id);
412
        $root->setAttribute('Version', '2.0');
413
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
414
415
        if ($this->destination !== null) {
416
            $root->setAttribute('Destination', $this->destination);
417
        }
418
        if ($this->consent !== null && $this->consent !== Constants::CONSENT_UNSPECIFIED) {
419
            $root->setAttribute('Consent', $this->consent);
420
        }
421
422
        if ($this->issuer !== null) {
423
            Utils::addString($root, Constants::NS_SAML, 'saml:Issuer', $this->issuer);
424
        }
425
426
        if (!empty($this->extensions)) {
427
            Extensions::addList($root, $this->extensions);
428
        }
429
430
        return $root;
431
    }
432
433
    /**
434
     * Convert this message to a signed XML document.
435
     *
436
     * This method sign the resulting XML document if the private key for
437
     * the signature is set.
438
     *
439
     * @return \DOMElement The root element of the DOM tree.
440
     */
441
    public function toSignedXML()
442
    {
443
        $root = $this->toUnsignedXML();
444
445
        if ($this->signatureKey === null) {
446
            /* We don't have a key to sign it with. */
447
448
            return $root;
449
        }
450
451
452
        /* Find the position we should insert the signature node at. */
453
        if ($this->issuer !== null) {
454
            /*
455
             * We have an issuer node. The signature node should come
456
             * after the issuer node.
457
             */
458
            $issuerNode = $root->firstChild;
459
            $insertBefore = $issuerNode->nextSibling;
460
        } else {
461
            /* No issuer node - the signature element should be the first element. */
462
            $insertBefore = $root->firstChild;
463
        }
464
465
466
        Utils::insertSignature($this->signatureKey, $this->certificates, $root, $insertBefore);
467
468
        return $root;
469
    }
470
471
472
    /**
473
     * Retrieve the private key we should use to sign the message.
474
     *
475
     * @return XMLSecurityKey|null The key, or NULL if no key is specified.
476
     */
477
    public function getSignatureKey()
478
    {
479
        return $this->signatureKey;
480
    }
481
482
483
    /**
484
     * Set the private key we should use to sign the message.
485
     *
486
     * If the key is null, the message will be sent unsigned.
487
     *
488
     * @param XMLSecurityKey|null $signatureKey
489
     */
490
    public function setSignatureKey(XMLsecurityKey $signatureKey = null)
491
    {
492
        $this->signatureKey = $signatureKey;
493
    }
494
495
496
    /**
497
     * Set the certificates that should be included in the message.
498
     *
499
     * The certificates should be strings with the PEM encoded data.
500
     *
501
     * @param array $certificates An array of certificates.
502
     */
503
    public function setCertificates(array $certificates)
504
    {
505
        $this->certificates = $certificates;
506
    }
507
508
509
    /**
510
     * Retrieve the certificates that are included in the message.
511
     *
512
     * @return array An array of certificates.
513
     */
514
    public function getCertificates()
515
    {
516
        return $this->certificates;
517
    }
518
519
520
    /**
521
     * Convert an XML element into a message.
522
     *
523
     * @param  \DOMElement    $xml The root XML element.
524
     * @return \SAML2\Message The message.
525
     * @throws \Exception
526
     */
527
    public static function fromXML(\DOMElement $xml)
528
    {
529 View Code Duplication
        if ($xml->namespaceURI !== Constants::NS_SAMLP) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
530
            throw new \Exception('Unknown namespace of SAML message: ' . var_export($xml->namespaceURI, true));
531
        }
532
533
        switch ($xml->localName) {
534
            case 'AttributeQuery':
535
                return new AttributeQuery($xml);
536
            case 'AuthnRequest':
537
                return new AuthnRequest($xml);
538
            case 'LogoutResponse':
539
                return new LogoutResponse($xml);
540
            case 'LogoutRequest':
541
                return new LogoutRequest($xml);
542
            case 'Response':
543
                return new Response($xml);
544
            case 'ArtifactResponse':
545
                return new ArtifactResponse($xml);
546
            case 'ArtifactResolve':
547
                return new ArtifactResolve($xml);
548
            default:
549
                throw new \Exception('Unknown SAML message: ' . var_export($xml->localName, true));
550
        }
551
    }
552
553
    /**
554
     * Retrieve the Extensions.
555
     *
556
     * @return \SAML2\XML\samlp\Extensions.
557
     */
558
    public function getExtensions()
559
    {
560
        return $this->extensions;
561
    }
562
563
    /**
564
     * Set the Extensions.
565
     *
566
     * @param array|null $extensions The Extensions.
567
     */
568
    public function setExtensions($extensions)
569
    {
570
        assert('is_array($extensions) || is_null($extensions)');
571
572
        $this->extensions = $extensions;
0 ignored issues
show
Documentation Bug introduced by
It seems like $extensions can be null. However, the property $extensions is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
573
    }
574
}
575