Passed
Push — master ( 7a2653...9948f7 )
by Tomasz
06:04
created

UserAPI::generateInstaller()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 28
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 23
nc 4
nop 5
dl 0
loc 28
rs 8.439
c 0
b 0
f 0
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
 * @package UserAPI
16
 *
17
 * Parts of this code are based on simpleSAMLPhp discojuice module.
18
 * This product includes GeoLite data created by MaxMind, available from
19
 * http://www.maxmind.com
20
 */
21
22
namespace core;
23
24
use GeoIp2\Database\Reader;
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 && $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 (!isset($attribs['profile:production']) || (isset($attribs['profile:production']) && $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 boolean|string the string with the path to the cached copy, or FALSE if no cached copy exists
114
     */
115
    private function getCache($device, $profile) {
116
        $deviceList = \devices\Devices::listDevices();
117
        $deviceConfig = $deviceList[$device];
118
        $noCache = (isset(\devices\Devices::$Options['no_cache']) && \devices\Devices::$Options['no_cache']) ? 1 : 0;
119
        if (isset($deviceConfig['options']['no_cache'])) {
120
            $noCache = $deviceConfig['options']['no_cache'] ? 1 : 0;
121
        }
122
        if ($noCache) {
123
            $this->loggerInstance->debug(5, "getCache: the no_cache option set for this device\n");
124
            return(FALSE);
125
        }
126
        $this->loggerInstance->debug(5, "getCache: caching option set for this device\n");
127
        $cache = $profile->testCache($device);
128
        $iPath = $cache['cache'];
129
        if ($iPath && is_file($iPath)) {
130
            return(['path' => $iPath, 'mime' => $cache['mime']]);
131
        }
132
        return(FALSE);
133
    }
134
135
    /**
136
     * Generates a new installer for the given combination of device and Profile
137
     * 
138
     * @param string $device
139
     * @param AbstractProfile $profile
140
     * @return array info about the new installer (mime and link)
141
     */
142
    private function generateNewInstaller($device, $profile, $generatedFor, $token, $password) {
143
        $this->loggerInstance->debug(5, "generateNewInstaller() - Enter");
144
        $factory = new DeviceFactory($device);
145
        $this->loggerInstance->debug(5, "generateNewInstaller() - created Device");
146
        $dev = $factory->device;
147
        $out = [];
148
        if (isset($dev)) {
149
            $dev->setup($profile, $token, $password);
150
            $this->loggerInstance->debug(5, "generateNewInstaller() - Device setup done");
151
            $installer = $dev->writeInstaller();
152
            $this->loggerInstance->debug(5, "generateNewInstaller() - writeInstaller complete");
153
            $iPath = $dev->FPATH . '/tmp/' . $installer;
154
            if ($iPath && is_file($iPath)) {
155
                if (isset($dev->options['mime'])) {
156
                    $out['mime'] = $dev->options['mime'];
157
                } else {
158
                    $info = new \finfo();
159
                    $out['mime'] = $info->file($iPath, FILEINFO_MIME_TYPE);
160
                }
161
                $this->installerPath = $dev->FPATH . '/' . $installer;
162
                rename($iPath, $this->installerPath);
163
                $integerEap = (new \core\common\EAP($dev->selectedEap))->getIntegerRep();
164
                $profile->updateCache($device, $this->installerPath, $out['mime'], $integerEap);
165
                if (CONFIG['DEBUG_LEVEL'] < 4) {
166
                    \core\common\Entity::rrmdir($dev->FPATH . '/tmp');
167
                }
168
                $this->loggerInstance->debug(4, "Generated installer: " . $this->installerPath . ": for: $device, EAP:" . $integerEap . "\n");
169
                $out['link'] = "API.php?action=downloadInstaller&lang=" . $this->languageInstance->getLang() . "&profile=" . $profile->identifier . "&device=$device&generatedfor=$generatedFor";
170
            } else {
171
                $this->loggerInstance->debug(2, "Installer generation failed for: " . $profile->identifier . ":$device:" . $this->languageInstance->getLang() . "\n");
172
                $out['link'] = 0;
173
            }
174
        }
175
        return($out);
176
    }
177
178
    /**
179
     * interface to Devices::listDevices() 
180
     */
181
    public function listDevices($showHidden = 0) {
182
        $dev = \devices\Devices::listDevices();
183
        $returnList = [];
184
        $count = 0;
185
        if ($showHidden !== 0 && $showHidden != 1) {
186
            throw new Exception("show_hidden is only be allowed to be 0 or 1, but it is $showHidden!");
187
        }
188
        foreach ($dev as $device => $deviceProperties) {
189
            if (isset($deviceProperties['options']['hidden']) && $deviceProperties['options']['hidden'] && $showHidden == 0) {
190
                continue;
191
            }
192
            $count++;
193
194
            $deviceProperties['device'] = $device;
195
196
            $group = isset($deviceProperties['group']) ? $deviceProperties['group'] : 'other';
197
            if (!isset($returnList[$group])) {
198
                $returnList[$group] = [];
199
            }
200
            $returnList[$group][$device] = $deviceProperties;
201
        }
202
        return $returnList;
203
    }
204
205
    /**
206
     * 
207
     * @param string $device
208
     * @param int $profileId
209
     */
210
    public function deviceInfo($device, $profileId) {
211
        $this->languageInstance->setTextDomain("devices");
212
        $validator = new \web\lib\common\InputValidation();
213
        $out = 0;
214
        $profile = $validator->Profile($profileId);
215
        $factory = new DeviceFactory($device);
216
        $dev = $factory->device;
217
        if (isset($dev)) {
218
            $dev->setup($profile);
219
            $out = $dev->writeDeviceInfo();
220
        }
221
        $this->languageInstance->setTextDomain("web_user");
222
        echo $out;
223
    }
224
225
    /**
226
     * Prepare the support data for a given profile
227
     *
228
     * @param int $profId profile identifier
229
     * @return array
230
     * array with the following fields:
231
     * - local_email
232
     * - local_phone
233
     * - local_url
234
     * - description
235
     * - devices - an array of device names and their statuses (for a given profile)
236
     */
237
    public function profileAttributes($profId) {
238
        $this->languageInstance->setTextDomain("devices");
239
        $validator = new \web\lib\common\InputValidation();
240
        $profile = $validator->Profile($profId);
241
        $attribs = $profile->getCollapsedAttributes();
242
        $returnArray = [];
243
        $returnArray['silverbullet'] = $profile instanceof ProfileSilverbullet ? 1 : 0;
244
        if (isset($attribs['support:email'])) {
245
            $returnArray['local_email'] = $attribs['support:email'][0];
246
        }
247
        if (isset($attribs['support:phone'])) {
248
            $returnArray['local_phone'] = $attribs['support:phone'][0];
249
        }
250
        if (isset($attribs['support:url'])) {
251
            $returnArray['local_url'] = $attribs['support:url'][0];
252
        }
253
        if (isset($attribs['profile:description'])) {
254
            $returnArray['description'] = $attribs['profile:description'][0];
255
        }
256
        $returnArray['devices'] = $profile->listDevices();
257
        $this->languageInstance->setTextDomain("web_user");
258
        return($returnArray);
259
    }
260
261
    /**
262
      this method needs to be used with care, it could give wrong results in some
263
      cicumstances
264
     */
265
    private function getRootURL() {
266
        $backtrace = debug_backtrace();
267
        $backtraceFileInfo = array_pop($backtrace);
268
        $fileTemp = $backtraceFileInfo['file'];
269
        $file = substr($fileTemp, strlen(dirname(__DIR__)));
270
        if ($file === FALSE) {
271
            throw new Exception("No idea what's going wrong - filename cropping returned FALSE!");
272
        }
273
        while (substr($file, 0, 1) == '/') {
274
            $file = substr($file, 1);
275
            if ($file === FALSE) {
276
                throw new Exception("Unable to crop leading / from a string known to start with / ???");
277
            }
278
        }
279
        $slashCount = count(explode('/', $file));
280
        $out = $_SERVER['SCRIPT_NAME'];
281
        for ($iterator = 0; $iterator < $slashCount; $iterator++) {
282
            $out = dirname($out);
283
        }
284
        if ($out == '/') {
285
            $out = '';
286
        }
287
        return '//' . $_SERVER['SERVER_NAME'] . $out;
288
    }
289
290
    /**
291
     *  wrapper JSON function
292
     * 
293
     * @param array|bool|null $data the core data to be converted to JSON
294
     * @param int $status extra status information, defaults to 1
295
     * @return string JSON encoded data
296
     */
297
    public function return_json($data, $status = 1) {
298
        $returnArray = [];
299
        $returnArray['status'] = $status;
300
        $returnArray['data'] = $data;
301
        $returnArray['tou'] = "Please consult Terms of Use at: " . $this->getRootURL() . "/tou.php";
302
        return(json_encode($returnArray));
303
    }
304
305
    /**
306
     * outputs the list of supported languages.
307
     */
308
    public function JSON_listLanguages() {
309
        $returnArray = [];
310
        foreach (CONFIG['LANGUAGES'] as $id => $val) {
311
            $returnArray[] = ['lang' => $id, 'display' => $val['display'], 'locale' => $val['locale']];
312
        }
313
        echo $this->return_json($returnArray);
314
    }
315
316
    /**
317
     * outputs the list of countries with configured IdPs
318
     *
319
     */
320
    public function JSON_listCountries() {
321
        $federations = $this->printCountryList(1);
322
        $returnArray = [];
323
        foreach ($federations as $id => $val) {
324
            $returnArray[] = ['federation' => $id, 'display' => $val];
325
        }
326
        echo $this->return_json($returnArray);
327
    }
328
329
    /**
330
     * outputs the list of IdPs in a given country
331
     *
332
     * @param string $country the country we are interested in
333
     */
334
    public function JSON_listIdentityProviders($country) {
335
        $idps = $this->listAllIdentityProviders(1, $country);
336
        $returnArray = [];
337 View Code Duplication
        foreach ($idps as $idp) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
338
            $returnArray[] = ['idp' => $idp['entityID'], 'display' => $idp['title']];
339
        }
340
        echo $this->return_json($returnArray);
341
    }
342
343
    /**
344
     * outputs the list of all active IdPs
345
     *
346
     * The IdP list is formatted for DiscoJuice consumption
347
     */
348
    public function JSON_listIdentityProvidersForDisco() {
349
        $idps = $this->listAllIdentityProviders(1);
350
        $returnArray = [];
351
        foreach ($idps as $idp) {
352
            $idp['idp'] = $idp['entityID'];
353
            $returnArray[] = $idp;
354
        }
355
        echo json_encode($returnArray);
356
    }
357
358
    /**
359
     * outputs the list of IdPs in a given country ordered with respect to their distance to the user's location
360
     * 
361
     * @param string $country the country in question
362
     * @param array $location the coordinates of the approximate user location
363
     *
364
     */
365
    public function JSON_orderIdentityProviders($country, $location = NULL) {
366
        $idps = $this->orderIdentityProviders($country, $location);
367
        $returnArray = [];
368 View Code Duplication
        foreach ($idps as $idp) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
369
            $returnArray[] = ['idp' => $idp['id'], 'display' => $idp['title']];
370
        }
371
        echo $this->return_json($returnArray);
372
    }
