Passed
Push — master ( efeff6...7a2653 )
by Tomasz
05:59
created

UserAPI::getLogo()   C

Complexity

Conditions 14
Paths 33

Size

Total Lines 51
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 42
nc 33
nop 4
dl 0
loc 51
rs 5.6426
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        $attribs = $profile->getCollapsedAttributes();
64
        // test if the profile is production-ready and if not if the authenticated user is an owner
65
        if (!isset($attribs['profile:production']) || (isset($attribs['profile:production']) && $attribs['profile:production'][0] != "on")) {
66
            $this->loggerInstance->debug(4, "Attempt to download a non-production ready installer fir profile: $profileId\n");
67
            $auth = new \web\lib\admin\Authentication();
68
            if (!$auth->isAuthenticated()) {
69
                $this->loggerInstance->debug(2, "User NOT authenticated, rejecting request for a non-production installer\n");
70
                header("HTTP/1.0 403 Not Authorized");
71
                return;
72
            }
73
74
            $userObject = new User($_SESSION['user']);
75
            if (!$userObject->isIdPOwner($profile->institution)) {
76
                $this->loggerInstance->debug(2, "User not an owner of a non-production profile - access forbidden\n");
77
                header("HTTP/1.0 403 Not Authorized");
78
                return;
79
            }
80
            $this->loggerInstance->debug(4, "User is the owner - allowing access\n");
81
        }
82
        $installerProperties = [];
83
        $installerProperties['profile'] = $profileId;
84
        $installerProperties['device'] = $device;
85
        $cache = $this->getCache($device, $profile);
86
        $this->installerPath = $cache['path'];
87
        if ($this->installerPath && $token == NULL && $password == NULL) {
88
            $this->loggerInstance->debug(4, "Using cached installer for: $device\n");
89
            $installerProperties['link'] = "API.php?action=downloadInstaller&lang=" . $this->languageInstance->getLang() . "&profile=$profileId&device=$device&generatedfor=$generatedFor";
90
            $installerProperties['mime'] = $cache['mime'];
91
        } else {
92
            $myInstaller = $this->generateNewInstaller($device, $profile, $generatedFor, $token, $password);
93
            if ($myInstaller['link'] !== 0) {
94
                $installerProperties['mime'] = $myInstaller['mime'];
95
            }
96
            $installerProperties['link'] = $myInstaller['link'];
97
        }
98
        $this->languageInstance->setTextDomain("web_user");
99
        return($installerProperties);
100
    }
101
102
    /**
103
     * This function tries to find a cached copy of an installer for a given
104
     * combination of Profile and device
105
     * @param string $device
106
     * @param AbstractProfile $profile
107
     * @return boolean|string the string with the path to the cached copy, or FALSE if no cached copy exists
108
     */
109
    private function getCache($device, $profile) {
110
        $deviceList = \devices\Devices::listDevices();
111
        $deviceConfig = $deviceList[$device];
112
        $noCache = (isset(\devices\Devices::$Options['no_cache']) && \devices\Devices::$Options['no_cache']) ? 1 : 0;
113
        if (isset($deviceConfig['options']['no_cache'])) {
114
            $noCache = $deviceConfig['options']['no_cache'] ? 1 : 0;
115
        }
116
        if ($noCache) {
117
            $this->loggerInstance->debug(5, "getCache: the no_cache option set for this device\n");
118
            return(FALSE);
119
        }
120
        $this->loggerInstance->debug(5, "getCache: caching option set for this device\n");
121
        $cache = $profile->testCache($device);
122
        $iPath = $cache['cache'];
123
        if ($iPath && is_file($iPath)) {
124
            return(['path' => $iPath, 'mime' => $cache['mime']]);
125
        }
126
        return(FALSE);
127
    }
128
129
    /**
130
     * Generates a new installer for the given combination of device and Profile
131
     * 
132
     * @param string $device
133
     * @param AbstractProfile $profile
134
     * @return array info about the new installer (mime and link)
135
     */
