Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

InstallRequirements::findWebserver()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 0
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev\Install;
4
5
use BadMethodCallException;
6
use Exception;
7
use InvalidArgumentException;
8
use RecursiveDirectoryIterator;
9
use RecursiveIteratorIterator;
10
use SilverStripe\Core\TempFolder;
11
use SplFileInfo;
12
13
/**
14
 * This class checks requirements
15
 * Each of the requireXXX functions takes an argument which gives a user description of the test.
16
 * It's an array of 3 parts:
17
 *  $description[0] - The test catetgory
18
 *  $description[1] - The test title
19
 *  $description[2] - The test error to show, if it goes wrong
20
 */
21
class InstallRequirements
22
{
23
    /**
24
     * List of errors
25
     *
26
     * @var array
27
     */
28
    protected $errors = [];
29
30
    /**
31
     * List of warnings
32
     *
33
     * @var array
34
     */
35
    protected $warnings = [];
36
37
    /**
38
     * List of tests
39
     *
40
     * @var array
41
     */
42
    protected $tests = [];
43
44
    /**
45
     * Backup of original ini settings
46
     * @var array
47
     */
48
    protected $originalIni = [];
49
50
    /**
51
     * Base path
52
     * @var
53
     */
54
    protected $baseDir;
55
56
    public function __construct($basePath = null)
57
    {
58
        if ($basePath) {
59
            $this->baseDir = $basePath;
60
        } elseif (defined('BASE_PATH')) {
61
            $this->baseDir = BASE_PATH;
62
        } else {
63
            throw new BadMethodCallException("No BASE_PATH defined");
64
        }
65
    }
66
67
    public function getBaseDir()
68
    {
69
        return rtrim($this->baseDir, '/\\') . '/';
70
    }
71
72
    /**
73
     * Check the database configuration. These are done one after another
74
     * starting with checking the database function exists in PHP, and
75
     * continuing onto more difficult checks like database permissions.
76
     *
77
     * @param array $databaseConfig The list of database parameters
78
     * @return boolean Validity of database configuration details
79
     */
80
    public function checkDatabase($databaseConfig)
81
    {
82
        // Check if support is available
83
        if (!$this->requireDatabaseFunctions(
84
            $databaseConfig,
85
            array(
86
                "Database Configuration",
87
                "Database support",
88
                "Database support in PHP",
89
                $this->getDatabaseTypeNice($databaseConfig['type'])
90
            )
91
        )
92
        ) {
93
            return false;
94
        }
95
96
        $path = empty($databaseConfig['path']) ? null : $databaseConfig['path'];
97
        $server = empty($databaseConfig['server']) ? null : $databaseConfig['server'];
98
99
        // Check if the server is available
100
        $usePath = $path && empty($server);
101
        if (!$this->requireDatabaseServer(
102
            $databaseConfig,
103
            array(
104
                "Database Configuration",
105
                "Database server",
106
                $usePath
107
                    ? "I couldn't write to path '{$path}'"
108
                    : "I couldn't find a database server on '{$server}'",
109
                $usePath
110
                    ? $path
111
                    : $server
112
            )
113
        )
114
        ) {
115
            return false;
116
        }
117
118
        // Check if the connection credentials allow access to the server / database
119
        if (!$this->requireDatabaseConnection(
120
            $databaseConfig,
121
            array(
122
                "Database Configuration",
123
                "Database access credentials",
124
                "That username/password doesn't work"
125
            )
126
        )
127
        ) {
128
            return false;
129
        }
130
131
        // Check the necessary server version is available
132
        if (!$this->requireDatabaseVersion(
133
            $databaseConfig,
134
            array(
135
                "Database Configuration",
136
                "Database server version requirement",
137
                '',
138
                'Version ' . $this->getDatabaseConfigurationHelper($databaseConfig['type'])->getDatabaseVersion($databaseConfig)
139
            )
140
        )
141
        ) {
142
            return false;
143
        }
144
145
        // Check that database creation permissions are available
146
        if (!$this->requireDatabaseOrCreatePermissions(
147
            $databaseConfig,
148
            array(
149
                "Database Configuration",
150
                "Can I access/create the database",
151
                "I can't create new databases and the database '$databaseConfig[database]' doesn't exist"
152
            )
153
        )
154
        ) {
155
            return false;
156
        }
157
158
        // Check alter permission (necessary to create tables etc)
159
        if (!$this->requireDatabaseAlterPermissions(
160
            $databaseConfig,
161
            array(
162
                "Database Configuration",
163
                "Can I ALTER tables",
164
                "I don't have permission to ALTER tables"
165
            )
166
        )
167
        ) {
168
            return false;
169
        }
170
171
        // Success!
172
        return true;
173
    }
174
175
    public function checkAdminConfig($adminConfig)
176
    {
177
        if (!$adminConfig['username']) {
178
            $this->error(array('', 'Please enter a username!'));
179
        }
180
        if (!$adminConfig['password']) {
181
            $this->error(array('', 'Please enter a password!'));
182
        }
183
    }
184
185
    /**
186
     * Check if the web server is IIS and version greater than the given version.
187
     *
188
     * @param int $fromVersion
189
     * @return bool
190
     */
191
    public function isIIS($fromVersion = 7)
192
    {
193
        $webserver = $this->findWebserver();
194
        if (preg_match('#.*IIS/(?<version>[.\\d]+)$#', $webserver, $matches)) {
0 ignored issues
show
Bug introduced by
It seems like $webserver can also be of type false; however, parameter $subject of preg_match() does only seem to accept string, 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

194
        if (preg_match('#.*IIS/(?<version>[.\\d]+)$#', /** @scrutinizer ignore-type */ $webserver, $matches)) {
Loading history...
195
            return version_compare($matches['version'], $fromVersion, '>=');
196
        }
197
        return false;
198
    }
199
200
    /**
201
     * @return bool
202
     */
203
    public function isApache()
204
    {
205
        return strpos($this->findWebserver(), 'Apache') !== false;
0 ignored issues
show
Bug introduced by
It seems like $this->findWebserver() can also be of type false; however, parameter $haystack of strpos() does only seem to accept string, 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

205
        return strpos(/** @scrutinizer ignore-type */ $this->findWebserver(), 'Apache') !== false;
Loading history...
206
    }
207
208
    /**
209
     * Find the webserver software running on the PHP host.
210
     *
211
     * @return string|false Server software or boolean FALSE
212
     */
213
    public function findWebserver()
214
    {
215
        // Try finding from SERVER_SIGNATURE or SERVER_SOFTWARE
216
        if (!empty($_SERVER['SERVER_SIGNATURE'])) {
217
            $webserver = $_SERVER['SERVER_SIGNATURE'];
218
        } elseif (!empty($_SERVER['SERVER_SOFTWARE'])) {
219
            $webserver = $_SERVER['SERVER_SOFTWARE'];
220
        } else {
221
            return false;
222
        }
223
224
        return strip_tags(trim($webserver));
225
    }
226
227
    /**
228
     * Check everything except the database
229
     */
230
    public function check($originalIni)
231
    {
232
        $this->originalIni = $originalIni;
233
        $this->errors = [];
234
        $isApache = $this->isApache();
235
        $isIIS = $this->isIIS();
236
        $webserver = $this->findWebserver();
237
238
        $this->requirePHPVersion('5.5.0', '5.5.0', array(
239
            "PHP Configuration",
240
            "PHP5 installed",
241
            null,
242
            "PHP version " . phpversion()
243
        ));
244
245
        // Check that we can identify the root folder successfully
246
        $this->requireFile('vendor/silverstripe/framework/src/Dev/Install/config-form.html', array(
247
            "File permissions",
248
            "Does the webserver know where files are stored?",
249
            "The webserver isn't letting me identify where files are stored.",
250
            $this->getBaseDir()
251
        ));
252
253
        $this->requireModule('mysite', array(
254
            "File permissions",
255
            "mysite/ directory exists?",
256
            ''
257
        ));
258
        $this->requireModule('vendor/silverstripe/framework', array(
259
            "File permissions",
260
            "vendor/silverstripe/framework/ directory exists?",
261
            '',
262
        ));
263
264
        $this->requireWriteable('index.php', array("File permissions", "Is the index.php file writeable?", null));
265
266
        $this->requireWriteable('.env', ["File permissions", "Is the .env file writeable?", null], false, false);
267
268
        if ($isApache) {
269
            $this->checkApacheVersion(array(
270
                "Webserver Configuration",
271
                "Webserver is not Apache 1.x",
272
                "SilverStripe requires Apache version 2 or greater",
273
                $webserver
274
            ));
275
            $this->requireWriteable('.htaccess', array("File permissions", "Is the .htaccess file writeable?", null));
276
        } elseif ($isIIS) {
277
            $this->requireWriteable('web.config', array("File permissions", "Is the web.config file writeable?", null));
278
        }
279
280
        $this->requireWriteable('mysite/_config.php', array(
281
            "File permissions",
282
            "Is the mysite/_config.php file writeable?",
283
            null
284
        ));
285
286
        $this->requireWriteable('mysite/_config/theme.yml', array(
287
            "File permissions",
288
            "Is the mysite/_config/theme.yml file writeable?",
289
            null
290
        ));
291
292
        if (!$this->checkModuleExists('cms')) {
293
            $this->requireWriteable('mysite/code/RootURLController.php', array(
294
                "File permissions",
295
                "Is the mysite/code/RootURLController.php file writeable?",
296
                null
297
            ));
298
        }
299
300
301
        // Ensure root assets dir is writable
302
        $this->requireWriteable('assets', array("File permissions", "Is the assets/ directory writeable?", null));
303
304
        // Ensure all assets files are writable
305
        $assetsDir = $this->getBaseDir() . 'assets';
306
        $innerIterator = new RecursiveDirectoryIterator($assetsDir, RecursiveDirectoryIterator::SKIP_DOTS);
307
        $iterator = new RecursiveIteratorIterator($innerIterator, RecursiveIteratorIterator::SELF_FIRST);
308
        /** @var SplFileInfo $file */
309
        foreach ($iterator as $file) {
310
            $relativePath = substr($file->getPathname(), strlen($this->getBaseDir()));
311
            $message = $file->isDir()
312
                ? "Is the {$relativePath} directory writeable?"
313
                : "Is the {$relativePath} file writeable?";
314
            $this->requireWriteable($relativePath, array("File permissions", $message, null));
315
        }
316
317
        try {
318
            $tempFolder = TempFolder::getTempFolder($this->getBaseDir());
319
        } catch (Exception $e) {
320
            $tempFolder = false;
321
        }
322
323
        $this->requireTempFolder(array('File permissions', 'Is a temporary directory available?', null, $tempFolder));
324
        if ($tempFolder) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tempFolder of type false|string 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...
325
            // in addition to the temp folder being available, check it is writable
326
            $this->requireWriteable($tempFolder, array(
327
                "File permissions",
328
                sprintf("Is the temporary directory writeable?", $tempFolder),
329
                null
330
            ), true);
331
        }
332
333
        // Check for web server, unless we're calling the installer from the command-line
334
        $this->isRunningWebServer(array("Webserver Configuration", "Server software", "Unknown", $webserver));
335
336
        if ($isApache) {
337
            $this->requireApacheRewriteModule('mod_rewrite', array(
338
                "Webserver Configuration",
339
                "URL rewriting support",
340
                "You need mod_rewrite to use friendly URLs with SilverStripe, but it is not enabled."
341
            ));
342
        } elseif ($isIIS) {
343
            $this->requireIISRewriteModule('IIS_UrlRewriteModule', array(
344
                "Webserver Configuration",
345
                "URL rewriting support",
346
                "You need to enable the IIS URL Rewrite Module to use friendly URLs with SilverStripe, "
347
                . "but it is not installed or enabled. Download it for IIS 7 from http://www.iis.net/expand/URLRewrite"
348
            ));
349
        } else {
350
            $this->warning(array(
351
                "Webserver Configuration",
352
                "URL rewriting support",
353
                "I can't tell whether any rewriting module is running.  You may need to configure a rewriting rule yourself."
354
            ));
355
        }
356
357
        $this->requireServerVariables(array('SCRIPT_NAME', 'HTTP_HOST', 'SCRIPT_FILENAME'), array(
358
            "Webserver Configuration",
359
            "Recognised webserver",
360
            "You seem to be using an unsupported webserver.  "
361
            . "The server variables SCRIPT_NAME, HTTP_HOST, SCRIPT_FILENAME need to be set."
362
        ));
363
364
        $this->requirePostSupport(array(
365
            "Webserver Configuration",
366
            "POST Support",
367
            'I can\'t find $_POST, make sure POST is enabled.'
368
        ));
369
370
        // Check for GD support
371
        if (!$this->requireFunction("imagecreatetruecolor", array(
372
            "PHP Configuration",
373
            "GD2 support",
374
            "PHP must have GD version 2."
375
        ))
376
        ) {
377
            $this->requireFunction("imagecreate", array(
378
                "PHP Configuration",
379
                "GD2 support",
380
                "GD support for PHP not included."
381
            ));
382
        }
383
384
        // Check for XML support
385
        $this->requireFunction('xml_set_object', array(
386
            "PHP Configuration",
387
            "XML support",
388
            "XML support not included in PHP."
389
        ));
390
        $this->requireClass('DOMDocument', array(
391
            "PHP Configuration",
392
            "DOM/XML support",
393
            "DOM/XML support not included in PHP."
394
        ));
395
        $this->requireFunction('simplexml_load_file', array(
396
            'PHP Configuration',
397
            'SimpleXML support',
398
            'SimpleXML support not included in PHP.'
399
        ));
400
401
        // Check for token_get_all
402
        $this->requireFunction('token_get_all', array(
403
            "PHP Configuration",
404
            "Tokenizer support",
405
            "Tokenizer support not included in PHP."
406
        ));
407
408
        // Check for CType support
409
        $this->requireFunction('ctype_digit', array(
410
            'PHP Configuration',
411
            'CType support',
412
            'CType support not included in PHP.'
413
        ));
414
415
        // Check for session support
416
        $this->requireFunction('session_start', array(
417
            'PHP Configuration',
418
            'Session support',
419
            'Session support not included in PHP.'
420
        ));
421
422
        // Check for iconv support
423
        $this->requireFunction('iconv', array(
424
            'PHP Configuration',
425
            'iconv support',
426
            'iconv support not included in PHP.'
427
        ));
428
429
        // Check for hash support
430
        $this->requireFunction('hash', array('PHP Configuration', 'hash support', 'hash support not included in PHP.'));
431
432
        // Check for mbstring support
433
        $this->requireFunction('mb_internal_encoding', array(
434
            'PHP Configuration',
435
            'mbstring support',
436
            'mbstring support not included in PHP.'
437
        ));
438
439
        // Check for Reflection support
440
        $this->requireClass('ReflectionClass', array(
441
            'PHP Configuration',
442
            'Reflection support',
443
            'Reflection support not included in PHP.'
444
        ));
445
446
        // Check for Standard PHP Library (SPL) support
447
        $this->requireFunction('spl_classes', array(
448
            'PHP Configuration',
449
            'SPL support',
450
            'Standard PHP Library (SPL) not included in PHP.'
451
        ));
452
453
        $this->requireDateTimezone(array(
454
            'PHP Configuration',
455
            'date.timezone setting and validity',
456
            'date.timezone option in php.ini must be set correctly.',
457
            $this->getOriginalIni('date.timezone')
458
        ));
459
460
        $this->suggestClass('finfo', array(
461
            'PHP Configuration',
462
            'fileinfo support',
463
            'fileinfo should be enabled in PHP. SilverStripe uses it for MIME type detection of files. '
464
            . 'SilverStripe will still operate, but email attachments and sending files to browser '
465
            . '(e.g. export data to CSV) may not work correctly without finfo.'
466
        ));
467
468
        $this->suggestFunction('curl_init', array(
469
            'PHP Configuration',
470
            'curl support',
471
            'curl should be enabled in PHP. SilverStripe uses it for consuming web services'
472
            . ' via the RestfulService class and many modules rely on it.'
473
        ));
474
475
        $this->suggestClass('tidy', array(
476
            'PHP Configuration',
477
            'tidy support',
478
            'Tidy provides a library of code to clean up your html. '
479
            . 'SilverStripe will operate fine without tidy but HTMLCleaner will not be effective.'
480
        ));
481
482
        $this->suggestPHPSetting('asp_tags', array(false), array(
483
            'PHP Configuration',
484
            'asp_tags option',
485
            'This should be turned off as it can cause issues with SilverStripe'
486
        ));
487
        $this->requirePHPSetting('magic_quotes_gpc', array(false), array(
488
            'PHP Configuration',
489
            'magic_quotes_gpc option',
490
            'This should be turned off, as it can cause issues with cookies. '
491
            . 'More specifically, unserializing data stored in cookies.'
492
        ));
493
        $this->suggestPHPSetting('display_errors', array(false), array(
494
            'PHP Configuration',
495
            'display_errors option',
496
            'Unless you\'re in a development environment, this should be turned off, '
497
            . 'as it can expose sensitive data to website users.'
498
        ));
499
        // on some weirdly configured webservers arg_separator.output is set to &amp;
500
        // which will results in links like ?param=value&amp;foo=bar which will not be i
501
        $this->suggestPHPSetting('arg_separator.output', array('&', ''), array(
502
            'PHP Configuration',
503
            'arg_separator.output option',
504
            'This option defines how URL parameters are concatenated. '
505
            . 'If not set to \'&\' this may cause issues with URL GET parameters'
506
        ));
507
508
        // always_populate_raw_post_data should be set to -1 if PHP < 7.0
509
        if (version_compare(PHP_VERSION, '7.0.0', '<')) {
510
            $this->suggestPHPSetting('always_populate_raw_post_data', ['-1'], [
511
                'PHP Configuration',
512
                'always_populate_raw_post_data option',
513
                'It\'s highly recommended to set this to \'-1\' in php 5.x, as $HTTP_RAW_POST_DATA is removed in php 7'
514
            ]);
515
        }
516
517
        // Check memory allocation
518
        $this->requireMemory(32 * 1024 * 1024, 64 * 1024 * 1024, array(
519
            "PHP Configuration",
520
            "Memory allocation (PHP config option 'memory_limit')",
521
            "SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.",
522
            $this->getOriginalIni("memory_limit")
523
        ));
524
525
        return $this->errors;
526
    }
527
528
    /**
529
     * Get ini setting
530
     *
531
     * @param string $settingName
532
     * @return mixed
533
     */
534
    protected function getOriginalIni($settingName)
535
    {
536
        if (isset($this->originalIni[$settingName])) {
537
            return $this->originalIni[$settingName];
538
        }
539
        return ini_get($settingName);
540
    }
541
542
    public function suggestPHPSetting($settingName, $settingValues, $testDetails)
543
    {
544
        $this->testing($testDetails);
545
546
        // special case for display_errors, check the original value before
547
        // it was changed at the start of this script.
548
        $val = $this->getOriginalIni($settingName);
549
550
        if (!in_array($val, $settingValues) && $val != $settingValues) {
551
            $this->warning($testDetails, "$settingName is set to '$val' in php.ini.  $testDetails[2]");
552
        }
553
    }
554
555
    public function requirePHPSetting($settingName, $settingValues, $testDetails)
556
    {
557
        $this->testing($testDetails);
558
559
        $val = $this->getOriginalIni($settingName);
560
        if (!in_array($val, $settingValues) && $val != $settingValues) {
561
            $this->error($testDetails, "$settingName is set to '$val' in php.ini.  $testDetails[2]");
562
        }
563
    }
564
565
    public function suggestClass($class, $testDetails)
566
    {
567
        $this->testing($testDetails);
568
569
        if (!class_exists($class)) {
570
            $this->warning($testDetails);
571
        }
572
    }
573
574
    public function suggestFunction($class, $testDetails)
575
    {
576
        $this->testing($testDetails);
577
578
        if (!function_exists($class)) {
579
            $this->warning($testDetails);
580
        }
581
    }
582
583
    public function requireDateTimezone($testDetails)
584
    {
585
        $this->testing($testDetails);
586
        $val = $this->getOriginalIni('date.timezone');
587
        $result = $val && in_array($val, timezone_identifiers_list());
0 ignored issues
show
Bug introduced by
It seems like timezone_identifiers_list() can also be of type false; however, parameter $haystack of in_array() 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

587
        $result = $val && in_array($val, /** @scrutinizer ignore-type */ timezone_identifiers_list());
Loading history...
588
        if (!$result) {
589
            $this->error($testDetails);
590
        }
591
    }
592
593
    public function requireMemory($min, $recommended, $testDetails)
594
    {
595
        $_SESSION['forcemem'] = false;
596
597
        $mem = $this->getPHPMemory();
598
        $memLimit = $this->getOriginalIni("memory_limit");
599
        if ($mem < (64 * 1024 * 1024)) {
600
            ini_set('memory_limit', '64M');
601
            $mem = $this->getPHPMemory();
602
            $testDetails[3] = $memLimit;
603
        }
604
605
        $this->testing($testDetails);
606
607
        if ($mem < $min && $mem > 0) {
608
            $message = $testDetails[2] . " You only have " . $memLimit . " allocated";
609
            $this->error($testDetails, $message);
610
            return false;
611
        } elseif ($mem < $recommended && $mem > 0) {
612
            $message = $testDetails[2] . " You only have " . $memLimit . " allocated";
613
            $this->warning($testDetails, $message);
614
            return false;
615
        } elseif ($mem == 0) {
616
            $message = $testDetails[2] . " We can't determine how much memory you have allocated. "
617
                . "Install only if you're sure you've allocated at least 20 MB.";
618
            $this->warning($testDetails, $message);
619
            return false;
620
        }
621
        return true;
622
    }
623
624
    public function getPHPMemory()
625
    {
626
        $memString = $this->getOriginalIni("memory_limit");
627
628
        switch (strtolower(substr($memString, -1))) {
629
            case "k":
630
                return round(substr($memString, 0, -1) * 1024);
631
632
            case "m":
633
                return round(substr($memString, 0, -1) * 1024 * 1024);
634
635
            case "g":
636
                return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
637
638
            default:
639
                return round($memString);
0 ignored issues
show
Bug introduced by
It seems like $memString can also be of type string; however, parameter $val of round() does only seem to accept double, 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

639
                return round(/** @scrutinizer ignore-type */ $memString);
Loading history...
640
        }
641
    }
642
643
644
    public function listErrors()
645
    {
646
        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...
647
            echo "<p>The following problems are preventing me from installing SilverStripe CMS:</p>\n\n";
648
            foreach ($this->errors as $error) {
649
                echo "<li>" . htmlentities(implode(", ", $error), ENT_COMPAT, 'UTF-8') . "</li>\n";
650
            }
651
        }
652
    }
