DeploymentManaged   F
last analyzed

Complexity

Total Complexity 112

Size/Duplication

Total Lines 916
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 112
eloc 442
c 3
b 1
f 0
dl 0
loc 916
rs 2

23 Methods

Rating   Name   Duplication   Size   Complexity  
F setRADIUSconfig() 0 54 18
A activate() 0 5 1
A deactivate() 0 5 1
A checkRADIUSHostandConfigDaemon() 0 16 5
F getRADIUSLogs() 0 78 20
A createTLScredentials() 0 25 1
A getOperatorName() 0 7 2
A updateFreshness() 0 4 1
A getFreshness() 0 7 2
A remove() 0 5 1
B sendMailtoAdmin() 0 31 8
B initialise() 0 37 6
B sendToRADIUS() 0 41 7
A tlsfromcsr() 0 34 1
A checkURL() 0 24 5
A setTLSSerialNumber() 0 11 3
B __construct() 0 78 9
A retrieveStatistics() 0 19 2
A getAllRealms() 0 18 2
A testRADIUSHost() 0 15 4
B findGoodServerLocation() 0 41 10
A renewtls() 0 5 1
A getLastActivity() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like DeploymentManaged often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DeploymentManaged, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
/**
24
 * This file contains the AbstractProfile class. It contains common methods for
25
 * both RADIUS/EAP profiles and SilverBullet profiles
26
 *
27
 * @author Stefan Winter <[email protected]>
28
 * @author Tomasz Wolniewicz <[email protected]>
29
 * @author Maja Górecka-Wolniewicz <[email protected]>
30
 *
31
 * @package Developer
32
 *
33
 */
34
35
namespace core;
36
37
use \Exception;
0 ignored issues
show
Bug introduced by
The type \Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
38
39
/**
40
 * This class represents an EAP Profile.
41
 * Profiles can inherit attributes from their IdP, if the IdP has some. Otherwise,
42
 * one can set attribute in the Profile directly. If there is a conflict between
43
 * IdP-wide and Profile-wide attributes, the more specific ones (i.e. Profile) win.
44
 * 
45
 * @author Stefan Winter <[email protected]>
46
 * @author Tomasz Wolniewicz <[email protected]>
47
 * @author Maja Gorecka-Wolniewicz <[email protected]>
48
 *
49
 * @license see LICENSE file in root directory
50
 *
51
 * @package Developer
52
 */
53
class DeploymentManaged extends AbstractDeployment
54
{
55
56
    /**
57
     * This is the limit for dual-stack hosts. Single stack uses half of the FDs
58
     * in FreeRADIUS and take twice as many. initialise() takes this into
59
     * account.
60
     */
61
    const MAX_CLIENTS_PER_SERVER = 200;
62
    const PRODUCTNAME = "Managed SP";
63
64
    /**
65
     * the primary RADIUS server port for this SP instance
66
     * 
67
     * @var integer
68
     */
69
    public $port1;
70
71
    /**
72
     * the backup RADIUS server port for this SP instance
73
     * 
74
     * @var integer
75
     */
76
    public $port2;
77
78
    /**
79
     * the shared secret for this SP instance
80
     * 
81
     * @var string
82
     */
83
    public $secret;
84
85
    /**
86
     * the IPv4 address of the primary RADIUS server for this SP instance 
87
     * (can be NULL)
88
     * 
89
     * @var string
90
     */
91
    public $host1_v4;
92
93
    /**
94
     * the IPv6 address of the primary RADIUS server for this SP instance 
95
     * (can be NULL)
96
     * 
97
     * @var string
98
     */
99
    public $host1_v6;
100
101
    /**
102
     * the IPv4 address of the backup RADIUS server for this SP instance 
103
     * (can be NULL)
104
     * 
105
     * @var string
106
     */
107
    public $host2_v4;
108
109
    /**
110
     * the IPv6 address of the backup RADIUS server for this SP instance 
111
     * (can be NULL)
112
     * 
113
     * @var string
114
     */
115
    public $host2_v6;
116
117
    /**
118
     * the primary RADIUS server instance for this SP instance
119
     * 
120
     * @var string
121
     */
122
    public $radius_instance_1;
123
124
    /**
125
     * the backup RADIUS server instance for this SP instance
126
     * 
127
     * @var string
128
     */
129
    public $radius_instance_2;
130
131
    /**
132
     * the primary RADIUS server hostname - for sending configuration requests
133
     * 
134
     * @var string
135
     */
136
    public $radius_hostname_1;
137
138
    /**
139
     * the backup RADIUS server hostname - for sending configuration requests
140
     * 
141
     * @var string
142
     */
143
    public $radius_hostname_2;
144
145
    /**
146
     * the primary RADIUS server status - last configuration request result
147
     * 
148
     * @var string
149
     */
150
    public $radius_status_1;
151
152
    /**
153
     * the backup RADIUS server status - last configuration request result
154
     * 
155
     * @var string
156
     */
157
    public $radius_status_2;
158
    
159
    /**
160
     * the client private key
161
     * 
162
     * @var string
163
     */
164
    public $radsec_priv;
165
    
166
    /**
167
     * the client certificate
168
     * 
169
     * @var string
170
     */
171
    public $radsec_cert; 
172
    
173
    /**
174
     * the client certificate srial number 
175
     * 
176
     * @var string
177
     */
178
    public $radsec_cert_serial_no;
179
    
180
    /**
181
     * the TLS-PSK key
182
     * 
183
     * @var string
184
     */
185
    public $pskkey;
186
    
187
    /**
188
     * the consortium this deployment is attached to
189
     * 
190
     * @var string
191
     */
192
    public $consortium;
193
194
    /**
195
     * the T&C of this service
196
     */
197
    public $termsAndConditions;
198
    
199
    public $server1_token;
200
    public $server2_token;
201
    public $server1_secret;
202
    public $server2_secret;
203
    public $server1_iv;
204
    public $server2_iv;
205
    
206
    
207
    /**
208
     * Class constructor for existing deployments (use 
209
     * IdP::newDeployment() to actually create one). Retrieves all 
210
     * attributes from the DB and stores them in the priv_ arrays.
211
     * 
212
     * @param IdP        $idpObject       optionally, the institution to which this Profile belongs. Saves the construction of the IdP instance. If omitted, an extra query and instantiation is executed to find out.
213
     * @param string|int $deploymentIdRaw identifier of the deployment in the DB
214
     * @param string     $consortium      identifier of the consortium to attach to (only relevant when initialising a new deployment for the first time
215
     * @throws Exception
216
     */
217
    public function __construct($idpObject, $deploymentIdRaw, $consortium = 'eduroam')
218
    {
219
        parent::__construct($idpObject, $deploymentIdRaw); // we now have access to our INST database handle and logging
220
        $this->entityOptionTable = "deployment_option";
221
        $this->entityIdColumn = "deployment_id";
222
        $this->type = AbstractDeployment::DEPLOYMENTTYPE_MANAGED;
223
        if (!is_numeric($deploymentIdRaw)) {
224
            throw new Exception("Managed SP instances have to have a numeric identifier");
225
        }
226
        $propertyQuery = "SELECT consortium,status,port_instance_1,port_instance_2,secret,radius_instance_1,radius_instance_2,radius_status_1,radius_status_2,radsec_priv,radsec_cert,pskkey FROM deployment WHERE deployment_id = ?";
227
        $queryExec = $this->databaseHandle->exec($propertyQuery, "i", $deploymentIdRaw);
228
        if (mysqli_num_rows(/** @scrutinizer ignore-type */ $queryExec) == 0) {
229
            throw new Exception("Attempt to construct an unknown DeploymentManaged!");
230
        }
231
        
232
        $this->identifier = $deploymentIdRaw;
233
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $queryExec)) {
234
            if ($iterator->secret == NULL && $iterator->radius_instance_1 == NULL) {
235
                // we are instantiated for the first time, or all previous init attempts failed - so initialise us
236
                // first time: note the consortium, permanently
237
                if ($iterator->consortium === NULL) {
238
                    $this->consortium = $consortium;
239
                    $consortiumQuery = "UPDATE deployment SET consortium = '$this->consortium' WHERE deployment_id = ?";
240
                    $this->databaseHandle->exec($consortiumQuery, "i", $deploymentIdRaw);
241
                }
242
                $details = $this->initialise();
243
                $this->port1 = $details["port_instance_1"];
244
                $this->port2 = $details["port_instance_2"];
245
                $this->secret = $details["secret"];
246
                $this->radius_instance_1 = $details["radius_instance_1"];
247
                $this->radius_instance_2 = $details["radius_instance_2"];
248
                $this->radius_status_1 = 1;
249
                $this->radius_status_2 = 1;
250
                $this->status = AbstractDeployment::INACTIVE;
251
            } else {
252
                $this->port1 = $iterator->port_instance_1;
253
                $this->port2 = $iterator->port_instance_2;
254
                $this->secret = $iterator->secret;
255
                $this->radius_instance_1 = $iterator->radius_instance_1;
256
                $this->radius_instance_2 = $iterator->radius_instance_2;
257
                $this->radius_status_1 = $iterator->radius_status_1;
258
                $this->radius_status_2 = $iterator->radius_status_2;
259
                $this->status = $iterator->status;
260
                $this->consortium = $iterator->consortium;
261
                $this->radsec_cert = $iterator->radsec_cert;
262
                $this->radsec_priv = $iterator->radsec_priv;
263
                $this->pskkey = $iterator->pskkey;
264
            }
265
        }
