Passed
Push — master ( 9948f7...7383fc )
by Tomasz
17:33
created

UserAPI::locateUser1()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 14

Duplication

Lines 3
Ratio 15 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 4
nop 0
dl 3
loc 20
rs 9.2
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
 * @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 GeoIp2\Database\Reader;
26
use \Exception;
27
28
/**
29
 * The basic methoods for the user GUI
30
 * @package UserAPI
31
 *
32
 */
33
class UserAPI extends CAT {
34
35
    /**
36
     * nothing special to be done here.
37
     */
38
    public function __construct() {
39
        parent::__construct();
40
    }
41
42
    /**
43
     * Prepare the device module environment and send back the link
44
     * This method creates a device module instance via the {@link DeviceFactory} call, 
45
     * then sets up the device module environment for the specific profile by calling 
46
     * {@link DeviceConfig::setup()} method and finally, called the devide writeInstaller meethod
47
     * passing the returned path name.
48
     * 
49
     * @param string $device identifier as in {@link devices.php}
50
     * @param int $profileId profile identifier
51
     *
52
     * @return array 
53
     *  array with the following fields: 
54
     *  profile - the profile identifier; 
55
     *  device - the device identifier; 
56
     *  link - the path name of the resulting installer
57
     *  mime - the mimetype of the installer
58
     */
59
    public function generateInstaller($device, $profileId, $generatedFor = "user", $token = NULL, $password = NULL) {
60
        $this->languageInstance->setTextDomain("devices");
61
        $this->loggerInstance->debug(4, "installer:$device:$profileId\n");
62
        $validator = new \web\lib\common\InputValidation();
63
        $profile = $validator->Profile($profileId);
64
        // test if the profile is production-ready and if not if the authenticated user is an owner
65
        if ($this->verifyDownloadAccess($profile) === FALSE) {
66
            return;
67
        }
68
        $installerProperties = [];
69
        $installerProperties['profile'] = $profileId;
70
        $installerProperties['device'] = $device;
71
        $cache = $this->getCache($device, $profile);
72
        $this->installerPath = $cache['path'];
73
        if ($this->installerPath && $token == NULL && $password == NULL) {
74
            $this->loggerInstance->debug(4, "Using cached installer for: $device\n");
75
            $installerProperties['link'] = "API.php?action=downloadInstaller&lang=" . $this->languageInstance->getLang() . "&profile=$profileId&device=$device&generatedfor=$generatedFor";
76
            $installerProperties['mime'] = $cache['mime'];
77
        } else {
78
            $myInstaller = $this->generateNewInstaller($device, $profile, $generatedFor, $token, $password);
79
            if ($myInstaller['link'] !== 0) {
80
                $installerProperties['mime'] = $myInstaller['mime'];
81
            }
82
            $installerProperties['link'] = $myInstaller['link'];
83
        }
84
        $this->languageInstance->setTextDomain("web_user");
85
        return($installerProperties);
86
    }
87
    
88
    private function verifyDownloadAccess($profile) {
89
        $attribs = $profile->getCollapsedAttributes();
90
        if (!isset($attribs['profile:production']) || (isset($attribs['profile:production']) && $attribs['profile:production'][0] != "on")) {
91
            $this->loggerInstance->debug(4, "Attempt to download a non-production ready installer for profile: $profile->identifier\n");
92
            $auth = new \web\lib\admin\Authentication();
93
            if (!$auth->isAuthenticated()) {
94
                $this->loggerInstance->debug(2, "User NOT authenticated, rejecting request for a non-production installer\n");
95
                header("HTTP/1.0 403 Not Authorized");
96
                return(FALSE);
97
            }
98
            $userObject = new User($_SESSION['user']);
99
            if (!$userObject->isIdPOwner($profile->institution)) {
100
                $this->loggerInstance->debug(2, "User not an owner of a non-production profile - access forbidden\n");
101
                header("HTTP/1.0 403 Not Authorized");
102
                return(FALSE);
103
            }
104
            $this->loggerInstance->debug(4, "User is the owner - allowing access\n");
105
        }
106
        return(TRUE);
107
    }
108
109
    /**
110
     * This function tries to find a cached copy of an installer for a given
111
     * combination of Profile and device
112
     * @param string $device
113
     * @param AbstractProfile $profile
114
     * @return boolean|string the string with the path to the cached copy, or FALSE if no cached copy exists
115
     */
116
    private function getCache($device, $profile) {
117
        $deviceList = \devices\Devices::listDevices();
118
        $deviceConfig = $deviceList[$device];
119
        $noCache = (isset(\devices\Devices::$Options['no_cache']) && \devices\Devices::$Options['no_cache']) ? 1 : 0;
120
        if (isset($deviceConfig['options']['no_cache'])) {
121
            $noCache = $deviceConfig['options']['no_cache'] ? 1 : 0;
122
        }
123
        if ($noCache) {
124
            $this->loggerInstance->debug(5, "getCache: the no_cache option set for this device\n");
125
            return(FALSE);
126
        }
127
        $this->loggerInstance->debug(5, "getCache: caching option set for this device\n");
128
        $cache = $profile->testCache($device);
129
        $iPath = $cache['cache'];
130
        if ($iPath && is_file($iPath)) {
131
            return(['path' => $iPath, 'mime' => $cache['mime']]);
132
        }
133
        return(FALSE);
134
    }
135
136
    /**
137
     * Generates a new installer for the given combination of device and Profile
138
     * 
139
     * @param string $device
140
     * @param AbstractProfile $profile
141
     * @return array info about the new installer (mime and link)
142
     */
143
    private function generateNewInstaller($device, $profile, $generatedFor, $token, $password) {
144
        $this->loggerInstance->debug(5, "generateNewInstaller() - Enter");
145
        $factory = new DeviceFactory($device);
146
        $this->loggerInstance->debug(5, "generateNewInstaller() - created Device");
147
        $dev = $factory->device;
148
        $out = [];
149
        if (isset($dev)) {
150
            $dev->setup($profile, $token, $password);
151
            $this->loggerInstance->debug(5, "generateNewInstaller() - Device setup done");
152
            $installer = $dev->writeInstaller();
153
            $this->loggerInstance->debug(5, "generateNewInstaller() - writeInstaller complete");
154
            $iPath = $dev->FPATH . '/tmp/' . $installer;
155
            if ($iPath && is_file($iPath)) {
156
                if (isset($dev->options['mime'])) {
157
                    $out['mime'] = $dev->options['mime'];
158
                } else {
159
                    $info = new \finfo();
160
                    $out['mime'] = $info->file($iPath, FILEINFO_MIME_TYPE);
161
                }
162
                $this->installerPath = $dev->FPATH . '/' . $installer;
163
                rename($iPath, $this->installerPath);
164
                $integerEap = (new \core\common\EAP($dev->selectedEap))->getIntegerRep();
165
                $profile->updateCache($device, $this->installerPath, $out['mime'], $integerEap);
166
                if (CONFIG['DEBUG_LEVEL'] < 4) {
167
                    \core\common\Entity::rrmdir($dev->FPATH . '/tmp');
168
                }
169
                $this->loggerInstance->debug(4, "Generated installer: " . $this->installerPath . ": for: $device, EAP:" . $integerEap . "\n");
170
                $out['link'] = "API.php?action=downloadInstaller&lang=" . $this->languageInstance->getLang() . "&profile=" . $profile->identifier . "&device=$device&generatedfor=$generatedFor";
171
            } else {
172
                $this->loggerInstance->debug(2, "Installer generation failed for: " . $profile->identifier . ":$device:" . $this->languageInstance->getLang() . "\n");
173
                $out['link'] = 0;
174
            }
175
        }
176
        return($out);
177
    }
178
179
    /**
180
     * interface to Devices::listDevices() 
181
     */
182
    public function listDevices($showHidden = 0) {
183
        $dev = \devices\Devices::listDevices();
184
        $returnList = [];
185
        $count = 0;
186
        if ($showHidden !== 0 && $showHidden != 1) {
187
            throw new Exception("show_hidden is only be allowed to be 0 or 1, but it is $showHidden!");
188
        }
189
        foreach ($dev as $device => $deviceProperties) {
190
            if (isset($deviceProperties['options']['hidden']) && $deviceProperties['options']['hidden'] && $showHidden == 0) {
191
                continue;
192
            }
193
            $count++;
194
195
            $deviceProperties['device'] = $device;
196
197
            $group = isset($deviceProperties['group']) ? $deviceProperties['group'] : 'other';
198
            if (!isset($returnList[$group])) {
199
                $returnList[$group] = [];
200
            }
201
            $returnList[$group][$device] = $deviceProperties;
202
        }
203
        return $returnList;
204
    }
205
206
    /**
207
     * 
208
     * @param string $device
209
     * @param int $profileId
210
     */
211
    public function deviceInfo($device, $profileId) {
212
        $this->languageInstance->setTextDomain("devices");
213
        $validator = new \web\lib\common\InputValidation();
214
        $out = 0;
215
        $profile = $validator->Profile($profileId);
216
        $factory = new DeviceFactory($device);
217
        $dev = $factory->device;
218
        if (isset($dev)) {
219
            $dev->setup($profile);
220
            $out = $dev->writeDeviceInfo();
221
        }
222
        $this->languageInstance->setTextDomain("web_user");
223
        echo $out;
224
    }
225
226
    /**
227
     * Prepare the support data for a given profile
228
     *
229
     * @param int $profId profile identifier
230
     * @return array
231
     * array with the following fields:
232
     * - local_email
233
     * - local_phone
234
     * - local_url
235
     * - description
236
     * - devices - an array of device names and their statuses (for a given profile)
237
     */
238
    public function profileAttributes($profId) {
239
        $this->languageInstance->setTextDomain("devices");
240
        $validator = new \web\lib\common\InputValidation();
241
        $profile = $validator->Profile($profId);
242
        $attribs = $profile->getCollapsedAttributes();
243
        $returnArray = [];
244
        $returnArray['silverbullet'] = $profile instanceof ProfileSilverbullet ? 1 : 0;
245
        if (isset($attribs['support:email'])) {
246
            $returnArray['local_email'] = $attribs['support:email'][0];
247
        }
248
        if (isset($attribs['support:phone'])) {
249
            $returnArray['local_phone'] = $attribs['support:phone'][0];
250
        }
251
        if (isset($attribs['support:url'])) {
252
            $returnArray['local_url'] = $attribs['support:url'][0];
253
        }
254
        if (isset($attribs['profile:description'])) {
255
            $returnArray['description'] = $attribs['profile:description'][0];
256
        }
257
        $returnArray['devices'] = $profile->listDevices();
258
        $this->languageInstance->setTextDomain("web_user");
259
        return($returnArray);
260
    }
261
262
    /**
263
      this method needs to be used with care, it could give wrong results in some
264
      cicumstances
265
     */
266
    protected function getRootURL() {
267
        $backtrace = debug_backtrace();
268
        $backtraceFileInfo = array_pop($backtrace);
269
        $fileTemp = $backtraceFileInfo['file'];
270
        $file = substr($fileTemp, strlen(dirname(__DIR__)));
271
        if ($file === FALSE) {
272
            throw new Exception("No idea what's going wrong - filename cropping returned FALSE!");
273
        }
274
        while (substr($file, 0, 1) == '/') {
275
            $file = substr($file, 1);
276
            if ($file === FALSE) {
277
                throw new Exception("Unable to crop leading / from a string known to start with / ???");
278
            }
279
        }
280
        $slashCount = count(explode('/', $file));
281
        $out = $_SERVER['SCRIPT_NAME'];
282
        for ($iterator = 0; $iterator < $slashCount; $iterator++) {
283
            $out = dirname($out);
284
        }
285
        if ($out == '/') {
286
            $out = '';
287
        }
288
        return '//' . $_SERVER['SERVER_NAME'] . $out;
289
    }
290
291
    /**
292
     * Generate and send the installer
293
     *
294
     * @param string $device identifier as in {@link devices.php}
295
     * @param int $prof_id profile identifier
296
     * @return string binary stream: installerFile
297
     */
298
    public function downloadInstaller($device, $prof_id, $generated_for = 'user', $token = NULL, $password = NULL) {
299
        $this->loggerInstance->debug(4, "downloadInstaller arguments: $device,$prof_id,$generated_for\n");
300
        $output = $this->generateInstaller($device, $prof_id, $generated_for, $token, $password);
301
        $this->loggerInstance->debug(4, "output from GUI::generateInstaller:");
302
        $this->loggerInstance->debug(4, print_r($output, true));
303
        if (empty($output['link']) || $output['link'] === 0) {
304
            header("HTTP/1.0 404 Not Found");
305
            return;
306
        }
307
        $validator = new \web\lib\common\InputValidation();
308
        $profile = $validator->Profile($prof_id);
309
        $profile->incrementDownloadStats($device, $generated_for);
310
        $file = $this->installerPath;
311
        $filetype = $output['mime'];
312
        $this->loggerInstance->debug(4, "installer MIME type:$filetype\n");
313
        header("Content-type: " . $filetype);
314
        header('Content-Disposition: inline; filename="' . basename($file) . '"');
315
        header('Content-Length: ' . filesize($file));
316
        ob_clean();
317
        flush();
318
        readfile($file);
319
    }
320
321
    /**
322
     * resizes image files
323
     * 
324
     * @param string $inputImage
325
     * @param string $destFile
326
     * @param int $width
327
     * @param int $height
328
     * @param bool $resize shall we do resizing? width and height are ignored otherwise
329
     * @return array
330
     */
331
    private function processImage($inputImage, $destFile, $width, $height, $resize) {
332
        $info = new \finfo();
333
        $filetype = $info->buffer($inputImage, FILEINFO_MIME_TYPE);
334
        $offset = 60 * 60 * 24 * 30;
335
        // gmdate cannot fail here - time() is its default argument (and integer), and we are adding an integer to it
336
        $expiresString = "Expires: " . /** @scrutinizer ignore-type */ gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
337
        $blob = $inputImage;
338
339
        if ($resize === TRUE) {
340
            $image = new \Imagick();
341
            $image->readImageBlob($inputImage);
342
            $image->setImageFormat('PNG');
343
            $image->thumbnailImage($width, $height, 1);
344
            $blob = $image->getImageBlob();
345
            $this->loggerInstance->debug(4, "Writing cached logo $destFile for IdP/Federation.\n");
346
            file_put_contents($destFile, $blob);
347
        }
348
349
        return ["filetype" => $filetype, "expires" => $expiresString, "blob" => $blob];
350
    }
351
352
    /**
353
     * Get and prepare logo file 
354
     *
355
     * When called for DiscoJuice, first check if file cache exists
356
     * If not then generate the file and save it in the cache
357
     * @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...
358
     * @param int $width maximum width of the generated image - if 0 then it is treated as no upper bound
359
     * @param int $height  maximum height of the generated image - if 0 then it is treated as no upper bound
360
     * @return array|null array with image information or NULL if there is no logo
361
     */
362
    protected function getLogo($identifier, $type, $width = 0, $height = 0) {
363
        $expiresString = '';
364
        $resize = FALSE;
365
        $logoFile = "";
366
        $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...
367
        $validator = new \web\lib\common\InputValidation();
368
        switch ($type) {
369
            case "federation":
370
                if (is_int($identifier)) {
371
                    throw new Exception("Federation identifiers are strings!");
372
                }
373
                $entity = $validator->Federation($identifier);
374
                $attributeName = "fed:logo_file";
375
                break;
376
            case "idp":
377
                if (!is_int($identifier)) {
378
                    throw new Exception("Institution identifiers are integers!");
379
                }
380
                $entity = $validator->IdP($identifier);
381
                $attributeName = "general:logo_file";
382
                break;
383
            default:
384
                throw new Exception("Unknown type of logo requested!");
385
                return;
0 ignored issues
show
Unused Code introduced by
return; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
386
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
387
        }
388
        $filetype = 'image/png'; // default, only one code path where it can become different
389
        if (($width || $height) && is_numeric($width) && is_numeric($height)) {
390
            $resize = TRUE;
391
            if ($height == 0) {
392
                $height = 10000;
393
            }
394
            if ($width == 0) {
395
                $width = 10000;
396
            }
397
            $logoFile = ROOT . '/web/downloads/logos/' . $identifier . '_' . $width . '_' . $height . '.png';
398
        }
399
        if ($resize === TRUE && is_file($logoFile)) {
400
            $this->loggerInstance->debug(4, "Using cached logo $logoFile for: $identifier\n");
401
            $blob = file_get_contents($logoFile);
402
        } else {
403
            $logoAttribute = $entity->getAttributes($attributeName);
404
            if (count($logoAttribute) == 0) {
405
                return(NULL);
406
            }
407
            $this->loggerInstance->debug(4,"RESIZE:$width:$height\n");
408
            $meta = $this->processImage($logoAttribute[0]['value'], $logoFile, $width, $height, $resize);
409
            $filetype = $meta['filetype'];
410
            $expiresString = $meta['expires'];
411
            $blob = $meta['blob'];
412
        }
413
        return ["filetype" => $filetype, "expires" => $expiresString, "blob" => $blob];
414
    }
415
   
416
    /**
417
     * outputs a logo
418
     * 
419
     * @param string|int $identifier
420
     * @param string $type "federation" or "idp"
421
     * @param int $width
422
     * @param int $height
423
     */
424 View Code Duplication
    public function sendLogo($identifier, $type, $width = 0, $height = 0) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
425
        $logo = $this->getLogo($identifier, $type, $width, $height);
426
        header("Content-type: " . $logo['filetype']);
427
        header("Cache-Control:max-age=36000, must-revalidate");
428
        header($logo['expires']);
429
        echo $logo['blob'];
430
    }
