Completed
Push — master ( 4ad6bd...3873e4 )
by Ingo
11:53
created

InstallRequirements::check()   F

Complexity

Conditions 10
Paths 288

Size

Total Lines 276
Code Lines 183

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 183
nc 288
nop 0
dl 0
loc 276
rs 3.1304
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
namespace SilverStripe\Dev\Install;
4
5
use Exception;
6
use InvalidArgumentException;
7
use SilverStripe\Core\TempFolder;
8
9
/**
10
 * This class checks requirements
11
 * Each of the requireXXX functions takes an argument which gives a user description of the test.
12
 * It's an array of 3 parts:
13
 *  $description[0] - The test catetgory
14
 *  $description[1] - The test title
15
 *  $description[2] - The test error to show, if it goes wrong
16
 */
17
class InstallRequirements
18
{
19
    protected $errors = [];
20
    protected $warnings = [];
21
    protected $tests = [];
22
23
    /**
24
     * Backup of original ini settings
25
     * @var array
26
     */
27
    protected $originalIni = [];
28
29
    /**
30
     * Check the database configuration. These are done one after another
31
     * starting with checking the database function exists in PHP, and
32
     * continuing onto more difficult checks like database permissions.
33
     *
34
     * @param array $databaseConfig The list of database parameters
35
     * @return boolean Validity of database configuration details
36
     */
37
    public function checkDatabase($databaseConfig)
38
    {
39
        // Check if support is available
40
        if (!$this->requireDatabaseFunctions(
41
            $databaseConfig,
42
            array(
43
                "Database Configuration",
44
                "Database support",
45
                "Database support in PHP",
46
                $this->getDatabaseTypeNice($databaseConfig['type'])
47
            )
48
        )
49
        ) {
50
            return false;
51
        }
52
53
        // Check if the server is available
54
        $usePath = !empty($databaseConfig['path']) && empty($databaseConfig['server']);
55
        if (!$this->requireDatabaseServer(
56
            $databaseConfig,
57
            array(
58
                "Database Configuration",
59
                "Database server",
60
                $usePath
61
                    ? "I couldn't write to path '$databaseConfig[path]'"
62
                    : "I couldn't find a database server on '$databaseConfig[server]'",
63
                $usePath ? $databaseConfig['path'] : $databaseConfig['server']
64
            )
65
        )
66
        ) {
67
            return false;
68
        }
69
70
        // Check if the connection credentials allow access to the server / database
71
        if (!$this->requireDatabaseConnection(
72
            $databaseConfig,
73
            array(
74
                "Database Configuration",
75
                "Database access credentials",
76
                "That username/password doesn't work"
77
            )
78
        )
79
        ) {
80
            return false;
81
        }
82
83
        // Check the necessary server version is available
84
        if (!$this->requireDatabaseVersion(
85
            $databaseConfig,
86
            array(
87
                "Database Configuration",
88
                "Database server version requirement",
89
                '',
90
                'Version ' . $this->getDatabaseConfigurationHelper($databaseConfig['type'])->getDatabaseVersion($databaseConfig)
91
            )
92
        )
93
        ) {
94
            return false;
95
        }
96
97
        // Check that database creation permissions are available
98
        if (!$this->requireDatabaseOrCreatePermissions(
99
            $databaseConfig,
100
            array(
101
                "Database Configuration",
102
                "Can I access/create the database",
103
                "I can't create new databases and the database '$databaseConfig[database]' doesn't exist"
104
            )
105
        )
106
        ) {
107
            return false;
108
        }
109
110
        // Check alter permission (necessary to create tables etc)
111
        if (!$this->requireDatabaseAlterPermissions(
112
            $databaseConfig,
113
            array(
114
                "Database Configuration",
115
                "Can I ALTER tables",
116
                "I don't have permission to ALTER tables"
117
            )
118
        )
119
        ) {
120
            return false;
121
        }
122
123
        // Success!
124
        return true;
125
    }
126
127
    public function checkAdminConfig($adminConfig)
128
    {
129
        if (!$adminConfig['username']) {
130
            $this->error(array('', 'Please enter a username!'));
131
        }
132
        if (!$adminConfig['password']) {
133
            $this->error(array('', 'Please enter a password!'));
134
        }
135
    }
136
137
    /**
138
     * Check if the web server is IIS and version greater than the given version.
139
     *
140
     * @param int $fromVersion
141
     * @return bool
142
     */
143
    public function isIIS($fromVersion = 7)
144
    {
145
        if (strpos($this->findWebserver(), 'IIS/') === false) {
146
            return false;
147
        }
148
        return substr(strstr($this->findWebserver(), '/'), -3, 1) >= $fromVersion;
149
    }
150
151
    public function isApache()
152
    {
153
        if (strpos($this->findWebserver(), 'Apache') !== false) {
154
            return true;
155
        } else {
156
            return false;
157
        }
158
    }
159
160
    /**
161
     * Find the webserver software running on the PHP host.
162
     * @return string|boolean Server software or boolean FALSE
163
     */
164
    public function findWebserver()
0 ignored issues
show
Coding Style introduced by
findWebserver uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
165
    {
166
        // Try finding from SERVER_SIGNATURE or SERVER_SOFTWARE
167
        if (!empty($_SERVER['SERVER_SIGNATURE'])) {
168
            $webserver = $_SERVER['SERVER_SIGNATURE'];
169
        } elseif (!empty($_SERVER['SERVER_SOFTWARE'])) {
170
            $webserver = $_SERVER['SERVER_SOFTWARE'];
171
        } else {
172
            return false;
173
        }
174
175
        return strip_tags(trim($webserver));
176
    }
177
178
    /**
179
     * Check everything except the database
180
     */
181
    public function check()
182
    {
183
        $this->errors = [];
184
        $isApache = $this->isApache();
185
        $isIIS = $this->isIIS();
186
        $webserver = $this->findWebserver();
187
188
        $this->requirePHPVersion('5.5.0', '5.5.0', array(
189
            "PHP Configuration",
190
            "PHP5 installed",
191
            null,
192
            "PHP version " . phpversion()
193
        ));
194
195
        // Check that we can identify the root folder successfully
196
        $this->requireFile('framework/src/Dev/Install/config-form.html', array(
197
            "File permissions",
198
            "Does the webserver know where files are stored?",
199
            "The webserver isn't letting me identify where files are stored.",
200
            $this->getBaseDir()
201
        ));
202
203
        $this->requireModule('mysite', array(
204
            "File permissions",
205
            "mysite/ directory exists?",
206
            ''
207
        ));
208
        $this->requireModule('framework', array(
209
            "File permissions",
210
            "framework/ directory exists?",
211
            '',
212
        ));
213
214
        if ($isApache) {
215
            $this->checkApacheVersion(array(
216
                "Webserver Configuration",
217
                "Webserver is not Apache 1.x",
218
                "SilverStripe requires Apache version 2 or greater",
219
                $webserver
220
            ));
221
            $this->requireWriteable('.htaccess', array("File permissions", "Is the .htaccess file writeable?", null));
222
        } elseif ($isIIS) {
223
            $this->requireWriteable('web.config', array("File permissions", "Is the web.config file writeable?", null));
224
        }
225
226
        $this->requireWriteable('mysite/_config.php', array(
227
            "File permissions",
228
            "Is the mysite/_config.php file writeable?",
229
            null
230
        ));
231
232
        $this->requireWriteable('mysite/_config/config.yml', array(
233
            "File permissions",
234
            "Is the mysite/_config/config.yml file writeable?",
235
            null
236
        ));
237
238
        if (!$this->checkModuleExists('cms')) {
239
            $this->requireWriteable('mysite/code/RootURLController.php', array(
240
                "File permissions",
241
                "Is the mysite/code/RootURLController.php file writeable?",
242
                null
243
            ));
244
        }
245
        $this->requireWriteable('assets', array("File permissions", "Is the assets/ directory writeable?", null));
246
247
        try {
248
            $tempFolder = TempFolder::getTempFolder(BASE_PATH);
249
        } catch (Exception $e) {
250
            $tempFolder = false;
251
        }
252
253
        $this->requireTempFolder(array('File permissions', 'Is a temporary directory available?', null, $tempFolder));
254
        if ($tempFolder) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tempFolder of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
255
            // in addition to the temp folder being available, check it is writable
256
            $this->requireWriteable($tempFolder, array(
257
                "File permissions",
258
                sprintf("Is the temporary directory writeable?", $tempFolder),
259
                null
260
            ), true);
261
        }
262
263
        // Check for web server, unless we're calling the installer from the command-line
264
        $this->isRunningWebServer(array("Webserver Configuration", "Server software", "Unknown", $webserver));
265
266
        if ($isApache) {
267
            $this->requireApacheRewriteModule('mod_rewrite', array(
268
                "Webserver Configuration",
269
                "URL rewriting support",
270
                "You need mod_rewrite to use friendly URLs with SilverStripe, but it is not enabled."
271
            ));
272
        } elseif ($isIIS) {
273
            $this->requireIISRewriteModule('IIS_UrlRewriteModule', array(
274
                "Webserver Configuration",
275
                "URL rewriting support",
276
                "You need to enable the IIS URL Rewrite Module to use friendly URLs with SilverStripe, "
277
                . "but it is not installed or enabled. Download it for IIS 7 from http://www.iis.net/expand/URLRewrite"
278
            ));
279
        } else {
280
            $this->warning(array(
281
                "Webserver Configuration",
282
                "URL rewriting support",
283
                "I can't tell whether any rewriting module is running.  You may need to configure a rewriting rule yourself."
284
            ));
285
        }
286
287
        $this->requireServerVariables(array('SCRIPT_NAME', 'HTTP_HOST', 'SCRIPT_FILENAME'), array(
288
            "Webserver Configuration",
289
            "Recognised webserver",
290
            "You seem to be using an unsupported webserver.  "
291
            . "The server variables SCRIPT_NAME, HTTP_HOST, SCRIPT_FILENAME need to be set."
292
        ));
293
294
        $this->requirePostSupport(array(
295
            "Webserver Configuration",
296
            "POST Support",
297
            'I can\'t find $_POST, make sure POST is enabled.'
298
        ));
299
300
        // Check for GD support
301
        if (!$this->requireFunction("imagecreatetruecolor", array(
302
            "PHP Configuration",
303
            "GD2 support",
304
            "PHP must have GD version 2."
305
        ))
306
        ) {
307
            $this->requireFunction("imagecreate", array(
308
                "PHP Configuration",
309
                "GD2 support",
310
                "GD support for PHP not included."
311
            ));
312
        }
313
314
        // Check for XML support
315
        $this->requireFunction('xml_set_object', array(
316
            "PHP Configuration",
317
            "XML support",
318
            "XML support not included in PHP."
319
        ));
320
        $this->requireClass('DOMDocument', array(
321
            "PHP Configuration",
322
            "DOM/XML support",
323
            "DOM/XML support not included in PHP."
324
        ));
325
        $this->requireFunction('simplexml_load_file', array(
326
            'PHP Configuration',
327
            'SimpleXML support',
328
            'SimpleXML support not included in PHP.'
329
        ));
330
331
        // Check for token_get_all
332
        $this->requireFunction('token_get_all', array(
333
            "PHP Configuration",
334
            "Tokenizer support",
335
            "Tokenizer support not included in PHP."
336
        ));
337
338
        // Check for CType support
339
        $this->requireFunction('ctype_digit', array(
340
            'PHP Configuration',
341
            'CType support',
342
            'CType support not included in PHP.'
343
        ));
344
345
        // Check for session support
346
        $this->requireFunction('session_start', array(
347
            'PHP Configuration',
348
            'Session support',
349
            'Session support not included in PHP.'
350
        ));
351
352
        // Check for iconv support
353
        $this->requireFunction('iconv', array(
354
            'PHP Configuration',
355
            'iconv support',
356
            'iconv support not included in PHP.'
357
        ));
358
359
        // Check for hash support
360
        $this->requireFunction('hash', array('PHP Configuration', 'hash support', 'hash support not included in PHP.'));
361
362
        // Check for mbstring support
363
        $this->requireFunction('mb_internal_encoding', array(
364
            'PHP Configuration',
365
            'mbstring support',
366
            'mbstring support not included in PHP.'
367
        ));
368
369
        // Check for Reflection support
370
        $this->requireClass('ReflectionClass', array(
371
            'PHP Configuration',
372
            'Reflection support',
373
            'Reflection support not included in PHP.'
374
        ));
375
376
        // Check for Standard PHP Library (SPL) support
377
        $this->requireFunction('spl_classes', array(
378
            'PHP Configuration',
379
            'SPL support',
380
            'Standard PHP Library (SPL) not included in PHP.'
381
        ));
382
383
        $this->requireDateTimezone(array(
384
            'PHP Configuration',
385
            'date.timezone setting and validity',
386
            'date.timezone option in php.ini must be set correctly.',
387
            $this->getOriginalIni('date.timezone')
388
        ));
389
390
        $this->suggestClass('finfo', array(
391
            'PHP Configuration',
392
            'fileinfo support',
393
            'fileinfo should be enabled in PHP. SilverStripe uses it for MIME type detection of files. '
394
            . 'SilverStripe will still operate, but email attachments and sending files to browser '
395
            . '(e.g. export data to CSV) may not work correctly without finfo.'
396
        ));
397
398
        $this->suggestFunction('curl_init', array(
399
            'PHP Configuration',
400
            'curl support',
401
            'curl should be enabled in PHP. SilverStripe uses it for consuming web services'
402
            . ' via the RestfulService class and many modules rely on it.'
403
        ));
404
405
        $this->suggestClass('tidy', array(
406
            'PHP Configuration',
407
            'tidy support',
408
            'Tidy provides a library of code to clean up your html. '
409
            . 'SilverStripe will operate fine without tidy but HTMLCleaner will not be effective.'
410
        ));
411
412
        $this->suggestPHPSetting('asp_tags', array(false), array(
413
            'PHP Configuration',
414
            'asp_tags option',
415
            'This should be turned off as it can cause issues with SilverStripe'
416
        ));
417
        $this->requirePHPSetting('magic_quotes_gpc', array(false), array(
418
            'PHP Configuration',
419
            'magic_quotes_gpc option',
420
            'This should be turned off, as it can cause issues with cookies. '
421
            . 'More specifically, unserializing data stored in cookies.'
422
        ));
423
        $this->suggestPHPSetting('display_errors', array(false), array(
424
            'PHP Configuration',
425
            'display_errors option',
426
            'Unless you\'re in a development environment, this should be turned off, '
427
            . 'as it can expose sensitive data to website users.'
428
        ));
429
        // on some weirdly configured webservers arg_separator.output is set to &amp;
430
        // which will results in links like ?param=value&amp;foo=bar which will not be i
431
        $this->suggestPHPSetting('arg_separator.output', array('&', ''), array(
432
            'PHP Configuration',
433
            'arg_separator.output option',
434
            'This option defines how URL parameters are concatenated. '
435
            . 'If not set to \'&\' this may cause issues with URL GET parameters'
436
        ));
437
438
        // always_populate_raw_post_data should be set to -1 if PHP < 7.0
439
        if (version_compare(PHP_VERSION, '7.0.0', '<')) {
440
            $this->suggestPHPSetting('always_populate_raw_post_data', ['-1'], [
441
                'PHP Configuration',
442
                'always_populate_raw_post_data option',
443
                'It\'s highly recommended to set this to \'-1\' in php 5.x, as $HTTP_RAW_POST_DATA is removed in php 7'
444
            ]);
445
        }
446
447
        // Check memory allocation
448
        $this->requireMemory(32 * 1024 * 1024, 64 * 1024 * 1024, array(
449
            "PHP Configuration",
450
            "Memory allocation (PHP config option 'memory_limit')",
451
            "SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.",
452
            $this->getOriginalIni("memory_limit")
453
        ));
454
455
        return $this->errors;
456
    }