266
        $server1 = $this->radius_instance_1;
267
        $server1details = $this->databaseHandle->exec("SELECT mgmt_hostname, radius_ip4, radius_ip6, server_token, server_secret, server_iv FROM managed_sp_servers WHERE server_id = ?", "s", $server1);
268
        while ($iterator2 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server1details)) {
269
            $this->host1_v4 = $iterator2->radius_ip4;
270
            $this->host1_v6 = $iterator2->radius_ip6;
271
            $this->radius_hostname_1 = $iterator2->mgmt_hostname;
272
            $this->server1_token = $iterator2->server_token;
273
            $this->server1_secret = $iterator2->server_secret;
274
            $this->server1_iv = $iterator2->server_iv;
275
        }
276
        $server2 = $this->radius_instance_2;
277
        $server2details = $this->databaseHandle->exec("SELECT mgmt_hostname, radius_ip4, radius_ip6, server_token, server_secret, server_iv FROM managed_sp_servers WHERE server_id = ?", "s", $server2);
278
        while ($iterator3 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server2details)) {
279
            $this->host2_v4 = $iterator3->radius_ip4;
280
            $this->host2_v6 = $iterator3->radius_ip6;
281
            $this->radius_hostname_2 = $iterator3->mgmt_hostname;
282
            $this->server2_token = $iterator3->server_token;
283
            $this->server2_secret = $iterator3->server_secret;
284
            $this->server2_iv = $iterator3->server_iv;
285
        }
286
        $thisLevelAttributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row_id 
287
                                            FROM $this->entityOptionTable
288
                                            WHERE $this->entityIdColumn = ?  
289
                                            ORDER BY option_name", "Profile");
290
        $tempAttribMergedIdP = $this->levelPrecedenceAttributeJoin($thisLevelAttributes, $this->idpAttributes, "IdP");
291
        $this->attributes = $this->levelPrecedenceAttributeJoin($tempAttribMergedIdP, $this->fedAttributes, "FED");
292
        $tmpName = $this->getAttributes('managedsp:name');
0 ignored issues
show
Unused Code introduced by
The assignment to $tmpName is dead and can be removed.
Loading history...
293
        $this->name = $this->getAttributes('managedsp:name')[0]['value'] ?? "SP".$this->identifier."-".$this->institution;
294
        $this->termsAndConditions = "<h2>Product Definition</h2>