136
    private function generateNewInstaller($device, $profile, $generatedFor, $token, $password) {
137
        $this->loggerInstance->debug(5, "generateNewInstaller() - Enter");
138
        $factory = new DeviceFactory($device);
139
        $this->loggerInstance->debug(5, "generateNewInstaller() - created Device");
140
        $dev = $factory->device;
141
        $out = [];
142
        if (isset($dev)) {
143
            $dev->setup($profile, $token, $password);
144
            $this->loggerInstance->debug(5, "generateNewInstaller() - Device setup done");
145
            $installer = $dev->writeInstaller();
146
            $this->loggerInstance->debug(5, "generateNewInstaller() - writeInstaller complete");
147
            $iPath = $dev->FPATH . '/tmp/' . $installer;
148
            if ($iPath && is_file($iPath)) {
149
                if (isset($dev->options['mime'])) {
150
                    $out['mime'] = $dev->options['mime'];
151
                } else {
152
                    $info = new \finfo();
153
                    $out['mime'] = $info->file($iPath, FILEINFO_MIME_TYPE);
154
                }
155
                $this->installerPath = $dev->FPATH . '/' . $installer;
156
                rename($iPath, $this->installerPath);
157
                $integerEap = (new \core\common\EAP($dev->selectedEap))->getIntegerRep();
158
                $profile->updateCache($device, $this->installerPath, $out['mime'], $integerEap);
159
                if (CONFIG['DEBUG_LEVEL'] < 4) {
160
                    \core\common\Entity::rrmdir($dev->FPATH . '/tmp');
161
                }
162
                $this->loggerInstance->debug(4, "Generated installer: " . $this->installerPath . ": for: $device, EAP:" . $integerEap . "\n");
163
                $out['link'] = "API.php?action=downloadInstaller&lang=" . $this->languageInstance->getLang() . "&profile=" . $profile->identifier . "&device=$device&generatedfor=$generatedFor";
164
            } else {
165
                $this->loggerInstance->debug(2, "Installer generation failed for: " . $profile->identifier . ":$device:" . $this->languageInstance->getLang() . "\n");
166
                $out['link'] = 0;
167
            }
168
        }
169
        return($out);
170
    }
171
172
    /**
173
     * interface to Devices::listDevices() 
174
     */
175
    public function listDevices($showHidden = 0) {
176
        $dev = \devices\Devices::listDevices();
177
        $returnList = [];
178
        $count = 0;
179
        if ($showHidden !== 0 && $showHidden != 1) {
180
            throw new Exception("show_hidden is only be allowed to be 0 or 1, but it is $showHidden!");
181
        }
182
        foreach ($dev as $device => $deviceProperties) {
183
            if (isset($deviceProperties['options']['hidden']) && $deviceProperties['options']['hidden'] && $showHidden == 0) {
184
                continue;
185
            }
186
            $count++;
187
188
            $deviceProperties['device'] = $device;
189
190
            $group = isset($deviceProperties['group']) ? $deviceProperties['group'] : 'other';
191
            if (!isset($returnList[$group])) {
192
                $returnList[$group] = [];
193
            }
194
            $returnList[$group][$device] = $deviceProperties;
195
        }
196
        return $returnList;
197
    }
198
199
    /**
200
     * 
201
     * @param string $device
202
     * @param int $profileId
203
     */
204
    public function deviceInfo($device, $profileId) {
205
        $this->languageInstance->setTextDomain("devices");
206
        $validator = new \web\lib\common\InputValidation();
207
        $out = 0;
208
        $profile = $validator->Profile($profileId);
209
        $factory = new DeviceFactory($device);
210
        $dev = $factory->device;
211
        if (isset($dev)) {
212
            $dev->setup($profile);
213
            $out = $dev->writeDeviceInfo();
214
        }
215
        $this->languageInstance->setTextDomain("web_user");
216
        echo $out;
217
    }
218
219
    /**
220
     * Prepare the support data for a given profile
221
     *
222
     * @param int $profId profile identifier
223
     * @return array
224
     * array with the following fields:
225
     * - local_email
226
     * - local_phone
227
     * - local_url
228
     * - description
229
     * - devices - an array of device names and their statuses (for a given profile)
230
     */