457
458
    /**
459
     * Get ini setting
460
     *
461
     * @param string $settingName
462
     * @return mixed
463
     */
464
    protected function getOriginalIni($settingName)
465
    {
466
        if (isset($this->originalIni[$settingName])) {
467
            return $this->originalIni[$settingName];
468
        }
469
        return ini_get($settingName);
470
    }
471
472
    public function suggestPHPSetting($settingName, $settingValues, $testDetails)
473
    {
474
        $this->testing($testDetails);
475
476
        // special case for display_errors, check the original value before
477
        // it was changed at the start of this script.
478
        $val = $this->getOriginalIni($settingName);
479
480
        if (!in_array($val, $settingValues) && $val != $settingValues) {
481
            $this->warning($testDetails, "$settingName is set to '$val' in php.ini.  $testDetails[2]");
482
        }
483
    }
484
485
    public function requirePHPSetting($settingName, $settingValues, $testDetails)
486
    {
487
        $this->testing($testDetails);
488
489
        $val = $this->getOriginalIni($settingName);
490
        if (!in_array($val, $settingValues) && $val != $settingValues) {
491
            $this->error($testDetails, "$settingName is set to '$val' in php.ini.  $testDetails[2]");
492
        }
493
    }
494
495
    public function suggestClass($class, $testDetails)