295
<p>eduroam Managed SP enables eligible institutions to outsource the technical setup of the roaming uplink functions in an eduroam SP to the eduroam Operations Team. eduroam SP administrators use the service instead of a local RADIUS infrastructure. A unique selling point of this service is that the typical limitation of being required to have a static IP address is waived - eduroam SP Wi-Fi infrastructure can be managed by the system even on IP connectivity with changing IP addresses (e.g. DSL, mobile networks).</p>
296
<p>The service includes:</p>
297
<ul><li>web-based user management interface where eduroam SP deployment details can be created and deleted;</li>
298
    <li>web-based institution management interface where institutions are enabled or disabled to use the service;</li>
299
    <li>technical infrastructure ('RADIUS') which accepts RADIUS/UDP requests independently of client IP addresses, processes them according to eduroam Service Definition best practices, and forwards them to eduroam IdPs via the established eduroam roaming infrastructure.</li>
300
</ul>
301
<p>The aspects of eduroam SP operation beyond the RADIUS uplink remain in the responsibility of the eduroam SP administrator, and are subject to the eduroam Service Definition as usual. This includes (but is not limited to) local logging of IP leases to MAC addresses in the Enterprise Wi-Fi session, having sufficient Wi-Fi coverage, and making sure the IP uplink works within expected parameters.</p>
302
<h2>Terms of Use</h2>
303
<p>In addition to the provisions in your participation agreement with your NRO and the eduroam Service Definition, you undertake to keep confidential the RADIUS server uplink details you receive here (which consist of the tuple IP address, port and shared secret).";
304
    }
305
306
    /**
307
     * finds a suitable server which is geographically close to the admin
308
     * 
309
     * @param array  $adminLocation      the current geographic position of the admin
310
     * @param string $federation         the federation this deployment belongs to
311
     * @param array  $blacklistedServers list of server to IGNORE
312
     * @return string the server ID
313
     * @throws Exception
314
     */
315
    private function findGoodServerLocation($adminLocation, $federation, $blacklistedServers)
316
    {
317
        // find a server near him (list of all servers with capacity, ordered by distance)
318
        // first, if there is a pool of servers specifically for this federation, prefer it
319
        // only check the consortium pool group we want to attach to
320
        $cons = $this->consortium;
321
        $servers = $this->databaseHandle->exec("SELECT server_id, radius_ip4, radius_ip6, location_lon, location_lat FROM managed_sp_servers WHERE pool = ? AND consortium = ?", "ss", $federation, $cons);
322
        $serverCandidates = [];
323
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $servers)) {
324
            $maxSupportedClients = DeploymentManaged::MAX_CLIENTS_PER_SERVER;
325
            if ($iterator->radius_ip4 == NULL || $iterator->radius_ip6 == NULL) {
326
                // half the amount of IP stacks means half the amount of FDs in use, so we can take twice as many
327
                $maxSupportedClients = $maxSupportedClients * 2;
328
            }
329
            $serverId = $iterator->server_id;
330
            $clientCount1 = $this->databaseHandle->exec("SELECT port_instance_1 AS tenants1 FROM deployment WHERE radius_instance_1 = ?", "s", $serverId);
331
            $clientCount2 = $this->databaseHandle->exec("SELECT port_instance_2 AS tenants2 FROM deployment WHERE radius_instance_2 = ?", "s", $serverId);
332
333
            $clients = $clientCount1->num_rows + $clientCount2->num_rows;
334
            if (in_array($iterator->server_id, $blacklistedServers)) {
335
                continue;
336
            }
337
            if ($clients < $maxSupportedClients) {
338
                $serverCandidates[IdPlist::geoDistance($adminLocation, ['lat' => $iterator->location_lat, 'lon' => $iterator->location_lon])] = $iterator->server_id;
339
            }
340
            if ($clients > $maxSupportedClients * 0.9) {
341
                $this->loggerInstance->debug(1, "A RADIUS server for Managed SP (" . $iterator->server_id . ") is serving at more than 90% capacity!");
342
            }
343
        }
344
        if (count($serverCandidates) == 0 && $federation != "DEFAULT") {
345
            // we look in the default pool instead
346
            // recursivity! Isn't that cool!
347
            return $this->findGoodServerLocation($adminLocation, "DEFAULT", $blacklistedServers);
348
        }
349
        if (count($serverCandidates) == 0) {
350
            throw new Exception("No available server found for new SP in $federation!");
351
        }
352
        // put the nearest server on top of the list
353
        ksort($serverCandidates);
354
        $this->loggerInstance->debug(1, $serverCandidates);
355
        return array_shift($serverCandidates);
356
    }
357
358
    /**
359
     * create unique serialNumber for a TLS client certificate
360
     * 
361
     * @throws Exception
362
     */
363
    private function setTLSSerialNumber($max=PHP_INT_MAX) {
364
        $nonDupSerialFound = FALSE;
365
        do {
366
            $serial = random_int(1000000000, $max);
367
            $dupeQuery = $this->databaseHandle->exec("SELECT radsec_cert_serial_number FROM deployment WHERE radsec_cert_serial_number = ?", "i", $serial);
368
            // SELECT -> resource, not boolean
369
            if (mysqli_num_rows(/** @scrutinizer ignore-type */$dupeQuery) == 0) {
370
                $nonDupSerialFound = TRUE;
371
            }
372
        } while (!$nonDupSerialFound);
373
        $this->radsec_cert_serial_no = $serial;
374
    }
375
    
376
    /**
377
     * create TLS credentials for client
378
     * 
379
     * @throws Exception
380
     */
381
    private function createTLScredentials()
