Issues (141)

src/Service/Skin.php (5 issues)

Labels
Severity
1
<?php
2
3
namespace CILogon\Service;
4
5
require_once 'DB.php';
6
7
use CILogon\Service\Util;
8
use tubalmartin\CssMin\Minifier as CSSmin;
9
use PEAR;
10
use DB;
11
12
/**
13
 * Skin
14
 *
15
 * This class reads in CSS and configuration options
16
 * for a 'skin'. The skin is set by passing the
17
 * '?skin=...' (or '?cilogon_skin=...') query parameter.
18
 * If found, the associated config XML and CSS are read
19
 * in from either the filesystem (under the /skin/NAME
20
 * directory) or the database. It also sets a PHP
21
 * session variable so that the skin name is remembered
22
 * across page loads. If no skin name is found, then the
23
 * default /skin/config.xml file is read in.
24
 *
25
 * Note that this class uses the SimpleXML class to parse
26
 * the config XML. This stores the XML in a special
27
 * SimpleXMLElement object, which is NOT an array. But
28
 * you can iterate over elements in the structure. See
29
 * the PHP SimpleXML online manual 'Basic Usage' for
30
 * more information.
31
 *
32
 * This class provides a getConfigOption() method to
33
 * access XML (sub)blocks to get at a config value.
34
 * It is important to rememeber that the values returned
35
 * by the getConfigOption() method must be typecast to
36
 * native datatypes in order to be used effectively.
37
 *
38
 * An example configuration file (with all available options) is at
39
 *     /var/www/html/skin/config-example.xml
40
 *
41
 * Example usage:
42
 *   require_once 'Skin.php';
43
 *   $skin = new Skin();
44
 *   // While outputting the <head> HTML block...
45
 *   $skin->printSkinCSS();
46
 *   // Get the value of a configuration option
47
 *   $idpgreenlit = $skin->getConfigOption('idpgreenlit');!!
48
 *   // Now, process entries in the $idpgreenlit list
49
 *   if ((!is_null($idpgreenlit)) && (!empty($idpgreenlit->idp))) {
50
 *       foreach ($idpgreenlit->idp as $entityID) {
51
 *           echo '<p>' , (string)$entityID , "<\p>\n";
52
 *       }
53
 *   }
54
 *   // Check to see if <hideportalinfo> is set
55
 *   $hideportalinfo = false;
56
 *   $hpi=$skin->getConfigOption('portallistaction','hideportalinfo');
57
 *   if ((!is_null($hpi)) && ((int)$hpi > 0)) {
58
 *       $hideportalinfo = true;
59
 *   }
60
 */