496
    {
497
        $this->testing($testDetails);
498
499
        if (!class_exists($class)) {
500
            $this->warning($testDetails);
501
        }
502
    }
503
504
    public function suggestFunction($class, $testDetails)
505
    {
506
        $this->testing($testDetails);
507
508
        if (!function_exists($class)) {
509
            $this->warning($testDetails);
510
        }
511
    }
512
513
    public function requireDateTimezone($testDetails)
514
    {
515
        $this->testing($testDetails);
516
        $val = $this->getOriginalIni('date.timezone');
517
        $result = $val && in_array($val, timezone_identifiers_list());
518
        if (!$result) {
519
            $this->error($testDetails);
520
        }
521
    }
522
523
    public function requireMemory($min, $recommended, $testDetails)
0 ignored issues
show
Coding Style introduced by
requireMemory uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
524
    {
525
        $_SESSION['forcemem'] = false;
526
527
        $mem = $this->getPHPMemory();
528
        $memLimit = $this->getOriginalIni("memory_limit");
529
        if ($mem < (64 * 1024 * 1024)) {
530
            ini_set('memory_limit', '64M');
531
            $mem = $this->getPHPMemory();
532
            $testDetails[3] = $memLimit;
533
        }
534
535
        $this->testing($testDetails);
536
537
        if ($mem < $min && $mem > 0) {
538
            $message = $testDetails[2] . " You only have " . $memLimit . " allocated";
539
            $this->error($testDetails, $message);
540
            return false;
541
        } elseif ($mem < $recommended && $mem > 0) {
542
            $message = $testDetails[2] . " You only have " . $memLimit . " allocated";
543
            $this->warning($testDetails, $message);
544
            return false;
545
        } elseif ($mem == 0) {
546
            $message = $testDetails[2] . " We can't determine how much memory you have allocated. "
547
                . "Install only if you're sure you've allocated at least 20 MB.";
548
            $this->warning($testDetails, $message);
549
            return false;
550
        }
551
        return true;
552
    }