431
432
    
433
    
434
    /**
435
     * find out where the user is currently located
436
     * @return array
437
     */
438
    public function locateUser() {
439
        $geoipVersion = CONFIG['GEOIP']['version'] ?? 0;
440
        switch ($geoipVersion) {
441
            case 0:
442
                return(['status' => 'error', 'error' => 'Geolocation not supported']);
443
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
444
            case 1:
445
                return($this->locateUser1());
446
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
447
            case 2:
448
                return($this->locateUser2());
449
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
450
            default:
451
                throw new Exception("This version of GeoIP is not known!");
452
        }
453
    }
454
    
455
    private function locateUser1() {
456 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...
457
            return ['status' => 'error', 'error' => 'Function for GEOIPv1 called, but config says this is not the version to use!'];
458
        }
459
        //$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...
460
        $host = filter_input(INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP);
461
        $record = geoip_record_by_name($host);
462
        if ($record === FALSE) {
463
            return ['status' => 'error', 'error' => 'Problem listing countries'];
464
        }
465
        $result = ['status' => 'ok'];
466
        $result['country'] = $record['country_code'];
467
//  the two lines below are a dirty hack to take of the error in naming the UK federation
468
        if ($result['country'] == 'GB') {
469
            $result['country'] = 'UK';
470
        }