382
    {
383
        $clientName = 'SP' . $this->identifier . '-' . $this->institution;
384
        $dn = array(
385
                    "organizationName" => "eduroam",
386
                    "organizationalUnitName" => "eduroam Managed SP",
387
                    "commonName" => $clientName
388
        );
389
        // Generate a new private (and public) key pair
390
        $privkey = openssl_pkey_new(array(
391
                                          "private_key_bits" => 4096,
392
                                          "private_key_type" => OPENSSL_KEYTYPE_RSA));
393
        // export private key to $clientprivateKey (as string)
394
        openssl_pkey_export($privkey, $this->radsec_priv);
395
        // Generate a certificate signing request
396
        $csr = openssl_csr_new($dn, $privkey,
0 ignored issues
show
Bug introduced by
It seems like $privkey can also be of type resource; however, parameter $private_key of openssl_csr_new() does only seem to accept OpenSSLAsymmetricKey, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

396
        $csr = openssl_csr_new($dn, /** @scrutinizer ignore-type */ $privkey,
Loading history...
397
                               array('digest_alg' => 'sha256', 'config' => ROOT . "/config/ManagedSPCerts/openssl.cnf"));
398
        // get CA certificate and private key
399
        $caprivkey = array(file_get_contents(ROOT . "/config/ManagedSPCerts/eduroamSP-CA.key"),
400
                            \config\Master::MANAGEDSP['capass']);
0 ignored issues
show
Bug introduced by
The constant config\Master::MANAGEDSP was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
401
        $cacert = file_get_contents(ROOT .  "/config/ManagedSPCerts/eduroamSP-CA.pem");
402
        $this->setTLSSerialNumber();
403
        $clientcert = openssl_csr_sign($csr, $cacert, $caprivkey, \config\Master::MANAGEDSP['daystoexpiry'],
404
                          array('digest_alg'=>'sha512', 'config' => ROOT . "/config/ManagedSPCerts/openssl.cnf"), $this->radsec_cert_serial_no);
0 ignored issues
show
Bug introduced by
$this->radsec_cert_serial_no of type string is incompatible with the type integer expected by parameter $serial of openssl_csr_sign(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

404
                          array('digest_alg'=>'sha512', 'config' => ROOT . "/config/ManagedSPCerts/openssl.cnf"), /** @scrutinizer ignore-type */ $this->radsec_cert_serial_no);
Loading history...
405
        openssl_x509_export($clientcert, $this->radsec_cert);
406
    } 
407
    /**
408
     * get last activity time for this deployment
409
     * 
410
     * @return string: activity_time or False
411
     * @throws Exception
412
     */
413
    public function getLastActivity()
414
    {
415
        $handle = DBConnection::handle('MSP_ACTIVITY');
416
        $activity = $handle->exec("SELECT activity_time FROM last_activity WHERE deployment_id = ?", "i", $this->identifier );
417
        if ($activity->num_rows == 0) {
418
            return False;
0 ignored issues
show
Bug Best Practice introduced by
The expression return False returns the type false which is incompatible with the documented return type string.
Loading history...
419
        }
420
        $la = mysqli_fetch_object(/** @scrutinizer ignore-type */ $activity);
421
        return $la->activity_time;
422
    }   
423
    
424
    /**
425
     * retrieves usage statistics for the deployment
426
     * 
427
     * @param int $backlog how many seconds back in time should we look
428
     * @param int $limit   max number of entries to retrieve (if supplied, backlog will be ignored)
429
     * @return array of arrays: activity_time, realm, mac, result
430
     * @throws Exception
431
     */
432
    public function retrieveStatistics(int $backlog, int $limit = 0)
433
    {
434
        // find a server near him (list of all servers with capacity, ordered by distance)
435
        // first, if there is a pool of servers specifically for this federation, prefer it
436
        // only check the consortium pool group we want to attach to
437
        // TODO: if we also collect stats from OpenRoaming hosts, differentiate the logs!
438
        $opName = $this->getOperatorName();
0 ignored issues
show
Unused Code introduced by
The assignment to $opName is dead and can be removed.
Loading history...
439
        $handle = DBConnection::handle('MSP_ACTIVITY');
440
        if ($limit !== 0) {
441
            $conditional1 = "";
442
            $conditional2 = "DESC LIMIT $limit";
443
        } else {
444
            $conditional1 = "AND activity_time > DATE_SUB(NOW(), INTERVAL $backlog SECOND )";
445
            $conditional2 = "DESC";
446
        }
447
        $client = 'SP' . $this->identifier . '-' . $this->institution;
448
        $stats = $handle->exec("SELECT activity_time, realm, mac, cui, result, ap_id, prot, outer_user FROM activity WHERE owner = ? $conditional1 ORDER BY activity_time $conditional2", "s", $client );
449
       
450
        return mysqli_fetch_all($stats, \MYSQLI_ASSOC);
451
    }
452
        
453
    /**
454
     * initialises a new SP
455
     * 
456
     * @return array details of the SP as generated during initialisation
457
     * @throws Exception
458
     */
459
    private function initialise()
460
    {
461
        // find out where the admin is located approximately
462
        $ourLocation = ['lon' => 0, 'lat' => 0];
463
        $geoip = DeviceLocation::locateDevice();
464
        if ($geoip['status'] == 'ok') {
465
            $ourLocation = ['lon' => $geoip['geo']['lon'], 'lat' => $geoip['geo']['lat']];
466
        }
467
        $inst = new IdP($this->institution);
468
        $ourserver = $this->findGoodServerLocation($ourLocation, $inst->federation, []);
469
        // now, find an unused port in the preferred server
470
        $foundFreePort1 = 0;
471
        while ($foundFreePort1 == 0) {
472
            $portCandidate = random_int(1200, 65535);
473
            $check = $this->databaseHandle->exec("SELECT port_instance_1 FROM deployment WHERE radius_instance_1 = ? AND port_instance_1 = ?", "si", $ourserver, $portCandidate);
474
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
475
                $foundFreePort1 = $portCandidate;
476
            }
477
        }
478
        $ourSecondServer = $this->findGoodServerLocation($ourLocation, $inst->federation, [$ourserver]);
479
        $foundFreePort2 = 0;
480
        while ($foundFreePort2 == 0) {
481
            $portCandidate = random_int(1200, 65535);
482
            $check = $this->databaseHandle->exec("SELECT port_instance_2 FROM deployment WHERE radius_instance_2 = ? AND port_instance_2 = ?", "si", $ourSecondServer, $portCandidate);
483
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
484
                $foundFreePort2 = $portCandidate;
485
            }
486
        }