231
    public function profileAttributes($profId) {
232
        $this->languageInstance->setTextDomain("devices");
233
        $validator = new \web\lib\common\InputValidation();
234
        $profile = $validator->Profile($profId);
235
        $attribs = $profile->getCollapsedAttributes();
236
        $returnArray = [];
237
        $returnArray['silverbullet'] = $profile instanceof ProfileSilverbullet ? 1 : 0;
238
        if (isset($attribs['support:email'])) {
239
            $returnArray['local_email'] = $attribs['support:email'][0];
240
        }
241
        if (isset($attribs['support:phone'])) {
242
            $returnArray['local_phone'] = $attribs['support:phone'][0];
243
        }
244
        if (isset($attribs['support:url'])) {
245
            $returnArray['local_url'] = $attribs['support:url'][0];
246
        }
247
        if (isset($attribs['profile:description'])) {
248
            $returnArray['description'] = $attribs['profile:description'][0];
249
        }
250
        $returnArray['devices'] = $profile->listDevices();
251
        $this->languageInstance->setTextDomain("web_user");
252
        return($returnArray);
253
    }
254
255
    /**
256
      this method needs to be used with care, it could give wrong results in some
257
      cicumstances
258
     */
259
    private function getRootURL() {
260
        $backtrace = debug_backtrace();
261
        $backtraceFileInfo = array_pop($backtrace);
262
        $fileTemp = $backtraceFileInfo['file'];
263
        $file = substr($fileTemp, strlen(dirname(__DIR__)));
264
        if ($file === FALSE) {
265
            throw new Exception("No idea what's going wrong - filename cropping returned FALSE!");
266
        }
267
        while (substr($file, 0, 1) == '/') {
268
            $file = substr($file, 1);
269
            if ($file === FALSE) {
270
                throw new Exception("Unable to crop leading / from a string known to start with / ???");
271
            }
272
        }
273
        $slashCount = count(explode('/', $file));
274
        $out = $_SERVER['SCRIPT_NAME'];
275
        for ($iterator = 0; $iterator < $slashCount; $iterator++) {
276
            $out = dirname($out);
277
        }
278
        if ($out == '/') {
279
            $out = '';
280
        }
281
        return '//' . $_SERVER['SERVER_NAME'] . $out;
282
    }
283
284
    /**
285
     *  wrapper JSON function
286
     * 
287
     * @param array|bool|null $data the core data to be converted to JSON
288
     * @param int $status extra status information, defaults to 1
289
     * @return string JSON encoded data
290
     */
291
    public function return_json($data, $status = 1) {
292
        $returnArray = [];
293
        $returnArray['status'] = $status;
294
        $returnArray['data'] = $data;
295
        $returnArray['tou'] = "Please consult Terms of Use at: " . $this->getRootURL() . "/tou.php";
296
        return(json_encode($returnArray));
297
    }
298
299
    /**
300
     * outputs the list of supported languages.
301
     */
302
    public function JSON_listLanguages() {
303
        $returnArray = [];
304
        foreach (CONFIG['LANGUAGES'] as $id => $val) {
305
            $returnArray[] = ['lang' => $id, 'display' => $val['display'], 'locale' => $val['locale']];
306
        }
307
        echo $this->return_json($returnArray);
308
    }
309
310
    /**
311
     * outputs the list of countries with configured IdPs
312
     *
313
     */
314
    public function JSON_listCountries() {
315
        $federations = $this->printCountryList(1);
316
        $returnArray = [];
317
        foreach ($federations as $id => $val) {
318
            $returnArray[] = ['federation' => $id, 'display' => $val];
319
        }
320
        echo $this->return_json($returnArray);
321
    }
322
323
    /**
324
     * outputs the list of IdPs in a given country
325
     *
326
     * @param string $country the country we are interested in
327
     */
328
    public function JSON_listIdentityProviders($country) {
329
        $idps = $this->listAllIdentityProviders(1, $country);
330
        $returnArray = [];
331 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...
332
            $returnArray[] = ['idp' => $idp['entityID'], 'display' => $idp['title']];
333
        }
334
        echo $this->return_json($returnArray);
335
    }
336
337
    /**
338
     * outputs the list of all active IdPs
339
     *
340
     * The IdP list is formatted for DiscoJuice consumption
341
     */
