Passed
Push — master ( 0af33f...4ff8a5 )
by Stefan
07:05
created

UserAPI::testForResize()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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