487
        // and make up a shared secret that is halfways readable
488
        $futureSecret = trim(chunk_split(bin2hex(openssl_random_pseudo_bytes(14)), 4, '-'), '-');
489
        $this->createTLScredentials();
490
        $futurePSKkey = bin2hex(openssl_random_pseudo_bytes(32));
491
        $cons = $this->consortium;
492
        $id = $this->identifier;
493
        
494
        $this->databaseHandle->exec("UPDATE deployment SET radius_instance_1 = ?, radius_instance_2 = ?, port_instance_1 = ?, port_instance_2 = ?, secret = ?, radsec_priv = ?, radsec_cert = ?, radsec_cert_serial_number = ?, pskkey = ?, consortium = ? WHERE deployment_id = ?", "ssiisssissi", $ourserver, $ourSecondServer, $foundFreePort1, $foundFreePort2, $futureSecret, $this->radsec_priv, $this->radsec_cert, $this->radsec_cert_serial_no, $futurePSKkey, $cons, $id);
495
        return ["port_instance_1" => $foundFreePort1, "port_instance_2" => $foundFreePort2, "secret" => $futureSecret, "radius_instance_1" => $ourserver, "radius_instance_2" => $ourSecondServer, "pskkey" => $futurePSKkey];
496
    }
497
498
    /**
499
     * update the last_changed timestamp for this deployment
500
     * 
501
     * @return void
502
     */
503
    public function updateFreshness()
504
    {
505
        $id = $this->identifier;
506
        $this->databaseHandle->exec("UPDATE deployment SET last_change = CURRENT_TIMESTAMP WHERE deployment_id = ?", "i", $id);
507
    }
508
509
    /**
510
     * gets the last-modified timestamp (useful for caching "dirty" check)
511
     * 
512
     * @return string the date in string form, as returned by SQL
513
     */
514
    public function getFreshness()
515
    {
516
        $id = $this->identifier;
517
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM deployment WHERE deployment_id = ?", "i", $id);
518
        // SELECT always returns a resource, never a boolean
519
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
520
            return $freshnessQuery->last_change;
521
        }
522
    }
523
524
    /**
525
     * Deletes the deployment from database
526
     * 
527
     * @return void
528
     */
529
    public function remove()
530
    {
531
        $id = $this->identifier;
532
        $this->databaseHandle->exec("DELETE FROM deployment_option WHERE deployment_id = ?", "i", $id);
533
        $this->databaseHandle->exec("DELETE FROM deployment WHERE deployment_id = ?", "i", $id);
534
    }
535
    
536
    /**
537
     * Renews the deployment TLS credentials
538
     * 
539
     * @return void
540
     */
541
    public function renewtls()
542
    {
543
       $id = $this->identifier;
544
       $futureTlsClient = $this->createTLScredentials();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $futureTlsClient is correct as $this->createTLScredentials() targeting core\DeploymentManaged::createTLScredentials() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Unused Code introduced by
The assignment to $futureTlsClient is dead and can be removed.
Loading history...
545
       $this->databaseHandle->exec("UPDATE deployment SET radsec_priv = ?, radsec_cert = ?, radsec_cert_serial_number = ? WHERE deployment_id = ?", "ssii", $this->radsec_priv, $this->radsec_cert, $this->radsec_cert_serial_no, $id);           
546
    }
547
    
548
    /**
549
     * Create new deployment TLS credentials based on uploaded CSR
550
     * 
551
     * @return void
552
     */
553
    public function tlsfromcsr($csr)
554
    {
555
       $id = $this->identifier;
556
       $dn = array();
557
       $dn['rdnSequence'] = array();
558
       $dn['rdnSequence'][0] = array();
559
       $dn['rdnSequence'][0][] = array('type' => 'id-at-organizationName', 'value' => array());
560
       $dn['rdnSequence'][0][0]['value']['utf8String'] = 'eduroam';
561
       $dn['rdnSequence'][1] = array();
562
       $dn['rdnSequence'][1][] = array('type' => 'id-at-organizationalUnitName', 'value' => array());
563
       $dn['rdnSequence'][1][0]['value']['utf8String'] = 'eduroam Managed SP';
564
       $dn['rdnSequence'][2] = array();
565
       $dn['rdnSequence'][2][] = array('type' => 'id-at-commonName', 'value' => array());
566
       $dn['rdnSequence'][2][0]['value']['utf8String'] = 'SP' . $this->identifier . "-" . $this->institution;
567
       $csr->setDN($dn);
568
       $pemcakey = file_get_contents(ROOT . "/config/ManagedSPCerts/eduroamSP-CA.key");
569
       $cakey = \phpseclib3\Crypt\PublicKeyLoader::loadPrivateKey($pemcakey, \config\Master::MANAGEDSP['capass'] );
0 ignored issues
show
Bug introduced by
The constant config\Master::MANAGEDSP was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
570
       $pemca = file_get_contents(ROOT .  "/config/ManagedSPCerts/eduroamSP-CA.pem");
571
       $ca = new \phpseclib3\File\X509();
572
       $ca->loadX509($pemca);
573
       $ca->setPrivateKey($cakey);
574
       // Sign the updated request, producing the certificate.
575
       $x509 = new \phpseclib3\File\X509();
576
       $csr->setExtension('id-ce-keyUsage', ['digitalSignature', 'nonRepudiation', 'keyEncipherment']);
577
       $csr->setExtension('id-ce-extKeyUsage', ['id-kp-clientAuth']);
578
       $csr->setExtension('id-ce-basicConstraints', ['cA' => false], false);
579
       $x509->setEndDate('+' . \config\Master::MANAGEDSP['daystoexpiry'] . ' days');
580
       $this->setTLSSerialNumber(999999999999999999);
581
       $x509->setSerialNumber($this->radsec_cert_serial_no, 10);
582
       $cert = $x509->loadX509($x509->saveX509($x509->sign($ca, $csr)));
0 ignored issues
show
Bug introduced by
It seems like $x509->sign($ca, $csr) can also be of type false; however, parameter $cert of phpseclib3\File\X509::saveX509() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

582
       $cert = $x509->loadX509($x509->saveX509(/** @scrutinizer ignore-type */ $x509->sign($ca, $csr)));
Loading history...
583
       $this->radsec_cert = $x509->saveX509($cert);
584
       $this->radsec_priv = NULL;
585
       //$futureTlsClient = $this->createTLScredentials();
586
       $this->databaseHandle->exec("UPDATE deployment SET radsec_priv = NULL, radsec_cert = ?, radsec_cert_serial_number = ? WHERE deployment_id = ?", "sii", $this->radsec_cert, $this->radsec_cert_serial_no, $id);           
587
    }