471
        $result['region'] = $record['region'];
472
        $result['geo'] = ['lat' => (float) $record['latitude'], 'lon' => (float) $record['longitude']];
473
        return($result);
474
    }
475
    
476
    /**
477
     * find out where the user is currently located, using GeoIP2
478
     * @return array
479
     */
480
    private function locateUser2() {
481 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...
482
            return ['status' => 'error', 'error' => 'Function for GEOIPv2 called, but config says this is not the version to use!'];
483
        }
484
        require_once CONFIG['GEOIP']['geoip2-path-to-autoloader'];
485
        $reader = new Reader(CONFIG['GEOIP']['geoip2-path-to-db']);
486
        $host = $_SERVER['REMOTE_ADDR'];
487
        try {
488
            $record = $reader->city($host);
489
        } catch (\Exception $e) {
490
            $result = ['status' => 'error', 'error' => 'Problem listing countries'];
491
            return($result);
492
        }
493
        $result = ['status' => 'ok'];
494
        $result['country'] = $record->country->isoCode;
495
//  the two lines below are a dirty hack to take of the error in naming the UK federation
496
        if ($result['country'] == 'GB') {
497
            $result['country'] = 'UK';
498
        }
499
        $result['region'] = $record->continent->name;
500
501
        $result['geo'] = ['lat' => (float) $record->location->latitude, 'lon' => (float) $record->location->longitude];