373
374
    /**
375
     * outputs a list of profiles available for a given IdP
376
     *
377
     * @param int $idpIdentifier the IdP identifier
378
     * @param int $sort should the result set be sorted? 0 = no, 1 = yes
379
     */
380
    public function JSON_listProfiles($idpIdentifier, $sort = 0) {
381
        $this->languageInstance->setTextDomain("web_user");
382
        $returnArray = [];
383
        try {
384
            $idp = new IdP($idpIdentifier);
385
        } catch (\Exception $fail) {
386
            echo $this->return_json($returnArray, 0);
387
            return;
388
        }
389
        $hasLogo = FALSE;
390
        $logo = $idp->getAttributes('general:logo_file');
391
        if (count($logo) > 0) {
392
            $hasLogo = 1;
393
        }
394
        $profiles = $idp->listProfiles(TRUE);
395
        if ($sort == 1) {
396
            usort($profiles, ["UserAPI", "profileSort"]);
397
        }
398
        foreach ($profiles as $profile) {
399
            $returnArray[] = ['profile' => $profile->identifier, 'display' => $profile->name, 'idp_name' => $profile->instName, 'logo' => $hasLogo];
400
        }
401
        echo $this->return_json($returnArray);
402
    }
403
404
    /**
405
     * outputs the list of devices available for the given profile
406
     *
407
     * @param int $profileId the Profile identifier
408
     */