653
654
    public function showTable($section = null)
655
    {
656
        if ($section) {
657
            $tests = $this->tests[$section];
658
            $id = strtolower(str_replace(' ', '_', $section));
659
            echo "<table id=\"{$id}_results\" class=\"testResults\" width=\"100%\">";
660
            foreach ($tests as $test => $result) {
661
                echo "<tr class=\"$result[0]\"><td>$test</td><td>"
662
                    . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
663
            }
664
            echo "</table>";
665
        } else {
666
            foreach ($this->tests as $section => $tests) {
667
                $failedRequirements = 0;
668
                $warningRequirements = 0;
669
670
                $output = "";
671
672
                foreach ($tests as $test => $result) {
673
                    if (isset($result['0'])) {
674
                        switch ($result['0']) {
675
                            case 'error':
676
                                $failedRequirements++;
677
                                break;
678
                            case 'warning':
679
                                $warningRequirements++;
680
                                break;
681
                        }
682
                    }
683
                    $output .= "<tr class=\"$result[0]\"><td>$test</td><td>"
684
                        . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
685
                }
686
                $className = "good";
687
                $text = "All Requirements Pass";
688
                $pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
689
690
                if ($failedRequirements > 0) {
691
                    $className = "error";
692
                    $pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
693
694
                    $text = $failedRequirements . ' Failed and ' . $warningRequirements . ' ' . $pluralWarnings;
695
                } elseif ($warningRequirements > 0) {
696
                    $className = "warning";
697
                    $text = "All Requirements Pass but " . $warningRequirements . ' ' . $pluralWarnings;
698
                }
699
700
                echo "<h5 class='requirement $className'>$section <a href='#'>Show All Requirements</a> <span>$text</span></h5>";
701
                echo "<table class=\"testResults\">";
702
                echo $output;
703
                echo "</table>";
704
            }
705
        }
706
    }
