Passed
Push — master ( 0208b2...1155ca )
by Robbie
09:03
created

InstallRequirements::isIIS()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
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\Path;
11
use SilverStripe\Core\TempFolder;
12
use SplFileInfo;
13
14
/**
15
 * This class checks requirements
16
 * Each of the requireXXX functions takes an argument which gives a user description of the test.
17
 * It's an array of 3 parts:
18
 *  $description[0] - The test catetgory
19
 *  $description[1] - The test title
20
 *  $description[2] - The test error to show, if it goes wrong
21
 */
22
class InstallRequirements
23
{
24
    use InstallEnvironmentAware;
25
26
    /**
27
     * List of errors
28
     *
29
     * @var array
30
     */
31
    protected $errors = [];
32
33
    /**
34
     * List of warnings
35
     *
36
     * @var array
37
     */
38
    protected $warnings = [];
39
40
    /**
41
     * List of tests
42
     *
43
     * @var array
44
     */
45
    protected $tests = [];
46
47
    /**
48
     * Backup of original ini settings
49
     * @var array
50
     */
51
    protected $originalIni = [];
52
53
    public function __construct($basePath = null)
54
    {
55
        $this->initBaseDir($basePath);
56
    }
57
58
    /**
59
     * Check the database configuration. These are done one after another
60
     * starting with checking the database function exists in PHP, and
61
     * continuing onto more difficult checks like database permissions.
62
     *
63
     * @param array $databaseConfig The list of database parameters
64
     * @return boolean Validity of database configuration details
65
     */
66
    public function checkDatabase($databaseConfig)
67
    {
68
        // Check if support is available
69
        if (!$this->requireDatabaseFunctions(
70
            $databaseConfig,
71
            array(
72
                "Database Configuration",
73
                "Database support",
74
                "Database support in PHP",
75
                $this->getDatabaseTypeNice($databaseConfig['type'])
76
            )
77
        )
78
        ) {
79
            return false;
80
        }
81
82
        $path = empty($databaseConfig['path']) ? null : $databaseConfig['path'];
83
        $server = empty($databaseConfig['server']) ? null : $databaseConfig['server'];
84
85
        // Check if the server is available
86
        $usePath = $path && empty($server);
87
        if (!$this->requireDatabaseServer(
88
            $databaseConfig,
89
            array(
90
                "Database Configuration",
91
                "Database server",
92
                $usePath
93
                    ? "I couldn't write to path '{$path}'"
94
                    : "I couldn't find a database server on '{$server}'",
95
                $usePath
96
                    ? $path
97
                    : $server
98
            )
99
        )
100
        ) {
101
            return false;
102
        }
103
104
        // Check if the connection credentials allow access to the server / database
105
        if (!$this->requireDatabaseConnection(
106
            $databaseConfig,
107
            array(
108
                "Database Configuration",
109
                "Database access credentials",
110
                "That username/password doesn't work"
111
            )
112
        )
113
        ) {
114
            return false;
115
        }
116
117
        // Check the necessary server version is available
118
        if (!$this->requireDatabaseVersion(
119
            $databaseConfig,
120
            array(
121
                "Database Configuration",
122
                "Database server version requirement",
123
                '',
124
                'Version ' . $this->getDatabaseConfigurationHelper($databaseConfig['type'])
125
                    ->getDatabaseVersion($databaseConfig)
126
            )
127
        )
128
        ) {
129
            return false;
130
        }
131
132
        // Check that database creation permissions are available
133
        if (!$this->requireDatabaseOrCreatePermissions(
134
            $databaseConfig,
135
            array(
136
                "Database Configuration",
137
                "Can I access/create the database",
138
                "I can't create new databases and the database '$databaseConfig[database]' doesn't exist"
139
            )
140
        )
141
        ) {
142
            return false;
143
        }
144
145
        // Check alter permission (necessary to create tables etc)
146
        if (!$this->requireDatabaseAlterPermissions(
147
            $databaseConfig,
148
            array(
149
                "Database Configuration",
150
                "Can I ALTER tables",
151
                "I don't have permission to ALTER tables"
152
            )
153
        )
154
        ) {
155
            return false;
156
        }
157
158
        // Success!
159
        return true;
160
    }
161
162
    public function checkAdminConfig($adminConfig)
163
    {
164
        if (!$adminConfig['username']) {
165
            $this->error(array('', 'Please enter a username!'));
166
        }
167
        if (!$adminConfig['password']) {
168
            $this->error(array('', 'Please enter a password!'));
169
        }
170
    }
171
172
    /**
173
     * Check everything except the database
174
     *
175
     * @param array $originalIni
176
     * @return array
177
     */
178
    public function check($originalIni)
179
    {
180
        $this->originalIni = $originalIni;
181
        $this->errors = [];
182
        $isApache = $this->isApache();
183
        $isIIS = $this->isIIS();
184
        $webserver = $this->findWebserver();
185
186
        // Get project dirs to inspect
187
        $projectDir = $this->getProjectDir();
188
        $projectSrcDir = $this->getProjectSrcDir();
189
190
        $this->requirePHPVersion('7.1.0', '7.1.0', [
191
            "PHP Configuration",
192
            "PHP5 installed",
193
            null,
194
            "PHP version " . phpversion()
195
        ]);
196
197
        // Check that we can identify the root folder successfully
198
        $this->requireFile('vendor/silverstripe/framework/src/Dev/Install/config-form.html', array(
199
            "File permissions",
200
            "Does the webserver know where files are stored?",
201
            "The webserver isn't letting me identify where files are stored.",
202
            $this->getBaseDir()
203
        ));
204
205
        $this->requireModule($projectDir, [
206
            "File permissions",
207
            "{$projectDir}/ directory exists?",
208
            ''
209
        ]);
210
        $this->requireModule('vendor/silverstripe/framework', array(
211
            "File permissions",
212
            "vendor/silverstripe/framework/ directory exists?",
213
            '',
214
        ));
215
216
217
        $this->requireWriteable(
218
            $this->getPublicDir() . 'index.php',
219
            ["File permissions", "Is the index.php file writeable?", null],
220
            true
221
        );
222
223
        $this->requireWriteable(
224
            '.env',
225
            ["File permissions", "Is the .env file writeable?", null],
226
            false,
227
            false
228
        );
229
230
        if ($isApache) {
231
            $this->checkApacheVersion(array(
232
                "Webserver Configuration",
233
                "Webserver is not Apache 1.x",
234
                "SilverStripe requires Apache version 2 or greater",
235
                $webserver
236
            ));
237
            $this->requireWriteable(
238
                $this->getPublicDir() . '.htaccess',
239
                ["File permissions", "Is the .htaccess file writeable?", null],
240
                true
241
            );
242
        } elseif ($isIIS) {
243
            $this->requireWriteable(
244
                $this->getPublicDir() . 'web.config',
245
                ["File permissions", "Is the web.config file writeable?", null],
246
                true
247
            );
248
        }
249
250
        $this->requireWriteable("{$projectDir}/_config.php", [
251
            "File permissions",
252
            "Is the {$projectDir}/_config.php file writeable?",
253
            null,
254
        ]);
255
256
        $this->requireWriteable("{$projectDir}/_config/theme.yml", [
257
            "File permissions",
258
            "Is the {$projectDir}/_config/theme.yml file writeable?",
259
            null,
260
        ]);
261
262
        if (!$this->checkModuleExists('cms')) {
263
            $this->requireWriteable("{$projectSrcDir}/RootURLController.php", [
264
                "File permissions",
265
                "Is the {$projectSrcDir}/RootURLController.php file writeable?",
266
                null,
267
            ]);
268
        }
269
270
        // Check public folder exists
271
        $this->requireFile(
272
            'public',
273
            [
274
                'File permissions',
275
                'Is there a public/ directory?',
276
                'It is recommended to have a separate public/ web directory',
277
            ],
278
            false,
279
            false
280
        );
281
282
        // Ensure root assets dir is writable
283
        $this->requireWriteable(
284
            ASSETS_PATH,
285
            ["File permissions", "Is the assets/ directory writeable?", null],
286
            true
287
        );
288
289
        // Ensure all assets files are writable
290
        $innerIterator = new RecursiveDirectoryIterator(ASSETS_PATH, RecursiveDirectoryIterator::SKIP_DOTS);
291
        $iterator = new RecursiveIteratorIterator($innerIterator, RecursiveIteratorIterator::SELF_FIRST);
292
        /** @var SplFileInfo $file */
293
        foreach ($iterator as $file) {
294
            // Only report file as error if not writable
295
            if ($file->isWritable()) {
296
                continue;
297
            }
298
            $relativePath = substr($file->getPathname(), strlen($this->getBaseDir()));
299
            $message = $file->isDir()
300
                ? "Is the {$relativePath} directory writeable?"
301
                : "Is the {$relativePath} file writeable?";
302
            $this->requireWriteable($relativePath, array("File permissions", $message, null));
303
        }
304
305
        try {
306
            $tempFolder = TempFolder::getTempFolder($this->getBaseDir());
307
        } catch (Exception $e) {
308
            $tempFolder = false;
309
        }
310
311
        $this->requireTempFolder(array('File permissions', 'Is a temporary directory available?', null, $tempFolder));
312
        if ($tempFolder) {
313
            // in addition to the temp folder being available, check it is writable
314
            $this->requireWriteable($tempFolder, array(
315
                "File permissions",
316
                sprintf("Is the temporary directory writeable?", $tempFolder),
317
                null
318
            ), true);
319
        }
320
321
        // Check for web server, unless we're calling the installer from the command-line
322
        $this->isRunningWebServer(array("Webserver Configuration", "Server software", "Unknown", $webserver));
323
324
        if ($isApache) {
325
            $this->requireApacheRewriteModule('mod_rewrite', array(
326
                "Webserver Configuration",
327
                "URL rewriting support",
328
                "You need mod_rewrite to use friendly URLs with SilverStripe, but it is not enabled."
329
            ));
330
        } elseif ($isIIS) {
331
            $this->requireIISRewriteModule('IIS_UrlRewriteModule', array(
332
                "Webserver Configuration",
333
                "URL rewriting support",
334
                "You need to enable the IIS URL Rewrite Module to use friendly URLs with SilverStripe, "
335
                . "but it is not installed or enabled. Download it for IIS 7 from http://www.iis.net/expand/URLRewrite"
336
            ));
337
        } else {
338
            $this->warning([
339
                "Webserver Configuration",
340
                "URL rewriting support",
341
                "I can't tell whether any rewriting module is running. You may need to configure a rewriting "
342
                . "rule yourself."
343
            ]);
344
        }
345
346
        $this->requireServerVariables(array('SCRIPT_NAME', 'HTTP_HOST', 'SCRIPT_FILENAME'), array(
347
            "Webserver Configuration",
348
            "Recognised webserver",
349
            "You seem to be using an unsupported webserver.  "
350
            . "The server variables SCRIPT_NAME, HTTP_HOST, SCRIPT_FILENAME need to be set."
351
        ));
352
353
        $this->requirePostSupport(array(
354
            "Webserver Configuration",
355
            "POST Support",
356
            'I can\'t find $_POST, make sure POST is enabled.'
357
        ));
358
359
        // Check for GD support
360
        if (!$this->requireFunction("imagecreatetruecolor", array(
361
            "PHP Configuration",
362
            "GD2 support",
363
            "PHP must have GD version 2."
364
        ))
365
        ) {
366
            $this->requireFunction("imagecreate", array(
367
                "PHP Configuration",
368
                "GD2 support",
369
                "GD support for PHP not included."
370
            ));
371
        }
372
373
        // Check for XML support
374
        $this->requireFunction('xml_set_object', array(
375
            "PHP Configuration",
376
            "XML support",
377
            "XML support not included in PHP."
378
        ));
379
        $this->requireClass('DOMDocument', array(
380
            "PHP Configuration",
381
            "DOM/XML support",
382
            "DOM/XML support not included in PHP."
383
        ));
384
        $this->requireFunction('simplexml_load_file', array(
385
            'PHP Configuration',
386
            'SimpleXML support',
387
            'SimpleXML support not included in PHP.'
388
        ));
389
390
        // Check for token_get_all
391
        $this->requireFunction('token_get_all', array(
392
            "PHP Configuration",
393
            "Tokenizer support",
394
            "Tokenizer support not included in PHP."
395
        ));
396
397
        // Check for CType support
398
        $this->requireFunction('ctype_digit', array(
399
            'PHP Configuration',
400
            'CType support',
401
            'CType support not included in PHP.'
402
        ));
403
404
        // Check for session support
405
        $this->requireFunction('session_start', array(
406
            'PHP Configuration',
407
            'Session support',
408
            'Session support not included in PHP.'
409
        ));
410
411
        // Check for iconv support
412
        $this->requireFunction('iconv', array(
413
            'PHP Configuration',
414
            'iconv support',
415
            'iconv support not included in PHP.'
416
        ));
417
418
        // Check for hash support
419
        $this->requireFunction(
420
            'hash',
421
            ['PHP Configuration', 'hash support', 'hash support not included in PHP.']
422
        );
423
424
        // Check for mbstring support
425
        $this->requireFunction('mb_internal_encoding', array(
426
            'PHP Configuration',
427
            'mbstring support',
428
            'mbstring support not included in PHP.'
429
        ));
430
431
        // Check for Reflection support
432
        $this->requireClass('ReflectionClass', array(
433
            'PHP Configuration',
434
            'Reflection support',
435
            'Reflection support not included in PHP.'
436
        ));
437
438
        // Check for Standard PHP Library (SPL) support
439
        $this->requireFunction('spl_classes', array(
440
            'PHP Configuration',
441
            'SPL support',
442
            'Standard PHP Library (SPL) not included in PHP.'
443
        ));
444
445
        $this->requireDateTimezone(array(
446
            'PHP Configuration',
447
            'date.timezone setting and validity',
448
            'date.timezone option in php.ini must be set correctly.',
449
            $this->getOriginalIni('date.timezone')
450
        ));
451
452
        $this->suggestClass('finfo', array(
453
            'PHP Configuration',
454
            'fileinfo support',
455
            'fileinfo should be enabled in PHP. SilverStripe uses it for MIME type detection of files. '
456
            . 'SilverStripe will still operate, but email attachments and sending files to browser '
457
            . '(e.g. export data to CSV) may not work correctly without finfo.'
458
        ));
459
460
        $this->suggestFunction('curl_init', array(
461
            'PHP Configuration',
462
            'curl support',
463
            'curl should be enabled in PHP. SilverStripe uses it for consuming web services'
464
            . ' via the RestfulService class and many modules rely on it.'
465
        ));
466
467
        $this->suggestClass('tidy', array(
468
            'PHP Configuration',
469
            'tidy support',
470
            'Tidy provides a library of code to clean up your html. '
471
            . 'SilverStripe will operate fine without tidy but HTMLCleaner will not be effective.'
472
        ));
473
474
        $this->suggestPHPSetting('asp_tags', array(false), array(
475
            'PHP Configuration',
476
            'asp_tags option',
477
            'This should be turned off as it can cause issues with SilverStripe'
478
        ));
479
        $this->requirePHPSetting('magic_quotes_gpc', array(false), array(
480
            'PHP Configuration',
481
            'magic_quotes_gpc option',
482
            'This should be turned off, as it can cause issues with cookies. '
483
            . 'More specifically, unserializing data stored in cookies.'
484
        ));
485
        $this->suggestPHPSetting('display_errors', array(false), array(
486
            'PHP Configuration',
487
            'display_errors option',
488
            'Unless you\'re in a development environment, this should be turned off, '
489
            . 'as it can expose sensitive data to website users.'
490
        ));
491
        // on some weirdly configured webservers arg_separator.output is set to &amp;
492
        // which will results in links like ?param=value&amp;foo=bar which will not be i
493
        $this->suggestPHPSetting('arg_separator.output', array('&', ''), array(
494
            'PHP Configuration',
495
            'arg_separator.output option',
496
            'This option defines how URL parameters are concatenated. '
497
            . 'If not set to \'&\' this may cause issues with URL GET parameters'
498
        ));
499
500
        // Check memory allocation
501
        $this->requireMemory(32 * 1024 * 1024, 64 * 1024 * 1024, array(
502
            "PHP Configuration",
503
            "Memory allocation (PHP config option 'memory_limit')",
504
            "SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.",
505
            $this->getOriginalIni("memory_limit")
506
        ));
507
508
        return $this->errors;
509
    }
510
511
    /**
512
     * Get ini setting
513
     *
514
     * @param string $settingName
515
     * @return mixed
516
     */
517
    protected function getOriginalIni($settingName)
518
    {
519
        if (isset($this->originalIni[$settingName])) {
520
            return $this->originalIni[$settingName];
521
        }
522
        return ini_get($settingName);
523
    }
524
525
    public function suggestPHPSetting($settingName, $settingValues, $testDetails)
526
    {
527
        $this->testing($testDetails);
528
529
        // special case for display_errors, check the original value before
530
        // it was changed at the start of this script.
531
        $val = $this->getOriginalIni($settingName);
532
533
        if (!in_array($val, $settingValues) && $val != $settingValues) {
534
            $this->warning($testDetails, "$settingName is set to '$val' in php.ini.  $testDetails[2]");
535
        }
536
    }
537
538
    public function requirePHPSetting($settingName, $settingValues, $testDetails)
539
    {
540
        $this->testing($testDetails);
541
542
        $val = $this->getOriginalIni($settingName);
543
        if (!in_array($val, $settingValues) && $val != $settingValues) {
544
            $this->error($testDetails, "$settingName is set to '$val' in php.ini.  $testDetails[2]");
545
        }
546
    }
547
548
    public function suggestClass($class, $testDetails)
549
    {
550
        $this->testing($testDetails);
551
552
        if (!class_exists($class)) {
553
            $this->warning($testDetails);
554
        }
555
    }
556
557
    public function suggestFunction($class, $testDetails)
558
    {
559
        $this->testing($testDetails);
560
561
        if (!function_exists($class)) {
562
            $this->warning($testDetails);
563
        }
564
    }
565
566
    public function requireDateTimezone($testDetails)
567
    {
568
        $this->testing($testDetails);
569
        $val = $this->getOriginalIni('date.timezone');
570
        $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

570
        $result = $val && in_array($val, /** @scrutinizer ignore-type */ timezone_identifiers_list());
Loading history...
571
        if (!$result) {
572
            $this->error($testDetails);
573
        }
574
    }
575
576
    public function requireMemory($min, $recommended, $testDetails)
577
    {
578
        $_SESSION['forcemem'] = false;
579
580
        $mem = $this->getPHPMemory();
581
        $memLimit = $this->getOriginalIni("memory_limit");
582
        if ($mem < (64 * 1024 * 1024)) {
583
            ini_set('memory_limit', '64M');
584
            $mem = $this->getPHPMemory();
585
            $testDetails[3] = $memLimit;
586
        }
587
588
        $this->testing($testDetails);
589
590
        if ($mem < $min && $mem > 0) {
591
            $message = $testDetails[2] . " You only have " . $memLimit . " allocated";
592
            $this->error($testDetails, $message);
593
            return false;
594
        } elseif ($mem < $recommended && $mem > 0) {
595
            $message = $testDetails[2] . " You only have " . $memLimit . " allocated";
596
            $this->warning($testDetails, $message);
597
            return false;
598
        } elseif ($mem == 0) {
599
            $message = $testDetails[2] . " We can't determine how much memory you have allocated. "
600
                . "Install only if you're sure you've allocated at least 20 MB.";
601
            $this->warning($testDetails, $message);
602
            return false;
603
        }
604
        return true;
605
    }
606
607
    public function getPHPMemory()
608
    {
609
        $memString = $this->getOriginalIni("memory_limit");
610
611
        switch (strtolower(substr($memString, -1))) {
612
            case "k":
613
                return round(substr($memString, 0, -1) * 1024);
614
615
            case "m":
616
                return round(substr($memString, 0, -1) * 1024 * 1024);
617
618
            case "g":
619
                return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
620
621
            default:
622
                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

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

911
    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...
912
    {
913
        $this->testing($testDetails);
914
        if ($this->testApacheRewriteExists()) {
915
            return true;
916
        } else {
917
            $this->error($testDetails);
918
            return false;
919
        }
920
    }
921
922
    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

922
    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...
923
    {
924
        $this->testing($testDetails);
925
        if ($this->testIISRewriteModuleExists()) {
926
            return true;
927
        } else {
928
            $this->warning($testDetails);
929
            return false;
930
        }
931
    }
932
933
    public function requireDatabaseFunctions($databaseConfig, $testDetails)
934
    {
935
        $this->testing($testDetails);
936
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
937
        if (!$helper) {
0 ignored issues
show
introduced by
$helper is of type SilverStripe\Dev\Install...baseConfigurationHelper, thus it always evaluated to true.
Loading history...
938
            $this->error($testDetails, "Couldn't load database helper code for " . $databaseConfig['type']);
939
            return false;
940
        }
941
        $result = $helper->requireDatabaseFunctions($databaseConfig);
942
        if ($result) {
943
            return true;
944
        } else {
945
            $this->error($testDetails);
946
            return false;
947
        }
948
    }
949
950
    public function requireDatabaseConnection($databaseConfig, $testDetails)
951
    {
952
        $this->testing($testDetails);
953
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
954
        $result = $helper->requireDatabaseConnection($databaseConfig);
955
        if ($result['success']) {
956
            return true;
957
        } else {
958
            $testDetails[2] .= ": " . $result['error'];
959
            $this->error($testDetails);
960
            return false;
961
        }
962
    }
963
964
    public function requireDatabaseVersion($databaseConfig, $testDetails)
965
    {
966
        $this->testing($testDetails);
967
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
968
        if (method_exists($helper, 'requireDatabaseVersion')) {
969
            $result = $helper->requireDatabaseVersion($databaseConfig);
970
            if ($result['success']) {
971
                return true;
972
            } else {
973
                $testDetails[2] .= $result['error'];
974
                $this->warning($testDetails);
975
                return false;
976
            }
977
        }
978
        // Skipped test because this database has no required version
979
        return true;
980
    }
981
982
    public function requireDatabaseServer($databaseConfig, $testDetails)
983
    {
984
        $this->testing($testDetails);
985
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
986
        $result = $helper->requireDatabaseServer($databaseConfig);
987
        if ($result['success']) {
988
            return true;
989
        } else {
990
            $message = $testDetails[2] . ": " . $result['error'];
991
            $this->error($testDetails, $message);
992
            return false;
993
        }
994
    }
995
996
    public function requireDatabaseOrCreatePermissions($databaseConfig, $testDetails)
997
    {
998
        $this->testing($testDetails);
999
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
1000
        $result = $helper->requireDatabaseOrCreatePermissions($databaseConfig);
1001
        if ($result['success']) {
1002
            if ($result['alreadyExists']) {
1003
                $testDetails[3] = "Database $databaseConfig[database]";
1004
            } else {
1005
                $testDetails[3] = "Able to create a new database";
1006
            }
1007
            $this->testing($testDetails);
1008
            return true;
1009
        } else {
1010
            if (empty($result['cannotCreate'])) {
1011
                $message = $testDetails[2] . ". Please create the database manually.";
1012
            } else {
1013
                $message = $testDetails[2] . " (user '$databaseConfig[username]' doesn't have CREATE "
1014
                    . "DATABASE permissions.)";
1015
            }
1016
1017
            $this->error($testDetails, $message);
1018
            return false;
1019
        }
1020
    }
1021
1022
    public function requireDatabaseAlterPermissions($databaseConfig, $testDetails)
1023
    {
1024
        $this->testing($testDetails);
1025
        $helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
1026
        $result = $helper->requireDatabaseAlterPermissions($databaseConfig);
1027
        if ($result['success']) {
1028
            return true;
1029
        } else {
1030
            $message = "Silverstripe cannot alter tables. This won't prevent installation, however it may "
1031
                . "cause issues if you try to run a /dev/build once installed.";
1032
            $this->warning($testDetails, $message);
1033
            return false;
1034
        }
1035
    }
1036
1037
    public function requireServerVariables($varNames, $testDetails)
1038
    {
1039
        $this->testing($testDetails);
1040
        $missing = array();
1041
1042
        foreach ($varNames as $varName) {
1043
            if (!isset($_SERVER[$varName]) || !$_SERVER[$varName]) {
1044
                $missing[] = '$_SERVER[' . $varName . ']';
1045
            }
1046
        }
1047
1048
        if (!$missing) {
1049
            return true;
1050
        }
1051
1052
        $message = $testDetails[2] . " (the following PHP variables are missing: " . implode(", ", $missing) . ")";
1053
        $this->error($testDetails, $message);
1054
        return false;
1055
    }
1056
1057
1058
    public function requirePostSupport($testDetails)
1059
    {
1060
        $this->testing($testDetails);
1061
1062
        if (!isset($_POST)) {
1063
            $this->error($testDetails);
1064
1065
            return false;
1066
        }
1067
1068
        return true;
1069
    }
1070
1071
    public function isRunningWebServer($testDetails)
1072
    {
1073
        $this->testing($testDetails);
1074
        if ($testDetails[3]) {
1075
            return true;
1076
        } else {
1077
            $this->warning($testDetails);
1078
            return false;
1079
        }
1080
    }
1081
1082
    public function testing($testDetails)
1083
    {
1084
        if (!$testDetails) {
1085
            return;
1086
        }
1087
1088
        $section = $testDetails[0];
1089
        $test = $testDetails[1];
1090
1091
        $message = "OK";
1092
        if (isset($testDetails[3])) {
1093
            $message .= " ($testDetails[3])";
1094
        }
1095
1096
        $this->tests[$section][$test] = array("good", $message);
1097
    }
1098
1099
    public function error($testDetails, $message = null)
1100
    {
1101
        if (!is_array($testDetails)) {
1102
            throw new InvalidArgumentException("Invalid error");
1103
        }
1104
        $section = $testDetails[0];
1105
        $test = $testDetails[1];
1106
        if (!$message && isset($testDetails[2])) {
1107
            $message = $testDetails[2];
1108
        }
1109
1110
        $this->tests[$section][$test] = array("error", $message);
1111
        $this->errors[] = $testDetails;
1112
    }
1113
1114
    public function warning($testDetails, $message = null)
1115
    {
1116
        if (!is_array($testDetails)) {
1117
            throw new InvalidArgumentException("Invalid warning");
1118
        }
1119
        $section = $testDetails[0];
1120
        $test = $testDetails[1];
1121
        if (!$message && isset($testDetails[2])) {
1122
            $message = $testDetails[2];
1123
        }
1124
1125
        $this->tests[$section][$test] = array("warning", $message);
1126
        $this->warnings[] = $testDetails;
1127
    }
1128
1129
    public function hasErrors()
1130
    {
1131
        return sizeof($this->errors);
1132
    }
1133
1134
    public function hasWarnings()
1135
    {
1136
        return sizeof($this->warnings);
1137
    }
1138
}
1139