409
    public function JSON_listDevices($profileId) {
410
        $this->languageInstance->setTextDomain("web_user");
411
        $returnArray = [];
412
        $profileAttributes = $this->profileAttributes($profileId);
413
        $thedevices = $profileAttributes['devices'];
414
        foreach ($thedevices as $D) {
415
            if (isset($D['options']) && isset($D['options']['hidden']) && $D['options']['hidden']) {
416
                continue;
417
            }
418
            if ($D['device'] === '0') {
419
                $disp = '';
420
            } else {
421
                $disp = $D['display'];
422
            }
423
            $returnArray[] = ['device' => $D['id'], 'display' => $disp, 'status' => $D['status'], 'redirect' => $D['redirect']];
424
        }
425
        echo $this->return_json($returnArray);
426
    }
427
428
    /**
429
     * outputs the link to the installers (additionally, actually generates it or takes it from cache)
430
     *
431
     * @param string $device identifier as in {@link devices.php}
432
     * @param int $prof_id profile identifier
433
     */
434
    public function JSON_generateInstaller($device, $prof_id) {
435
        $this->loggerInstance->debug(4, "JSON::generateInstaller arguments: $device,$prof_id\n");
436
        $output = $this->generateInstaller($device, $prof_id);
437
        $this->loggerInstance->debug(4, "output from GUI::generateInstaller:");
438
        $this->loggerInstance->debug(4, print_r($output, true));
439
        $this->loggerInstance->debug(4, json_encode($output));
440
//    header('Content-type: application/json; utf-8');
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
441
        echo $this->return_json($output);
442
    }