707
708
    public function requireFunction($funcName, $testDetails)
709
    {
710
        $this->testing($testDetails);
711
712
        if (!function_exists($funcName)) {
713
            $this->error($testDetails);
714
            return false;
715
        }
716
        return true;
717
    }
718
719
    public function requireClass($className, $testDetails)
720
    {
721
        $this->testing($testDetails);
722
        if (!class_exists($className)) {
723
            $this->error($testDetails);
724
            return false;
725
        }
726
        return true;
727
    }
728
729
    /**
730
     * Require that the given class doesn't exist
731
     *
732
     * @param array $classNames
733
     * @param array $testDetails
734
     * @return bool
735
     */
736
    public function requireNoClasses($classNames, $testDetails)
737
    {
738
        $this->testing($testDetails);
739
        $badClasses = array();
740
        foreach ($classNames as $className) {
741
            if (class_exists($className)) {
742
                $badClasses[] = $className;
743
            }
744
        }
745
        if ($badClasses) {
746
            $message = $testDetails[2] . ".  The following classes are at fault: " . implode(', ', $badClasses);
747
            $this->error($testDetails, $message);
748
            return false;
749
        }
750
        return true;
751
    }
752
753
    public function checkApacheVersion($testDetails)
754
    {
755
        $this->testing($testDetails);
756
757
        $is1pointx = preg_match('#Apache[/ ]1\.#', $testDetails[3]);
758
        if ($is1pointx) {
759
            $this->error($testDetails);
760
        }
761
762
        return true;
763
    }