342
    public function JSON_listIdentityProvidersForDisco() {
343
        $idps = $this->listAllIdentityProviders(1);
344
        $returnArray = [];
345
        foreach ($idps as $idp) {
346
            $idp['idp'] = $idp['entityID'];
347
            $returnArray[] = $idp;
348
        }
349
        echo json_encode($returnArray);
350
    }
351
352
    /**
353
     * outputs the list of IdPs in a given country ordered with respect to their distance to the user's location
354
     * 
355
     * @param string $country the country in question
356
     * @param array $location the coordinates of the approximate user location
357
     *
358
     */
359
    public function JSON_orderIdentityProviders($country, $location = NULL) {
360
        $idps = $this->orderIdentityProviders($country, $location);
361
        $returnArray = [];
362 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...
363
            $returnArray[] = ['idp' => $idp['id'], 'display' => $idp['title']];
364
        }
365
        echo $this->return_json($returnArray);
366
    }
367
368
    /**
369
     * outputs a list of profiles available for a given IdP
370
     *
371
     * @param int $idpIdentifier the IdP identifier
372
     * @param int $sort should the result set be sorted? 0 = no, 1 = yes
373
     */
374
    public function JSON_listProfiles($idpIdentifier, $sort = 0) {
375
        $this->languageInstance->setTextDomain("web_user");
376
        $returnArray = [];
377
        try {
378
            $idp = new IdP($idpIdentifier);
379
        } catch (\Exception $fail) {
380
            echo $this->return_json($returnArray, 0);
381
            return;
382
        }
383
        $hasLogo = FALSE;
384
        $logo = $idp->getAttributes('general:logo_file');
385
        if (count($logo) > 0) {
386
            $hasLogo = 1;
387
        }
388
        $profiles = $idp->listProfiles(TRUE);
389
        if ($sort == 1) {
390
            usort($profiles, ["UserAPI", "profileSort"]);
391
        }
392
        foreach ($profiles as $profile) {
393
            $returnArray[] = ['profile' => $profile->identifier, 'display' => $profile->name, 'idp_name' => $profile->instName, 'logo' => $hasLogo];
394
        }
395
        echo $this->return_json($returnArray);
396
    }
397
398
    /**
399
     * outputs the list of devices available for the given profile
400
     *
401
     * @param int $profileId the Profile identifier
402
     */
403
    public function JSON_listDevices($profileId) {
404
        $this->languageInstance->setTextDomain("web_user");
405
        $returnArray = [];
406
        $profileAttributes = $this->profileAttributes($profileId);
407
        $thedevices = $profileAttributes['devices'];
408
        foreach ($thedevices as $D) {
409
            if (isset($D['options']) && isset($D['options']['hidden']) && $D['options']['hidden']) {
410
                continue;
411
            }
412
            if ($D['device'] === '0') {
413
                $disp = '';
414
            } else {
415
                $disp = $D['display'];
416
            }
417
            $returnArray[] = ['device' => $D['id'], 'display' => $disp, 'status' => $D['status'], 'redirect' => $D['redirect']];
418
        }
419
        echo $this->return_json($returnArray);
420
    }
421
422
    /**
423
     * outputs the link to the installers (additionally, actually generates it or takes it from cache)
424
     *
425
     * @param string $device identifier as in {@link devices.php}
426
     * @param int $prof_id profile identifier
427
     */
428
    public function JSON_generateInstaller($device, $prof_id) {
429
        $this->loggerInstance->debug(4, "JSON::generateInstaller arguments: $device,$prof_id\n");
430
        $output = $this->generateInstaller($device, $prof_id);
431
        $this->loggerInstance->debug(4, "output from GUI::generateInstaller:");
432
        $this->loggerInstance->debug(4, print_r($output, true));
433
        $this->loggerInstance->debug(4, json_encode($output));
434
//    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...
435
        echo $this->return_json($output);
436
    }
437
438
    /**
439
     * Generate and send the installer
440
     *
441
     * @param string $device identifier as in {@link devices.php}
442
     * @param int $prof_id profile identifier
443
     * @return string binary stream: installerFile
444
     */