553
554
    public function getPHPMemory()
555
    {
556
        $memString = $this->getOriginalIni("memory_limit");
557
558
        switch (strtolower(substr($memString, -1))) {
559
            case "k":
560
                return round(substr($memString, 0, -1) * 1024);
561
562
            case "m":
563
                return round(substr($memString, 0, -1) * 1024 * 1024);
564
565
            case "g":
566
                return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
567
568
            default:
569
                return round($memString);
570
        }
571
    }
572
573
574
    public function listErrors()
575
    {
576
        if ($this->errors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->errors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
577
            echo "<p>The following problems are preventing me from installing SilverStripe CMS:</p>\n\n";
578
            foreach ($this->errors as $error) {
579
                echo "<li>" . htmlentities(implode(", ", $error), ENT_COMPAT, 'UTF-8') . "</li>\n";
580
            }
581
        }
582
    }
583
584
    public function showTable($section = null)
585
    {
586
        if ($section) {
587
            $tests = $this->tests[$section];
588
            $id = strtolower(str_replace(' ', '_', $section));
589
            echo "<table id=\"{$id}_results\" class=\"testResults\" width=\"100%\">";
590
            foreach ($tests as $test => $result) {
591
                echo "<tr class=\"$result[0]\"><td>$test</td><td>"
592
                    . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
593
            }
594
            echo "</table>";
595
        } else {
596
            foreach ($this->tests as $section => $tests) {
597
                $failedRequirements = 0;
598
                $warningRequirements = 0;
599
600
                $output = "";
601
602
                foreach ($tests as $test => $result) {
603
                    if (isset($result['0'])) {
604
                        switch ($result['0']) {
605
                            case 'error':
606
                                $failedRequirements++;
607
                                break;
608
                            case 'warning':
609
                                $warningRequirements++;
610
                                break;
611
                        }
612
                    }
613
                    $output .= "<tr class=\"$result[0]\"><td>$test</td><td>"
614
                        . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
615
                }
616
                $className = "good";
617
                $text = "All Requirements Pass";
618
                $pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
619
620
                if ($failedRequirements > 0) {
621
                    $className = "error";
622
                    $pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
623
624
                    $text = $failedRequirements . ' Failed and ' . $warningRequirements . ' ' . $pluralWarnings;
625
                } elseif ($warningRequirements > 0) {
626
                    $className = "warning";
627
                    $text = "All Requirements Pass but " . $warningRequirements . ' ' . $pluralWarnings;
628
                }
629
630
                echo "<h5 class='requirement $className'>$section <a href='#'>Show All Requirements</a> <span>$text</span></h5>";
631
                echo "<table class=\"testResults\">";
632
                echo $output;
633
                echo "</table>";
634
            }
635
        }
636
    }