502
        return($result);
503
    }
504
505
    /**
506
     * Calculate the distance in km between two points given their
507
     * geo coordinates.
508
     * @param array $point1 - first point as an 'lat', 'lon' array 
509
     * @param array $profile1 - second point as an 'lat', 'lon' array 
510
     * @return float distance in km
511
     */
512
    private function geoDistance($point1, $profile1) {
513
514
        $distIntermediate = sin(deg2rad($point1['lat'])) * sin(deg2rad($profile1['lat'])) +
515
                cos(deg2rad($point1['lat'])) * cos(deg2rad($profile1['lat'])) * cos(deg2rad($point1['lon'] - $profile1['lon']));
516
        $dist = rad2deg(acos($distIntermediate)) * 60 * 1.1852;
517
        return(round($dist));
518
    }
519
520
    /**
521
     * Order active identity providers according to their distance and name
522
     * @param array $currentLocation - current location
523
     * @return array $IdPs -  list of arrays ('id', 'name');
524
     */
525
    public function orderIdentityProviders($country, $currentLocation = NULL) {
526
        $idps = $this->listAllIdentityProviders(1, $country);
527
528
        if (is_null($currentLocation)) {
529
            $currentLocation = ['lat' => "90", 'lon' => "0"];
530
            $userLocation = $this->locateUser();
531
            if ($userLocation['status'] == 'ok') {
532
                $currentLocation = $userLocation['geo'];
533
            }
534
        }
535
        $idpTitle = [];
536
        $resultSet = [];
537
        foreach ($idps as $idp) {
538
            $idpTitle[$idp['entityID']] = $idp['title'];
539
            $dist = 10000;
540
            if (isset($idp['geo'])) {
541
                $G = $idp['geo'];
542
                if (isset($G['lon'])) {
543
                    $d1 = $this->geoDistance($currentLocation, $G);
544
                    if ($d1 < $dist) {
545
                        $dist = $d1;
546
                    }
547
                } else {
548
                    foreach ($G as $g) {
549
                        $d1 = $this->geoDistance($currentLocation, $g);
550
                        if ($d1 < $dist) {
551
                            $dist = $d1;
552
                        }
553
                    }
554
                }
555
            }
556
            if ($dist > 100) {
557
                $dist = 10000;
558
            }
559
            $d = sprintf("%06d", $dist);
560
            $resultSet[$idp['entityID']] = $d . " " . $idp['title'];
561
        }
562
        asort($resultSet);
563
        $outarray = [];
564
        foreach (array_keys($resultSet) as $r) {
565
            $outarray[] = ['idp' => $r, 'title' => $idpTitle[$r]];
566
        }
567
        return($outarray);
568
    }