445
    public function downloadInstaller($device, $prof_id, $generated_for = 'user', $token = NULL, $password = NULL) {
446
        $this->loggerInstance->debug(4, "downloadInstaller arguments: $device,$prof_id,$generated_for\n");
447
        $output = $this->generateInstaller($device, $prof_id, $generated_for, $token, $password);
448
        $this->loggerInstance->debug(4, "output from GUI::generateInstaller:");
449
        $this->loggerInstance->debug(4, print_r($output, true));
450
        if (empty($output['link']) || $output['link'] === 0) {
451
            header("HTTP/1.0 404 Not Found");
452
            return;
453
        }
454
        $validator = new \web\lib\common\InputValidation();
455
        $profile = $validator->Profile($prof_id);
456
        $profile->incrementDownloadStats($device, $generated_for);
457
        $file = $this->installerPath;
458
        $filetype = $output['mime'];
459
        $this->loggerInstance->debug(4, "installer MIME type:$filetype\n");
460
        header("Content-type: " . $filetype);
461
        header('Content-Disposition: inline; filename="' . basename($file) . '"');
462
        header('Content-Length: ' . filesize($file));
463
        ob_clean();
464
        flush();
465
        readfile($file);
466
    }
467
468
    /**
469
     * resizes image files
470
     * 
471
     * @param string $inputImage
472
     * @param string $destFile
473
     * @param int $width
474
     * @param int $height
475
     * @param bool $resize shall we do resizing? width and height are ignored otherwise
476
     * @return array
477
     */
478
    private function processImage($inputImage, $destFile, $width, $height, $resize) {
479
        $info = new \finfo();
480
        $filetype = $info->buffer($inputImage, FILEINFO_MIME_TYPE);
481
        $offset = 60 * 60 * 24 * 30;
482
        // gmdate cannot fail here - time() is its default argument (and integer), and we are adding an integer to it
483
        $expiresString = "Expires: " . /** @scrutinizer ignore-type */ gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
484
        $blob = $inputImage;
485
486
        if ($resize === TRUE) {
487
            $image = new \Imagick();
488
            $image->readImageBlob($inputImage);
489
            $image->setImageFormat('PNG');
490
            $image->thumbnailImage($width, $height, 1);
491
            $blob = $image->getImageBlob();
492
            $this->loggerInstance->debug(4, "Writing cached logo $destFile for IdP/Federation.\n");
493
            file_put_contents($destFile, $blob);
494
        }
495
496
        return ["filetype" => $filetype, "expires" => $expiresString, "blob" => $blob];
497
    }
498
499
    /**
500
     * Get and prepare logo file 
501
     *
502
     * When called for DiscoJuice, first check if file cache exists
503
     * If not then generate the file and save it in the cache
504
     * @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...
505
     * @param int $width maximum width of the generated image - if 0 then it is treated as no upper bound
506
     * @param int $height  maximum height of the generated image - if 0 then it is treated as no upper bound
507
     * @return array|null array with image information or NULL if there is no logo
508
     */
509
    private function getLogo($identifier, $type, $width = 0, $height = 0) {
510
        $expiresString = '';
511
        $resize = FALSE;
512
        $logoFile = "";
513
        $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...
514
        $validator = new \web\lib\common\InputValidation();
515
        switch ($type) {
516
            case "federation":
517
                if (is_int($identifier)) {
518
                    throw new Exception("Federation identifiers are strings!");
519
                }
520
                $entity = $validator->Federation($identifier);
521
                $attributeName = "fed:logo_file";
522
                break;
523
            case "idp":
524
                if (!is_int($identifier)) {
525
                    throw new Exception("Institution identifiers are integers!");
526
                }
527
                $entity = $validator->IdP($identifier);
528
                $attributeName = "general:logo_file";
529
                break;
530
            default:
531
                throw new Exception("Unknown type of logo requested!");
532
        }
533
        $filetype = 'image/png'; // default, only one code path where it can become different
534
        if (($width || $height) && is_numeric($width) && is_numeric($height)) {
535
            $resize = TRUE;
536
            if ($height == 0) {
537
                $height = 10000;
538
            }
539
            if ($width == 0) {
540
                $width = 10000;
541
            }
542
            $logoFile = ROOT . '/web/downloads/logos/' . $identifier . '_' . $width . '_' . $height . '.png';
543
        }
544
        if ($resize === TRUE && is_file($logoFile)) {
545
            $this->loggerInstance->debug(4, "Using cached logo $logoFile for: $identifier\n");
546
            $blob = file_get_contents($logoFile);
547
        } else {
548
            $logoAttribute = $entity->getAttributes($attributeName);
549
            if (count($logoAttribute) == 0) {
550
                return(NULL);
551
            }
552
            $this->loggerInstance->debug(4,"RESIZE:$width:$height\n");
553
            $meta = $this->processImage($logoAttribute[0]['value'], $logoFile, $width, $height, $resize);
554
            $filetype = $meta['filetype'];
555
            $expiresString = $meta['expires'];
556
            $blob = $meta['blob'];
557
        }
558
        return ["filetype" => $filetype, "expires" => $expiresString, "blob" => $blob];
559
    }