637
638
    public function requireFunction($funcName, $testDetails)
639
    {
640
        $this->testing($testDetails);
641
642
        if (!function_exists($funcName)) {
643
            $this->error($testDetails);
644
            return false;
645
        }
646
        return true;
647
    }
648
649
    public function requireClass($className, $testDetails)
650
    {
651
        $this->testing($testDetails);
652
        if (!class_exists($className)) {
653
            $this->error($testDetails);
654
            return false;
655
        }
656
        return true;
657
    }
658
659
    /**
660
     * Require that the given class doesn't exist
661
     *
662
     * @param array $classNames
663
     * @param array $testDetails
664
     * @return bool
665
     */
666
    public function requireNoClasses($classNames, $testDetails)
667
    {
668
        $this->testing($testDetails);
669
        $badClasses = array();
670
        foreach ($classNames as $className) {
671
            if (class_exists($className)) {
672
                $badClasses[] = $className;
673
            }
674
        }
675
        if ($badClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $badClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
676
            $message = $testDetails[2] . ".  The following classes are at fault: " . implode(', ', $badClasses);
677
            $this->error($testDetails, $message);
678
            return false;
679
        }
680
        return true;
681
    }
682
683
    public function checkApacheVersion($testDetails)
684
    {
685
        $this->testing($testDetails);
686
687
        $is1pointx = preg_match('#Apache[/ ]1\.#', $testDetails[3]);
688
        if ($is1pointx) {
689
            $this->error($testDetails);
690
        }
691
692
        return true;
693
    }
694
695
    public function requirePHPVersion($recommendedVersion, $requiredVersion, $testDetails)
696
    {
697
        $this->testing($testDetails);
698
699
        $installedVersion = phpversion();
700
701
        if (version_compare($installedVersion, $requiredVersion, '<')) {
702
            $message = "SilverStripe requires PHP version $requiredVersion or later.\n
703
                PHP version $installedVersion is currently installed.\n
704
                While SilverStripe requires at least PHP version $requiredVersion, upgrading to $recommendedVersion or later is recommended.\n
705
                If you are installing SilverStripe on a shared web server, please ask your web hosting provider to upgrade PHP for you.";
706
            $this->error($testDetails, $message);
707
            return false;
708
        }
709
710
        if (version_compare($installedVersion, $recommendedVersion, '<')) {
711
            $message = "PHP version $installedVersion is currently installed.\n
712
                Upgrading to at least PHP version $recommendedVersion is recommended.\n
713
                SilverStripe should run, but you may run into issues. Future releases may require a later version of PHP.\n";
714
            $this->warning($testDetails, $message);
715
            return false;
716
        }
717
718
        return true;
719
    }
