Completed
Push — master ( ed4cb5...b14699 )
by Terrence
14:21
created

IdpList::create()   F

Complexity

Conditions 83
Paths 4

Size

Total Lines 397

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6972

Importance

Changes 0
Metric Value
dl 0
loc 397
ccs 0
cts 296
cp 0
rs 3.3333
c 0
b 0
f 0
cc 83
nc 4
nop 0
crap 6972

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace CILogon\Service;
4
5
use CILogon\Service\Util;
6
use DOMDocument;
7
use DOMImplementation;
8
use XSLTProcessor;
9
use SimpleXMLElement;
10
11
/**
12
 * IdpList
13
 *
14
 * This class manages the list of InCommon IdPs and their
15
 * attributes of interest. Since the InCommon-metadata.xml
16
 * file is rather large and slow to parse using xpath
17
 * queries, this class creates/reads/writes a smaller
18
 * file containing only the IdPs and the few attributes
19
 * needed by the CILogon Service.
20
 *
21
 * When you create a new instance of this class via
22
 * '$idplist = new idplist();', the code first tries to
23
 * read in a previously created idplist file. If no
24
 * such file can be read in successfully, the 'new' method
25
 * then reads in the big InCommon metadata in order to
26
 * create the class idparray and idpdom and write the
27
 * idplist file. You can (re)create the file at any time
28
 * by calling create() (which re-reads the InCommon
29
 * metadata) followed by write() (which writes the idplist
30
 * to file).
31
 *
32
 * There are several constants in the class that you
33
 * should set for your particular set up:
34
 *
35
 * DEFAULTIDPFILENAME - this is the full path and name
36
 *     of the processed IdP list file used by the CILogon
37
 *     Service. It should have read/write permissions for
38
 *     apache (via either owner or group).
39
 *
40
 * DEFAULTINCOMMONFILENAME - this is the full path and
41
 *     name of the InCommon metadata file used by the
42
 *     CILogon Service. It should have read permissions
43
 *     for apache (via either owner or group).
44
 *
45
 * TESTIDPFILENAME - this is the full path and name
46
 *     of an XML-formatted list of test IdPs. If found,
47
 *     these test IdPs will be added to the full IdP
48
 *     list when create()/write() is called. This file
49
 *     should have read/write permissions for apache.
50
 *
51
 * Note that this class previously defaulted to writing the idplist
52
 * as an XMl file and then reading that XML file back in. When all
53
 * InCommon and eduGAIN IdPs were 'whitelisted', the xpath queries
54
 * used on the read-in XML file were painfully slow. So now the default
55
 * is to read/write a JSON file and use a protected $idparray which is
56
 * an order of magnitude faster than xpath queries. You can still
57
 * read/write the XML file either by calling the associated read/write
58
 * methods (readXML/writeXML) or by setting $filetype='xml' in the
59
 * contructor. You will probably want to do this for the hourly cronjob
60
 * since the idplist.xml file is known to the world and should be
61
 * updated regularly.
62
 *
63
 * Example usage:
64
 *    require_once 'IdpList.php';
65
 *    // Read in extant idplist.json file, or create one from scratch
66
 *    $idplist = new IdpList();
67
 *    // Rescan InCommon metadata, update IdP list, and write to file
68
 *    $idplist->create();
69
 *    $idplist->setFilename('/tmp/newidplist.json');
70
 *    $idplist->write();
71
 */
