Passed
Push — master ( 938f46...25d030 )
by Tomasz
03:36
created

UserAPI::orderIdentityProviders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 2
1
<?php
2
3
/*
4
 * ******************************************************************************
5
 * Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 
6
 * and GN4-2 consortia
7
 *
8
 * License: see the web/copyright.php file in the file structure
9
 * ******************************************************************************
10
 */
11
12
/**
13
 * This is the collection of methods dedicated for the user GUI
14
 * @author Tomasz Wolniewicz <[email protected]>
15
 * @author Stefan Winter <[email protected]>
16
 * @package UserAPI
17
 *
18
 * Parts of this code are based on simpleSAMLPhp discojuice module.
19
 * This product includes GeoLite data created by MaxMind, available from
20
 * http://www.maxmind.com
21
 */
22
23
namespace core;
24
25
use \Exception;
26
27
/**
28
 * The basic methoods for the user GUI
29
 * @package UserAPI
30
 *
31
 */
32
class UserAPI extends CAT {
33
34
    /**
35
     * nothing special to be done here.
36
     */
37
    public function __construct() {
38
        parent::__construct();
39
    }
40
41
    /**
42
     * Prepare the device module environment and send back the link
43
     * This method creates a device module instance via the {@link DeviceFactory} call, 
44
     * then sets up the device module environment for the specific profile by calling 
45
     * {@link DeviceConfig::setup()} method and finally, called the devide writeInstaller meethod
46
     * passing the returned path name.
47
     * 
48
     * @param string $device identifier as in {@link devices.php}
49
     * @param int $profileId profile identifier
50
     *
51
     * @return array 
52
     *  array with the following fields: 
53
     *  profile - the profile identifier; 
54
     *  device - the device identifier; 
55
     *  link - the path name of the resulting installer
56
     *  mime - the mimetype of the installer
57
     */
58
    public function generateInstaller($device, $profileId, $generatedFor = "user", $token = NULL, $password = NULL) {
59
        $this->languageInstance->setTextDomain("devices");
60
        $this->loggerInstance->debug(4, "installer:$device:$profileId\n");
61
        $validator = new \web\lib\common\InputValidation();
62
        $profile = $validator->Profile($profileId);
63
        // test if the profile is production-ready and if not if the authenticated user is an owner
64
        if ($this->verifyDownloadAccess($profile) === FALSE) {
65
            return;
66
        }
67
        $installerProperties = [];
68
        $installerProperties['profile'] = $profileId;
69
        $installerProperties['device'] = $device;
70
        $cache = $this->getCache($device, $profile);
71
        $this->installerPath = $cache['path'];
72
        if ($this->installerPath !== NULL && $token == NULL && $password == NULL) {
73
            $this->loggerInstance->debug(4, "Using cached installer for: $device\n");
74
            $installerProperties['link'] = "API.php?action=downloadInstaller&lang=" . $this->languageInstance->getLang() . "&profile=$profileId&device=$device&generatedfor=$generatedFor";
75
            $installerProperties['mime'] = $cache['mime'];
76
        } else {
77
            $myInstaller = $this->generateNewInstaller($device, $profile, $generatedFor, $token, $password);
78
            if ($myInstaller['link'] !== 0) {
79
                $installerProperties['mime'] = $myInstaller['mime'];
80
            }
81
            $installerProperties['link'] = $myInstaller['link'];
82
        }
83
        $this->languageInstance->setTextDomain("web_user");
84
        return($installerProperties);
85
    }
86
    
87
    private function verifyDownloadAccess($profile) {
88
        $attribs = $profile->getCollapsedAttributes();
89
        if (\core\common\Entity::getAttributeValue($attribs, 'profile:production', 0) !== 'on') {
90
            $this->loggerInstance->debug(4, "Attempt to download a non-production ready installer for profile: $profile->identifier\n");
91
            $auth = new \web\lib\admin\Authentication();
92
            if (!$auth->isAuthenticated()) {
93
                $this->loggerInstance->debug(2, "User NOT authenticated, rejecting request for a non-production installer\n");
94
                header("HTTP/1.0 403 Not Authorized");
95
                return(FALSE);
96
            }
97
            $userObject = new User($_SESSION['user']);
98
            if (!$userObject->isIdPOwner($profile->institution)) {
99
                $this->loggerInstance->debug(2, "User not an owner of a non-production profile - access forbidden\n");
100
                header("HTTP/1.0 403 Not Authorized");
101
                return(FALSE);
102
            }
103
            $this->loggerInstance->debug(4, "User is the owner - allowing access\n");
104
        }
105
        return(TRUE);
106
    }
107
108
    /**
109
     * This function tries to find a cached copy of an installer for a given
110
     * combination of Profile and device
111
     * @param string $device
112
     * @param AbstractProfile $profile
113
     * @return array containing path to the installer and mime type of the file, the path is set to NULL if no cache can be returned
114
     */
115
    private function getCache($device, $profile) {
116
        $deviceConfig = \devices\Devices::listDevices()[$device];
117
        $noCache = (isset(\devices\Devices::$Options['no_cache']) && \devices\Devices::$Options['no_cache']) ? 1 : 0;
118
        if (isset($deviceConfig['options']['no_cache'])) {
119
            $noCache = $deviceConfig['options']['no_cache'] ? 1 : 0;
120
        }
121
        if ($noCache) {
122
            $this->loggerInstance->debug(5, "getCache: the no_cache option set for this device\n");
123
            return(['path' => NULL, 'mime' => NULL]);
124
        }
125
        $this->loggerInstance->debug(5, "getCache: caching option set for this device\n");
126
        $cache = $profile->testCache($device);
127
        $iPath = $cache['cache'];
128
        if ($iPath && is_file($iPath)) {
129
            return(['path' => $iPath, 'mime' => $cache['mime']]);
130
        }
131
        return(['path' => NULL, 'mime' => NULL]);
132
    }
133
134
    /**
135
     * Generates a new installer for the given combination of device and Profile
136
     * 
137
     * @param string $device
138
     * @param AbstractProfile $profile
139
     * @return array info about the new installer (mime and link)
140
     */
141
    private function generateNewInstaller($device, $profile, $generatedFor, $token, $password) {
142
        $this->loggerInstance->debug(5, "generateNewInstaller() - Enter");
143
        $factory = new DeviceFactory($device);
144
        $this->loggerInstance->debug(5, "generateNewInstaller() - created Device");
145
        $dev = $factory->device;
146
        $out = [];
147
        if (isset($dev)) {
148
            $dev->setup($profile, $token, $password);
149
            $this->loggerInstance->debug(5, "generateNewInstaller() - Device setup done");
150
            $installer = $dev->writeInstaller();
151
            $this->loggerInstance->debug(5, "generateNewInstaller() - writeInstaller complete");
152
            $iPath = $dev->FPATH . '/tmp/' . $installer;
153
            if ($iPath && is_file($iPath)) {
154
                if (isset($dev->options['mime'])) {
155
                    $out['mime'] = $dev->options['mime'];
156
                } else {
157
                    $info = new \finfo();
158
                    $out['mime'] = $info->file($iPath, FILEINFO_MIME_TYPE);
159
                }
160
                $this->installerPath = $dev->FPATH . '/' . $installer;
161
                rename($iPath, $this->installerPath);
162
                $integerEap = (new \core\common\EAP($dev->selectedEap))->getIntegerRep();
163
                $profile->updateCache($device, $this->installerPath, $out['mime'], $integerEap);
164
                if (CONFIG['DEBUG_LEVEL'] < 4) {
165
                    \core\common\Entity::rrmdir($dev->FPATH . '/tmp');
166
                }
167
                $this->loggerInstance->debug(4, "Generated installer: " . $this->installerPath . ": for: $device, EAP:" . $integerEap . "\n");
168
                $out['link'] = "API.php?action=downloadInstaller&lang=" . $this->languageInstance->getLang() . "&profile=" . $profile->identifier . "&device=$device&generatedfor=$generatedFor";
169
            } else {
170
                $this->loggerInstance->debug(2, "Installer generation failed for: " . $profile->identifier . ":$device:" . $this->languageInstance->getLang() . "\n");
171
                $out['link'] = 0;
172
            }
173
        }
174
        return($out);
175
    }
176
177
    /**
178
     * interface to Devices::listDevices() 
179
     */
180
    public function listDevices($showHidden = 0) {
181
        $returnList = [];
182
        $count = 0;
183
        if ($showHidden !== 0 && $showHidden != 1) {
184
            throw new Exception("show_hidden is only be allowed to be 0 or 1, but it is $showHidden!");
185
        }
186
        foreach (\devices\Devices::listDevices() as $device => $deviceProperties) {
187
            if (\core\common\Entity::getAttributeValue($deviceProperties, 'options', 'hidden') === 1 && $showHidden === 0) {
188
                continue;
189
            }
190
            $count++;
191
            $deviceProperties['device'] = $device;
192
            $group = isset($deviceProperties['group']) ? $deviceProperties['group'] : 'other';
193
            if (!isset($returnList[$group])) {
194
                $returnList[$group] = [];
195
            }
196
            $returnList[$group][$device] = $deviceProperties;
197
        }
198
        return $returnList;
199
    }
200
201
    /**
202
     * 
203
     * @param string $device
204
     * @param int $profileId
205
     */
206
    public function deviceInfo($device, $profileId) {
207
        $this->languageInstance->setTextDomain("devices");
208
        $validator = new \web\lib\common\InputValidation();
209
        $out = 0;
210
        $profile = $validator->Profile($profileId);
211
        $factory = new DeviceFactory($device);
212
        $dev = $factory->device;
213
        if (isset($dev)) {
214
            $dev->setup($profile);
215
            $out = $dev->writeDeviceInfo();
216
        }
217
        $this->languageInstance->setTextDomain("web_user");
218
        echo $out;
219
    }
220
221
    /**
222
     * Prepare the support data for a given profile
223
     *
224
     * @param int $profId profile identifier
225
     * @return array
226
     * array with the following fields:
227
     * - local_email
228
     * - local_phone
229
     * - local_url
230
     * - description
231
     * - devices - an array of device names and their statuses (for a given profile)
232
     */
233
    public function profileAttributes($profId) {
234
        $this->languageInstance->setTextDomain("devices");
235
        $validator = new \web\lib\common\InputValidation();
236
        $profile = $validator->Profile($profId);
237
        $attribs = $profile->getCollapsedAttributes();
238
        $returnArray = [];
239
        $returnArray['silverbullet'] = $profile instanceof ProfileSilverbullet ? 1 : 0;
240
        if (isset($attribs['support:email'])) {
241
            $returnArray['local_email'] = $attribs['support:email'][0];
242
        }
243
        if (isset($attribs['support:phone'])) {
244
            $returnArray['local_phone'] = $attribs['support:phone'][0];
245
        }
246
        if (isset($attribs['support:url'])) {
247
            $returnArray['local_url'] = $attribs['support:url'][0];
248
        }
249
        if (isset($attribs['profile:description'])) {
250
            $returnArray['description'] = $attribs['profile:description'][0];
251
        }
252
        $returnArray['devices'] = $profile->listDevices();
253
        $this->languageInstance->setTextDomain("web_user");
254
        return($returnArray);
255
    }
256
257
    /**
258
     * Generate and send the installer
259
     *
260
     * @param string $device identifier as in {@link devices.php}
261
     * @param int $prof_id profile identifier
262
     * @return string binary stream: installerFile
263
     */
264
    public function downloadInstaller($device, $prof_id, $generated_for = 'user', $token = NULL, $password = NULL) {
265
        $this->loggerInstance->debug(4, "downloadInstaller arguments: $device,$prof_id,$generated_for\n");
266
        $output = $this->generateInstaller($device, $prof_id, $generated_for, $token, $password);
267
        $this->loggerInstance->debug(4, "output from GUI::generateInstaller:");
268
        $this->loggerInstance->debug(4, print_r($output, true));
269
        if (empty($output['link']) || $output['link'] === 0) {
270
            header("HTTP/1.0 404 Not Found");
271
            return;
272
        }
273
        $validator = new \web\lib\common\InputValidation();
274
        $profile = $validator->Profile($prof_id);
275
        $profile->incrementDownloadStats($device, $generated_for);
276
        $file = $this->installerPath;
277
        $filetype = $output['mime'];
278
        $this->loggerInstance->debug(4, "installer MIME type:$filetype\n");
279
        header("Content-type: " . $filetype);
280
        header('Content-Disposition: inline; filename="' . basename($file) . '"');
281
        header('Content-Length: ' . filesize($file));
282
        ob_clean();
283
        flush();
284
        readfile($file);
285
    }
286
287
    /**
288
     * resizes image files
289
     * 
290
     * @param string $inputImage
291
     * @param string $destFile
292
     * @param int $width
293
     * @param int $height
294
     * @param bool $resize shall we do resizing? width and height are ignored otherwise
295
     * @return array
296
     */
297
    private function processImage($inputImage, $destFile, $width, $height, $resize) {
298
        $info = new \finfo();
299
        $filetype = $info->buffer($inputImage, FILEINFO_MIME_TYPE);
300
        $offset = 60 * 60 * 24 * 30;
301
        // gmdate cannot fail here - time() is its default argument (and integer), and we are adding an integer to it
302
        $expiresString = "Expires: " . /** @scrutinizer ignore-type */ gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
303
        $blob = $inputImage;
304
305
        if ($resize === TRUE) {
306
            $image = new \Imagick();
307
            $image->readImageBlob($inputImage);
308
            $image->setImageFormat('PNG');
309
            $image->thumbnailImage($width, $height, 1);
310
            $blob = $image->getImageBlob();
311
            $this->loggerInstance->debug(4, "Writing cached logo $destFile for IdP/Federation.\n");
312
            file_put_contents($destFile, $blob);
313
        }
314
315
        return ["filetype" => $filetype, "expires" => $expiresString, "blob" => $blob];
316
    }
317
318
    /**
319
     * Get and prepare logo file 
320
     *
321
     * When called for DiscoJuice, first check if file cache exists
322
     * If not then generate the file and save it in the cache
323
     * @param int|string $identifier IdP of Federation identifier
324
     * @param string $type either 'idp' or 'federation' is allowed 
325
     * @param int $widthIn maximum width of the generated image - if 0 then it is treated as no upper bound
326
     * @param int $heightIn  maximum height of the generated image - if 0 then it is treated as no upper bound
327
     * @return array|null array with image information or NULL if there is no logo
328
     */
329
    protected function getLogo($identifier, $type, $widthIn = 0, $heightIn = 0) {
330
        $expiresString = '';
331
        $attributeName = [
332
            'federation' => "fed:logo_file",
333
            'federation_from_idp' => "fed:logo_file",
334
            'idp' => "general:logo_file",
335
        ];
336
        
337
        $logoFile = "";
338
        $validator = new \web\lib\common\InputValidation();
339
//        print "Type=$type; $identifier";
340
//        exit;
341
        switch ($type) {
342
            case "federation":
343
                $entity = $validator->Federation($identifier);
344
                break;
345
            case "idp":
346
                $entity = $validator->IdP($identifier);
347
                break;
348
            case "federation_from_idp":
349
                $idp = $validator->IdP($identifier);
350
                $entity = $validator->Federation($idp->federation);
351
                break;
352
            default:
353
                throw new Exception("Unknown type of logo requested!");
354
        }
355
        $filetype = 'image/png'; // default, only one code path where it can become different
356
        list($width, $height, $resize) = $this->testForResize($widthIn, $heightIn);
357
        if ($resize) {
358
            $logoFile = ROOT . '/web/downloads/logos/' . $identifier . '_' . $width . '_' . $height . '.png';
359
        }
360
        if (is_file($logoFile)) { // $logoFile could be an empty string but then we will get a FALSE
361
            $this->loggerInstance->debug(4, "Using cached logo $logoFile for: $identifier\n");
362
            $blob = file_get_contents($logoFile);
363
        } else {
364
            $logoAttribute = $entity->getAttributes($attributeName[$type]);
365
            if (count($logoAttribute) == 0) {
366
                return(NULL);
367
            }
368
            $this->loggerInstance->debug(4,"RESIZE:$width:$height\n");
369
            $meta = $this->processImage($logoAttribute[0]['value'], $logoFile, $width, $height, $resize);
370
            $filetype = $meta['filetype'];
371
            $expiresString = $meta['expires'];
372
            $blob = $meta['blob'];
373
        }
374
        return ["filetype" => $filetype, "expires" => $expiresString, "blob" => $blob];
375
    }
376
    
377
    private function testForResize($width, $height) {
378
        if (is_numeric($width) && is_numeric($height) && ($width > 0 || $height > 0)) {
379
            if ($height == 0) {
380
                $height = 10000;
381
            }
382
            if ($width == 0) {
383
                $width = 10000;
384
            }
385
            $resize = TRUE;
386
        } else {
387
            $width = 0;
388
            $height = 0;
389
            $resize = FALSE;
390
        }
391
        return ([$width, $height, $resize]);
392
    }
393
394
    /**
395
     * find out where the device is currently located
396
     * @return array
397
     */
398
    public function locateDevice() {
399
        return(\core\DeviceLocation::locateDevice());
400
    }
401
    
402
    /**
403
     * Lists all identity providers in the database
404
     * adding information required by DiscoJuice.
405
     * 
406
     * @param int $activeOnly if set to non-zero will cause listing of only those institutions which have some valid profiles defined.
407
     * @param string $country if set, only list IdPs in a specific country
408
     * @return array the list of identity providers
409
     *
410
     */
411
    public function listAllIdentityProviders($activeOnly = 0, $country = "") {
412
        return(IdPlist::listAllIdentityProviders($activeOnly, $country));
413
    }
414
    
415
    /**
416
     * Order active identity providers according to their distance and name
417
     * @param array $currentLocation - current location
418
     * @return array $IdPs -  list of arrays ('id', 'name');
419
     */
420
    public function orderIdentityProviders($country, $currentLocation = NULL) {
421
        return(IdPlist::orderIdentityProviders($country, $currentLocation));
422
    }
423
424
    /**
425
     * Detect the best device driver form the browser
426
     * Detects the operating system and returns its id 
427
     * display name and group membership (as in devices.php)
428
     * @return array|FALSE OS information, indexed by 'id', 'display', 'group'
429
     */
430
    public function detectOS() {
431
        $oldDomain = $this->languageInstance->setTextDomain("devices");
432
        $Dev = \devices\Devices::listDevices();
433
        $this->languageInstance->setTextDomain($oldDomain);
434
        $devId = $this->deviceFromRequest();
435
        if ($devId !== NULL) {
436
            $ret = $this->returnDevice($devId, $Dev[$devId]);
437
            if ($ret !== FALSE) {
438
                return($ret);
439
            } 
440
        }
441
// the device has not been specified or not specified correctly, try to detect if from the browser ID
442
        $browser = filter_input(INPUT_SERVER, 'HTTP_USER_AGENT', FILTER_SANITIZE_STRING);
443
        $this->loggerInstance->debug(4, "HTTP_USER_AGENT=$browser\n");
444
        foreach ($Dev as $devId => $device) {
445
            if (!isset($device['match'])) {
446
                continue;
447
            }
448
            if (preg_match('/' . $device['match'] . '/', $browser)) {
449
                return ($this->returnDevice($devId, $device));
450
            }
451
        }
452
        $this->loggerInstance->debug(2, "Unrecognised system: $browser\n");
453
        return(false);
454
    }
455
    
456
    /*
457
     * test if devise is defined and is not hidden. If all is fine return extracted information.
458
     * Return FALSE if the device has not been correctly specified
459
     */
460
    private function returnDevice($devId, $device) {
461
        if (\core\common\Entity::getAttributeValue($device, 'options', 'hidden') !== 1) {
462
            $this->loggerInstance->debug(4, "Browser_id: $devId\n");
463
            return(['device' => $devId, 'display' => $device['display'], 'group' => $device['group']]);
464
        }
465
        return(FALSE);
466
    }
467
   
468
    /**
469
     * This methods cheks if the devide has been specified as the HTTP parameters
470
     * @return device id|NULL if correcty specified or FALSE otherwise
471
     */
472
    private function deviceFromRequest() {
473
        $devId = filter_input(INPUT_GET, 'device', FILTER_SANITIZE_STRING) ?? filter_input(INPUT_POST, 'device', FILTER_SANITIZE_STRING);
474
        if ($devId === NULL || $devId === FALSE) {
475
            $this->loggerInstance->debug(2, "Invalid device id provided\n");
476
            return(NULL);
477
        }
478
        if (!isset(\devices\Devices::listDevices()[$devId])) {
479
            $this->loggerInstance->debug(2, "Unrecognised system: $devId\n");
480
            return(NULL);
481
        }
482
        return($devId);
483
    }
484
485
    /**
486
     * finds all the user certificates that originated in a given token
487
     * @param string $token
488
     * @return array|false returns FALSE if a token is invalid, otherwise array of certs
489
     */
490
    public function getUserCerts($token) {
491
        $validator = new \web\lib\common\InputValidation();
492
        $cleanToken = $validator->token($token);
493
        if ($cleanToken) {
494
            // check status of this silverbullet token according to info in DB:
495
            // it can be VALID (exists and not redeemed, EXPIRED, REDEEMED or INVALID (non existent)
496
            $invitationObject = new \core\SilverbulletInvitation($cleanToken);
497
        } else {
498
            return false;
499
        }
500
        $profile = new \core\ProfileSilverbullet($invitationObject->profile, NULL);
501
        $userdata = $profile->userStatus($invitationObject->userId);
502
        $allcerts = [];
503
        foreach ($userdata as $content) {
504
            $allcerts = array_merge($allcerts, $content->associatedCertificates);
505
        }
506
        return $allcerts;
507
    }
508
509
    public $device;
510
    private $installerPath;
511
512
    /**
513
     * helper function to sort profiles by their name
514
     * @param \core\AbstractProfile $profile1 the first profile's information
515
     * @param \core\AbstractProfile $profile2 the second profile's information
516
     * @return int
517
     */
518
    private static function profileSort($profile1, $profile2) {
519
        return strcasecmp($profile1->name, $profile2->name);
520
    }
521
522
}
523