720
721
    /**
722
     * Check that a module exists
723
     *
724
     * @param string $dirname
725
     * @return bool
726
     */
727
    public function checkModuleExists($dirname)
728
    {
729
        $path = $this->getBaseDir() . $dirname;
730
        return file_exists($path) && ($dirname == 'mysite' || file_exists($path . '/_config.php'));
731
    }
732
733
    /**
734
     * The same as {@link requireFile()} but does additional checks
735
     * to ensure the module directory is intact.
736
     *
737
     * @param string $dirname
738
     * @param array $testDetails
739
     */
740
    public function requireModule($dirname, $testDetails)
741
    {
742
        $this->testing($testDetails);
743
        $path = $this->getBaseDir() . $dirname;
744
        if (!file_exists($path)) {
745
            $testDetails[2] .= " Directory '$path' not found. Please make sure you have uploaded the SilverStripe files to your webserver correctly.";
746
            $this->error($testDetails);
747
        } elseif (!file_exists($path . '/_config.php') && $dirname != 'mysite') {
748
            $testDetails[2] .= " Directory '$path' exists, but is missing files. Please make sure you have uploaded "
749
                . "the SilverStripe files to your webserver correctly.";
750
            $this->error($testDetails);
751
        }
752
    }
753
754
    public function requireFile($filename, $testDetails)
755
    {
756
        $this->testing($testDetails);
757
        $filename = $this->getBaseDir() . $filename;
758
        if (!file_exists($filename)) {
759
            $testDetails[2] .= " (file '$filename' not found)";
760
            $this->error($testDetails);
761
        }
762
    }
763
764
    public function requireWriteable($filename, $testDetails, $absolute = false)
765
    {
766
        $this->testing($testDetails);
767
768
        if ($absolute) {
769
            $filename = str_replace('/', DIRECTORY_SEPARATOR, $filename);
770
        } else {
771
            $filename = $this->getBaseDir() . str_replace('/', DIRECTORY_SEPARATOR, $filename);
772
        }
773
774
        if (file_exists($filename)) {
775
            $isWriteable = is_writeable($filename);
776
        } else {
777
            $isWriteable = is_writeable(dirname($filename));
778
        }
779
780
        if (!$isWriteable) {
781
            if (function_exists('posix_getgroups')) {
782
                $userID = posix_geteuid();
783
                $user = posix_getpwuid($userID);
784
785
                $currentOwnerID = fileowner(file_exists($filename) ? $filename : dirname($filename));
786
                $currentOwner = posix_getpwuid($currentOwnerID);
787
788
                $testDetails[2] .= "User '$user[name]' needs to be able to write to this file:\n$filename\n\nThe "
789
                    . "file is currently owned by '$currentOwner[name]'.  ";
790
791
                if ($user['name'] == $currentOwner['name']) {
792
                    $testDetails[2] .= "We recommend that you make the file writeable.";
793
                } else {
794
                    $groups = posix_getgroups();
795
                    $groupList = array();
796
                    foreach ($groups as $group) {
797
                        $groupInfo = posix_getgrgid($group);
798
                        if (in_array($currentOwner['name'], $groupInfo['members'])) {
799
                            $groupList[] = $groupInfo['name'];
800
                        }
801
                    }
802
                    if ($groupList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupList of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
803
                        $testDetails[2] .= "    We recommend that you make the file group-writeable "
804
                            . "and change the group to one of these groups:\n - " . implode("\n - ", $groupList)
805
                            . "\n\nFor example:\nchmod g+w $filename\nchgrp " . $groupList[0] . " $filename";
806
                    } else {
807
                        $testDetails[2] .= "  There is no user-group that contains both the web-server user and the "
808
                            . "owner of this file.  Change the ownership of the file, create a new group, or "
809
                            . "temporarily make the file writeable by everyone during the install process.";
810
                    }
811
                }
812
            } else {
813
                $testDetails[2] .= "The webserver user needs to be able to write to this file:\n$filename";
814
            }
815
816
            $this->error($testDetails);
817
        }
818
    }
819
820
    public function requireTempFolder($testDetails)
821
    {
822
        $this->testing($testDetails);
823
824
        try {
825
            $tempFolder = TempFolder::getTempFolder(BASE_PATH);
826
        } catch (Exception $e) {
827
            $tempFolder = false;
828
        }
829
830
        if (!$tempFolder) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tempFolder of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
831
            $testDetails[2] = "Permission problem gaining access to a temp directory. " .
832
                "Please create a folder named silverstripe-cache in the base directory " .
833
                "of the installation and ensure it has the adequate permissions.";
834
            $this->error($testDetails);
835
        }