588
    /**
589
     * marks the deployment as deactivated 
590
     * 
591
     * @return void
592
     */
593
    public function deactivate()
594
    {
595
        $id = $this->identifier;
596
        $inactive = DeploymentManaged::INACTIVE;
597
        $this->databaseHandle->exec("UPDATE deployment SET status = ? WHERE deployment_id = ?", "ii", $inactive, $id);
598
    }
599
    
600
    /**
601
     * marks the deployment as active.
602
     * 
603
     * @return void
604
     */
605
    public function activate()
606
    {
607
        $id = $this->identifier;
608
        $active = DeploymentManaged::ACTIVE;
609
        $this->databaseHandle->exec("UPDATE deployment SET status = ? WHERE deployment_id = ?", "ii", $active, $id);
610
    }
611
612
    /**
613
     * determines the Operator-Name attribute content to use in the RADIUS config
614
     * 
615
     * @return string
616
     */
617
    public function getOperatorName()
618
    {
619
        $customAttrib = $this->getAttributes("managedsp:operatorname");
620
        if (count($customAttrib) == 0) {
621
            return "1sp." . $this->identifier . "-" . $this->institution . \config\ConfAssistant::SILVERBULLET['realm_suffix'];
622
        }
623
        return $customAttrib[0]["value"];
624
    }
625
626
    /**
627
     * send request to RADIUS configuration daemom
628
     *
629
     * @param  integer $idx  server index 1 (primary) or 2 (backup)
630
     * @param  string  $post string to POST 
631
     * @return string  OK or FAILURE
632
     */
633
    private function sendToRADIUS(int $idx, $post)
634
    {
635
        $hostname = "radius_hostname_$idx";
636
        $p = "server$idx" . "_secret";
637
        $key = $this->$p;
638
        $p = "server$idx" . "_iv";
639
        $iv = $this->$p;
640
        $p = "server$idx" . "_token";
641
        $token = $this->$p;
642
        $encrypted = openssl_encrypt($post . "&token=$token", "CHACHA20", $key, 0, $iv);
643
        if ($encrypted !== false) {
644
            $post = "enc=". urlencode(base64_encode($encrypted));
645
        }
646
        $ch = curl_init("http://" . $this->$hostname . ':' . \config\Master::MANAGEDSP['radiusconfigport']);
0 ignored issues
show
Bug introduced by
The constant config\Master::MANAGEDSP was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
647
        if ($ch === FALSE) {
648
            $res = 'FAILURE';
649
        } else {
650
            curl_setopt($ch, CURLOPT_USERAGENT, "CAT-ManagedSP");
651
            curl_setopt($ch, CURLOPT_POST, 1);
652
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
653
            $this->loggerInstance->debug(1, "Posting to http://" . $this->$hostname . ':' . \config\Master::MANAGEDSP['radiusconfigport'] . "/$post\n");
654
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
655
            curl_setopt($ch, CURLOPT_HEADER, 0);
656
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
657
            $exec = curl_exec($ch);
658
            if (!is_string($exec)) {
659
                $this->loggerInstance->debug(1, "curl_exec failure");
660
                $res = 'FAILURE';
661
            } else {
662
                $res = $exec;
663
            }
664
            curl_close($ch);
665
            $this->loggerInstance->debug(1, "Response from FR configurator: $res\n");
666
            $this->loggerInstance->debug(1, $this);
667
        }
668
        $id = $this->identifier;
669
        if ($res == 'OK' || $res == 'FAILURE') {
670
            $resValue = ($res == 'OK' ? \core\AbstractDeployment::RADIUS_OK : \core\AbstractDeployment::RADIUS_FAILURE);
671
            $this->databaseHandle->exec("UPDATE deployment SET radius_status_$idx = ? WHERE deployment_id = ?", "ii", $resValue, $id);
672
        }
673
        return $res;
674
    }
675
676
    /**
677
     * prepare and send email message to support mail
678
     *
679
     * @param  int    $remove   the flag indicating remove request
680
     * @param  array  $response setRADIUSconfig result
681
     * @param  string $status   the flag indicating status (FAILURE or OK)
682
     * @return void
683
     * 
684
     */
685
    private function sendMailtoAdmin($remove, $response, $status)
686
    {
687
        $txt = '';
688
        if ($status == 'OK') {
689
            $txt = $remove ? _("Profile deactivation succeeded") : _("Profile activation/modification succeeded");
690
        } else {
691
            $txt = $remove ? _("Profile deactivation failed") : _("Profile activation/modification failed");
692
        }
693
        $txt = $txt . ' ';
694
        if (array_count_values($response)[$status] == 2) {
695
            $txt = $txt . _("on both RADIUS servers: primary and backup") . '.';
696
        } else {
697
            if ($response['res[1]'] == $status) {
698
                $txt = $txt . _("on primary RADIUS server") . '.';
699
            } else {
700
                $txt = $txt . _("on backup RADIUS server") . '.';
701
            }
702
        }
703
        $mail = \core\common\OutsideComm::mailHandle();
704
        $email = $this->getAttributes("support:email")[0]['value'];
705
        $mail->FromName = \config\Master::APPEARANCE['productname'] . " Notification System";
706
        $mail->addAddress($email);
707
        if ($status == 'OK') {
708
            $mail->Subject = _("RADIUS profile update problem fixed");
709
        } else {
710
            $mail->Subject = _("RADIUS profile update problem");
711
        }
712
        $mail->Body = $txt;
713
        $sent = $mail->send();
714
        if ($sent === FALSE) {
715
            $this->loggerInstance->debug(1, 'Mailing on RADIUS problem failed');
716
        }
717
    }
