Completed
Push — master ( d95b29...1c1079 )
by Terrence
23:33 queued 08:35
created

IdpList::array2DOM()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 0
cts 0
cp 0
rs 9.568
c 0
b 0
f 0
cc 4
nc 2
nop 1
crap 20
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
                    // Add a <Whitelisted> block for all IdPs
688
                    // not in the blacklist.txt file.
689
                    if (!array_key_exists($entityID, $blackidps)) {
690
                        $this->addNode($dom, $idp, 'Whitelisted', '1');
691
                    }
692
                }
693
694
                // Read in any test IdPs and add them to the list
695
                if ((is_readable(static::TESTIDPFILENAME)) &&
696
                    (($dom2 = DOMDocument::load(
697
                        static::TESTIDPFILENAME
698
                    )) !== false)) {
699
                    $idpnodes = $dom2->getElementsByTagName('idp');
700
                    foreach ($idpnodes as $idpnode) {
701
                        // Check if the entityID already exists. If so,
702
                        // delete it from both the idps DOM and the idparray
703
                        // and instead add the one from the testidplist.
704
                        $entityID = $idpnode->attributes->item(0)->value;
705
                        if (array_key_exists($entityID, $this->idparray)) {
706
                            // Easy - simply delete the array entry for the
707
                            // existing entityID
708
                            unset($this->idparray[$entityID]);
709
710
                            // Hard - search through the current DOM for a
711
                            // matching entityID to get the DOMNode, which
712
                            // can then be removed from the DOM.
713
                            $curridpnodes = $dom->getElementsByTagName('idp');
714
                            foreach ($curridpnodes as $curridpnode) {
715
                                $currEntityID =
716
                                    $curridpnode->attributes->item(0)->value;
717
                                if ($currEntityID == $entityID) {
718
                                    $idps->removeChild($curridpnode);
719
                                    break;
720
                                }
721
                            }
722
                        }
723
724
                        // Add the new idp node to the DOM
725
                        $node = $dom->importNode($idpnode, true);
726
                        $idps->appendChild($node);
727
728
                        // Add the testidplist nodes to the $idparray
729
                        foreach ($node->childNodes as $child) {
730
                            if ($child->nodeName != '#text') {
731
                                $this->idparray[$entityID][$child->nodeName] =
732
                                    $child->nodeValue;
733
                            }
734
                        }
735
                    }
736
                }
737
738
                // Sort the DOMDocument and idparray by Display_Name
739
                $this->idpdom = $this->sortDOM($dom);
740
                uasort($this->idparray, function ($a, $b) {
741
                    return strcasecmp(
742
                        $a['Display_Name'],
743
                        $b['Display_Name']
744
                    );
745
                });
746
747
                $retval = true;
748
            }
749
        }
750
751
        return $retval;
752
    }
753
754
    /**
755
     * getFilename
756
     *
757
     * This function returns a string of the full path of the IdP list
758
     * filename.  See also setFilename().
759
     *
760
     * @return string The IdP list filename.
761
     */
762
    public function getFilename()
763
    {
764
        return $this->idpfilename;
765
    }
766
767
    /**
768
     * setFilename
769
     *
770
     * This function sets the string of the full path of the IdP list
771
     * filename.  See also getFilename().
772
     *
773
     * @param string $filename he new name of the IdP list filename.
774
     */
775
    public function setFilename($filename)
776
    {
777
        $this->idpfilename = $filename;
778
    }
779
780
    /**
781
     * getInCommonFilename
782
     *
783
     * This function returns a string of the full path of the InCommon
784
     * metadata filename.  See also setInCommonFilename().
785
     *
786
     * @return string The InCommon metadata filename.
787
     */
788
    public function getInCommonFilename()
789
    {
790
        return $this->incommonfilename;
791
    }
792
793
    /**
794
     * setInCommonFilename
795
     *
796
     * This function sets the string of the full path of the InCommon
797
     * metadata filename.  See also getInCommonFilename().
798
     *
799
     * @param string $filename The new name of the InCommon metadata filename.
800
     */
801
    public function setInCommonFilename($filename)
802
    {
803
        $this->incommonfilename = $filename;
804
    }
