Completed
Push — master ( c190ee...433241 )
by Jaime Pérez
05:56
created

Message   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 585
Duplicated Lines 5.64 %

Coupling/Cohesion

Components 3
Dependencies 13

Importance

Changes 0
Metric Value
wmc 57
lcom 3
cbo 13
dl 33
loc 585
rs 6.433
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 64 10
A addValidator() 0 9 1
B validate() 25 25 4
A getId() 0 4 1
A setId() 0 6 1
A getIssueInstant() 0 4 1
A setIssueInstant() 0 6 1
A getDestination() 0 4 1
A setDestination() 0 6 1
A setConsent() 0 6 1
A getConsent() 0 4 1
A getIssuer() 0 8 3
A setIssuer() 0 6 1
A isMessageConstructedWithSignature() 0 4 1
A getRelayState() 0 4 1
A setRelayState() 0 6 1
C toUnsignedXML() 5 36 8
B toSignedXML() 0 27 3
A getSignatureKey() 0 4 1
A setSignatureKey() 0 4 1
A setCertificates() 0 4 1
A getCertificates() 0 4 1
D fromXML() 3 25 9
A getExtensions() 0 4 1
A setExtensions() 0 6 1
A getSignatureMethod() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Message often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Message, and based on these observations, apply Extract Interface, too.

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
 *
16
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
17
 */