72
class IdpList
73
{
74
75
    // Set the constants to correspond to your particular set up.
76
    /**
77
     * @var string DEFAULTIDPFILENAME The full path/filename of the
78
     *      generated list of IdPs in JSON format
79
     */
80
    const DEFAULTIDPFILENAME = '/var/www/html/include/idplist.json';
81
82
    /**
83
     * @var string DEFAULTINCOMMONFILENAME The full path/filename of the
84
     *      InCommon metadata XML file.
85
     */
86
    const DEFAULTINCOMMONFILENAME =
87
        '/var/cache/shibboleth/InCommon-metadata.xml';
88
89
    /**
90
     * @var string TESTIDPFILENAME The fill path/filename of the XML file
91
     *      containing test IdPs.
92
     */
93
    const TESTIDPFILENAME = '/var/www/html/include/testidplist.xml';
94
95
    /**
96
     * @var DOMDocument $idpdom A DOMDocument which holds the list of IdP
97
     *      entityIDs and their corresponding attributes.
98
     */
99
    protected $idpdom = null;
100
101
    /**
102
     * @var mixed $idparray An array version of $idpdom. It is used
103
     * primarily since searching an array is faster than xpath query.
104
     */
105
    protected $idparray = null;
106
107
    /**
108
     * @var string $idpfilename The name of the IdP list in JSON format.
109
     *      Defaults to DEFAULTIDPFILENAME.
110
     */
111
    protected $idpfilename;
112
113
    /**
114
     * @var string $incommonfilename The name of the InCommon metadata XML
115
     *      file. Defaults to DEFAULTINCOMMONFILENAME.
116
     */
117
    protected $incommonfilename;
118
119
    /**
120
     * __construct
121
     *
122
     * Default constructor. This method first attempts to read in an
123
     * existing idplist from a file and store it in the idparray /
124
     * idpdom. If a valid idplist file cannot be read and
125
     * $createfile is true, neW idparray / idpdom is created and
126
     * written to file.
127
     *
128
     * @param string $idpfilename (Optional) The name of the idplist file to
129
     *        read/write. Defaults to DEFAULTIDPFILENAME.
130
     * @param string $incommonfilename (Optional) The name of the InCommon
131
     *        metadata file to read. Defaults to DEFAULTINCOMMONFILENAME.
132
     * @param bool $createfile (Optional) Create idplist file if it doesn't
133
     *         exist? Defaults to true.
134
     * @param string $filetype (Optional) The type of file to read/write,
135
     *        one of 'xml' or 'json'. Defaults to 'json'.
136
     */
137
    public function __construct(
138
        $idpfilename = self::DEFAULTIDPFILENAME,
139
        $incommonfilename = self::DEFAULTINCOMMONFILENAME,
140
        $createfile = true,
141
        $filetype = 'json'
142
    ) {
143
        $this->setFilename($idpfilename);
144
        $this->setInCommonFilename($incommonfilename);
145
        $result = $this->read($filetype);
146
        if (($result === false) && ($createfile)) {
147
            $this->create();
148
            $this->write($filetype);
149
        }
150
    }
151
152
    /**
153
     * read
154
     *
155
     * This reads in the idplixt file based on the input filetype.
156
     * Defaults to reading in a JSON file.
157
     *
158
     * @param string $filetype (Optional) Type type of file to read, either
159
     *        'xml' or 'json'. Defaults to 'json'.
160
     * @return bool True if the idplist was read from file. False otherwise.
161
     */
162
    public function read($filetype = 'json')
163
    {
164
        if ($filetype == 'xml') {
165
            return $this->readXML();
166
        } elseif ($filetype == 'json') {
167
            return $this->readJSON();
168
        }
169
    }
170
171
    /**
172
     * readXML
173
     *
174
     * This method attempts to read in an existing idplist XML file and
175
     * store its contents in the class $idpdom DOMDocument. It also
176
     * converts the $idpdom to the internal $idparray if not already
177
     * set.
178
     *
179
     * @return bool True if the idplist file was read in correctly.
180
     *         False otherwise.
181
     */
182
    public function readXML()
183
    {
184
        $retval = false;  // Assume read failed
185
186
        $filename = $this->getFilename();
187
        if ((is_readable($filename)) &&
188
            (($dom = DOMDocument::load($filename, LIBXML_NOBLANKS)) !== false)) {
189
            $this->idpdom = $dom;
190
            $this->idpdom->preserveWhiteSpace = false;
191
            $this->idpdom->formatOutput = true;
192
            // Convert the read-in DOM to idparray for later use
193
            if (is_null($this->idparray)) {
194
                $this->idparray = $this->DOM2Array($this->idpdom);
195
            }
196
            $retval = true;
197
        } else {
198
            $this->idpdom = null;
199
        }
200
201
        return $retval;
202
    }
203
204
    /**
205
     * readJSON
206
     *
207
     * This method attempts to read in an existing idplist file
208
     * (containing JSON) and store its contents in the class $idparray.
209
     * Note that this does not update the internal $idpdom.
210
     *
211
     * @return bool True if the idplist file was read in correctly.
212
     *         False otherwise.
213
     */
214
    public function readJSON()
215
    {
216
        $retval = false;  // Assume read/json_decode failed
217
218
        $filename = $this->getFilename();
219
        if ((is_readable($filename)) &&
220
            (($contents = file_get_contents($filename)) !== false) &&
221
            (($tempjson = json_decode($contents, true)) !== null)) {
222
            $this->idparray = $tempjson;
223
            $retval = true;
224
        } else {
225
            $this->idparray = null;
226
        }
227
        return $retval;
228
    }
229
230
    /**
231
     * write
232
     *
233
     * This writes out the idplixt file based on the input filetype.
234
     * Defaults to writing a JSON file.
235
     *
236
     * @param string $filetype (Optional) Type type of file to write, either
237
     *        'xml' or 'json'. Defaults to 'json'.
238
     * @return bool True if the idplist was written to file. False
239
     *         otherwise.
240
     */
241
    public function write($filetype = 'json')
242
    {
243
        if ($filetype == 'xml') {
244
            return $this->writeXML();
245
        } elseif ($filetype == 'json') {
246
            return $this->writeJSON();
247
        }
248
    }
249
250
    /**
251
     * writeXML
252
     *
253
     * This method writes the class $idpdom to an XML file. It does
254
     * this by first writing to a temporary file in /tmp, then renaming
255
     * the temp file to the final idplist XML filename. Note that if
256
     * the internal $idpdom does not exist, it attempts to first
257
     * convert the internal $idparray to DOM and then write it.
258
     *
259
     * @return bool True if the idpdom was written to the idplist XML
260
     *         file. False otherwise.
261
     */
262
    public function writeXML()
263
    {
264
        $retval = false; // Assume write failed
265
266
        // If no idpdom, convert idparray to DOM
267
        if (is_null($this->idpdom)) {
268
            $this->idpdom = $this->array2DOM($this->idparray);
269
        }
270
271
        if (!is_null($this->idpdom)) {
272
            $this->idpdom->preserveWhiteSpace = false;
273
            $this->idpdom->formatOutput = true;
274
            $filename = $this->getFilename();
275
            $tmpfname = tempnam('/tmp', 'IDP');
276
            if (($this->idpdom->save($tmpfname) > 0) &&
277
                (@rename($tmpfname, $filename))) {
278
                chmod($filename, 0664);
279
                $retval = true;
280
            } else {
281
                @unlink($tmpfname);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
282
            }
283
        }
284
285
        return $retval;
286
    }
287
288
    /**
289
     * writeJSON
290
     *
291
     * This method writes the class $idparray to a JSON file
292
     * It does this by first writing to a temporary file in /tmp,
293
     * then renaming the temp file to the final idplist JSON filename.
294
     *
295
     * @return bool True if the idparray was written to the idplist
296
     *         JSON file. False otherwise.
297
     */
298
    public function writeJSON()
299
    {
300
        $retval = false; // Assume write failed
301
302
        if (!is_null($this->idparray)) {
303
            $filename = $this->getFilename();
304
            $tmpfname = tempnam('/tmp', 'JSON');
305
            $json = json_encode($this->idparray, JSON_FORCE_OBJECT);
306
            if (((file_put_contents($tmpfname, $json)) !== false) &&
307
                (@rename($tmpfname, $filename))) {
308
                chmod($filename, 0664);
309
                $retval = true;
310
            } else {
311
                @unlink($tmpfname);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
312
            }
313
        }
314
315
        return $retval;
316
    }
317
318
    /**
319
     * addNode
320
     *
321
     * This is a convenience method used by create() to add a new
322
     * child node (such as 'Organization_Name') to a parent idp node.
323
     * It also adds elements to the internal $idparray, thus creating
324
     * the internal idparray at the same time as the idpdom.
325
     *
326
     * @param \DOMDocument $dom A DOMDocument object
327
     * @param \DOMElement $idpnode A pointer to a parent <idp> DOMElement
328
     * @param string $nodename The name of the new child node DOMElement
329
     * @param string $nodevalue The value of the new child node DOMElement
330
     */
331
    private function addNode($dom, $idpnode, $nodename, $nodevalue)
332
    {
333
        $nodename = trim($nodename);    // Remove leading/trailing
334
        $nodevalue = trim($nodevalue);  // spaces, tabs, etc.
335
        $elemnode = $dom->createElement($nodename);
336
        $textnode = $dom->createTextNode($nodevalue);
337
        $elemnode->appendChild($textnode);
338
        $idpnode->appendChild($elemnode);
339
        // Also add element to the internal $idparray for later use
340
        $this->idparray[$idpnode->getAttribute('entityID')][$nodename] =
341
            $nodevalue;
342
    }
343
344
    /**
345
     * sortDOM
346
     *
347
     * This method is called by create() to sort the newly created
348
     * DOMDocument <idp> nodes by Display_Name. It uses an XSL
349
     * transformation to do the work. A new DOMDocument is created
350
     * and returned.
351
     *
352
     * @param DOMDocument $dom A DOMDocument to be sorted by Display_Name
353
     * @return DOMDocument A new DOMDocument with the <idp> elements sorted by
354
     *         Display_Name.
355
     */
356
    private function sortDOM($dom)
357
    {
358
        $xsltsort = <<<EOT
359
            <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
360
                            version="1.0">
361
            <xsl:output method="xml" encoding="UTF-8"/>
362
            <xsl:template match="node() | @*">
363
              <xsl:copy>
364
                <xsl:apply-templates select="node() | @*">
365
                  <xsl:sort select="translate(Display_Name,
366
                      'abcdefghijklmnopqrstuvwxyz',
367
                      'ABCDEFGHIJKLMNOPQRSTUVWXYZ')"
368
                  data-type="text" order="ascending"/>
369
                </xsl:apply-templates>
370
              </xsl:copy>
371
            </xsl:template>
372
            </xsl:stylesheet>
373
EOT;
374
        $xsl = new DOMDocument('1.0');
375
        $xsl->loadXML($xsltsort);
376
        $proc = new XSLTProcessor();
377
        $proc->importStyleSheet($xsl);
378
        $newdom = $proc->transformToDoc($dom);
379
        return $newdom;
380
    }