569
570
    /**
571
     * Detect the best device driver form the browser
572
     *
573
     * Detects the operating system and returns its id 
574
     * display name and group membership (as in devices.php)
575
     * @return array|false OS information, indexed by 'id', 'display', 'group'
576
     */
577
    public function detectOS() {
578
        $oldDomain = $this->languageInstance->setTextDomain("devices");
579
        $Dev = \devices\Devices::listDevices();
580
        $this->languageInstance->setTextDomain($oldDomain);
581
        $devId = $this->deviceFromRequest();
582
        if ($devId !== FALSE) {
583
            $ret = $this->returnDevice($devId, $Dev[$devId]);
584
            if ($ret !== FALSE) {
585
                return($ret);
586
            } 
587
        }
588
// the device has not been specified or not specified correctly, try to detect if from the browser ID
589
        $browser = filter_input(INPUT_SERVER, 'HTTP_USER_AGENT', FILTER_SANITIZE_STRING);
590
        $this->loggerInstance->debug(4, "HTTP_USER_AGENT=$browser\n");
591
        foreach ($Dev as $devId => $device) {
592
            if (!isset($device['match'])) {
593
                continue;
594
            }
595
            if (preg_match('/' . $device['match'] . '/', $browser)) {
596
                return ($this->returnDevice($devId, $device));
597
            }
598
        }
599
        $this->loggerInstance->debug(2, "Unrecognised system: $browser\n");
600
        return(false);
601
    }