718
719
    /**
720
     * check if URL responds with 200
721
     *
722
     * @param integer $idx server index 1 (primary) or 2 (backup)
723
     * @return integer or NULL
724
     */
725
    private function checkURL($idx)
726
    {
727
        $ch = curl_init();
728
        if ($ch === FALSE) {
729
            return NULL;
730
        }
731
        if ($idx == 1) {
732
            $host = $this->radius_hostname_1;
733
        } elseif ($idx == 2) {
734
            $host = $this->radius_hostname_2;
735
        } else {
736
            return NULL;
737
        }
738
        $timeout = 10;
739
        curl_setopt($ch, CURLOPT_URL, 'http://' . $host);
740
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
741
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
742
        curl_exec($ch);
743
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
744
        curl_close($ch);
745
        if ($http_code == 200) {
746
            return 1;
747
        }
748
        return 0;
749
    }
750
751
    /**
752
     * check whether the configured RADIUS hosts actually exist
753
     * 
754
     * @param integer $idx server index 1 (primary) or 2 (backup)
755
     * @return integer or NULL
756
     */
757
    private function testRADIUSHost($idx)
758
    {
759
        if ($idx == 1) {
760
            $host = $this->radius_hostname_1;
761
        } elseif ($idx == 2) {
762
            $host = $this->radius_hostname_2;
763
        } else {
764
            return NULL;
765
        }
766
        $statusServer = new diag\RFC5997Tests($host, \config\Diagnostics::RADIUSSPTEST['port'], \config\Diagnostics::RADIUSSPTEST['secret']);
767
        $this->loggerInstance->debug(1, $statusServer);
768
        if ($statusServer->statusServerCheck() === diag\AbstractTest::RETVAL_OK) {
769
            return 1;
770
        }
771
        return 0;
772
    }
773
774
    /**
775
     * get institution realms
776
     * 
777
     * @return array of strings
778
     */
779
    private function getAllRealms()
780
    {
781
        $idp = new IdP($this->institution);
782
        $allProfiles = $idp->listProfiles(TRUE);
0 ignored issues
show
Unused Code introduced by
The assignment to $allProfiles is dead and can be removed.
Loading history...
783
        $allRealms = [];
784
        if (($this->getAttributes("managedsp:realmforvlan") ?? NULL)) {
785
            $allRealms = array_values(array_unique(array_column($this->getAttributes("managedsp:realmforvlan"), "value")));
786
        }
787
        /*
788
        foreach ($allProfiles as $profile) {
789
            if ($realm = ($profile->getAttributes("internal:realm")[0]['value'] ?? NULL)) {
790
                if (!in_array($realm, $allRealms)) {
791
                    $allRealms[] = $realm;
792
                }
793
            }
794
        }
795
        */
796
        return $allRealms;
797
    }
798
799
    /**
800
     * check if RADIUS configuration daemon is listening for requests
801
     *
802
     * @return array index res[1] indicate primary RADIUS status, index res[2] backup RADIUS status
803
     */
804
    public function checkRADIUSHostandConfigDaemon()
805
    {
806
        $res = array();
807
        if ($this->radius_status_1 == \core\AbstractDeployment::RADIUS_FAILURE) {
808
            $res[1] = $this->checkURL(1);
809
            if ($res[1]) {
810
                $res[1] = $this->testRADIUSHost(1);
811
            }
812
        }
813
        if ($this->radius_status_2 == \core\AbstractDeployment::RADIUS_FAILURE) {
814
            $res[2] = $this->checkURL(2);
815
            if ($res[2]) {
816
                $res[2] = $this->testRADIUSHost(2);
817
            }
818
        }
819
        return $res;
820
    }
821
822
    /**
823
     * prepare request to add/modify RADIUS settings for given deployment
824
     *
825
     * @param int $onlyone the flag indicating on which server to conduct modifications
826
     * @param int $notify  the flag indicating that an email notification should be sent
827
     * @return array index res[1] indicate primary RADIUS status, index res[2] backup RADIUS status
828
     */
829
    public function setRADIUSconfig($onlyone = 0, $notify = 0, $torevoke = "")
830
    {
831
        $toPost = ($onlyone ? array($onlyone => '') : array(1 => '', 2 => ''));
832
        if ($torevoke != '') {
833
            $toPostTemplate = 'instid=' . $this->institution . '&deploymentid=' . $this->identifier .
834
                    "&torevoke=$torevoke";
835
            foreach (array_keys($toPost) as $key) {
836
                $toPost[$key] = $toPostTemplate;
837
            }
838
        } else {
839
            $remove = ($this->status == \core\AbstractDeployment::INACTIVE) ? 0 : 1;
840
            $toPostTemplate = 'instid=' . $this->institution . '&deploymentid=' . $this->identifier . 
841
                '&secret=' . $this->secret .
842
                '&country=' . $this->getAttributes("internal:country")[0]['value'] .
843
                '&pskkey=' . $this->pskkey . '&';
844
            if ($remove) {
845
                $toPostTemplate = $toPostTemplate . 'remove=1&';
846
            } else {
847
                $toPostTemplate = $toPostTemplate . 'operatorname=' . $this->getOperatorName() . '&'; 
848
                if ($this->getAttributes("managedsp:vlan")[0]['value'] ?? NULL) {
849
                    $allRealms = $this->getAllRealms();
850
                    if (!empty($allRealms)) {
851
                        $toPostTemplate = $toPostTemplate . 'vlan=' . $this->getAttributes("managedsp:vlan")[0]['value'] . '&';
852
                        $toPostTemplate = $toPostTemplate . 'realmforvlan[]=' . implode('&realmforvlan[]=', $allRealms) . '&';
853
                    }
854
                }
855
                if ($this->getAttributes("managedsp:guest_vlan")[0]['value'] ?? NULL) {
856
                    $toPostTemplate = $toPostTemplate . 'guest_vlan=' . $this->getAttributes("managedsp:guest_vlan")[0]['value'] . '&';
857
                }
858
            }
859
            foreach (array_keys($toPost) as $key) {
860
                $elem = 'port' . $key;
861
                $toPost[$key] = $toPostTemplate . 'port=' . $this->$elem;
862
            }
863
        }
864
        $response = array();
865
        foreach ($toPost as $key => $value) {
866
            $this->loggerInstance->debug(1, 'toPost ' . $toPost[$key] . "\n");
867
            // temporarly one server $response['res[' . $key . ']'] = $this->sendToRADIUS($key, $toPost[$key]);
868
            /*if ($key == 2) {
869
                $response['res[2]'] = 'OK'; 
870
            } else { */
871
            $response['res[' . $key . ']'] = $this->sendToRADIUS($key, $toPost[$key]);
872
            //}
873
        }
874
        if ($onlyone) {
875
            $response['res[' . ($onlyone == 1) ? 2 : 1 . ']'] = \core\AbstractDeployment::RADIUS_OK;
876
        }
877
        foreach (array('OK', 'FAILURE') as $status) {
878
            if ( ( ($status == 'OK' && $notify) || ($status == 'FAILURE') ) && ( in_array($status, $response) ) ) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($status == 'OK' && $not...ray($status, $response), Probably Intended Meaning: $status == 'OK' && $noti...ay($status, $response))
Loading history...
879
                $this->sendMailtoAdmin($remove, $response, $status);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $remove does not seem to be defined for all execution paths leading up to this point.
Loading history...
880
            }
881
        }