764
765
    public function requirePHPVersion($recommendedVersion, $requiredVersion, $testDetails)
766
    {
767
        $this->testing($testDetails);
768
769
        $installedVersion = phpversion();
770
771
        if (version_compare($installedVersion, $requiredVersion, '<')) {
772
            $message = "SilverStripe requires PHP version $requiredVersion or later.\n
773
                PHP version $installedVersion is currently installed.\n
774
                While SilverStripe requires at least PHP version $requiredVersion, upgrading to $recommendedVersion or later is recommended.\n
775
                If you are installing SilverStripe on a shared web server, please ask your web hosting provider to upgrade PHP for you.";
776
            $this->error($testDetails, $message);
777
            return false;
778
        }
779
780
        if (version_compare($installedVersion, $recommendedVersion, '<')) {
781
            $message = "PHP version $installedVersion is currently installed.\n
782
                Upgrading to at least PHP version $recommendedVersion is recommended.\n
783
                SilverStripe should run, but you may run into issues. Future releases may require a later version of PHP.\n";
784
            $this->warning($testDetails, $message);
785
            return false;
786
        }
787
788
        return true;
789
    }
790
791
    /**
792
     * Check that a module exists
793
     *
794
     * @param string $dirname
795
     * @return bool
796
     */
797
    public function checkModuleExists($dirname)