602
    
603
    /*
604
     * test if devise is defined and is not hidden. If all is fine return extracted information.
605
     * Return FALSE if the device has not been correctly specified
606
     */
607
    private function returnDevice($devId, $device) {
608
        if (!isset($device['options']['hidden']) || $device['options']['hidden'] == 0) {
609
            $this->loggerInstance->debug(4, "Browser_id: $devId\n");
610
            return(['device' => $devId, 'display' => $device['display'], 'group' => $device['group']]);
611
        }
612
        return(FALSE);
613
    }
614
   
615
    /**
616
     * This methods cheks if the devide has been specified as the HTTP parameters
617
     * @return device id is correcty specified or FALSE otherwise
618
     */
619
    private function deviceFromRequest() {
620
        $devId = filter_input(INPUT_GET, 'device', FILTER_SANITIZE_STRING) ?? filter_input(INPUT_POST, 'device', FILTER_SANITIZE_STRING);
621
        if ($devId === NULL || $devId === FALSE) {
622
            $this->loggerInstance->debug(2, "Invalid device id provided\n");
623
            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...
624
        }
625
        $Dev = \devices\Devices::listDevices();
626
        if (!isset($Dev['$devId'])) {
627
            $this->loggerInstance->debug(2, "Unrecognised system: $devId\n");
628
            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...
629
        }
630
        return($devId);
631
    }