560
   
561
    /**
562
     * outputs a logo
563
     * 
564
     * @param string|int $identifier
565
     * @param string $type "federation" or "idp"
566
     * @param int $width
567
     * @param int $height
568
     */
569
    public function sendLogo($identifier, $type, $width = 0, $height = 0) {
570
        $logo = $this->getLogo($identifier, $type, $width, $height);
571
        header("Content-type: " . $logo['filetype']);
572
        header("Cache-Control:max-age=36000, must-revalidate");
573
        header($logo['expires']);
574
        echo $logo['blob'];
575
    }
576
577
    /**
578
     * find out where the user is currently located
579
     * @return array
580
     */
581
    public function locateUser() {
582 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...
583
            return ['status' => 'error', 'error' => 'Function for GEOIPv1 called, but config says this is not the version to use!'];
584
        }
585
        //$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...
586
        $host = filter_input(INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP);
587
        $record = geoip_record_by_name($host);
588
        if ($record === FALSE) {
589
            return ['status' => 'error', 'error' => 'Problem listing countries'];
590
        }
591
        $result = ['status' => 'ok'];
592
        $result['country'] = $record['country_code'];
593
//  the two lines below are a dirty hack to take of the error in naming the UK federation
594
        if ($result['country'] == 'GB') {
595
            $result['country'] = 'UK';
596
        }
597
        $result['region'] = $record['region'];
598
        $result['geo'] = ['lat' => (float) $record['latitude'], 'lon' => (float) $record['longitude']];
599
        return($result);
600
    }
601
    
602
    /**
603
     * find out where the user is currently located, using GeoIP2
604
     * @return array
605
     */
606
    public function locateUser2() {
607 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...
608
            return ['status' => 'error', 'error' => 'Function for GEOIPv2 called, but config says this is not the version to use!'];
609
        }
610
        require_once CONFIG['GEOIP']['geoip2-path-to-autoloader'];
611
        $reader = new Reader(CONFIG['GEOIP']['geoip2-path-to-db']);
612
        $host = $_SERVER['REMOTE_ADDR'];
613
        try {
614
            $record = $reader->city($host);
615
        } catch (\Exception $e) {
616
            $result = ['status' => 'error', 'error' => 'Problem listing countries'];
617
            return($result);
618
        }
619
        $result = ['status' => 'ok'];
620
        $result['country'] = $record->country->isoCode;
621
//  the two lines below are a dirty hack to take of the error in naming the UK federation
622
        if ($result['country'] == 'GB') {
623
            $result['country'] = 'UK';
624
        }
625
        $result['region'] = $record->continent->name;
626
627
        $result['geo'] = ['lat' => (float) $record->location->latitude, 'lon' => (float) $record->location->longitude];
628
        return($result);
629
    }
630
631
    /**
632
     * outputs the user location as JSON
633
     * @throws Exception
634
     */