805
806
    /**
807
     * getEntityIDs
808
     *
809
     * This method returns the entityIDs of the idplist as an array.
810
     *
811
     * @return array An array of the entityIDs
812
     */
813
    public function getEntityIDs()
814
    {
815
        $retarr = array();
816
        if (is_array($this->idparray)) {
817
            $retarr = array_keys($this->idparray);
818
        }
819
        return $retarr;
820
    }
821
822
    /**
823
     * getOrganizationName
824
     *
825
     * This function returns the Organization_Name of the selected
826
     * $entityID.
827
     *
828
     * @param string $entityID The entityID to search for
829
     * @return string The Organization_Name for the $entityID. Return
830
     *         string is empty if no matching $entityID found.
831
     */
832
    public function getOrganizationName($entityID)
833
    {
834
        $retval = '';
835
        if (isset($this->idparray[$entityID]['Organization_Name'])) {
836
            $retval = $this->idparray[$entityID]['Organization_Name'];
837
        }
838
        return $retval;
839
    }
840
841
    /**
842
     * getDisplayName
843
     *
844
     * This function returns the Display_Name of the selected
845
     * $entityID.
846
     *
847
     * @param string $entityID The entityID to search for
848
     * @return string The Display_Name for the $entityID. Return
849
     *         string is empty if no matching $entityID found.
850
     */
851
    public function getDisplayName($entityID)
852
    {
853
        $retval = '';
854
        if (isset($this->idparray[$entityID]['Display_Name'])) {
855
            $retval = $this->idparray[$entityID]['Display_Name'];
856
        }
857
        return $retval;
858
    }
859
860
    /**
861
     * entityIDExists
862
     *
863
     * This function searchs for the given idp entityID.
864
     *
865
     * @param string $entityID The entityID to search for
866
     * @return bool True if the given entityID is found. False otherwise.
867
     */
868
    public function entityIDExists($entityID)
869
    {
870
        return (isset($this->idparray[$entityID]));
871
    }
872
873
    /**
874
     * exists
875
     *
876
     * This is simply a convenience function for entityIDExists.
877
     *
878
     * @param string $entityID The enityID to search for
879
     * @return bool True if the given entityID is found. False otherwise.
880
     */
881
    public function exists($entityID)
882
    {
883
        return $this->entityIDExists($entityID);
884
    }
885
886
    /**
887
     * isAttributeSet
888
     *
889
     * This function checks if the passed-in $attr is set to '1' for
890
     * the entityID, and returns true if so.
891
     *
892
     * @param string $entityID The enityID to search for.
893
     * @param string $attr The attribute in question.
894
     * @return bool True if the given attribute is '1' for the entityID.
895
     *         False otherwise.
896
     */
897
    public function isAttributeSet($entityID, $attr)
898
    {
899
        return (isset($this->idparray[$entityID][$attr]) &&
900
                     ($this->idparray[$entityID][$attr] == 1));
901
    }
902
903
    /**
904
     * isWhitelisted
905
     *
906
     * This method searches for the given entityID and checks if the
907
     *'Whitelisted' entry has been set to '1'.
908
     *
909
     * @param string $entityID The enityID to search for
910
     * @return bool True if the given entityID is marked 'Whitelisted'.
911
     *         False otherwise.
912
     */
913
    public function isWhitelisted($entityID)
914
    {
915
        return $this->isAttributeSet($entityID, 'Whitelisted');
916
    }
917
918
    /**
919
     * isSilver
920
     *
921
     * This method searches for the given entityID and checks if the
922
     *'Silver' entry has been set to '1'.
923
     *
924
     * @param string $entityID The enityID to search for
925
     * @return bool True if the given entityID is certified 'Silver'.
926
     *         False otherwise.
927
     */
928
    public function isSilver($entityID)
929
    {
930
        return $this->isAttributeSet($entityID, 'Silver');
931
    }
932
933
    /**
934
     * isBronze
935
     *
936
     * This method searches for the given entityID and checks if the
937
     *'Bronze' entry has been set to '1'.
938
     *
939
     * @param string $entityID The enityID to search for
940
     * @return bool True if the given entityID is certified 'Bronze'.
941
     *         False otherwise.
942
     */
943
    public function isBronze($entityID)