632
633
    /**
634
     * finds all the user certificates that originated in a given token
635
     * @param string $token
636
     * @return array|false returns FALSE if a token is invalid, otherwise array of certs
637
     */
638
    public function getUserCerts($token) {
639
        $validator = new \web\lib\common\InputValidation();
640
        $cleanToken = $validator->token($token);
641
        if ($cleanToken) {
642
            // check status of this silverbullet token according to info in DB:
643
            // it can be VALID (exists and not redeemed, EXPIRED, REDEEMED or INVALID (non existent)
644
            $tokenStatus = \core\ProfileSilverbullet::tokenStatus($cleanToken);
645
        } else {
646
            return false;
647
        }
648
        $profile = new \core\ProfileSilverbullet($tokenStatus['profile'], NULL);
649
        $userdata = $profile->userStatus($tokenStatus['db_id']);
650
        $allcerts = [];
651
        foreach ($userdata as $index => $content) {
652
            $allcerts = array_merge($allcerts, $content['cert_status']);
653
        }
654
        return $allcerts;
655
    }
656
657
658
    public $device;
659
    private $installerPath;
660
661
    /**
662
     * helper function to sort profiles by their name
663
     * @param \core\AbstractProfile $profile1 the first profile's information
664
     * @param \core\AbstractProfile $profile2 the second profile's information
665
     * @return int
666
     */
667
    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...
668
        return strcasecmp($profile1->name, $profile2->name);
669
    }
670
671
}
672