443
444
    /**
445
     * Generate and send the installer
446
     *
447
     * @param string $device identifier as in {@link devices.php}
448
     * @param int $prof_id profile identifier
449
     * @return string binary stream: installerFile
450
     */
451
    public function downloadInstaller($device, $prof_id, $generated_for = 'user', $token = NULL, $password = NULL) {
452
        $this->loggerInstance->debug(4, "downloadInstaller arguments: $device,$prof_id,$generated_for\n");
453
        $output = $this->generateInstaller($device, $prof_id, $generated_for, $token, $password);
454
        $this->loggerInstance->debug(4, "output from GUI::generateInstaller:");
455
        $this->loggerInstance->debug(4, print_r($output, true));
456
        if (empty($output['link']) || $output['link'] === 0) {
457
            header("HTTP/1.0 404 Not Found");
458
            return;
459
        }
460
        $validator = new \web\lib\common\InputValidation();
461
        $profile = $validator->Profile($prof_id);
462
        $profile->incrementDownloadStats($device, $generated_for);
463
        $file = $this->installerPath;
464
        $filetype = $output['mime'];
465
        $this->loggerInstance->debug(4, "installer MIME type:$filetype\n");
466
        header("Content-type: " . $filetype);
467
        header('Content-Disposition: inline; filename="' . basename($file) . '"');
468
        header('Content-Length: ' . filesize($file));
469
        ob_clean();
470
        flush();
471
        readfile($file);
472
    }