944
    {
945
        return $this->isAttributeSet($entityID, 'Bronze');
946
    }
947
948
    /**
949
     * isRandS
950
     *
951
     * This method searches for the given entityID and checks if the
952
     *'RandS' entry has been set to '1'.
953
     *
954
     * @param string $entityID The enityID to search for
955
     * @return bool True if the given entityID is listed as 'RandS'
956
     *         (research-and-scholarship). False otherwise.
957
     */
958
    public function isRandS($entityID)
959
    {
960
        return $this->isAttributeSet($entityID, 'RandS');
961
    }
962
963
    /**
964
     * isInCommonRandS
965
     *
966
     * This method searches for the given entityID and checks if the
967
     *'InCommon_RandS' entry has been set to '1'.
968
     *
969
     * @param string $entityID The enityID to search for
970
     * @return bool True if the given entityID is listed as
971
     *        'InCommon_RandS'. False otherwise.
972
     */
973
    public function isInCommonRandS($entityID)
974
    {
975
        return $this->isAttributeSet($entityID, 'InCommon_RandS');
976
    }
977
978
    /**
979
     * isREFEDSRandS
980
     *
981
     * This method searches for the given entityID and checks if the
982
     *'REFEDS_RandS' entry has been set to '1'.
983
     *
984
     * @param string $entityID The enityID to search for
985
     * @return bool True if the given entityID is listed as
986
     *         'REFEDS_RandS'. False otherwise.
987
     */
988
    public function isREFEDSRandS($entityID)
989
    {
990
        return $this->isAttributeSet($entityID, 'REFEDS_RandS');
991
    }
992
993
    /**
994
     * isRegisteredByInCommon
995
     *
996
     * This method searches for the given entityID and checks if the
997
     *'Registered_By_InCommon' entry has been set to '1'.
998
     *
999
     * @param string $entityID The enityID to search for
1000
     * @return bool True if the given entityID is listed as
1001
     *         'Registered_By_InCommon'. False otherwise.
1002
     */
1003
    public function isRegisteredByInCommon($entityID)
1004
    {
1005
        return $this->isAttributeSet($entityID, 'Registered_By_InCommon');
1006
    }
1007
1008
    /**
1009
     * isSIRTFI
1010
     *
1011
     * This method searches for the given entityID and checks if the
1012
     *'SIRTFI' entry has been set to '1'.
1013
     *
1014
     * @param string $entityID The enityID to search for
1015
     * @return bool True if the given entityID is listed as
1016
     *         SIRTFI. False otherwise.
1017
     */
1018
    public function isSIRTFI($entityID)
1019
    {
1020
        return $this->isAttributeSet($entityID, 'SIRTFI');
1021
    }
1022
1023
    /**
1024
     * getInCommonIdPs
1025
     *
1026
     * This method returns a two-dimensional array of InCommon IdPs. 
1027
     * The primary key of the array is the entityID, the secondary key is
1028
     * either 'Organization_Name' (corresponds to OrganizationDisplayName)
1029
     * or 'Display_Name' (corresponds to mdui:DisplayName). 
1030
     * If a non-null parameter is passed in it returns a subset of the
1031
     * InCommon IdPs. 0 means list only non-whitelisted IdPs, 1 means list
1032
     * only whitelisted IdPs, 2 means list only R&S IdPs.
1033
     *
1034
     * @param int $filter
1035
     *        null => all InCommonIdPs
1036
     *        0    => non-whitelisted InCommon IdPs
1037
     *        1    => whitelisted InCommon IdPs
1038
     *        2    => R&S InCommon IdPs
1039
     * $return array An array of InCommon IdP Organization Names and Display
1040
     *         Names, possibly filtered by whitelisted / non-whitelisted / R&S.
1041
     */
1042
    public function getInCommonIdPs($filter = null)
1043
    {
1044
        $retarr = array();
1045
1046
        foreach ($this->idparray as $key => $value) {
1047
            if ((!is_null($filter)) &&
1048
                (($filter === 0) &&
1049
                 ($this->isWhitelisted($key))) ||
1050
                (($filter === 1) &&
1051
                 (!$this->isWhitelisted($key))) ||
1052
                (($filter === 2) &&
1053
                 (!$this->isRandS($key)))) {
1054
                continue;
1055
            }
1056
            $retarr[$key]['Organization_Name'] = $this->idparray[$key]['Organization_Name'];
1057
            $retarr[$key]['Display_Name'] = $this->idparray[$key]['Display_Name'];
1058
        }
1059
1060
        return $retarr;
1061
    }