381
382
    /**
383
     * create
384
     *
385
     * This method is used to populate the class $idpdom DOMDocument
386
     * using information from the InCommon metadata file. Note that
387
     * method updates $idpdom and $idparray. If you want to save either
388
     * to a file, be sure to call write() afterwards.
389
     *
390
     * @return bool True upon successful extraction of IdP information
391
     *         from the InCommon metadata file into the class
392
     *         $idpdom DOMDocument. False otherwise.
393
     */
394
    public function create()
395
    {
396
        $retval = false; // Assume create failed
397
        if (is_readable($this->getInCommonFilename())) {
398
            // Read in the InCommon metadata file
399
            $xmlstr = @file_get_contents($this->getInCommonFilename());
400
            if (strlen($xmlstr) > 0) {
401
                // Need to fix the namespace for Xpath queries to work
402
                $xmlstr = str_replace('xmlns=', 'ns=', $xmlstr);
403
                $xml = new SimpleXMLElement($xmlstr);
404
405
                // Select only IdPs from the InCommon metadata
406
                $result = $xml->xpath(
407
                    "//EntityDescriptor/IDPSSODescriptor" .
408
                    "/ancestor::EntityDescriptor"
409
                );
410
411
                // CIL-401 - Read in the global blacklist.txt file.
412
                // Don't add a <Whitelsited> tag for IdPs in this file.
413
                $blackidps = Util::readArrayFromFile(
414
                    '/var/www/html/include/blacklist.txt'
415
                );
416
417
                // Create a DOMDocument to build up the list of IdPs.
418
                $dom = DOMImplementation::createDocument(null, 'idps');
419
                $idps = $dom->documentElement; // Top level <idps> element
420
421
                // Loop through the IdPs searching for desired attributes
422
                foreach ($result as $idx) {
423
                    // Need to set namespace prefixes for xpath queries to work
424
                    $sxe = $idx[0];
425
                    $sxe->registerXPathNamespace(
426
                        'mdattr',
427
                        'urn:oasis:names:tc:SAML:metadata:attribute'
428
                    );
429
                    $sxe->registerXPathNamespace(
430
                        'saml',
431
                        'urn:oasis:names:tc:SAML:2.0:assertion'
432
                    );
433
                    $sxe->registerXPathNamespace(
434
                        'mdrpi',
435
                        'urn:oasis:names:tc:SAML:metadata:rpi'
436
                    );
437
                    $sxe->registerXPathNamespace(
438
                        'mdui',
439
                        'urn:oasis:names:tc:SAML:metadata:ui'
440
                    );
441
442
                    // Skip any hide-from-discovery entries
443
                    $xp = $sxe->xpath(
444
                        "Extensions/mdattr:EntityAttributes/" .
445
                        "saml:Attribute[@Name='" .
446
                        "http://macedir.org/entity-category']/" .
447
                        "saml:AttributeValue"
448
                    );
449
                    if (($xp !== false) && (count($xp) > 0)) {
450
                        $hide = false;
451
                        foreach ($xp as $value) {
452
                            if ($value == 'http://refeds.org/category/hide-from-discovery') {
453
                                $hide = true;
454
                                break;
455
                            }
456
                        }
457
                        if ($hide) {
458
                            continue;
459
                        }
460
                    }
461
462
                    // Get the entityID of the IdP. Save it for later.
463
                    // The entityID will be the keys of the class idpdom.
464
                    $entityID = '';
0 ignored issues
show
Unused Code introduced by
$entityID is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
465
                    $xp = $idx[0]->xpath('attribute::entityID');
466
                    if (($xp !== false) && (count($xp) > 0)) {
467
                        $entityID = (string)$xp[0]->entityID;
468
                    } else { // No entityID is bad!
469
                        continue;
470
                    }
471
472
                    // Create an <idp> element to hold sub elements
473
                    $idp = $dom->createElement('idp');
474
                    $idp->setAttribute('entityID', $entityID);
475
                    $idps->appendChild($idp);
476
477
                    // Search for the desired <idp> attribute sub-blocks
478
479
                    // Look for OrganizationDisplayName and mdui:DisplayName.
480
                    $Organization_Name = '';
481
                    $Display_Name = '';
482
483
                    $xp = $idx[0]->xpath(
484
                        "Organization/OrganizationDisplayName[starts-with(@xml:lang,'en')]"
485
                    );
486
                    if (($xp !== false) && (count($xp) > 0)) {
487
                        $Organization_Name = (string)$xp[0];
488
                    }
489
490
                    $xp = $sxe->xpath(
491
                        "IDPSSODescriptor/Extensions/mdui:UIInfo/mdui:DisplayName[starts-with(@xml:lang,'en')]"
492
                    );
493
                    if (($xp !== false) && (count($xp) > 0)) {
494
                        $Display_Name = (string)$xp[0];
495
                    }
496
497
                    // If neither OrganizationDisplayName nor mdui:DisplayName
498
                    // was found, then use the entityID as a last resort.
499
                    if ((strlen($Organization_Name) == 0) &&
500
                        (strlen($Display_Name) == 0)) {
501
                        $Organization_Name = $entityID;
502
                        $Display_Name = $entityID;
503
                    }
504
505
                    // Add nodes for both Organization_Name and Display_Name,
506
                    // using the value of the other if one is empty.
507
                    $this->addNode(
508
                        $dom,
509
                        $idp,
510
                        'Organization_Name',
511
                        ((strlen($Organization_Name) > 0) ?
512
                            $Organization_Name :
513
                            $Display_Name)
514
                    );
515
                    $this->addNode(
516
                        $dom,
517
                        $idp,
518
                        'Display_Name',
519
                        ((strlen($Display_Name) > 0) ?
520
                            $Display_Name :
521
                            $Organization_Name)
522
                    );
523
524
                    $xp = $idx[0]->xpath('Organization/OrganizationURL');
525
                    if (($xp !== false) && (count($xp) > 0)) {
526
                        $this->addNode($dom, $idp, 'Home_Page', (string)$xp[0]);
527
                    }
528
529
                    $name = '';
530
                    $xp = $idx[0]->xpath(
531
                        "ContactPerson[@contactType='support']/GivenName"
532
                    );
533
                    if (($xp !== false) && (count($xp) > 0)) {
534
                        $name = (string)$xp[0];
535
                    }
536
                    $xp = $idx[0]->xpath(
537
                        "ContactPerson[@contactType='support']/SurName"
538
                    );
539
                    if (($xp !== false) && (count($xp) > 0)) {
540
                        $name .= ((strlen($name) > 0) ? ' ' : '') .
541
                            (string)($xp[0]);
542
                    }
543
                    if (strlen($name) > 0) {
544
                        $this->addNode($dom, $idp, 'Support_Name', $name);
545
                    }
546
547
                    $xp = $idx[0]->xpath(
548
                        "ContactPerson[@contactType='support']/EmailAddress"
549
                    );
550
                    if (($xp !== false) && (count($xp) > 0)) {
551
                        $this->addNode(
552
                            $dom,
553
                            $idp,
554
                            'Support_Address',
555
                            (string)$xp[0]
556
                        );
557
                    }
558
559
                    $name = '';
560
                    $xp = $idx[0]->xpath(
561
                        "ContactPerson[@contactType='technical']/GivenName"
562
                    );
563
                    if (($xp !== false) && (count($xp) > 0)) {
564
                        $name = (string)$xp[0];
565
                    }
566
                    $xp = $idx[0]->xpath(
567
                        "ContactPerson[@contactType='technical']/SurName"
568
                    );
569
                    if (($xp !== false) && (count($xp) > 0)) {
570
                        $name .= ((strlen($name) > 0) ? ' ' : '') .
571
                            (string)($xp[0]);
572
                    }
573
                    if (strlen($name) > 0) {
574
                        $this->addNode($dom, $idp, 'Technical_Name', $name);
575
                    }
576
577
                    $xp = $idx[0]->xpath(
578
                        "ContactPerson[@contactType='technical']/EmailAddress"
579
                    );
580
                    if (($xp !== false) && (count($xp) > 0)) {
581
                        $this->addNode(
582
                            $dom,
583
                            $idp,
584
                            'Technical_Address',
585
                            (string)$xp[0]
586
                        );
587
                    }
588
589
                    $name = '';
590
                    $xp = $idx[0]->xpath(
591
                        "ContactPerson[@contactType='administrative']/GivenName"
592
                    );
593
                    if (($xp !== false) && (count($xp) > 0)) {
594
                        $name = (string)$xp[0];
595
                    }
596
                    $xp = $idx[0]->xpath(
597
                        "ContactPerson[@contactType='administrative']/SurName"
598
                    );
599
                    if (($xp !== false) && (count($xp) > 0)) {
600
                        $name .= ((strlen($name) > 0) ? ' ' : '') .
601
                            (string)($xp[0]);
602
                    }
603
                    if (strlen($name) > 0) {
604
                        $this->addNode($dom, $idp, 'Administrative_Name', $name);
605
                    }
606
607
                    $xp = $idx[0]->xpath(
608
                        "ContactPerson[@contactType='administrative']/EmailAddress"
609
                    );
610
                    if (($xp !== false) && (count($xp) > 0)) {
611
                        $this->addNode(
612
                            $dom,
613
                            $idp,
614
                            'Administrative_Address',
615
                            (string)$xp[0]
616
                        );
617
                    }
618
619
                    // Check for assurance-certification = silver, bronze, or SIRTFI
620
                    $xp = $sxe->xpath(
621
                        "Extensions/mdattr:EntityAttributes/" .
622
                        "saml:Attribute[@Name='" .
623
                        "urn:oasis:names:tc:SAML:attribute:" .
624
                        "assurance-certification']/saml:AttributeValue"
625
                    );
626
                    if (($xp !== false) && (count($xp) > 0)) {
627
                        foreach ($xp as $value) {
628
                            if ($value == 'http://id.incommon.org/assurance/silver') {
629
                                $this->addNode($dom, $idp, 'Silver', '1');
630
                            } elseif ($value == 'http://id.incommon.org/assurance/bronze') {
631
                                $this->addNode($dom, $idp, 'Bronze', '1');
632
                            } elseif ($value == 'https://refeds.org/sirtfi') {
633
                                $this->addNode($dom, $idp, 'SIRTFI', '1');
634
                            }
635
                        }
636
                    }
637
638
                    // Check for registered-by-incommon
639
                    $xp = $sxe->xpath(
640
                        "Extensions/mdattr:EntityAttributes/" .
641
                        "saml:Attribute[@Name='" .
642
                        "http://macedir.org/entity-category']/" .
643
                        "saml:AttributeValue"
644
                    );
645
                    if (($xp !== false) && (count($xp) > 0)) {
646
                        foreach ($xp as $value) {
647
                            if ($value == 'http://id.incommon.org/category/registered-by-incommon') {
648
                                $this->addNode(
649
                                    $dom,
650
                                    $idp,
651
                                    'Registered_By_InCommon',
652
                                    '1'
653
                                );
654
                                break;
655
                            }
656
                        }
657
                    }
658
659
                    // Check for research-and-scholarship
660
                    $xp = $sxe->xpath(
661
                        "Extensions/mdattr:EntityAttributes/" .
662
                        "saml:Attribute[@Name='" .
663
                        "http://macedir.org/entity-category-support']/" .
664
                        "saml:AttributeValue"
665
                    );
666
                    if (($xp !== false) && (count($xp) > 0)) {
667
                        $addedrands = false;
668
                        $incommonrands = false;
669
                        $refedsrands = false;
670
                        foreach ($xp as $value) {
671
                            if ($value == 'http://id.incommon.org/category/research-and-scholarship') {
672
                                $incommonrands = true;
673
                                $this->addNode($dom, $idp, 'InCommon_RandS', '1');
674
                            }
675
                            if ($value == 'http://refeds.org/category/research-and-scholarship') {
676
                                $refedsrands = true;
677
                                $this->addNode($dom, $idp, 'REFEDS_RandS', '1');
678
                            }
679
                            if ((!$addedrands) &&
680
                                ($incommonrands || $refedsrands)) {
681
                                $addedrands = true;
682
                                $this->addNode($dom, $idp, 'RandS', '1');
683
                            }
684
                        }
685
                    }
686
687
                    // CIL-558 Check for <SingleLogoutService>
688
                    $Logout = '';
689
                    // First, check for HTTP-Redirect version
690
                    $xp = $sxe->xpath("IDPSSODescriptor/SingleLogoutService" .
691
                        "[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']");
692
                    if (($xp !== false) && (count($xp) > 0)) {
693
                        $Logout = (string)($xp[0]->attributes())['Location'];
694
                    }
695
                    // If no HTTP-Redirect, check for HTTP-POST
696
                    if (empty($Logout)) {
697
                        $xp = $sxe->xpath("IDPSSODescriptor/SingleLogoutService" .
698
                            "[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']");
699
                        if (($xp !== false) && (count($xp) > 0)) {
700
                            $Logout = (string)($xp[0]->attributes())['Location'];
701
                        }
702
                    }
703
                    // Finally, a hack for Shibboleth-based IdPs.
704
                    // Check for <SingleSignOnService> HTTP-Redirect
705
                    // and regex for the built-in Simple Logout URL.
706
                    if (empty($Logout)) {
707
                        $xp = $sxe->xpath("IDPSSODescriptor/SingleSignOnService" .
708
                            "[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']");
709
                        if (($xp !== false) && (count($xp) > 0)) {
710
                            $tmp = (string)($xp[0]->attributes())['Location'];
711
                            if (preg_match('|^(.*)/profile/|', $tmp, $matches)) {
712
                                $Logout = $matches[1] . '/profile/Logout';
713
                            }
714
                        }
715
                    }
716
                    if (!empty($Logout)) {
717
                        // If Shib IdP, transform URL into Simple Logout URL
718
                        // https://wiki.shibboleth.net/confluence/x/AAJSAQ
719
                        if (preg_match('|^(.*)/profile/|', $Logout, $matches)) {
720
                            $Logout = $matches[1] . '/profile/Logout';
721
                        }
722
                        $this->addNode($dom, $idp, 'Logout', $Logout);
723
                    }
724
725
                    // Add a <Whitelisted> block for all IdPs
726
                    // not in the blacklist.txt file.
727
                    if (!array_key_exists($entityID, $blackidps)) {
728
                        $this->addNode($dom, $idp, 'Whitelisted', '1');
729
                    }
730
                }
731
732
                // Read in any test IdPs and add them to the list
733
                if ((is_readable(static::TESTIDPFILENAME)) &&
734
                    (($dom2 = DOMDocument::load(
735
                        static::TESTIDPFILENAME
736
                    )) !== false)) {
737
                    $idpnodes = $dom2->getElementsByTagName('idp');
738
                    foreach ($idpnodes as $idpnode) {
739
                        // Check if the entityID already exists. If so,
740
                        // delete it from both the idps DOM and the idparray
741
                        // and instead add the one from the testidplist.
742
                        $entityID = $idpnode->attributes->item(0)->value;
743
                        if (array_key_exists($entityID, $this->idparray)) {
744
                            // Easy - simply delete the array entry for the
745
                            // existing entityID
746
                            unset($this->idparray[$entityID]);
747
748
                            // Hard - search through the current DOM for a
749
                            // matching entityID to get the DOMNode, which
750
                            // can then be removed from the DOM.
751
                            $curridpnodes = $dom->getElementsByTagName('idp');
752
                            foreach ($curridpnodes as $curridpnode) {
753
                                $currEntityID =
754
                                    $curridpnode->attributes->item(0)->value;
755
                                if ($currEntityID == $entityID) {
756
                                    $idps->removeChild($curridpnode);
757
                                    break;
758
                                }
759
                            }
760
                        }
761
762
                        // Add the new idp node to the DOM
763
                        $node = $dom->importNode($idpnode, true);
764
                        $idps->appendChild($node);
765
766
                        // Add the testidplist nodes to the $idparray
767
                        foreach ($node->childNodes as $child) {
768
                            if ($child->nodeName != '#text') {
769
                                $this->idparray[$entityID][$child->nodeName] =
770
                                    $child->nodeValue;
771
                            }
772
                        }
773
                    }
774
                }
775
776
                // Sort the DOMDocument and idparray by Display_Name
777
                $this->idpdom = $this->sortDOM($dom);
778
                uasort($this->idparray, function ($a, $b) {
779
                    return strcasecmp(
780
                        $a['Display_Name'],
781
                        $b['Display_Name']
782
                    );
783
                });
784
785
                $retval = true;
786
            }
787
        }
788
789
        return $retval;
790
    }