882
        return $response;
883
    }
884
    /**
885
     * prepare request to get RADIUS logs for given deployment
886
     * @param int $onlyone the flag indicating on which server to conduct modifications
887
     *
888
     * 
889
     * @return array index res[1] indicate primary RADIUS status, index res[2] backup RADIUS status
890
     */
891
    public function getRADIUSLogs($onlyone = 0, $logs = 0)
892
    {
893
        $toPost = ($onlyone ? array($onlyone => '') : array(1 => '', 2 => ''));
894
        $randomiv = "";
895
        if ($logs) {
896
            $randomiv = bin2hex(random_bytes(8));
897
            $toPostTemplate = 'logid=DEBUG-' . $this->identifier . '-' .$this->institution . "&backlog=$logs&iv=$randomiv";
898
            foreach (array_keys($toPost) as $key) {
899
                $toPost[$key] = $toPostTemplate;
900
            }
901
        }
902
        $response = array();
903
        $tempdir = \core\common\Entity::createTemporaryDirectory("test");
904
        $zipdir = $tempdir['dir'];
905
        foreach ($toPost as $key => $value) {
906
            $this->loggerInstance->debug(1, 'toPost ' . $toPost[$key] . "\n");
907
            $p = "server$key" . "_secret";
908
            $secret = $this->$p;
909
            $p = "server$key" . "_token";
910
            $token = $this->$p;
911
            $response['res[' . $key . ']'] = $this->sendToRADIUS($key, $toPost[$key]);
912
            $paths = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $paths is dead and can be removed.
Loading history...
913
            if (substr($response['res[' . $key . ']'], 0, 8) == 'ZIPDATA:' && $randomiv != '') {
914
                $encrypted = substr($response['res[' . $key . ']'], 8);
915
                $data = openssl_decrypt($encrypted, "CHACHA20", $secret, 0, $randomiv);
916
                if ($data !== false && substr($data, 0, strlen($token)) == $token) {
917
                    $data = substr($data, strlen($token));
918
                }
919
                if (!file_exists("$zipdir/$key")) {
920
                    mkdir("$zipdir/$key", 0755, true );
921
                }
922
                $fileHandle = fopen("$zipdir/$key/detail.zip", "wb");
923
                fwrite($fileHandle, $data);
924
                fclose($fileHandle);
925
            }
926
        }
927
        $zipt = new \ZipArchive;
928
        $zipt->open("$zipdir/detail-" . $this->identifier . '-' .$this->institution . '.zip', \ZipArchive::CREATE);
929
        $cnt = 0;
930
        foreach ($toPost as $key => $value) {
931
            if (file_exists("$zipdir/$key/detail.zip")) {
932
                $zipf = new \ZipArchive;
933
                $zipf->open("$zipdir/$key/detail.zip");
934
                if ($zipf->numFiles > 0) {
935
                    $zipf->extractTo("$zipdir/$key/");
936
                }
937
                $zipf->close();
938
                unlink("$zipdir/$key/detail.zip");
939
                $files = scandir("$zipdir/$key/");
940
                foreach($files as $file) {
941
                  if ($file == '.' || $file == '..') continue;
942
                  $data = file_get_contents("$zipdir/$key/$file");
943
                  $zipt->addFromString("radius-$key/$file", $data);
944
                  $cnt += 1;
945
                  unlink("$zipdir/$key/$file");
946
                } 
947
                if (file_exists("$zipdir/$key")) {
948
                    rmdir("$zipdir/$key");
949
                }
950
            }
951
        }
952
        if ($cnt == 0) {
953
            $zipt->addEmptyDir('.');
954
        }
955
        $zipt->close();
956
        if (file_exists("$zipdir/detail-" . $this->identifier . '-' .$this->institution . '.zip')) {
957
            $data = file_get_contents("$zipdir/detail-" . $this->identifier . '-' .$this->institution . '.zip');
958
            unlink("$zipdir/detail-" . $this->identifier . '-' .$this->institution . '.zip'); 
959
            rmdir($zipdir);
960
        }     
961
        if ($data !== FALSE) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data does not seem to be defined for all execution paths leading up to this point.
Loading history...
962
            header('Content-Type: application/zip');
963
            header("Content-Disposition: attachment; filename=\"detail-".$this->identifier . '-' .$this->institution.".zip\"");
964
            header("Content-Transfer-Encoding: binary");
965
            echo $data;
966
        } 
967
        
968
        return;
969
    }
970
}
971