836
    }
837
838
    public function requireApacheModule($moduleName, $testDetails)
839
    {
840
        $this->testing($testDetails);
841
        if (!in_array($moduleName, apache_get_modules())) {
842
            $this->error($testDetails);
843
            return false;
844
        } else {
845
            return true;
846
        }
847
    }
848
849
    public function testApacheRewriteExists($moduleName = 'mod_rewrite')
0 ignored issues
show
Coding Style introduced by
testApacheRewriteExists uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
850
    {
851
        if (function_exists('apache_get_modules') && in_array($moduleName, apache_get_modules())) {
852
            return true;
853
        }
854
        if (isset($_SERVER['HTTP_MOD_REWRITE']) && $_SERVER['HTTP_MOD_REWRITE'] == 'On') {
855
            return true;
856
        }
857
        if (isset($_SERVER['REDIRECT_HTTP_MOD_REWRITE']) && $_SERVER['REDIRECT_HTTP_MOD_REWRITE'] == 'On') {
858
            return true;
859
        }
860
        return false;
861
    }
862
863
    public function testIISRewriteModuleExists($moduleName = 'IIS_UrlRewriteModule')
0 ignored issues
show
Coding Style introduced by
testIISRewriteModuleExists uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
864
    {
865
        if (isset($_SERVER[$moduleName]) && $_SERVER[$moduleName]) {
866
            return true;
867
        } else {
868
            return false;
869
        }
870
    }
871
872
    public function requireApacheRewriteModule($moduleName, $testDetails)
0 ignored issues
show
Unused Code introduced by
The parameter $moduleName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
873
    {
874
        $this->testing($testDetails);
875
        if ($this->testApacheRewriteExists()) {
876
            return true;
877
        } else {
878
            $this->warning($testDetails);
879
            return false;
880
        }
881
    }
882
883
    /**
884
     * Determines if the web server has any rewriting capability.
885
     * @return boolean
886
     */
887
    public function hasRewritingCapability()
888
    {
889
        return ($this->testApacheRewriteExists() || $this->testIISRewriteModuleExists());
890
    }
891
892
    public function requireIISRewriteModule($moduleName, $testDetails)
0 ignored issues
show
Unused Code introduced by
The parameter $moduleName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
893
    {
894
        $this->testing($testDetails);
895
        if ($this->testIISRewriteModuleExists()) {
896
            return true;
897
        } else {
898
            $this->warning($testDetails);
899
            return false;
900
        }
901
    }
902
903
    public function getDatabaseTypeNice($databaseClass)
904
    {
905
        return substr($databaseClass, 0, -8);
906
    }
907
908
    /**
909
     * Get an instance of a helper class for the specific database.
910
     *
911
     * @param string $databaseClass e.g. MySQLDatabase or MSSQLDatabase
912
     * @return DatabaseConfigurationHelper
913
     */
914
    public function getDatabaseConfigurationHelper($databaseClass)
915
    {
916
        return DatabaseAdapterRegistry::getDatabaseConfigurationHelper($databaseClass);
917
    }
918
919
    public function requireDatabaseFunctions($databaseConfig, $testDetails)
920
    {
921
        $this->testing($testDetails);
922
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
923
        if (!$helper) {
924
            $this->error($testDetails, "Couldn't load database helper code for " . $databaseConfig['type']);
925
            return false;
926
        }
927
        $result = $helper->requireDatabaseFunctions($databaseConfig);
928
        if ($result) {
929
            return true;
930
        } else {
931
            $this->error($testDetails);
932
            return false;
933
        }
934
    }
935
936
    public function requireDatabaseConnection($databaseConfig, $testDetails)
937
    {
938
        $this->testing($testDetails);
939
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
940
        $result = $helper->requireDatabaseConnection($databaseConfig);
941
        if ($result['success']) {
942
            return true;
943
        } else {
944
            $testDetails[2] .= ": " . $result['error'];
945
            $this->error($testDetails);
946
            return false;
947
        }
948
    }
949
950
    public function requireDatabaseVersion($databaseConfig, $testDetails)
951
    {
952
        $this->testing($testDetails);
953
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
954
        if (method_exists($helper, 'requireDatabaseVersion')) {
955
            $result = $helper->requireDatabaseVersion($databaseConfig);
956
            if ($result['success']) {
957
                return true;
958
            } else {
959
                $testDetails[2] .= $result['error'];
960
                $this->warning($testDetails);
961
                return false;
962
            }
963
        }
964
        // Skipped test because this database has no required version
965
        return true;
966
    }
967
968
    public function requireDatabaseServer($databaseConfig, $testDetails)
969
    {
970
        $this->testing($testDetails);
971
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
972
        $result = $helper->requireDatabaseServer($databaseConfig);
973
        if ($result['success']) {
974
            return true;
975
        } else {
976
            $message = $testDetails[2] . ": " . $result['error'];
977
            $this->error($testDetails, $message);
978
            return false;
979
        }
980
    }