791
792
    /**
793
     * getFilename
794
     *
795
     * This function returns a string of the full path of the IdP list
796
     * filename.  See also setFilename().
797
     *
798
     * @return string The IdP list filename.
799
     */
800
    public function getFilename()
801
    {
802
        return $this->idpfilename;
803
    }
804
805
    /**
806
     * setFilename
807
     *
808
     * This function sets the string of the full path of the IdP list
809
     * filename.  See also getFilename().
810
     *
811
     * @param string $filename he new name of the IdP list filename.
812
     */
813
    public function setFilename($filename)
814
    {
815
        $this->idpfilename = $filename;
816
    }
817
818
    /**
819
     * getInCommonFilename
820
     *
821
     * This function returns a string of the full path of the InCommon
822
     * metadata filename.  See also setInCommonFilename().
823
     *
824
     * @return string The InCommon metadata filename.
825
     */
826
    public function getInCommonFilename()
827
    {
828
        return $this->incommonfilename;
829
    }
830
831
    /**
832
     * setInCommonFilename
833
     *
834
     * This function sets the string of the full path of the InCommon
835
     * metadata filename.  See also getInCommonFilename().
836
     *
837
     * @param string $filename The new name of the InCommon metadata filename.
838
     */
839
    public function setInCommonFilename($filename)