798
    {
799
        // Mysite is base-only and doesn't need _config.php to be counted
800
        if ($dirname === 'mysite') {
801
            return file_exists($this->getBaseDir() . $dirname);
802
        }
803
804
        $paths = [
805
            "vendor/silverstripe/{$dirname}/",
806
            "{$dirname}/",
807
        ];
808
        foreach ($paths as $path) {
809
            $checks = ['_config', '_config.php'];
810
            foreach ($checks as $check) {
811
                if (file_exists($this->getBaseDir() . $path . $check)) {
812
                    return true;
813
                }
814
            }
815
        }
816
817
        return false;
818
    }
819
820
    /**
821
     * The same as {@link requireFile()} but does additional checks
822
     * to ensure the module directory is intact.
823
     *
824
     * @param string $dirname
825
     * @param array $testDetails
826
     */
827
    public function requireModule($dirname, $testDetails)
828
    {
829
        $this->testing($testDetails);
830
        $path = $this->getBaseDir() . $dirname;
831
        if (!file_exists($path)) {
832
            $testDetails[2] .= " Directory '$path' not found. Please make sure you have uploaded the SilverStripe files to your webserver correctly.";
833
            $this->error($testDetails);
834
        } elseif (!file_exists($path . '/_config.php') && $dirname != 'mysite') {
835
            $testDetails[2] .= " Directory '$path' exists, but is missing files. Please make sure you have uploaded "
836
                . "the SilverStripe files to your webserver correctly.";
837
            $this->error($testDetails);
838
        }
839
    }