1062
1063
    /**
1064
     * getNonWhitelistedIdPs
1065
     *
1066
     * This method returns an array of non-whitelisted IdPs where the
1067
     * keys of the array are the entityIDs and the values are the
1068
     * pretty print Organization Names.
1069
     *
1070
     * @return array An array of non-whitelisted IdPs.
1071
     */
1072
    public function getNonWhitelistedIdPs()
1073
    {
1074
        return $this->getInCommonIdPs(0);
1075
    }
1076
1077
    /**
1078
     * getWhitelistedIdPs
1079
     *
1080
     * This method returns an array of whitelisted IdPs where the keys
1081
     * of the array are the entityIDs and the values are the
1082
     * pretty print Organization Names.
1083
     *
1084
     * @return array An array of whitelisted IdPs.
1085
     */
1086
    public function getWhitelistedIdPs()
1087
    {
1088
        return $this->getInCommonIdPs(1);
1089
    }
1090
1091
    /**
1092
     * getRandSIdPs
1093
     *
1094
     * This method returns an array of R&S IdPs where the keys
1095
     * of the array are the entityIDs and the values are the
1096
     * pretty print Organization Names.
1097
     *
1098
     * @return array An array of Research and Scholarship (R&S) IdPs.
1099
     */
1100
    public function getRandSIdPs()
1101
    {
1102
        return $this->getInCommonIdPs(2);
1103
    }
1104
1105
    /**
1106
     * getShibInfo
1107
     *
1108
     * This function returns an array with two types of Shibboleth
1109
     * information.  The first set of info is specific to the user's
1110
     * current Shibboleth session, such as REMOTE_USER. The second set
1111
     * of info reads info from the passed-in metadata file specific to
1112
     * the IdP, such as the pretty-print name of the IdP.
1113
     *
1114
     * @param string $entityID (Optional) The entityID to search for in
1115
     *        the InCommon metadata. Defaults to the HTTP header
1116
     *        HTTP_SHIB_IDENTITY_PROVIDER.
1117
     * @return array  An array containing the various shibboleth
1118
     *         attributes for the current Shibboleth session. The
1119
     *         keys of the array are 'pretty print' names of the
1120
     *         various attribute value names (such as
1121
     *         'User Identifier' for REMOTE_USER) and the values
1122
     *         of the array are the actual Shibboleth session values.
1123
     */
1124
    public function getShibInfo($entityID = '')