840
    {
841
        $this->incommonfilename = $filename;
842
    }
843
844
    /**
845
     * getEntityIDs
846
     *
847
     * This method returns the entityIDs of the idplist as an array.
848
     *
849
     * @return array An array of the entityIDs
850
     */
851
    public function getEntityIDs()
852
    {
853
        $retarr = array();
854
        if (is_array($this->idparray)) {
855
            $retarr = array_keys($this->idparray);
856
        }
857
        return $retarr;
858
    }
859
860
    /**
861
     * getOrganizationName
862
     *
863
     * This function returns the Organization_Name of the selected
864
     * $entityID.
865
     *
866
     * @param string $entityID The entityID to search for
867
     * @return string The Organization_Name for the $entityID. Return
868
     *         string is empty if no matching $entityID found.
869
     */
870
    public function getOrganizationName($entityID)
871
    {
872
        $retval = '';
873
        if (isset($this->idparray[$entityID]['Organization_Name'])) {
874
            $retval = $this->idparray[$entityID]['Organization_Name'];
875
        }
876
        return $retval;
877
    }
878
879
    /**
880
     * getDisplayName
881
     *
882
     * This function returns the Display_Name of the selected
883
     * $entityID.
884
     *
885
     * @param string $entityID The entityID to search for
886
     * @return string The Display_Name for the $entityID. Return
887
     *         string is empty if no matching $entityID found.
888
     */