840
841
    public function requireFile($filename, $testDetails)
842
    {
843
        $this->testing($testDetails);
844
        $filename = $this->getBaseDir() . $filename;
845
        if (!file_exists($filename)) {
846
            $testDetails[2] .= " (file '$filename' not found)";
847
            $this->error($testDetails);
848
        }
849
    }
850
851
    public function requireWriteable($filename, $testDetails, $absolute = false, $error = true)
852
    {
853
        $this->testing($testDetails);
854
855
        if ($absolute) {
856
            $filename = str_replace('/', DIRECTORY_SEPARATOR, $filename);
857
        } else {
858
            $filename = $this->getBaseDir() . str_replace('/', DIRECTORY_SEPARATOR, $filename);
859
        }
860
861
        if (file_exists($filename)) {
862
            $isWriteable = is_writeable($filename);
863
        } else {
864
            $isWriteable = is_writeable(dirname($filename));
865
        }
866
867
        if (!$isWriteable) {
868
            if (function_exists('posix_getgroups')) {
869
                $userID = posix_geteuid();
870
                $user = posix_getpwuid($userID);
871
872
                $currentOwnerID = fileowner(file_exists($filename) ? $filename : dirname($filename));
873
                $currentOwner = posix_getpwuid($currentOwnerID);
874
875
                $testDetails[2] .= "User '$user[name]' needs to be able to write to this file:\n$filename\n\nThe "
876
                    . "file is currently owned by '$currentOwner[name]'.  ";
877
878
                if ($user['name'] == $currentOwner['name']) {
879
                    $testDetails[2] .= "We recommend that you make the file writeable.";
880
                } else {
881
                    $groups = posix_getgroups();
882
                    $groupList = array();
883
                    foreach ($groups as $group) {
884
                        $groupInfo = posix_getgrgid($group);
885
                        if (in_array($currentOwner['name'], $groupInfo['members'])) {
886
                            $groupList[] = $groupInfo['name'];
887
                        }
888
                    }
889
                    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...
890
                        $testDetails[2] .= "    We recommend that you make the file group-writeable "
891
                            . "and change the group to one of these groups:\n - " . implode("\n - ", $groupList)
892
                            . "\n\nFor example:\nchmod g+w $filename\nchgrp " . $groupList[0] . " $filename";
893
                    } else {
894
                        $testDetails[2] .= "  There is no user-group that contains both the web-server user and the "
895
                            . "owner of this file.  Change the ownership of the file, create a new group, or "
896
                            . "temporarily make the file writeable by everyone during the install process.";
897
                    }
898
                }
899
            } else {
900
                $testDetails[2] .= "The webserver user needs to be able to write to this file:\n$filename";
901
            }
902
903
            if ($error) {
904
                $this->error($testDetails);
905
            } else {
906
                $this->warning($testDetails);
907
            }
908
        }
909
    }
910
911
    public function requireTempFolder($testDetails)