473
474
    /**
475
     * resizes image files
476
     * 
477
     * @param string $inputImage
478
     * @param string $destFile
479
     * @param int $width
480
     * @param int $height
481
     * @param bool $resize shall we do resizing? width and height are ignored otherwise
482
     * @return array
483
     */
484
    private function processImage($inputImage, $destFile, $width, $height, $resize) {
485
        $info = new \finfo();
486
        $filetype = $info->buffer($inputImage, FILEINFO_MIME_TYPE);
487
        $offset = 60 * 60 * 24 * 30;
488
        // gmdate cannot fail here - time() is its default argument (and integer), and we are adding an integer to it
489
        $expiresString = "Expires: " . /** @scrutinizer ignore-type */ gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
490
        $blob = $inputImage;
491
492
        if ($resize === TRUE) {
493
            $image = new \Imagick();
494
            $image->readImageBlob($inputImage);
495
            $image->setImageFormat('PNG');
496
            $image->thumbnailImage($width, $height, 1);
497
            $blob = $image->getImageBlob();
498
            $this->loggerInstance->debug(4, "Writing cached logo $destFile for IdP/Federation.\n");
499
            file_put_contents($destFile, $blob);
500
        }
501
502
        return ["filetype" => $filetype, "expires" => $expiresString, "blob" => $blob];
503
    }
504
505
    /**
506
     * Get and prepare logo file 
507
     *
508
     * When called for DiscoJuice, first check if file cache exists
509
     * If not then generate the file and save it in the cache
510
     * @param int $idp IdP identifier
0 ignored issues
show
Bug introduced by
There is no parameter named $idp. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
511
     * @param int $width maximum width of the generated image - if 0 then it is treated as no upper bound
512
     * @param int $height  maximum height of the generated image - if 0 then it is treated as no upper bound
513
     * @return array|null array with image information or NULL if there is no logo
514
     */
515
    private function getLogo($identifier, $type, $width = 0, $height = 0) {
516
        $expiresString = '';
517
        $resize = FALSE;
518
        $logoFile = "";
519
        $attributeName = "";
0 ignored issues
show
Unused Code introduced by
$attributeName 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...
520
        $validator = new \web\lib\common\InputValidation();
521
        switch ($type) {
522
            case "federation":
523
                if (is_int($identifier)) {
524
                    throw new Exception("Federation identifiers are strings!");
525
                }
526
                $entity = $validator->Federation($identifier);
527
                $attributeName = "fed:logo_file";
528
                break;
529
            case "idp":
530
                if (!is_int($identifier)) {
531
                    throw new Exception("Institution identifiers are integers!");
532
                }
533
                $entity = $validator->IdP($identifier);
534
                $attributeName = "general:logo_file";
535
                break;
536
            default:
537
                throw new Exception("Unknown type of logo requested!");
538
        }
539
        $filetype = 'image/png'; // default, only one code path where it can become different
540
        if (($width || $height) && is_numeric($width) && is_numeric($height)) {
541
            $resize = TRUE;
542
            if ($height == 0) {
543
                $height = 10000;
544
            }
545
            if ($width == 0) {
546
                $width = 10000;
547
            }
548
            $logoFile = ROOT . '/web/downloads/logos/' . $identifier . '_' . $width . '_' . $height . '.png';
549
        }
550
        if ($resize === TRUE && is_file($logoFile)) {
551
            $this->loggerInstance->debug(4, "Using cached logo $logoFile for: $identifier\n");
552
            $blob = file_get_contents($logoFile);
553
        } else {
554
            $logoAttribute = $entity->getAttributes($attributeName);
555
            if (count($logoAttribute) == 0) {
556
                return(NULL);
557
            }
558
            $this->loggerInstance->debug(4,"RESIZE:$width:$height\n");
559
            $meta = $this->processImage($logoAttribute[0]['value'], $logoFile, $width, $height, $resize);
560
            $filetype = $meta['filetype'];
561
            $expiresString = $meta['expires'];
562
            $blob = $meta['blob'];
563
        }
564
        return ["filetype" => $filetype, "expires" => $expiresString, "blob" => $blob];
565
    }