889
    public function getDisplayName($entityID)
890
    {
891
        $retval = '';
892
        if (isset($this->idparray[$entityID]['Display_Name'])) {
893
            $retval = $this->idparray[$entityID]['Display_Name'];
894
        }
895
        return $retval;
896
    }
897
898
    /**
899
     * getLogout
900
     *
901
     * This function returns the Logout URL of the selected $entityID.
902
     *
903
     * @param string $entityID The entityID to search for
904
     * @return string The Logout  URLfor the $entityID. Return
905
     *         string is empty if no matching $entityID found.
906
     */
907
    public function getLogout($entityID)
908
    {
909
        $retval = '';
910
        if (isset($this->idparray[$entityID]['Logout'])) {
911
            $retval = $this->idparray[$entityID]['Logout'];
912
        }
913
        return $retval;
914
    }
915
916
    /**
917
     * entityIDExists
918
     *
919
     * This function searchs for the given idp entityID.
920
     *
921
     * @param string $entityID The entityID to search for
922
     * @return bool True if the given entityID is found. False otherwise.
923
     */
924
    public function entityIDExists($entityID)
925
    {
926
        return (isset($this->idparray[$entityID]));
927
    }
928
929
    /**
930
     * exists
931
     *
932
     * This is simply a convenience function for entityIDExists.
933
     *
934
     * @param string $entityID The enityID to search for
935
     * @return bool True if the given entityID is found. False otherwise.
936
     */
937
    public function exists($entityID)
938
    {
939
        return $this->entityIDExists($entityID);
940
    }
941
942
    /**
943
     * isAttributeSet
944
     *
945
     * This function checks if the passed-in $attr is set to '1' for
946
     * the entityID, and returns true if so.
947
     *
948
     * @param string $entityID The enityID to search for.
949
     * @param string $attr The attribute in question.
950
     * @return bool True if the given attribute is '1' for the entityID.
951
     *         False otherwise.
952
     */
953
    public function isAttributeSet($entityID, $attr)
954
    {
955
        return (isset($this->idparray[$entityID][$attr]) &&
956
                     ($this->idparray[$entityID][$attr] == 1));
957
    }
958
959
    /**
960
     * isWhitelisted
961
     *
962
     * This method searches for the given entityID and checks if the
963
     *'Whitelisted' entry has been set to '1'.
964
     *
965
     * @param string $entityID The enityID to search for
966
     * @return bool True if the given entityID is marked 'Whitelisted'.
967
     *         False otherwise.
968
     */
969
    public function isWhitelisted($entityID)
970
    {
971
        return $this->isAttributeSet($entityID, 'Whitelisted');
972
    }
973
974
    /**
975
     * isSilver
976
     *
977
     * This method searches for the given entityID and checks if the
978
     *'Silver' entry has been set to '1'.
979
     *
980
     * @param string $entityID The enityID to search for
981
     * @return bool True if the given entityID is certified 'Silver'.
982
     *         False otherwise.
983
     */
984
    public function isSilver($entityID)
985
    {
986
        return $this->isAttributeSet($entityID, 'Silver');
987
    }
988
989
    /**
990
     * isBronze
991
     *
992
     * This method searches for the given entityID and checks if the
993
     *'Bronze' entry has been set to '1'.
994
     *
995
     * @param string $entityID The enityID to search for
996
     * @return bool True if the given entityID is certified 'Bronze'.
997
     *         False otherwise.
998
     */
999
    public function isBronze($entityID)
1000
    {
1001
        return $this->isAttributeSet($entityID, 'Bronze');
1002
    }