912
    {
913
        $this->testing($testDetails);
914
915
        try {
916
            $tempFolder = TempFolder::getTempFolder($this->getBaseDir());
917
        } catch (Exception $e) {
918
            $tempFolder = false;
919
        }
920
921
        if (!$tempFolder) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tempFolder of type false|string 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...
922
            $testDetails[2] = "Permission problem gaining access to a temp directory. " .
923
                "Please create a folder named silverstripe-cache in the base directory " .
924
                "of the installation and ensure it has the adequate permissions.";
925
            $this->error($testDetails);
926
        }
927
    }
928
929
    public function requireApacheModule($moduleName, $testDetails)
930
    {
931
        $this->testing($testDetails);
932
        if (!in_array($moduleName, apache_get_modules())) {
933
            $this->error($testDetails);
934
            return false;
935
        } else {
936
            return true;
937
        }
938
    }
939
940
    public function testApacheRewriteExists($moduleName = 'mod_rewrite')
941
    {
942
        if (function_exists('apache_get_modules') && in_array($moduleName, apache_get_modules())) {
943
            return true;
944
        }
945
        if (isset($_SERVER['HTTP_MOD_REWRITE']) && $_SERVER['HTTP_MOD_REWRITE'] == 'On') {
946
            return true;
947
        }
948
        if (isset($_SERVER['REDIRECT_HTTP_MOD_REWRITE']) && $_SERVER['REDIRECT_HTTP_MOD_REWRITE'] == 'On') {
949
            return true;
950
        }
951
        return false;
952
    }
953
954
    public function testIISRewriteModuleExists($moduleName = 'IIS_UrlRewriteModule')
955
    {
956
        if (isset($_SERVER[$moduleName]) && $_SERVER[$moduleName]) {
957
            return true;
958
        } else {
959
            return false;
960
        }
961
    }
962
963
    public function requireApacheRewriteModule($moduleName, $testDetails)
0 ignored issues
show
Unused Code introduced by
The parameter $moduleName is not used and could be removed. ( Ignorable by Annotation )

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

963
    public function requireApacheRewriteModule(/** @scrutinizer ignore-unused */ $moduleName, $testDetails)

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

Loading history...
964
    {
965
        $this->testing($testDetails);
966
        if ($this->testApacheRewriteExists()) {
967
            return true;
968
        } else {
969
            $this->error($testDetails);
970
            return false;
971
        }
972
    }
973
974
    /**
975
     * Determines if the web server has any rewriting capability.
976
     * @return boolean
977
     */
978
    public function hasRewritingCapability()
979
    {
980
        return ($this->testApacheRewriteExists() || $this->testIISRewriteModuleExists());
981
    }
982
983
    public function requireIISRewriteModule($moduleName, $testDetails)
0 ignored issues
show
Unused Code introduced by
The parameter $moduleName is not used and could be removed. ( Ignorable by Annotation )

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

983
    public function requireIISRewriteModule(/** @scrutinizer ignore-unused */ $moduleName, $testDetails)

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

Loading history...
984
    {
985
        $this->testing($testDetails);
986
        if ($this->testIISRewriteModuleExists()) {
987
            return true;
988
        } else {
989
            $this->warning($testDetails);
990
            return false;
991
        }
992
    }
993
994
    public function getDatabaseTypeNice($databaseClass)
995
    {
996
        return substr($databaseClass, 0, -8);
997
    }
998
999
    /**
1000
     * Get an instance of a helper class for the specific database.
1001
     *
1002
     * @param string $databaseClass e.g. MySQLDatabase or MSSQLDatabase
1003
     * @return DatabaseConfigurationHelper
1004
     */
1005
    public function getDatabaseConfigurationHelper($databaseClass)
1006
    {
1007
        return DatabaseAdapterRegistry::getDatabaseConfigurationHelper($databaseClass);
1008
    }
1009
1010
    public function requireDatabaseFunctions($databaseConfig, $testDetails)
1011
    {
1012
        $this->testing($testDetails);
1013
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
1014
        if (!$helper) {
1015
            $this->error($testDetails, "Couldn't load database helper code for " . $databaseConfig['type']);
1016
            return false;
1017
        }
1018
        $result = $helper->requireDatabaseFunctions($databaseConfig);
1019
        if ($result) {
1020
            return true;
1021
        } else {
1022
            $this->error($testDetails);
1023
            return false;
1024
        }
1025
    }
1026
1027
    public function requireDatabaseConnection($databaseConfig, $testDetails)
1028
    {
1029
        $this->testing($testDetails);
1030
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
1031
        $result = $helper->requireDatabaseConnection($databaseConfig);
1032
        if ($result['success']) {
1033
            return true;
1034
        } else {
1035
            $testDetails[2] .= ": " . $result['error'];
1036
            $this->error($testDetails);
1037
            return false;
1038
        }
1039
    }
1040
1041
    public function requireDatabaseVersion($databaseConfig, $testDetails)
1042
    {
1043
        $this->testing($testDetails);
1044
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
1045
        if (method_exists($helper, 'requireDatabaseVersion')) {
1046
            $result = $helper->requireDatabaseVersion($databaseConfig);
1047
            if ($result['success']) {
1048
                return true;
1049
            } else {
1050
                $testDetails[2] .= $result['error'];
1051
                $this->warning($testDetails);
1052
                return false;
1053
            }
1054
        }
1055
        // Skipped test because this database has no required version
1056
        return true;
1057
    }