981
982
    public function requireDatabaseOrCreatePermissions($databaseConfig, $testDetails)
983
    {
984
        $this->testing($testDetails);
985
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
986
        $result = $helper->requireDatabaseOrCreatePermissions($databaseConfig);
987
        if ($result['success']) {
988
            if ($result['alreadyExists']) {
989
                $testDetails[3] = "Database $databaseConfig[database]";
990
            } else {
991
                $testDetails[3] = "Able to create a new database";
992
            }
993
            $this->testing($testDetails);
994
            return true;
995
        } else {
996
            if (empty($result['cannotCreate'])) {
997
                $message = $testDetails[2] . ". Please create the database manually.";
998
            } else {
999
                $message = $testDetails[2] . " (user '$databaseConfig[username]' doesn't have CREATE DATABASE permissions.)";
1000
            }
1001
1002
            $this->error($testDetails, $message);
1003
            return false;
1004
        }
1005
    }
1006
1007
    public function requireDatabaseAlterPermissions($databaseConfig, $testDetails)
1008
    {
1009
        $this->testing($testDetails);
1010
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
1011
        $result = $helper->requireDatabaseAlterPermissions($databaseConfig);
1012
        if ($result['success']) {
1013
            return true;
1014
        } else {
1015
            $message = "Silverstripe cannot alter tables. This won't prevent installation, however it may "
1016
                . "cause issues if you try to run a /dev/build once installed.";
1017
            $this->warning($testDetails, $message);
1018
            return false;
1019
        }
1020
    }
1021
1022
    public function requireServerVariables($varNames, $testDetails)
0 ignored issues
show
Coding Style introduced by
requireServerVariables uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1023
    {
1024
        $this->testing($testDetails);
1025
        $missing = array();
1026
1027
        foreach ($varNames as $varName) {
1028
            if (!isset($_SERVER[$varName]) || !$_SERVER[$varName]) {
1029
                $missing[] = '$_SERVER[' . $varName . ']';
1030
            }
1031
        }
1032
1033
        if (!$missing) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $missing of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1034
            return true;
1035
        }
1036
1037
        $message = $testDetails[2] . " (the following PHP variables are missing: " . implode(", ", $missing) . ")";
1038
        $this->error($testDetails, $message);
1039
        return false;
1040
    }
1041
1042
1043
    public function requirePostSupport($testDetails)
0 ignored issues
show
Coding Style introduced by
requirePostSupport uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1044
    {
1045
        $this->testing($testDetails);
1046
1047
        if (!isset($_POST)) {
1048
            $this->error($testDetails);
1049
1050
            return false;
1051
        }
1052
1053
        return true;
1054
    }
1055
1056
    public function isRunningWebServer($testDetails)
1057
    {
1058
        $this->testing($testDetails);
1059
        if ($testDetails[3]) {
1060
            return true;
1061
        } else {
1062
            $this->warning($testDetails);
1063
            return false;
1064
        }
1065
    }
1066
1067
    // Must be PHP4 compatible
1068
    var $baseDir;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $baseDir.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
1069
1070
    public function getBaseDir()
1071
    {
1072
        return BASE_PATH . '/';
1073
    }
1074
1075
    public function testing($testDetails)
1076
    {
1077
        if (!$testDetails) {
1078
            return;
1079
        }
1080
1081
        $section = $testDetails[0];
1082
        $test = $testDetails[1];
1083
1084
        $message = "OK";
1085
        if (isset($testDetails[3])) {
1086
            $message .= " ($testDetails[3])";
1087
        }
1088
1089
        $this->tests[$section][$test] = array("good", $message);
1090
    }
1091
1092
    public function error($testDetails, $message = null)
1093
    {
1094
        if (!is_array($testDetails)) {
1095
            throw new InvalidArgumentException("Invalid error");
1096
        }
1097
        $section = $testDetails[0];
1098
        $test = $testDetails[1];
1099
        if (!$message && isset($testDetails[2])) {
1100
            $message = $testDetails[2];
1101
        }
1102
1103
        $this->tests[$section][$test] = array("error", $message);
1104
        $this->errors[] = $testDetails;
1105
    }
1106
1107
    public function warning($testDetails, $message = null)
1108
    {
1109
        if (!is_array($testDetails)) {
1110
            throw new InvalidArgumentException("Invalid warning");
1111
        }
1112
        $section = $testDetails[0];
1113
        $test = $testDetails[1];
1114
        if (!$message && isset($testDetails[2])) {
1115
            $message = $testDetails[2];
1116
        }
1117
1118
        $this->tests[$section][$test] = array("warning", $message);
1119
        $this->warnings[] = $testDetails;
1120
    }
1121
1122
    public function hasErrors()
1123
    {
1124
        return sizeof($this->errors);
1125
    }
1126
1127
    public function hasWarnings()
1128
    {
1129
        return sizeof($this->warnings);
1130
    }
1131
}
1132