1003
1004
    /**
1005
     * isRandS
1006
     *
1007
     * This method searches for the given entityID and checks if the
1008
     *'RandS' entry has been set to '1'.
1009
     *
1010
     * @param string $entityID The enityID to search for
1011
     * @return bool True if the given entityID is listed as 'RandS'
1012
     *         (research-and-scholarship). False otherwise.
1013
     */
1014
    public function isRandS($entityID)
1015
    {
1016
        return $this->isAttributeSet($entityID, 'RandS');
1017
    }
1018
1019
    /**
1020
     * isInCommonRandS
1021
     *
1022
     * This method searches for the given entityID and checks if the
1023
     *'InCommon_RandS' entry has been set to '1'.
1024
     *
1025
     * @param string $entityID The enityID to search for
1026
     * @return bool True if the given entityID is listed as
1027
     *        'InCommon_RandS'. False otherwise.
1028
     */
1029
    public function isInCommonRandS($entityID)
1030
    {
1031
        return $this->isAttributeSet($entityID, 'InCommon_RandS');
1032
    }
1033
1034
    /**
1035
     * isREFEDSRandS
1036
     *
1037
     * This method searches for the given entityID and checks if the
1038
     *'REFEDS_RandS' entry has been set to '1'.
1039
     *
1040
     * @param string $entityID The enityID to search for
1041
     * @return bool True if the given entityID is listed as
1042
     *         'REFEDS_RandS'. False otherwise.
1043
     */
1044
    public function isREFEDSRandS($entityID)
1045
    {
1046
        return $this->isAttributeSet($entityID, 'REFEDS_RandS');
1047
    }
1048
1049
    /**
1050
     * isRegisteredByInCommon
1051
     *
1052
     * This method searches for the given entityID and checks if the
1053
     *'Registered_By_InCommon' entry has been set to '1'.
1054
     *
1055
     * @param string $entityID The enityID to search for
1056
     * @return bool True if the given entityID is listed as
1057
     *         'Registered_By_InCommon'. False otherwise.
1058
     */
1059
    public function isRegisteredByInCommon($entityID)
1060
    {
1061
        return $this->isAttributeSet($entityID, 'Registered_By_InCommon');
1062
    }
1063
1064
    /**
1065
     * isSIRTFI
1066
     *
1067
     * This method searches for the given entityID and checks if the
1068
     *'SIRTFI' entry has been set to '1'.
1069
     *
1070
     * @param string $entityID The enityID to search for
1071
     * @return bool True if the given entityID is listed as
1072
     *         SIRTFI. False otherwise.
1073
     */
1074
    public function isSIRTFI($entityID)
1075
    {
1076
        return $this->isAttributeSet($entityID, 'SIRTFI');
1077
    }
1078
1079
    /**
1080
     * getInCommonIdPs
1081
     *
1082
     * This method returns a two-dimensional array of InCommon IdPs.
1083
     * The primary key of the array is the entityID, the secondary key is
1084
     * either 'Organization_Name' (corresponds to OrganizationDisplayName)
1085
     * or 'Display_Name' (corresponds to mdui:DisplayName).
1086
     * If a non-null parameter is passed in it returns a subset of the
1087
     * InCommon IdPs. 0 means list only non-whitelisted IdPs, 1 means list
1088
     * only whitelisted IdPs, 2 means list only R&S IdPs.
1089
     *
1090
     * @param int $filter
1091
     *        null => all InCommonIdPs
1092
     *        0    => non-whitelisted InCommon IdPs
1093
     *        1    => whitelisted InCommon IdPs
1094
     *        2    => R&S InCommon IdPs
1095
     * $return array An array of InCommon IdP Organization Names and Display
1096
     *         Names, possibly filtered by whitelisted / non-whitelisted / R&S.
1097
     */
1098
    public function getInCommonIdPs($filter = null)
1099
    {
1100
        $retarr = array();
1101
1102
        foreach ($this->idparray as $key => $value) {
1103
            if ((!is_null($filter)) &&
1104
                (($filter === 0) &&
1105
                 ($this->isWhitelisted($key))) ||
1106
                (($filter === 1) &&
1107
                 (!$this->isWhitelisted($key))) ||
1108
                (($filter === 2) &&
1109
                 (!$this->isRandS($key)))) {
1110
                continue;
1111
            }
1112
            $retarr[$key]['Organization_Name'] = $this->idparray[$key]['Organization_Name'];
1113
            $retarr[$key]['Display_Name'] = $this->idparray[$key]['Display_Name'];
1114
        }
1115
1116
        return $retarr;
1117
    }
1118
1119
    /**
1120
     * getNonWhitelistedIdPs
1121
     *
1122
     * This method returns an array of non-whitelisted IdPs where the
1123
     * keys of the array are the entityIDs and the values are the
1124
     * pretty print Organization Names.
1125
     *
1126
     * @return array An array of non-whitelisted IdPs.
1127
     */
1128
    public function getNonWhitelistedIdPs()
1129
    {
1130
        return $this->getInCommonIdPs(0);
1131
    }
1132
1133
    /**
1134
     * getWhitelistedIdPs
1135
     *
1136
     * This method returns an array of whitelisted IdPs where the keys
1137
     * of the array are the entityIDs and the values are the
1138
     * pretty print Organization Names.
1139
     *
1140
     * @return array An array of whitelisted IdPs.
1141
     */
1142
    public function getWhitelistedIdPs()
1143
    {
1144
        return $this->getInCommonIdPs(1);
1145
    }
1146
1147
    /**
1148
     * getRandSIdPs
1149
     *
1150
     * This method returns an array of R&S IdPs where the keys
1151
     * of the array are the entityIDs and the values are the
1152
     * pretty print Organization Names.
1153
     *
1154
     * @return array An array of Research and Scholarship (R&S) IdPs.
1155
     */
1156
    public function getRandSIdPs()
1157
    {
1158
        return $this->getInCommonIdPs(2);
1159
    }
1160
1161
    /**
1162
     * getShibInfo
1163
     *
1164
     * This function returns an array with two types of Shibboleth
1165
     * information.  The first set of info is specific to the user's
1166
     * current Shibboleth session, such as REMOTE_USER. The second set
1167
     * of info reads info from the passed-in metadata file specific to
1168
     * the IdP, such as the pretty-print name of the IdP.
1169
     *
1170
     * @param string $entityID (Optional) The entityID to search for in
1171
     *        the InCommon metadata. Defaults to the HTTP header
1172
     *        HTTP_SHIB_IDENTITY_PROVIDER.
1173
     * @return array  An array containing the various shibboleth
1174
     *         attributes for the current Shibboleth session. The
1175
     *         keys of the array are 'pretty print' names of the
1176
     *         various attribute value names (such as
1177
     *         'User Identifier' for REMOTE_USER) and the values
1178
     *         of the array are the actual Shibboleth session values.
1179
     */