1125
    {
1126
        $shibarray = array();  // Array to be returned
1127
1128
        // Set the blob set of info, namely those shib attributes which
1129
        // were given by the IdP when the user authenticated.
1130
        if (strlen($entityID) == 0) {
1131
            $entityID = Util::getServerVar('HTTP_SHIB_IDENTITY_PROVIDER');
1132
        }
1133
        // CIL-254 - For LIGO backup IdPs, remap entityID to the main IdP
1134
        if (preg_match(
1135
            '%(https://login)[^\.]*(.ligo.org/idp/shibboleth)%',
1136
            $entityID,
1137
            $matches
1138
        )) {
1139
            $entityID = $matches[1] . $matches[2];
1140
        }
1141
        $shibarray['Identity Provider'] = $entityID;
1142
        $shibarray['User Identifier'] = Util::getServerVar('REMOTE_USER');
1143
        $shibarray['ePPN'] = Util::getServerVar('HTTP_EPPN');
1144
        $shibarray['ePTID'] = Util::getServerVar('HTTP_PERSISTENT_ID');
1145
        $shibarray['First Name'] = Util::getServerVar('HTTP_GIVENNAME');
1146
        $shibarray['Last Name'] = Util::getServerVar('HTTP_SN');
1147
        $shibarray['Display Name'] = Util::getServerVar('HTTP_DISPLAYNAME');
1148
        $shibarray['Email Address'] = Util::getServerVar('HTTP_MAIL');
1149
        $shibarray['Level of Assurance'] = Util::getServerVar('HTTP_ASSURANCE');
1150
        $shibarray['Affiliation'] = Util::getServerVar('HTTP_AFFILIATION');
1151
        $shibarray['OU'] = Util::getServerVar('HTTP_OU');
1152
        $shibarray['Member'] = Util::getServerVar('HTTP_MEMBER');
1153
        $shibarray['Authn Context'] = Util::getServerVar('HTTP_SHIB_AUTHNCONTEXT_CLASS');
1154
1155
        // Make sure to use only the first of multiple values.
1156
        $attrs = array('ePPN','ePTID','First Name','Last Name',
1157
                       'Display Name','Email Address');
1158
        foreach ($attrs as $attr) {
1159
            if (($pos = strpos($shibarray[$attr], ';')) !== false) {
1160
                $shibarray[$attr] = substr($shibarray[$attr], 0, $pos);
1161
            }
1162
        }
1163
1164
        // Next, read the attributes for the given IdP. This includes
1165
        // values such as the display name for the IdP, the home page
1166
        // of the organization, and contact information.
1167
        $attrarray = array(
1168
            'Organization_Name',
1169
            'Display_Name',
1170
            'Home_Page',
1171
            'Support_Name',
1172
            'Support_Address',
1173
            'Technical_Name',
1174
            'Technical_Address',
1175
            'Administrative_Name',
1176
            'Administrative_Address'
1177
        );
1178
1179
        foreach ($attrarray as $attr) {
1180
            if (isset($this->idparray[$entityID][$attr])) {
1181
                $shibarray[preg_replace('/_/', ' ', $attr)] =
1182
                    $this->idparray[$entityID][$attr];
1183
            }
1184
        }
1185
1186
        return $shibarray;
1187
    }
1188
1189
    /**
1190
     * DOM2Array
1191
     *
1192
     * This function sorts the passed-in DOM corresponding to
1193
     * idplist.xml and returns a 2D array where the keys are entityIDs
1194
     * and the values are arrays of attributes for each IdP.
1195
     *
1196
     * @param DOMDocument The DOM containing the list of IdPs to convert to
1197
     *        an array. Returns null on error.
1198
     * @return array An array corresponding to the DOM of the IdPs.
1199
     */
1200
    public function DOM2Array($dom)
1201
    {
1202
        $retarr = null;
1203
1204
        if (!is_null($dom)) {
1205
            foreach ($dom->childNodes as $idps) {
1206
                // Top-level DOM has 'idps' only
1207
                foreach ($idps->childNodes as $idp) {
1208
                    // Loop through each <idp> element
1209
                    $entityID = $idp->attributes->item(0)->value;
1210
                    foreach ($idp->childNodes as $attr) {
1211
                        // Get all sub-attributes of the current <idp>
1212
                        if ($attr->nodeName != '#text') {
1213
                            $retarr[$entityID][$attr->nodeName] = $attr->nodeValue;
1214
                        }
1215
                    }
1216
                }
1217
            }
1218
        }
1219
1220
        return $retarr;
1221
    }
1222
1223
    /**
1224
     * array2DOM
1225
     *
1226
     * This function takes an array of IdPs (such as idparray) and
1227
     * returns a corresponding DOM which can be written to XML.
1228
     *
1229
     * @param array $arr An array corresponding to the idplist.
1230
     * @return DOMDocument A DOM for the idplist which can be written to XML.
1231
     */
1232
    public function array2DOM($arr)
1233
    {
1234
        $retdom = null;
1235
1236
        if (!is_null($arr)) {
1237
            $dom = DOMImplementation::createDocument(null, 'idps');
1238
            $idps = $dom->documentElement; // Top level <idps> element
1239
1240
            foreach ($arr as $entityID => $attrs) {
1241
                // Create an <idp> element to hold sub elements
1242
                $idp = $dom->createElement('idp');
1243
                $idp->setAttribute('entityID', $entityID);
1244
                $idps->appendChild($idp);
1245
                foreach ($attrs as $attr => $value) {
1246
                    $this->addNode($dom, $idp, $attr, $value);
1247
                }
1248
            }
1249
            $retdom = $this->sortDOM($dom);
1250
        }
1251
1252
        return $retdom;
1253
    }
1254
}
1255