1058
1059
    public function requireDatabaseServer($databaseConfig, $testDetails)
1060
    {
1061
        $this->testing($testDetails);
1062
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
1063
        $result = $helper->requireDatabaseServer($databaseConfig);
1064
        if ($result['success']) {
1065
            return true;
1066
        } else {
1067
            $message = $testDetails[2] . ": " . $result['error'];
1068
            $this->error($testDetails, $message);
1069
            return false;
1070
        }
1071
    }
1072
1073
    public function requireDatabaseOrCreatePermissions($databaseConfig, $testDetails)
1074
    {
1075
        $this->testing($testDetails);
1076
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
1077
        $result = $helper->requireDatabaseOrCreatePermissions($databaseConfig);
1078
        if ($result['success']) {
1079
            if ($result['alreadyExists']) {
1080
                $testDetails[3] = "Database $databaseConfig[database]";
1081
            } else {
1082
                $testDetails[3] = "Able to create a new database";
1083
            }
1084
            $this->testing($testDetails);
1085
            return true;
1086
        } else {
1087
            if (empty($result['cannotCreate'])) {
1088
                $message = $testDetails[2] . ". Please create the database manually.";
1089
            } else {
1090
                $message = $testDetails[2] . " (user '$databaseConfig[username]' doesn't have CREATE DATABASE permissions.)";
1091
            }
1092
1093
            $this->error($testDetails, $message);
1094
            return false;
1095
        }
1096
    }
1097
1098
    public function requireDatabaseAlterPermissions($databaseConfig, $testDetails)
1099
    {
1100
        $this->testing($testDetails);
1101
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
1102
        $result = $helper->requireDatabaseAlterPermissions($databaseConfig);
1103
        if ($result['success']) {
1104
            return true;
1105
        } else {
1106
            $message = "Silverstripe cannot alter tables. This won't prevent installation, however it may "
1107
                . "cause issues if you try to run a /dev/build once installed.";
1108
            $this->warning($testDetails, $message);
1109
            return false;
1110
        }
1111
    }
1112
1113
    public function requireServerVariables($varNames, $testDetails)
1114
    {
1115
        $this->testing($testDetails);
1116
        $missing = array();
1117
1118
        foreach ($varNames as $varName) {
1119
            if (!isset($_SERVER[$varName]) || !$_SERVER[$varName]) {
1120
                $missing[] = '$_SERVER[' . $varName . ']';
1121
            }
1122
        }
1123
1124
        if (!$missing) {
1125
            return true;
1126
        }
1127
1128
        $message = $testDetails[2] . " (the following PHP variables are missing: " . implode(", ", $missing) . ")";
1129
        $this->error($testDetails, $message);
1130
        return false;
1131
    }
1132
1133
1134
    public function requirePostSupport($testDetails)
1135
    {
1136
        $this->testing($testDetails);
1137
1138
        if (!isset($_POST)) {
1139
            $this->error($testDetails);
1140
1141
            return false;
1142
        }
1143
1144
        return true;
1145
    }
1146
1147
    public function isRunningWebServer($testDetails)
1148
    {
1149
        $this->testing($testDetails);
1150
        if ($testDetails[3]) {
1151
            return true;
1152
        } else {
1153
            $this->warning($testDetails);
1154
            return false;
1155
        }
1156
    }
1157
1158
    public function testing($testDetails)
1159
    {
1160
        if (!$testDetails) {
1161
            return;
1162
        }
1163
1164
        $section = $testDetails[0];
1165
        $test = $testDetails[1];
1166
1167
        $message = "OK";
1168
        if (isset($testDetails[3])) {
1169
            $message .= " ($testDetails[3])";
1170
        }
1171
1172
        $this->tests[$section][$test] = array("good", $message);
1173
    }
1174
1175
    public function error($testDetails, $message = null)
1176
    {
1177
        if (!is_array($testDetails)) {
1178
            throw new InvalidArgumentException("Invalid error");
1179
        }
1180
        $section = $testDetails[0];
1181
        $test = $testDetails[1];
1182
        if (!$message && isset($testDetails[2])) {
1183
            $message = $testDetails[2];
1184
        }
1185
1186
        $this->tests[$section][$test] = array("error", $message);
1187
        $this->errors[] = $testDetails;
1188
    }
1189
1190
    public function warning($testDetails, $message = null)
1191
    {
1192
        if (!is_array($testDetails)) {
1193
            throw new InvalidArgumentException("Invalid warning");
1194
        }
1195
        $section = $testDetails[0];
1196
        $test = $testDetails[1];
1197
        if (!$message && isset($testDetails[2])) {
1198
            $message = $testDetails[2];
1199
        }
1200
1201
        $this->tests[$section][$test] = array("warning", $message);
1202
        $this->warnings[] = $testDetails;
1203
    }
1204
1205
    public function hasErrors()
1206
    {
1207
        return sizeof($this->errors);
0 ignored issues
show
Bug introduced by
The call to sizeof() has too few arguments starting with mode. ( Ignorable by Annotation )

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

1207
        return /** @scrutinizer ignore-call */ sizeof($this->errors);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1208
    }
1209
1210
    public function hasWarnings()
1211
    {
1212
        return sizeof($this->warnings);
0 ignored issues
show
Bug introduced by
The call to sizeof() has too few arguments starting with mode. ( Ignorable by Annotation )

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

1212
        return /** @scrutinizer ignore-call */ sizeof($this->warnings);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1213
    }
1214
}
1215