1180
    public function getShibInfo($entityID = '')
1181
    {
1182
        $shibarray = array();  // Array to be returned
1183
1184
        // Set the blob set of info, namely those shib attributes which
1185
        // were given by the IdP when the user authenticated.
1186
        if (strlen($entityID) == 0) {
1187
            $entityID = Util::getServerVar('HTTP_SHIB_IDENTITY_PROVIDER');
1188
        }
1189
        // CIL-254 - For LIGO backup IdPs, remap entityID to the main IdP
1190
        if (preg_match(
1191
            '%(https://login)[^\.]*(.ligo.org/idp/shibboleth)%',
1192
            $entityID,
1193
            $matches
1194
        )) {
1195
            $entityID = $matches[1] . $matches[2];
1196
        }
1197
        $shibarray['Identity Provider'] = $entityID;
1198
        $shibarray['User Identifier'] = Util::getServerVar('REMOTE_USER');
1199
        $shibarray['ePPN'] = Util::getServerVar('HTTP_EPPN');
1200
        $shibarray['ePTID'] = Util::getServerVar('HTTP_PERSISTENT_ID');
1201
        $shibarray['First Name'] = Util::getServerVar('HTTP_GIVENNAME');
1202
        $shibarray['Last Name'] = Util::getServerVar('HTTP_SN');
1203
        $shibarray['Display Name'] = Util::getServerVar('HTTP_DISPLAYNAME');
1204
        $shibarray['Email Address'] = Util::getServerVar('HTTP_MAIL');
1205
        $shibarray['Level of Assurance'] = Util::getServerVar('HTTP_ASSURANCE');
1206
        $shibarray['Affiliation'] = Util::getServerVar('HTTP_AFFILIATION');
1207
        $shibarray['OU'] = Util::getServerVar('HTTP_OU');
1208
        $shibarray['Member'] = Util::getServerVar('HTTP_MEMBER');
1209
        $shibarray['Authn Context'] = Util::getServerVar('HTTP_SHIB_AUTHNCONTEXT_CLASS');
1210
        $shibarray['Entitlement'] = Util::getServerVar('HTTP_ENTITLEMENT');
1211
1212
        // Make sure to use only the first of multiple values.
1213
        $attrs = array('ePPN','ePTID','First Name','Last Name',
1214
                       'Display Name','Email Address');
1215
        foreach ($attrs as $attr) {
1216
            if (($pos = strpos($shibarray[$attr], ';')) !== false) {
1217
                $shibarray[$attr] = substr($shibarray[$attr], 0, $pos);
1218
            }
1219
        }
1220
1221
        // Next, read the attributes for the given IdP. This includes
1222
        // values such as the display name for the IdP, the home page
1223
        // of the organization, and contact information.
1224
        $attrarray = array(
1225
            'Organization_Name',
1226
            'Home_Page',
1227
            'Support_Name',
1228
            'Support_Address',
1229
            'Technical_Name',
1230
            'Technical_Address',
1231
            'Administrative_Name',
1232
            'Administrative_Address'
1233
        );
1234
1235
        foreach ($attrarray as $attr) {
1236
            if (isset($this->idparray[$entityID][$attr])) {
1237
                $shibarray[preg_replace('/_/', ' ', $attr)] =
1238
                    $this->idparray[$entityID][$attr];
1239
            }
1240
        }
1241
1242
        return $shibarray;
1243
    }
1244
1245
    /**
1246
     * DOM2Array
1247
     *
1248
     * This function sorts the passed-in DOM corresponding to
1249
     * idplist.xml and returns a 2D array where the keys are entityIDs
1250
     * and the values are arrays of attributes for each IdP.
1251
     *
1252
     * @param DOMDocument The DOM containing the list of IdPs to convert to
1253
     *        an array. Returns null on error.
1254
     * @return array An array corresponding to the DOM of the IdPs.
1255
     */
1256
    public function DOM2Array($dom)
1257
    {
1258
        $retarr = null;
1259
1260
        if (!is_null($dom)) {
1261
            foreach ($dom->childNodes as $idps) {
1262
                // Top-level DOM has 'idps' only
1263
                foreach ($idps->childNodes as $idp) {
1264
                    // Loop through each <idp> element
1265
                    $entityID = $idp->attributes->item(0)->value;
1266
                    foreach ($idp->childNodes as $attr) {
1267
                        // Get all sub-attributes of the current <idp>
1268
                        if ($attr->nodeName != '#text') {
1269
                            $retarr[$entityID][$attr->nodeName] = $attr->nodeValue;
1270
                        }
1271
                    }
1272
                }
1273
            }
1274
        }
1275
1276
        return $retarr;
1277
    }
1278
1279
    /**
1280
     * array2DOM
1281
     *
1282
     * This function takes an array of IdPs (such as idparray) and
1283
     * returns a corresponding DOM which can be written to XML.
1284
     *
1285
     * @param array $arr An array corresponding to the idplist.
1286
     * @return DOMDocument A DOM for the idplist which can be written to XML.
1287
     */
1288
    public function array2DOM($arr)
1289
    {
1290
        $retdom = null;
1291
1292
        if (!is_null($arr)) {
1293
            $dom = DOMImplementation::createDocument(null, 'idps');
1294
            $idps = $dom->documentElement; // Top level <idps> element
1295
1296
            foreach ($arr as $entityID => $attrs) {
1297
                // Create an <idp> element to hold sub elements
1298
                $idp = $dom->createElement('idp');
1299
                $idp->setAttribute('entityID', $entityID);
1300
                $idps->appendChild($idp);
1301
                foreach ($attrs as $attr => $value) {
1302
                    $this->addNode($dom, $idp, $attr, $value);
1303
                }
1304
            }
1305
            $retdom = $this->sortDOM($dom);
1306
        }
1307
1308
        return $retdom;
1309
    }
1310
}
1311