18
abstract class Message implements SignedElement
19
{
20
    /**
21
     * Request extensions.
22
     *
23
     * @var array
24
     */
25
    protected $extensions;
26
27
    /**
28
     * The name of the root element of the DOM tree for the message.
29
     *
30
     * Used when creating a DOM tree from the message.
31
     *
32
     * @var string
33
     */
34
    private $tagName;
35
36
    /**
37
     * The identifier of this message.
38
     *
39
     * @var string
40
     */
41
    private $id;
42
43
    /**
44
     * The issue timestamp of this message, as an UNIX timestamp.
45
     *
46
     * @var int
47
     */
48
    private $issueInstant;
49
50
    /**
51
     * The destination URL of this message if it is known.
52
     *
53
     * @var string|null
54
     */
55
    private $destination;
56
57
    /**
58
     * The destination URL of this message if it is known.
59
     *
60
     * @var string|null
61
     */
62
    private $consent = Constants::CONSENT_UNSPECIFIED;
63
64
    /**
65
     * The entity id of the issuer of this message, or null if unknown.
66
     *
67
     * @var string|\SAML2\XML\saml\Issuer|null
68
     */
69
    private $issuer;
70
71
    /**
72
     * The RelayState associated with this message.
73
     *
74
     * @var string|null
75
     */
76
    private $relayState;
77
78
    /**
79
     * The \DOMDocument we are currently building.
80
     *
81
     * This variable is used while generating XML from this message. It holds the
82
     * \DOMDocument of the XML we are generating.
83
     *
84
     * @var \DOMDocument
85
     */
86
    protected $document;
87
88
    /**
89
     * The private key we should use to sign the message.
90
     *
91
     * The private key can be null, in which case the message is sent unsigned.
92
     *
93
     * @var XMLSecurityKey|null
94
     */
95
    private $signatureKey;
96
97
    /**
98
     * @var bool
99
     */
100
    protected $messageContainedSignatureUponConstruction = false;
101
102
    /**
103
     * List of certificates that should be included in the message.
104
     *
105
     * @var array
106
     */
107
    private $certificates;
108
109
    /**
110
     * Available methods for validating this message.
111
     *
112
     * @var array
113
     */
114
    private $validators;
115
116
    /**
117
     * @var null|string
118
     */
119
    private $signatureMethod;
120
121
    /**
122
     * Initialize a message.
123
     *
124
     * This constructor takes an optional parameter with a \DOMElement. If this
125
     * parameter is given, the message will be initialized with data from that
126
     * XML element.
127
     *
128
     * If no XML element is given, the message is initialized with suitable
129
     * default values.
130
     *
131
     * @param string           $tagName The tag name of the root element
132
     * @param \DOMElement|null $xml     The input message
133
     *
134
     * @throws \Exception
135
     */
136
    protected function __construct($tagName, \DOMElement $xml = null)
137
    {
138
        assert('is_string($tagName)');
139
        $this->tagName = $tagName;
140
141
        $this->id = Utils::getContainer()->generateId();
142
        $this->issueInstant = Temporal::getTime();
143
        $this->certificates = array();
144
        $this->validators = array();
145
146
        if ($xml === null) {
147
            return;
148
        }
149
150
        if (!$xml->hasAttribute('ID')) {
151
            throw new \Exception('Missing ID attribute on SAML message.');
152
        }
153
        $this->id = $xml->getAttribute('ID');
154
155
        if ($xml->getAttribute('Version') !== '2.0') {
156
            /* Currently a very strict check. */
157
            throw new \Exception('Unsupported version: '.$xml->getAttribute('Version'));
158
        }
159
160
        $this->issueInstant = Utils::xsDateTimeToTimestamp($xml->getAttribute('IssueInstant'));
161
162
        if ($xml->hasAttribute('Destination')) {
163
            $this->destination = $xml->getAttribute('Destination');
164
        }
165
166
        if ($xml->hasAttribute('Consent')) {
167
            $this->consent = $xml->getAttribute('Consent');
168
        }
169
170
        $issuer = Utils::xpQuery($xml, './saml_assertion:Issuer');
171
        if (!empty($issuer)) {
172
            $this->issuer = new XML\saml\Issuer($issuer[0]);
173
            if ($this->issuer->Format === Constants::NAMEID_ENTITY) {
174
                $this->issuer = $this->issuer->value;
175
            }
176
        }
177
178
        /* Validate the signature element of the message. */
179
        try {
180
            /** @var null|\DOMAttr $signatureMethod */
181
            $signatureMethod = Utils::xpQuery($xml, './ds:Signature/ds:SignedInfo/ds:SignatureMethod/@Algorithm');
182
183
            $sig = Utils::validateElement($xml);
184
185
            if ($sig !== false) {
186
                $this->messageContainedSignatureUponConstruction = true;
187
                $this->certificates = $sig['Certificates'];
188
                $this->validators[] = array(
189
                    'Function' => array('\SAML2\Utils', 'validateSignature'),
190
                    'Data' => $sig,
191
                );
192
                $this->signatureMethod = $signatureMethod[0]->value;
193
            }
194
        } catch (\Exception $e) {
195
            /* Ignore signature validation errors. */
196
        }
197
198
        $this->extensions = Extensions::getList($xml);
199
    }
200
201
    /**
202
     * Add a method for validating this message.
203
     *
204
     * This function is used by the HTTP-Redirect binding, to make it possible to
205
     * check the signature against the one included in the query string.
206
     *
207
     * @param callback $function The function which should be called
208
     * @param mixed    $data     The data that should be included as the first parameter to the function
209
     */
210
    public function addValidator($function, $data)
211
    {
212
        assert('is_callable($function)');
213
214
        $this->validators[] = array(
215
            'Function' => $function,
216
            'Data' => $data,
217
        );
218
    }
219
220
    /**
221
     * Validate this message against a public key.
222
     *
223
     * true is returned on success, false is returned if we don't have any
224
     * signature we can validate. An exception is thrown if the signature
225
     * validation fails.
226
     *
227
     * @param XMLSecurityKey $key The key we should check against
228
     *
229
     * @return bool true on success, false when we don't have a signature
230
     *
231
     * @throws \Exception
232
     */
233 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...
234
    {
235
        if (count($this->validators) === 0) {
236
            return false;
237
        }
238
239
        $exceptions = array();
240
241
        foreach ($this->validators as $validator) {
242
            $function = $validator['Function'];
243
            $data = $validator['Data'];
244
245
            try {
246
                call_user_func($function, $data, $key);
247
                /* We were able to validate the message with this validator. */
248
249
                return true;
250
            } catch (\Exception $e) {
251
                $exceptions[] = $e;
252
            }
253
        }
254
255
        /* No validators were able to validate the message. */
256
        throw $exceptions[0];
257
    }
258
259
    /**
260
     * Retrieve the identifier of this message.
261
     *
262
     * @return string The identifier of this message
263
     */
264
    public function getId()
265
    {
266
        return $this->id;
267
    }
268
269
    /**
270
     * Set the identifier of this message.
271
     *
272
     * @param string $id The new identifier of this message
273
     */
274
    public function setId($id)
275
    {
276
        assert('is_string($id)');
277
278
        $this->id = $id;
279
    }
280
281
    /**
282
     * Retrieve the issue timestamp of this message.
283
     *
284
     * @return int The issue timestamp of this message, as an UNIX timestamp
285
     */
286
    public function getIssueInstant()
287
    {
288
        return $this->issueInstant;
289
    }
290
291
    /**
292
     * Set the issue timestamp of this message.
293
     *
294
     * @param int $issueInstant The new issue timestamp of this message, as an UNIX timestamp
295
     */
296
    public function setIssueInstant($issueInstant)
297
    {
298
        assert('is_int($issueInstant)');
299
300
        $this->issueInstant = $issueInstant;
301
    }
302
303
    /**
304
     * Retrieve the destination of this message.
305
     *
306
     * @return string|null The destination of this message, or NULL if no destination is given
307
     */
308
    public function getDestination()
309
    {
310
        return $this->destination;
311
    }
312
313
    /**
314
     * Set the destination of this message.
315
     *
316
     * @param string|null $destination The new destination of this message
317
     */
318
    public function setDestination($destination)
319
    {
320
        assert('is_string($destination) || is_null($destination)');
321
322
        $this->destination = $destination;
323
    }
324
325
    /**
326
     * Set the given consent for this message.
327
     *
328
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
329
     *
330
     * @see \SAML2\Constants
331
     *
332
     * @param string $consent
333
     */
334
    public function setConsent($consent)
335
    {
336
        assert('is_string($consent)');
337
338
        $this->consent = $consent;
339
    }
340
341
    /**
342
     * Set the given consent for this message.
343
     *
344
     * Most likely (though not required) a value of rn:oasis:names:tc:SAML:2.0:consent.
345
     *
346
     * @see \SAML2\Constants
347
     *
348
     * @return string Consent
349
     */
350
    public function getConsent()
351
    {
352
        return $this->consent;
353
    }
354
355
    /**
356
     * Retrieve the issuer if this message.
357
     *
358
     * @return string|\SAML2\XML\saml\Issuer|null The issuer of this message, or NULL if no issuer is given
359
     */
360
    public function getIssuer()
361
    {
362
        if (is_string($this->issuer) || $this->issuer instanceof XML\saml\Issuer) {
363
            return $this->issuer;
364
        }
365
366
        return null;
367
    }
368
369
    /**
370
     * Set the issuer of this message.
371
     *
372
     * @param string|\SAML2\XML\saml\Issuer|null $issuer The new issuer of this message
373
     */
374
    public function setIssuer($issuer)
375
    {
376
        assert('is_string($issuer) || $issuer instanceof \SAML2\XML\saml\Issuer || is_null($issuer)');
377
378
        $this->issuer = $issuer;
379
    }
380
381
    /**
382
     * Query whether or not the message contained a signature at the root level when the object was constructed.
383
     *
384
     * @return bool
385
     */
386
    public function isMessageConstructedWithSignature()
387
    {
388
        return $this->messageContainedSignatureUponConstruction;
389
    }
390
391
    /**
392
     * Retrieve the RelayState associated with this message.
393
     *
394
     * @return string|null The RelayState, or NULL if no RelayState is given
395
     */
396
    public function getRelayState()
397
    {
398
        return $this->relayState;
399
    }
400
401
    /**
402
     * Set the RelayState associated with this message.
403
     *
404
     * @param string|null $relayState The new RelayState
405
     */
406
    public function setRelayState($relayState)
407
    {
408
        assert('is_string($relayState) || is_null($relayState)');
409
410
        $this->relayState = $relayState;
411
    }
412
413
    /**
414
     * Convert this message to an unsigned XML document.
415
     *
416
     * This method does not sign the resulting XML document.
417
     *
418
     * @return \DOMElement The root element of the DOM tree
419
     */
420
    public function toUnsignedXML()
421
    {
422
        $this->document = DOMDocumentFactory::create();
423
424
        $root = $this->document->createElementNS(Constants::NS_SAMLP, 'samlp:'.$this->tagName);
425
        $this->document->appendChild($root);
426
427
        /* Ugly hack to add another namespace declaration to the root element. */
428
        $root->setAttributeNS(Constants::NS_SAML, 'saml:tmp', 'tmp');
429
        $root->removeAttributeNS(Constants::NS_SAML, 'tmp');
430
431
        $root->setAttribute('ID', $this->id);
432
        $root->setAttribute('Version', '2.0');
433
        $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
434
435
        if ($this->destination !== null) {
436
            $root->setAttribute('Destination', $this->destination);
437
        }
438
        if ($this->consent !== null && $this->consent !== Constants::CONSENT_UNSPECIFIED) {
439
            $root->setAttribute('Consent', $this->consent);
440
        }
441
442
        if ($this->issuer !== null) {
443 View Code Duplication
            if (is_string($this->issuer)) {
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...
444
                Utils::addString($root, \SAML2_Const::NS_SAML, 'saml:Issuer', $this->issuer);
445
            } elseif ($this->issuer instanceof XML\saml\Issuer) {
446
                $this->issuer->toXML($root);
447
            }
448
        }
449
450
        if (!empty($this->extensions)) {
451
            Extensions::addList($root, $this->extensions);
452
        }
453
454
        return $root;
455
    }
456
457
458
    /**
459
     * Convert this message to a signed XML document.
460
     *
461
     * This method sign the resulting XML document if the private key for
462
     * the signature is set.
463
     *
464
     * @return \DOMElement The root element of the DOM tree
465
     */
466
    public function toSignedXML()
467
    {
468
        $root = $this->toUnsignedXML();
469
470
        if ($this->signatureKey === null) {
471
            /* We don't have a key to sign it with. */
472
473
            return $root;
474
        }
475
476
        /* Find the position we should insert the signature node at. */
477
        if ($this->issuer !== null) {
478
            /*
479
             * We have an issuer node. The signature node should come
480
             * after the issuer node.
481
             */
482
            $issuerNode = $root->firstChild;
483
            $insertBefore = $issuerNode->nextSibling;
484
        } else {
485
            /* No issuer node - the signature element should be the first element. */
486
            $insertBefore = $root->firstChild;
487
        }
488
489
        Utils::insertSignature($this->signatureKey, $this->certificates, $root, $insertBefore);
490
491
        return $root;
492
    }
493
494
    /**
495
     * Retrieve the private key we should use to sign the message.
496
     *
497
     * @return XMLSecurityKey|null The key, or NULL if no key is specified
498
     */
499
    public function getSignatureKey()
500
    {
501
        return $this->signatureKey;
502
    }
503
504
    /**
505
     * Set the private key we should use to sign the message.
506
     *
507
     * If the key is null, the message will be sent unsigned.
508
     *
509
     * @param XMLSecurityKey|null $signatureKey
510
     */
511
    public function setSignatureKey(XMLsecurityKey $signatureKey = null)
512
    {
513
        $this->signatureKey = $signatureKey;
514
    }
515
516
    /**
517
     * Set the certificates that should be included in the message.
518
     *
519
     * The certificates should be strings with the PEM encoded data.
520
     *
521
     * @param array $certificates An array of certificates
522
     */
523
    public function setCertificates(array $certificates)
524
    {
525
        $this->certificates = $certificates;
526
    }
527
528
    /**
529
     * Retrieve the certificates that are included in the message.
530
     *
531
     * @return array An array of certificates
532
     */
533
    public function getCertificates()
534
    {
535
        return $this->certificates;
536
    }
537
538
    /**
539
     * Convert an XML element into a message.
540
     *
541
     * @param \DOMElement $xml The root XML element
542
     *
543
     * @return \SAML2\Message The message
544
     *
545
     * @throws \Exception
546
     */
547
    public static function fromXML(\DOMElement $xml)
548
    {
549 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...
550
            throw new \Exception('Unknown namespace of SAML message: '.var_export($xml->namespaceURI, true));
551
        }
552
553
        switch ($xml->localName) {
554
            case 'AttributeQuery':
555
                return new AttributeQuery($xml);
556
            case 'AuthnRequest':
557
                return new AuthnRequest($xml);
558
            case 'LogoutResponse':
559
                return new LogoutResponse($xml);
560
            case 'LogoutRequest':
561
                return new LogoutRequest($xml);
562
            case 'Response':
563
                return new Response($xml);
564
            case 'ArtifactResponse':
565
                return new ArtifactResponse($xml);
566
            case 'ArtifactResolve':
567
                return new ArtifactResolve($xml);
568
            default:
569
                throw new \Exception('Unknown SAML message: '.var_export($xml->localName, true));
570
        }
571
    }
572
573
    /**
574
     * Retrieve the Extensions.
575
     *
576
     * @return \SAML2\XML\samlp\Extensions
577
     */
578
    public function getExtensions()
579
    {
580
        return $this->extensions;
581
    }
582
583
    /**
584
     * Set the Extensions.
585
     *
586
     * @param array|null $extensions The Extensions
587
     */
588
    public function setExtensions($extensions)
589
    {
590
        assert('is_array($extensions) || is_null($extensions)');
591
592
        $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...
593
    }
594
595
    /**
596
     * @return null|string
597
     */
598
    public function getSignatureMethod()
599
    {
600
        return $this->signatureMethod;
601
    }
602
}
603