566
   
567
    /**
568
     * outputs a logo
569
     * 
570
     * @param string|int $identifier
571
     * @param string $type "federation" or "idp"
572
     * @param int $width
573
     * @param int $height
574
     */
575
    public function sendLogo($identifier, $type, $width = 0, $height = 0) {
576
        $logo = $this->getLogo($identifier, $type, $width, $height);
577
        header("Content-type: " . $logo['filetype']);
578
        header("Cache-Control:max-age=36000, must-revalidate");
579
        header($logo['expires']);
580
        echo $logo['blob'];
581
    }
582
583
    /**
584
     * find out where the user is currently located
585
     * @return array
586
     */
587
    public function locateUser() {
588 View Code Duplication
        if (CONFIG['GEOIP']['version'] != 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
589
            return ['status' => 'error', 'error' => 'Function for GEOIPv1 called, but config says this is not the version to use!'];
590
        }
591
        //$host = $_SERVER['REMOTE_ADDR'];
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
592
        $host = filter_input(INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP);
593
        $record = geoip_record_by_name($host);
594
        if ($record === FALSE) {
595
            return ['status' => 'error', 'error' => 'Problem listing countries'];
596
        }
597
        $result = ['status' => 'ok'];
598
        $result['country'] = $record['country_code'];
599
//  the two lines below are a dirty hack to take of the error in naming the UK federation
600
        if ($result['country'] == 'GB') {
601
            $result['country'] = 'UK';
602
        }
603
        $result['region'] = $record['region'];
604
        $result['geo'] = ['lat' => (float) $record['latitude'], 'lon' => (float) $record['longitude']];
605
        return($result);
606
    }
607
    
608
    /**
609
     * find out where the user is currently located, using GeoIP2
610
     * @return array
611
     */
612
    public function locateUser2() {
613 View Code Duplication
        if (CONFIG['GEOIP']['version'] != 2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
614
            return ['status' => 'error', 'error' => 'Function for GEOIPv2 called, but config says this is not the version to use!'];
615
        }
616
        require_once CONFIG['GEOIP']['geoip2-path-to-autoloader'];
617
        $reader = new Reader(CONFIG['GEOIP']['geoip2-path-to-db']);
618
        $host = $_SERVER['REMOTE_ADDR'];
619
        try {
620
            $record = $reader->city($host);
621
        } catch (\Exception $e) {
622
            $result = ['status' => 'error', 'error' => 'Problem listing countries'];
623
            return($result);
624
        }
625
        $result = ['status' => 'ok'];
626
        $result['country'] = $record->country->isoCode;
627
//  the two lines below are a dirty hack to take of the error in naming the UK federation
628
        if ($result['country'] == 'GB') {
629
            $result['country'] = 'UK';
630
        }
631
        $result['region'] = $record->continent->name;
632
633
        $result['geo'] = ['lat' => (float) $record->location->latitude, 'lon' => (float) $record->location->longitude];
634
        return($result);
635
    }
636
637
    /**
638
     * outputs the user location as JSON
639
     * @throws Exception
640
     */
641
    public function JSON_locateUser() {
642
        header('Content-type: application/json; utf-8');
643
644
        $geoipVersion = CONFIG['GEOIP']['version'] ?? 0;
645
646
        switch ($geoipVersion) {
647
            case 0:
648
                echo json_encode(['status' => 'error', 'error' => 'Geolocation not supported']);
649
                break;
650
            case 1:
651
                echo json_encode($this->locateUser());
652
                break;
653
            case 2:
654
                echo json_encode($this->locateUser2());
655
                break;
656
            default:
657
                throw new Exception("This version of GeoIP is not known!");
658
        }
659
    }
660
661
    /**
662
     * outputs support data prepared within {@link GUI::profileAttributes()}
663
     */
664
    public function JSON_profileAttributes($prof_id) {
665
//    header('Content-type: application/json; utf-8');
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
666
        echo $this->return_json($this->profileAttributes($prof_id));
667
    }