635
    public function JSON_locateUser() {
636
        header('Content-type: application/json; utf-8');
637
638
        $geoipVersion = CONFIG['GEOIP']['version'] ?? 0;
639
640
        switch ($geoipVersion) {
641
            case 0:
642
                echo json_encode(['status' => 'error', 'error' => 'Geolocation not supported']);
643
                break;
644
            case 1:
645
                echo json_encode($this->locateUser());
646
                break;
647
            case 2:
648
                echo json_encode($this->locateUser2());
649
                break;
650
            default:
651
                throw new Exception("This version of GeoIP is not known!");
652
        }
653
    }
654
655
    /**
656
     * outputs support data prepared within {@link GUI::profileAttributes()}
657
     */
658
    public function JSON_profileAttributes($prof_id) {
659
//    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...
660
        echo $this->return_json($this->profileAttributes($prof_id));
661
    }
662
663
    /**
664
     * Calculate the distance in km between two points given their
665
     * geo coordinates.
666
     * @param array $point1 - first point as an 'lat', 'lon' array 
667
     * @param array $profile1 - second point as an 'lat', 'lon' array 
668
     * @return float distance in km
669
     */
670
    private function geoDistance($point1, $profile1) {
671
672
        $distIntermediate = sin(deg2rad($point1['lat'])) * sin(deg2rad($profile1['lat'])) +
673
                cos(deg2rad($point1['lat'])) * cos(deg2rad($profile1['lat'])) * cos(deg2rad($point1['lon'] - $profile1['lon']));
674
        $dist = rad2deg(acos($distIntermediate)) * 60 * 1.1852;
675
        return(round($dist));
676
    }
677
678
    /**
679
     * Order active identity providers according to their distance and name
680
     * @param array $currentLocation - current location
681
     * @return array $IdPs -  list of arrays ('id', 'name');
682
     */
683
    public function orderIdentityProviders($country, $currentLocation = NULL) {
684
        $idps = $this->listAllIdentityProviders(1, $country);
685
686
        if (is_null($currentLocation)) {
687
            $currentLocation = ['lat' => "90", 'lon' => "0"];
688
            $userLocation = $this->locateUser();
689
            if ($userLocation['status'] == 'ok') {
690
                $currentLocation = $userLocation['geo'];
691
            }
692
        }
693
        $idpTitle = [];
694
        $resultSet = [];
695
        foreach ($idps as $idp) {
696
            $idpTitle[$idp['entityID']] = $idp['title'];
697
            $dist = 10000;
698
            if (isset($idp['geo'])) {
699
                $G = $idp['geo'];
700
                if (isset($G['lon'])) {
701
                    $d1 = $this->geoDistance($currentLocation, $G);
702
                    if ($d1 < $dist) {
703
                        $dist = $d1;
704
                    }
705
                } else {
706
                    foreach ($G as $g) {
707
                        $d1 = $this->geoDistance($currentLocation, $g);
708
                        if ($d1 < $dist) {
709
                            $dist = $d1;
710
                        }
711
                    }
712
                }
713
            }
714
            if ($dist > 100) {
715
                $dist = 10000;
716
            }
717
            $d = sprintf("%06d", $dist);
718
            $resultSet[$idp['entityID']] = $d . " " . $idp['title'];
719
        }
720
        asort($resultSet);
721
        $outarray = [];
722
        foreach (array_keys($resultSet) as $r) {
723
            $outarray[] = ['idp' => $r, 'title' => $idpTitle[$r]];
724
        }
725
        return($outarray);
726
    }
727
728
    /**
729
     * Detect the best device driver form the browser
730
     *
731
     * Detects the operating system and returns its id 
732
     * display name and group membership (as in devices.php)
733
     * @return array|false OS information, indexed by 'id', 'display', 'group'
734
     */
735
    public function detectOS() {
736
        $oldDomain = $this->languageInstance->setTextDomain("devices");
737
        $Dev = \devices\Devices::listDevices();
738
        $this->languageInstance->setTextDomain($oldDomain);
739
        $devId = $this->deviceFromRequest();
740
        if ($devId !== FALSE) {
741
            $ret = $this->returnDevice($devId, $Dev[$devId]);
742
            if ($ret !== FALSE) {
743
                return($ret);
744
            } 
745
        }
746
// the device has not been specified or not specified correctly, try to detect if from the browser ID
747
        $browser = filter_input(INPUT_SERVER, 'HTTP_USER_AGENT', FILTER_SANITIZE_STRING);
748
        $this->loggerInstance->debug(4, "HTTP_USER_AGENT=$browser\n");
749
        foreach ($Dev as $devId => $device) {
750
            if (!isset($device['match'])) {
751
                continue;
752
            }
753
            if (preg_match('/' . $device['match'] . '/', $browser)) {
754
                return ($this->returnDevice($devId, $device));
755
            }
756
        }
757
        $this->loggerInstance->debug(2, "Unrecognised system: $browser\n");
758
        return(false);
759
    }