61
class Skin
62
{
63
    /**
64
     * @var string $skinname The name of the skin
65
     */
66
    protected $skinname;
67
68
    /**
69
     * @var \SimpleXMLElement $configxml A SimpleXMLElement object for the
70
     *      config.xml file
71
     */
72
    protected $configxml;
73
74
    /**
75
     * @var string $css The un-minified CSS for the skin
76
     */
77
    protected $css;
78
79
    /**
80
     * @var array $forcearray An array of (URI,skinname) pairs for forcing
81
     *      skin application
82
     */
83
    protected $forcearray;
84
85
    /**
86
     *  __construct
87
     *
88
     * Default constructor. Calls init() to do the actual work.
89
     *
90
     * @return Skin A new Skin object.
91
     */
92
    public function __construct()
93
    {
94
        $this->init();
95
    }
96
97
    /**
98
     * init
99
     *
100
     * This function does the work of (re)initializing the skin object.
101
     * It finds the name of the skin (if any) and reads in the skin's
102
     * config XML file (if found). Call this function to reset the
103
     * skin in case of possible forced skin due to IdP or portal
104
     * callbackURL.
105
     *
106
     * @param bool $reset True to reset the 'cilogon_skin' PHP session var
107
     *        to blank so that readSkinConfig doesn't check for it.
108
     *        Defaults to 'false'.
109
     */
110
    public function init($reset = false)
111
    {
112
        if ($reset) {
113
            Util::unsetSessionVar('cilogon_skin');
114
        }
115
116
        $this->skinname = '';
117
        $this->configxml = null;
118
        $this->css = '';
119
        $this->forcearray = FORCE_SKIN_ARRAY;
0 ignored issues
show
The constant CILogon\Service\FORCE_SKIN_ARRAY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
120
        $this->readSkinConfig();
121
        $this->setMyProxyInfo();
122
    }
123
124
    /**
125
     * readSkinConfig
126
     *
127
     * This function checks for the name of the skin in several places:
128
     * (1) The FORCE_SKIN_ARRAY for a matching IdP entityID or portal
129
     * callbackURL, (2) in a URL parameter (can be '?skin=',
130
     * '?cilogon_skin=', '?vo='), (3) cilogon_vo form input variable
131
     * (POST for ECP case), or (4) 'cilogon_skin' PHP session
132
     * variable.  If it finds the skin name in any of these, it then
133
     * checks to see if such a named skin exists, either on the filesystem
134
     * or in the database. If found, the class variable $skinname AND the
135
     * 'cilogon_skin' PHP session variable (for use on future page
136
     * loads by the user) are set. It then reads in the config XML and the
137
     * CSS and stores them in the class variables $configxml and $css.
138
     * If no skin name is found, read in the default /skin/config.xml file.
139
     *
140
     * Side Effect: Sets the 'cilogon_skin' session variable if needed.
141
     */
142
    protected function readSkinConfig()
143
    {
144
        $skinvar = '';
145
146
        // Check for matching IdP, callbackURI (OAuth1),
147
        // redirect_uri (OAuth2), or client_id (OAuth2)
148
        // in the FORCE_SKIN_ARRAY.
149
        $uristocheck = array(
150
            Util::getGetVar('redirect_uri'),
151
            Util::getGetVar('client_id'),
152
            Util::getSessionVar('callbackuri'),
153
            Util::getSessionVar('idp')
154
        );
155
156
        foreach ($uristocheck as $value) {
157
            if (strlen($value) > 0) {
158
                $skin = $this->getForceSkin($value);
159
                if (strlen($skin) > 0) {
160
                    $skinvar = $skin;
161
                    break;
162
                }
163
            }
164
        }
165
166
        // If no force skin found, check GET and POST parameters, as well as
167
        // previously set cilogon_skin PHP session variable.
168
        if (strlen($skinvar) == 0) {
169
            // First, look for '?skin=...'
170
            $skinvar = Util::getGetVar('skin');
171
        }
172
        if (strlen($skinvar) == 0) {
173
            // Next, look for '?cilogon_skin=...'
174
            $skinvar = Util::getGetVar('cilogon_skin');
175
        }
176
        if (strlen($skinvar) == 0) {
177
            // Next, look for '?vo=...'
178
            $skinvar = Util::getGetVar('vo');
179
        }
180
        if (strlen($skinvar) == 0) {
181
            // Next, check 'cilogon_vo' form input variable
182
            $skinvar = Util::getPostVar('cilogon_vo');
183
        }
184
        if (strlen($skinvar) == 0) {
185
            // Finally, check 'cilogon_skin' PHP session variable
186
            $skinvar = Util::getSessionVar('cilogon_skin');
187
        }
188
189
        // If we found $skinvar, attempt to read the skin config/css from
190
        // either the filesystem or the database, or failing that, read the
191
        // default /skin/config.xml file.
192
        $skinvar = strtolower($skinvar); // All skin dirs are lowercase
193
        $this->readSkinFromFile($skinvar) ||
194
            $this->readSkinFromDatabase($skinvar) ||
195
            $this->readDefaultSkin();
196
    }
197
198
    /**
199
     * readSkinFromFile
200
     *
201
     * This function reads in the skin config XML and CSS from the
202
     * filesystem into the class variables $configxml and $css.
203
     *
204
     * @param string $skinvar The name of the skin as found by
205
     *        readSkinConfig().
206
     * @return bool True if at least one of config.xml or skin.css could
207
     *         be found (and read in) for the skin (i.e., the skin
208
     *         directory exists and isn't empty). False otherwise.
209
     */
210
    protected function readSkinFromFile($skinvar)
211
    {
212
        $readin = false; // Make sure we read in either XML or CSS (or both)
213
214
        if (strlen($skinvar) > 0) {
215
            $skindir = Util::getServerVar('DOCUMENT_ROOT') . "/skin/$skinvar";
216
            if (is_dir($skindir)) {
217
                // Read in the config XML
218
                $skinconf = $skindir . '/config.xml';
219
                if (is_readable($skinconf)) {
220
                    if (($xml = @simplexml_load_file($skinconf)) !== false) {
221
                        $this->configxml = $xml;
222
                        $readin = true;
223
                    }
224
                }
225
                //Read in the CSS
226
                $skincss = $skindir . '/skin.css';
227
                if (is_readable($skincss)) {
228
                    if (($css = file_get_contents($skincss)) !== false) {
229
                        $this->css = $css;
230
                        $readin = true;
231
                    }
232
                }
233
            }
234
        }
235
236
        if ($readin) {
237
            $this->skinname = $skinvar;
238
            Util::setSessionVar('cilogon_skin', $skinvar);
239
        } else {
240
            Util::unsetSessionVar('cilogon_skin');
241
        }
242
243
        return $readin;
244
    }
245
246
    /**
247
     * readSkinFromDatabase
248
     *
249
     * This function reads in the skin config XML and CSS from the
250
     * database into the class variables $configxml and $css.
251
     *
252
     * @param string $skinvar The name of the skin as found by
253
     *        readSkinConfig().
254
     * @return bool True if at least one of config XML or CSS could
255
     *         be read from the database for the skin. False otherwise.
256
     */
257
    protected function readSkinFromDatabase($skinvar)
258
    {
259
        $readin = false; // Make sure we read in either XML or CSS (or both)
260
261
        if (strlen($skinvar) > 0) {
262
            $dsn = array(
263
                'phptype'  => 'mysqli',
264
                'username' => MYSQLI_USERNAME,
0 ignored issues
show
The constant CILogon\Service\MYSQLI_USERNAME was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
265
                'password' => MYSQLI_PASSWORD,
0 ignored issues
show
The constant CILogon\Service\MYSQLI_PASSWORD was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
266
                'database' => MYSQLI_DATABASE,
0 ignored issues
show
The constant CILogon\Service\MYSQLI_DATABASE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
267
                'hostspec' => MYSQLI_HOSTSPEC
0 ignored issues
show
The constant CILogon\Service\MYSQLI_HOSTSPEC was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
268
            );
269
270
            $opts = array(
271
                'persistent'  => true,
272
                'portability' => DB_PORTABILITY_ALL
273
            );
274
275
            $db = DB::connect($dsn, $opts);
276
            if (!PEAR::isError($db)) {
277
                $data = $db->getRow(
278
                    'SELECT * from skins WHERE name = ?',
279
                    array($skinvar),
280
                    DB_FETCHMODE_ASSOC
281
                );
282
                if ((!DB::isError($data)) && (!empty($data))) {
283
                    // Read in the config XML
284
                    if (
285
                        (strlen(@$data['config']) > 0) &&
286
                        (($xml = @simplexml_load_string($data['config'])) !== false)
287
                    ) {
288
                        $this->configxml = $xml;
289
                        $readin = true;
290
                    }
291
                    //Read in the CSS
292
                    if (strlen(@$data['css']) > 0) {
293
                        $this->css = $data['css'];
294
                        $readin = true;
295
                    }
296
                }
297
                $db->disconnect();
298
            }
299
        }
300
301
        if ($readin) {
302
            $this->skinname = $skinvar;
303
            Util::setSessionVar('cilogon_skin', $skinvar);
304
        } else {
305
            Util::unsetSessionVar('cilogon_skin');
306
        }
307
308
        return $readin;
309
    }
310
311
    /**
312
     * readDefaultSkin
313
     *
314
     * If we don't read a skin from the filesystem or the database, then
315
     * read in the default "/skin/config.xml" file.
316
     *
317
     * @return bool True if the default config.xml file was read in.
318
     *         False otherwise.
319
     */
320
    protected function readDefaultSkin()
321
    {
322
        $readin = false;
323
324
        $this->skinname = '';
325
        $this->css = '';
326
        Util::unsetSessionVar('cilogon_skin');
327
328
        $skinconf = Util::getServerVar('DOCUMENT_ROOT') . '/skin/config.xml';
329
        if (is_readable($skinconf)) {
330
            if (($xml = @simplexml_load_file($skinconf)) !== false) {
331
                $this->configxml = $xml;
332
                $readin = true;
333
            }
334
        }
335
336
        return $readin;
337
    }
338
339
    /**
340
     * getConfigOption
341
     *
342
     * This method returns a SimpleXMLElement block corresponding to
343
     * the passed in arguments.  For example, to get the redlit list of
344
     * idps, call $idps = getConfigOption('idpredlit') and then
345
     * iterate over $idps with foreach($idps as $idp) { ... }.  To get
346
     * a single subblock value such as the initial lifetime number for
347
     * the PKCS12 download option, call $life =
348
     * (int)getConfigOption('pkcs12','initiallifetime','number'). Note
349
     * that you should explicitly cast the values to int, string,
350
     * float, etc., when you use them.
351
     *
352
     * @param mixed $args Variable number of parameters corresponding to XML
353
     *              blocks (and possible sub-blocks).
354
     * @return \SimpleXMLElement|null A SimpleXMLElement corresponding to the
355
     *         passed-in XML option, or 'null' if no such option exists.
356
     */
357
    public function getConfigOption(...$args)
358
    {
359
        $retval = null;
360
        $numargs = count($args);
361
        if ($numargs > 0) {
362
            $retval = $this->configxml;
363
        }
364
        for ($i = 0; $i < $numargs; $i++) {
365
            $argval = $args[$i];
366
            if (empty($retval->$argval)) {
367
                $retval = null;
368
                break;
369
            } else {
370
                $retval = $retval->$argval;
371
            }
372
        }
373
        return $retval;
374
    }
375
376
    /**
377
     * printSkinCSS
378
     *
379
     * Call this function in the HTML <head> block to print out the
380
     * <style> tag for the internal CSS of the skin. The CSS is minified
381
     * to remove whitespace. Note that you should call readSkinConfig
382
     * to set the contents of $css.
383
     */
384
    public function printSkinCSS()
385
    {
386
        if (strlen($this->css) > 0) {
387
            $cssmin = new CSSmin();
388
            $cssmin->removeImportantComments();
389
            $cssmin->setLineBreakPosition(255);
390
            $outputcss = $cssmin->run($this->css);
391
            echo "<style>$outputcss</style>";
392
        }
393
    }
394
395
    /**
396
     * hasGreenlitIdps
397
     *
398
     * This function checks for the presence of a <idpgreenlit> block
399
     * in the skin's config file.  There must be at least one <idp>
400
     * in the <idpgreenlit>.
401
     *
402
     * @return bool True if skin has a non-empty <idpgreenlit>.
403
     */
404
    public function hasGreenlitIdps()
405
    {
406
        $retval = false;  // Assume no <idpgreenlit> configured
407
        $idpgreenlit = $this->getConfigOption('idpgreenlit');
408
        if ((!is_null($idpgreenlit)) && (!empty($idpgreenlit->idp))) {
409
            $retval = true;
410
        }
411
        return $retval;
412
    }
413
414
    /**
415
     * hasRedlitIdps
416
     *
417
     * This function checks for the presence of a <idpredlit> block
418
     * in the skin's config file.  There must be at least one <idp>
419
     * in the <idpredlit>.
420
     *
421
     * @return bool True if skin has a non-empty <idpredlit>.
422
     */
423
    public function hasRedlitIdps()
424
    {
425
        $retval = false;  // Assume no <idpredlit> configured
426
        $idpredlit = $this->getConfigOption('idpredlit');
427
        if ((!is_null($idpredlit)) && (!empty($idpredlit->idp))) {
428
            $retval = true;
429
        }
430
        return $retval;
431
    }
432
433
    /**
434
     * idpGreenlit
435
     *
436
     * This method checks to see if a given entityId of an IdP
437
     * is greenlit. 'Greenlit' in this case means either (a) the
438
     * entityId is in the skin's <idpgreenlit> list or (b) the skin
439
     * doesn't have a <idpgreenlit> at all.  In the second case, all
440
     * IdPs are by default 'greenlit'.  If you want to find if an
441
     * IdP should be listed in the WAYF, use 'idpAvailable' which
442
     * checks the greenlit AND the redlit lists.
443
     *
444
     * @param string $entityId The entityId of an IdP to check for greenlit.
445
     * @return bool True if the given IdP entityId is in the skin's
446
     *         greenlit list (or if the skin doesn't have a greenlit list).
447
     */
448
    public function idpGreenlit($entityId)
449
    {
450
        $retval = true;  // Assume the entityId is 'greenlit'
451
        if ($this->hasGreenlitIdps()) {
452
            $idpgreenlit = $this->getConfigOption('idpgreenlit');
453
            $found = false;
454
            foreach ($idpgreenlit->idp as $greenidp) {
455
                if ($entityId == ((string)$greenidp)) {
456
                    $found = true;
457
                    break;
458
                }
459
            }
460
            if (!$found) {
461
                $retval = false;
462
            }
463
        }
464
        return $retval;
465
    }
466
467
    /**
468
     * idpRedlit
469
     *
470
     * This method checks to see if a given entityId of an IdP
471
     * appears in the skin's <idpredlit>.
472
     *
473
     * @param string $entityId The entityId of an IdP to check for
474
     *        redlit status.
475
     * @return bool True if the given IdP entityId is in the skin's
476
     *         redlit list.
477
     */
478
    public function idpRedlit($entityId)
479
    {
480
        $retval = false;  // Assume entityId is NOT in the idpredlit
481
        if ($this->hasRedlitIdps()) {
482
            $idpredlit = $this->getConfigOption('idpredlit');
483
            foreach ($idpredlit->idp as $redidp) {
484
                if ($entityId == ((string)$redidp)) {
485
                    $retval = true;
486
                    break;
487
                }
488
            }
489
        }
490
        return $retval;
491
    }
492
493
    /**
494
     * idpAvailable
495
     *
496
     * This method combines idpGreenlit and idpRedlit to return
497
     * a 'yes/no' for if a given IdP should be made available for
498
     * selection in the WAYF.  It first checks to see if the IdP is
499
     * greenlit.  If not, it returns false. Otherwise, it then
500
     * checks if the IdP is redlit.  If not, it returns true.
501
     *
502
     * @param string $entityId The entityId of an IdP to check to see if it
503
     *        should be available in the WAYF.
504
     * @return bool True if the given IdP entityId is available to be
505
     *         selected in the WAYF.
506
     */
507
    public function idpAvailable($entityId)
508
    {
509
        $retval = false;   // Assume IdP is not available in the WAYF
510
        if (
511
            ($this->idpGreenlit($entityId)) &&
512
            (!$this->idpRedlit($entityId))
513
        ) {
514
            $retval = true;
515
        }
516
        return $retval;
517
    }
518
519
    /**
520
     * setMyProxyInfo
521
     *
522
     * This method sets the 'myproxyinfo' PHP session variable.  The
523
     * variable has the form 'info:key1=value1,key2=value2,...' and is
524
     * passed to the 'myproxy-logon' command as part of the username
525
     * when fetching a credential.  The MyProxy server will do extra
526
     * processing based on the content of this 'info:...' tag.  If the
527
     * skinname is not empty, that is added to the info tag.  Also,
528
     * the apache REMOTE_ADDR is added.  For other key=value pairs that
529
     * get added, see the code below.
530
     */
531
    public function setMyProxyInfo()
532
    {
533
        $infostr = '';
534
535
        // Add the skinname if available
536
        if (strlen($this->skinname) > 0) {
537
            $infostr .= 'cilogon_skin=' . $this->skinname;
538
        }
539
540
        // Add the REMOTE_ADDR
541
        $remoteaddr = Util::getServerVar('REMOTE_ADDR');
542
        if (strlen($remoteaddr) > 0) {
543
            $infostr .= (strlen($infostr) > 0 ? ',' : '') .
544
                        "remote_addr=$remoteaddr";
545
        }
546
547
        // Add eppn, eptid, open_id, and oidc if available
548
        // Note that these values are lowercase after an update to make
549
        // them the same as those used by the dbService. BUT, MyProxy
550
        // expects the old versions. So this array maps the new lowercase
551
        // versions back into the old ones.
552
        $mpid = array(
553
            'eppn' => 'ePPN',
554
            'eptid' => 'ePTID',
555
            'open_id' => 'openidID',
556
            'oidc' => 'oidcID'
557
        );
558
        foreach (array('eppn','eptid','open_id','oidc') as $id) {
559
            $sessvar = Util::getSessionVar($id);
560
            if (strlen($sessvar) > 0) {
561
                $infostr .= (strlen($infostr) > 0 ? ',' : '') .
562
                    $mpid[$id] . "=" . $sessvar;
563
            }
564
        }
565
566
        // Finally, set the 'myproxyinfo' PHP session variable
567
        if (strlen($infostr) > 0) {
568
            Util::setSessionVar('myproxyinfo', 'info:' . $infostr);
569
        } else {
570
            Util::unsetSessionVar('myproxyinfo');
571
        }
572
    }
573
574
    /**
575
     * hasPortalList
576
     *
577
     * This function checks for the presence of a <portallist> block in
578
     * the skin's config file.  There must be at least one <portal> in
579
     * the <portallist>.
580
     *
581
     * @return bool True if skin has a non-empty <portallist>.
582
     */
583
    public function hasPortalList()
584
    {
585
        $retval = false;  // Assume no <portallist> configured
586
        $portallist = $this->getConfigOption('portallist');
587
        if ((!is_null($portallist)) && (!empty($portallist->portal))) {
588
            $retval = true;
589
        }
590
        return $retval;
591
    }
592
593
    /**
594
     * inPortalList
595
     *
596
     * This function takes in a 'callback' URL of a portal passed to
597
     * the CILogon Delegate service.  It then looks through the list
598
     * of <portal> patterns in the skin's <portallist>.  If the
599
     * callback URL matches any of these patterns, true is returned.
600
     * This is used to hide the 'Site Name / Site URL / Service URL'
601
     * box on the delegation WAYF page, for example.
602
     *
603
     * @param string $portalurl A 'callback' URL of a portal accessing the
604
     *        delegate service.
605
     * @return bool True if the callback URL matches one of the patterns
606
     *         in the skin's <portallist>.  False otherwise.
607
     */
608
    public function inPortalList($portalurl)
609
    {
610
        $retval = false;  // Assume the portalurl not a listed <portal>
611
        if ($this->hasPortalList()) {
612
            $portallist = $this->getConfigOption('portallist');
613
            foreach ($portallist->portal as $portalmatch) {
614
                if (preg_match(((string)$portalmatch), $portalurl)) {
615
                    $retval = true;
616
                    break;
617
                }
618
            }
619
        }
620
        return $retval;
621
    }
622
623
    /**
624
     * getForceSkin
625
     *
626
     * The FORCE_SKIN_ARRAY contains 'uripattern'=>'skinname' pairs
627
     * corresponding to IdP entityIDs, portal callbackurls, or client_ids
628
     * that should have a particular skin force-applied. This function
629
     * looks in the FORCE_SKIN_ARRAY for a pattern-matched URI and returns
630
     * the corresponding skin name if found. If not found, empty
631
     * string is returned.
632
     *
633
     * @param string $uri A URI to search for in the FORCE_SKIN_ARRAY.
634
     * @return string The skin name for the URI, or empty string if not
635
     *         found.
636
     */
637
    protected function getForceSkin($uri)
638
    {
639
        $retval = '';  // Assume uri is not in FORCE_SKIN_ARRAY
640
641
        foreach ($this->forcearray as $key => $value) {
642
            if (preg_match($key, $uri)) {
643
                $retval = $value;
644
                break;
645
            }
646
        }
647
        return $retval;
648
    }
649
}
650