668
669
    /**
670
     * Calculate the distance in km between two points given their
671
     * geo coordinates.
672
     * @param array $point1 - first point as an 'lat', 'lon' array 
673
     * @param array $profile1 - second point as an 'lat', 'lon' array 
674
     * @return float distance in km
675
     */
676
    private function geoDistance($point1, $profile1) {
677
678
        $distIntermediate = sin(deg2rad($point1['lat'])) * sin(deg2rad($profile1['lat'])) +
679
                cos(deg2rad($point1['lat'])) * cos(deg2rad($profile1['lat'])) * cos(deg2rad($point1['lon'] - $profile1['lon']));
680
        $dist = rad2deg(acos($distIntermediate)) * 60 * 1.1852;
681
        return(round($dist));
682
    }
683
684
    /**
685
     * Order active identity providers according to their distance and name
686
     * @param array $currentLocation - current location
687
     * @return array $IdPs -  list of arrays ('id', 'name');
688
     */
689
    public function orderIdentityProviders($country, $currentLocation = NULL) {
690
        $idps = $this->listAllIdentityProviders(1, $country);
691
692
        if (is_null($currentLocation)) {
693
            $currentLocation = ['lat' => "90", 'lon' => "0"];
694
            $userLocation = $this->locateUser();
695
            if ($userLocation['status'] == 'ok') {
696
                $currentLocation = $userLocation['geo'];
697
            }
698
        }
699
        $idpTitle = [];
700
        $resultSet = [];
701
        foreach ($idps as $idp) {
702
            $idpTitle[$idp['entityID']] = $idp['title'];
703
            $dist = 10000;
704
            if (isset($idp['geo'])) {
705
                $G = $idp['geo'];
706
                if (isset($G['lon'])) {
707
                    $d1 = $this->geoDistance($currentLocation, $G);
708
                    if ($d1 < $dist) {
709
                        $dist = $d1;
710
                    }
711
                } else {
712
                    foreach ($G as $g) {
713
                        $d1 = $this->geoDistance($currentLocation, $g);
714
                        if ($d1 < $dist) {
715
                            $dist = $d1;
716
                        }
717
                    }
718
                }
719
            }
720
            if ($dist > 100) {
721
                $dist = 10000;
722
            }
723
            $d = sprintf("%06d", $dist);
724
            $resultSet[$idp['entityID']] = $d . " " . $idp['title'];
725
        }
726
        asort($resultSet);
727
        $outarray = [];
728
        foreach (array_keys($resultSet) as $r) {
729
            $outarray[] = ['idp' => $r, 'title' => $idpTitle[$r]];
730
        }
731
        return($outarray);
732
    }
733
734
    /**
735
     * Detect the best device driver form the browser
736
     *
737
     * Detects the operating system and returns its id 
738
     * display name and group membership (as in devices.php)
739
     * @return array|false OS information, indexed by 'id', 'display', 'group'
740
     */
741
    public function detectOS() {
742
        $oldDomain = $this->languageInstance->setTextDomain("devices");
743
        $Dev = \devices\Devices::listDevices();
744
        $this->languageInstance->setTextDomain($oldDomain);
745
        $devId = $this->deviceFromRequest();
746
        if ($devId !== FALSE) {
747
            $ret = $this->returnDevice($devId, $Dev[$devId]);
748
            if ($ret !== FALSE) {
749
                return($ret);
750
            } 
751
        }
752
// the device has not been specified or not specified correctly, try to detect if from the browser ID
753
        $browser = filter_input(INPUT_SERVER, 'HTTP_USER_AGENT', FILTER_SANITIZE_STRING);
754
        $this->loggerInstance->debug(4, "HTTP_USER_AGENT=$browser\n");
755
        foreach ($Dev as $devId => $device) {
756
            if (!isset($device['match'])) {
757
                continue;
758
            }
759
            if (preg_match('/' . $device['match'] . '/', $browser)) {
760
                return ($this->returnDevice($devId, $device));
761
            }
762
        }
763
        $this->loggerInstance->debug(2, "Unrecognised system: $browser\n");
764
        return(false);
765
    }