760
    
761
    /*
762
     * test if devise is defined and is not hidden. If all is fine return extracted information.
763
     * Return FALSE if the device has not been correctly specified
764
     */
765
    private function returnDevice($devId, $device) {
766
        if (!isset($device['options']['hidden']) || $device['options']['hidden'] == 0) {
767
            $this->loggerInstance->debug(4, "Browser_id: $devId\n");
768
            return(['device' => $devId, 'display' => $device['display'], 'group' => $device['group']]);
769
        }
770
        return(FALSE);
771
    }
772
   
773
    /**
774
     * This methods cheks if the devide has been specified as the HTTP parameters
775
     * @return device id is correcty specified or FALSE otherwise
776
     */
777
    private function deviceFromRequest() {
778
        $devId = filter_input(INPUT_GET, 'device', FILTER_SANITIZE_STRING) ?? filter_input(INPUT_POST, 'device', FILTER_SANITIZE_STRING);
779
        if ($devId === NULL || $devId === FALSE) {
780
            $this->loggerInstance->debug(2, "Invalid device id provided\n");
781
            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...
782
        }
783
        $Dev = \devices\Devices::listDevices();
784
        if (!isset($Dev['$devId'])) {
785
            $this->loggerInstance->debug(2, "Unrecognised system: $devId\n");
786
            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...
787
        }
788
        return($devId);
789
    }
790
    
791
            
792
793
    /**
794
     * outputs OS guess in JSON
795
     */
796
    public function JSON_detectOS() {
797
        $returnArray = $this->detectOS();
798
        if (is_array($returnArray)) {
799
            $status = 1;
800
        } else {
801
            $status = 0;
802
        }
803
        echo $this->return_json($returnArray, $status);
804
    }
805
806
    /**
807
     * finds all the user certificates that originated in a given token
808
     * @param string $token
809
     * @return array|false returns FALSE if a token is invalid, otherwise array of certs
810
     */
811
    public function getUserCerts($token) {
812
        $validator = new \web\lib\common\InputValidation();
813
        $cleanToken = $validator->token($token);
814
        if ($cleanToken) {
815
            // check status of this silverbullet token according to info in DB:
816
            // it can be VALID (exists and not redeemed, EXPIRED, REDEEMED or INVALID (non existent)
817
            $tokenStatus = \core\ProfileSilverbullet::tokenStatus($cleanToken);
818
        } else {
819
            return false;
820
        }
821
        $profile = new \core\ProfileSilverbullet($tokenStatus['profile'], NULL);
822
        $userdata = $profile->userStatus($tokenStatus['db_id']);
823
        $allcerts = [];
824
        foreach ($userdata as $index => $content) {
825
            $allcerts = array_merge($allcerts, $content['cert_status']);
826
        }
827
        return $allcerts;
828
    }
829
830
    /**
831
     * outputs user certificates pertaining to a given token in JSON
832
     * @param string $token
833
     */
834
    public function JSON_getUserCerts($token) {
835
        $returnArray = $this->getUserCerts($token);
836
        if ($returnArray) {
837
            $status = 1;
838
        } else {
839
            $status = 0;
840
        }
841
        echo $this->return_json($returnArray, $status);
842
    }
843
844
    public $device;
845
    private $installerPath;
846
847
    /**
848
     * helper function to sort profiles by their name
849
     * @param \core\AbstractProfile $profile1 the first profile's information
850
     * @param \core\AbstractProfile $profile2 the second profile's information
851
     * @return int
852
     */
853
    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...
854
        return strcasecmp($profile1->name, $profile2->name);
855
    }
856
857
}
858