766
    
767
    /*
768
     * test if devise is defined and is not hidden. If all is fine return extracted information.
769
     * Return FALSE if the device has not been correctly specified
770
     */
771
    private function returnDevice($devId, $device) {
772
        if (!isset($device['options']['hidden']) || $device['options']['hidden'] == 0) {
773
            $this->loggerInstance->debug(4, "Browser_id: $devId\n");
774
            return(['device' => $devId, 'display' => $device['display'], 'group' => $device['group']]);
775
        }
776
        return(FALSE);
777
    }
778
   
779
    /**
780
     * This methods cheks if the devide has been specified as the HTTP parameters
781
     * @return device id is correcty specified or FALSE otherwise
782
     */
783
    private function deviceFromRequest() {
784
        $devId = filter_input(INPUT_GET, 'device', FILTER_SANITIZE_STRING) ?? filter_input(INPUT_POST, 'device', FILTER_SANITIZE_STRING);
785
        if ($devId === NULL || $devId === FALSE) {
786
            $this->loggerInstance->debug(2, "Invalid device id provided\n");
787
            return(FALSE);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return FALSE; (false) is incompatible with the return type documented by core\UserAPI::deviceFromRequest of type core\device.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
788
        }
789
        $Dev = \devices\Devices::listDevices();
790
        if (!isset($Dev['$devId'])) {
791
            $this->loggerInstance->debug(2, "Unrecognised system: $devId\n");
792
            return(FALSE);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return FALSE; (false) is incompatible with the return type documented by core\UserAPI::deviceFromRequest of type core\device.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
793
        }
794
        return($devId);
795
    }
796
    
797
            
798
799
    /**
800
     * outputs OS guess in JSON
801
     */
802
    public function JSON_detectOS() {
803
        $returnArray = $this->detectOS();
804
        if (is_array($returnArray)) {
805
            $status = 1;
806
        } else {
807
            $status = 0;
808
        }
809
        echo $this->return_json($returnArray, $status);
810
    }
811
812
    /**
813
     * finds all the user certificates that originated in a given token
814
     * @param string $token
815
     * @return array|false returns FALSE if a token is invalid, otherwise array of certs
816
     */
817
    public function getUserCerts($token) {
818
        $validator = new \web\lib\common\InputValidation();
819
        $cleanToken = $validator->token($token);
820
        if ($cleanToken) {
821
            // check status of this silverbullet token according to info in DB:
822
            // it can be VALID (exists and not redeemed, EXPIRED, REDEEMED or INVALID (non existent)
823
            $tokenStatus = \core\ProfileSilverbullet::tokenStatus($cleanToken);
824
        } else {
825
            return false;
826
        }
827
        $profile = new \core\ProfileSilverbullet($tokenStatus['profile'], NULL);
828
        $userdata = $profile->userStatus($tokenStatus['db_id']);
829
        $allcerts = [];
830
        foreach ($userdata as $index => $content) {
831
            $allcerts = array_merge($allcerts, $content['cert_status']);
832
        }
833
        return $allcerts;
834
    }
835
836
    /**
837
     * outputs user certificates pertaining to a given token in JSON
838
     * @param string $token
839
     */
840
    public function JSON_getUserCerts($token) {
841
        $returnArray = $this->getUserCerts($token);
842
        if ($returnArray) {
843
            $status = 1;
844
        } else {
845
            $status = 0;
846
        }
847
        echo $this->return_json($returnArray, $status);
848
    }
849
850
    public $device;
851
    private $installerPath;
852
853
    /**
854
     * helper function to sort profiles by their name
855
     * @param \core\AbstractProfile $profile1 the first profile's information
856
     * @param \core\AbstractProfile $profile2 the second profile's information
857
     * @return int
858
     */
859
    private static function profileSort($profile1, $profile2) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
860
        return strcasecmp($profile1->name, $profile2->name);
861
    }
862
863
}
864