Completed
Push — 4.0 ( b59aea...80f83b )
by Loz
52s queued 21s
created

Installer::statusMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev\Install;
4
5
use Exception;
6
use SilverStripe\Control\Cookie;
7
use SilverStripe\Control\HTTPApplication;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Control\HTTPRequestBuilder;
10
use SilverStripe\Core\CoreKernel;
11
use SilverStripe\Core\EnvironmentLoader;
12
use SilverStripe\Core\Kernel;
13
use SilverStripe\Core\Startup\ParameterConfirmationToken;
14
use SilverStripe\ORM\DatabaseAdmin;
15
use SilverStripe\Security\DefaultAdminService;
16
use SilverStripe\Security\Security;
17
18
/**
19
 * This installer doesn't use any of the fancy SilverStripe stuff in case it's unsupported.
20
 */
21
class Installer extends InstallRequirements
22
{
23
    /**
24
     * value='' attribute placeholder for password fields
25
     */
26
    const PASSWORD_PLACEHOLDER = '********';
27
28
    protected function installHeader()
29
    {
30
        ?>
31
        <html>
32
        <head>
33
            <meta charset="utf-8"/>
34
            <title>Installing SilverStripe...</title>
35
            <link rel="stylesheet" type="text/css"
36
                  href="resources/silverstripe/framework/src/Dev/Install/client/styles/install.css"/>
37
            <script src="//code.jquery.com/jquery-1.7.2.min.js"></script>
38
        </head>
39
        <body>
40
        <div class="install-header">
41
            <div class="inner">
42
                <div class="brand">
43
                    <span class="logo"></span>
44
45
                    <h1>SilverStripe</h1>
46
                </div>
47
            </div>
48
        </div>
49
50
        <div id="Navigation">&nbsp;</div>
51
        <div class="clear"><!-- --></div>
52
53
        <div class="main">
54
            <div class="inner">
55
                <h2>Installing SilverStripe...</h2>
56
57
                <p>I am now running through the installation steps (this should take about 30 seconds)</p>
58
59
                <p>If you receive a fatal error, refresh this page to continue the installation</p>
60
                <ul>
61
        <?php
62
    }
63
64
    public function install($config)
65
    {
66
        // Render header
67
        $this->installHeader();
68
69
        $isIIS = $this->isIIS();
70
        $isApache = $this->isApache();
71
72
        flush();
73
74
        // Send install stats
75
        if (!empty($config['stats'])) {
76
            $this->sendInstallStats($config);
77
        }
78
79
        // Cleanup _config.php
80
        if (file_exists('mysite/_config.php')) {
81
            // Truncate the contents of _config instead of deleting it - we can't re-create it because Windows handles permissions slightly
82
            // differently to UNIX based filesystems - it takes the permissions from the parent directory instead of retaining them
83
            $fh = fopen('mysite/_config.php', 'wb');
84
            fclose($fh);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

84
            fclose(/** @scrutinizer ignore-type */ $fh);
Loading history...
85
        }
86
87
        // Write all files
88
        $this->writeIndexPHP();
89
        $this->writeConfigPHP($config);
90
        $this->writeConfigYaml($config);
91
        $this->writeConfigEnv($config);
92
93
        // Write other stuff
94
        if (!$this->checkModuleExists('cms')) {
95
            $this->writeToFile("mysite/code/RootURLController.php", <<<PHP
96
<?php
97
98
use SilverStripe\\Control\\Controller;
99
100
class RootURLController extends Controller {
101
102
    public function index() {
103
        echo "<html>Your site is now set up. Start adding controllers to mysite to get started.</html>";
104
    }
105
106
}
107
PHP
108
            );
109
        }
110
111
        // Write the appropriate web server configuration file for rewriting support
112
        if ($this->hasRewritingCapability()) {
113
            if ($isApache) {
114
                $this->createHtaccess();
115
            } elseif ($isIIS) {
116
                $this->createWebConfig();
117
            }
118
        }
119
120
        // Build request
121
        $request = HTTPRequestBuilder::createFromEnvironment();
122
123
        // Install kernel (fix to dev)
124
        $kernel = new CoreKernel(BASE_PATH);
125
        $kernel->setEnvironment(Kernel::DEV);
126
        $app = new HTTPApplication($kernel);
127
128
        // Build db within HTTPApplication
129
        $app->execute($request, function (HTTPRequest $request) use ($config) {
130
            // Suppress cookie errors on install
131
            Cookie::config()->set('report_errors', false);
132
133
            // Start session and execute
134
            $request->getSession()->init($request);
135
136
            // Output status
137
            $this->statusMessage("Building database schema...");
138
139
            // Setup DB
140
            $dbAdmin = new DatabaseAdmin();
141
            $dbAdmin->setRequest($request);
142
            $dbAdmin->pushCurrent();
143
            $dbAdmin->doInit();
144
            $dbAdmin->doBuild(true);
145
146
            // Create default administrator user and group in database
147
            // (not using Security::setDefaultAdmin())
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
148
            $username = $config['admin']['username'];
149
            $password = $config['admin']['password'];
150
            $adminMember = DefaultAdminService::singleton()
151
                ->findOrCreateAdmin(
152
                    $username,
153
                    _t('SilverStripe\\Security\\DefaultAdminService.DefaultAdminFirstname', 'Default Admin')
154
                );
155
            $adminMember->Email = $username;
156
            $adminMember->Password = $password;
157
            $adminMember->PasswordEncryption = Security::config()->get('encryption_algorithm');
158
159
            try {
160
                $this->statusMessage('Creating default CMS admin account...');
161
                $adminMember->write();
162
            } catch (Exception $e) {
163
                $this->statusMessage(
164
                    sprintf('Warning: Default CMS admin account could not be created (error: %s)', $e->getMessage())
165
                );
166
            }
167
168
            $request->getSession()->set('username', $username);
169
            $request->getSession()->set('password', $password);
170
            $request->getSession()->save($request);
171
        }, true);
172
173
        // Check result of install
174
        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...
175
            if (isset($_SERVER['HTTP_HOST']) && $this->hasRewritingCapability()) {
176
                $this->statusMessage("Checking that friendly URLs work...");
177
                $this->checkRewrite();
178
            } else {
179
                $token = new ParameterConfirmationToken('flush', $request);
180
                $params = http_build_query($token->params());
181
182
                $destinationURL = 'index.php/' .
183
                    ($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params");
184
185
                echo <<<HTML
186
                <li>SilverStripe successfully installed; I am now redirecting you to your SilverStripe site...</li>
187
                <script>
188
                    setTimeout(function() {
189
                        window.location = "$destinationURL";
190
                    }, 2000);
191
                </script>
192
                <noscript>
193
                <li><a href="$destinationURL">Click here to access your site.</a></li>
194
                </noscript>
195
HTML;
196
            }
197
        }
198
199
        return $this->errors;
200
    }
201
202
    protected function writeIndexPHP()
203
    {
204
        $content = <<<'PHP'
205
<?php
206
207
use SilverStripe\Control\HTTPApplication;
208
use SilverStripe\Control\HTTPRequestBuilder;
209
use SilverStripe\Core\CoreKernel;
210
use SilverStripe\Core\Startup\ErrorControlChainMiddleware;
211
212
require __DIR__ . '/vendor/autoload.php';
213
214
// Build request and detect flush
215
$request = HTTPRequestBuilder::createFromEnvironment();
216
217
// Default application
218
$kernel = new CoreKernel(BASE_PATH);
219
$app = new HTTPApplication($kernel);
220
$app->addMiddleware(new ErrorControlChainMiddleware($app));
221
$response = $app->handle($request);
222
$response->output();
223
PHP;
224
        $this->writeToFile('index.php', $content);
225
    }
226
227
    /**
228
     * Write all .env files
229
     *
230
     * @param $config
231
     */
232
    protected function writeConfigEnv($config)
233
    {
234
        if (!$config['usingEnv']) {
235
            return;
236
        }
237
238
        $path = $this->getBaseDir() . '.env';
239
        $vars = [];
240
241
        // Retain existing vars
242
        $env = new EnvironmentLoader();
243
        if (file_exists($path)) {
244
            $vars = $env->loadFile($path) ?: [];
245
        }
246
247
        // Set base URL
248
        if (!isset($vars['SS_BASE_URL']) && isset($_SERVER['HTTP_HOST'])) {
249
            $vars['SS_BASE_URL'] = 'http://' . $_SERVER['HTTP_HOST'] . BASE_URL;
250
        }
251
252
        // Set DB env
253
        if (empty($config['db']['database'])) {
254
            $vars['SS_DATABASE_CHOOSE_NAME'] = true;
255
        } else {
256
            $vars['SS_DATABASE_NAME'] = $config['db']['database'];
257
        }
258
        $vars['SS_DATABASE_CLASS'] = $config['db']['type'];
259
        if (isset($config['db']['server'])) {
260
            $vars['SS_DATABASE_SERVER'] = $config['db']['server'];
261
        }
262
        if (isset($config['db']['username'])) {
263
            $vars['SS_DATABASE_USERNAME'] = $config['db']['username'];
264
        }
265
        if (isset($config['db']['password'])) {
266
            $vars['SS_DATABASE_PASSWORD'] = $config['db']['password'];
267
        }
268
        if (isset($config['db']['path'])) {
269
            $vars['SS_DATABASE_PATH'] = $config['db']['path'];
270
            // sqlite compat
271
            $vars['SS_SQLITE_DATABASE_PATH'] = $config['db']['path'];
272
        }
273
        if (isset($config['db']['key'])) {
274
            $vars['SS_DATABASE_KEY'] = $config['db']['key'];
275
            // sqlite compat
276
            $vars['SS_SQLITE_DATABASE_KEY'] = $config['db']['key'];
277
        }
278
279
        // Write all env vars
280
        $lines = [
281
            '# Generated by SilverStripe Installer'
282
        ];
283
        ksort($vars);
284
        foreach ($vars as $key => $value) {
285
            $lines[] = $key . '="' . addcslashes($value, '"') . '"';
286
        }
287
288
        $this->writeToFile('.env', implode("\n", $lines));
289
290
        // Re-load env vars for installer access
291
        $env->loadFile($path);
292
    }
293
294
    /**
295
     * Write all *.php files
296
     *
297
     * @param array $config
298
     */
299
    protected function writeConfigPHP($config)
300
    {
301
        if ($config['usingEnv']) {
302
            $this->writeToFile("mysite/_config.php", "<?php\n ");
303
            return;
304
        }
305
306
        // Create databaseConfig
307
        $lines = [];
308
        foreach ($config['db'] as $key => $value) {
309
            $lines[] = sprintf(
310
                "    '%s' => '%s'",
311
                addslashes($key),
312
                addslashes($value)
313
            );
314
        }
315
        $databaseConfigContent = implode(",\n", $lines);
316
        $this->writeToFile("mysite/_config.php", <<<PHP
317
<?php
318
319
use SilverStripe\\ORM\\DB;
320
321
DB::setConfig([
322
{$databaseConfigContent}
323
]);
324
325
PHP
326
        );
327
    }
328
329
    /**
330
     * Write yml files
331
     *
332
     * @param array $config
333
     */
334
    protected function writeConfigYaml($config)
335
    {
336
        // Escape user input for safe insertion into PHP file
337
        $locale = $this->ymlString($config['locale']);
338
339
        // Set either specified, or no theme
340
        if ($config['theme'] && $config['theme'] !== 'tutorial') {
341
            $theme = $this->ymlString($config['theme']);
342
            $themeYML = <<<YML
343
    - '$theme'
344
    - '\$default'
345
YML;
346
        } else {
347
            $themeYML = <<<YML
348
    - '\$default'
349
YML;
350
        }
351
352
        // Write theme.yml
353
        $this->writeToFile("mysite/_config/theme.yml", <<<YML
354
---
355
Name: mytheme
356
---
357
SilverStripe\\View\\SSViewer:
358
  themes:
359
$themeYML
360
SilverStripe\\i18n\\i18n:
361
  default_locale: '$locale'
362
YML
363
        );
364
    }
365
366
    /**
367
     * Escape yml string
368
     *
369
     * @param string $string
370
     * @return mixed
371
     */
372
    protected function ymlString($string)
373
    {
374
        // just escape single quotes using ''
375
        return str_replace("'", "''", $string);
376
    }
377
378
    /**
379
     * Write file to given location
380
     *
381
     * @param $filename
382
     * @param $content
383
     * @return bool
384
     */
385
    public function writeToFile($filename, $content)
386
    {
387
        $base = $this->getBaseDir();
388
        $this->statusMessage("Setting up $base$filename");
389
390
        if ((@$fh = fopen($base . $filename, 'wb')) && fwrite($fh, $content) && fclose($fh)) {
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, 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

390
        if ((@$fh = fopen($base . $filename, 'wb')) && fwrite(/** @scrutinizer ignore-type */ $fh, $content) && fclose($fh)) {
Loading history...
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

390
        if ((@$fh = fopen($base . $filename, 'wb')) && fwrite($fh, $content) && fclose(/** @scrutinizer ignore-type */ $fh)) {
Loading history...
391
            // Set permissions to writable
392
            @chmod($base . $filename, 0775);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

392
            /** @scrutinizer ignore-unhandled */ @chmod($base . $filename, 0775);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
393
            return true;
394
        }
395
        $this->error("Couldn't write to file $base$filename");
396
        return false;
397
    }
398
399
    /**
400
     * Ensure root .htaccess is setup
401
     */
402
    public function createHtaccess()
403
    {
404
        $start = "### SILVERSTRIPE START ###\n";
405
        $end = "\n### SILVERSTRIPE END ###";
406
407
        $base = dirname($_SERVER['SCRIPT_NAME']);
408
        if (defined('DIRECTORY_SEPARATOR')) {
409
            $base = str_replace(DIRECTORY_SEPARATOR, '/', $base);
410
        } else {
411
            $base = str_replace("\\", '/', $base);
412
        }
413
414
        if ($base != '.') {
415
            $baseClause = "RewriteBase '$base'\n";
416
        } else {
417
            $baseClause = "";
418
        }
419
        if (strpos(strtolower(php_sapi_name()), "cgi") !== false) {
420
            $cgiClause = "RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n";
421
        } else {
422
            $cgiClause = "";
423
        }
424
        $rewrite = <<<TEXT
425
# Deny access to templates (but allow from localhost)
426
<Files *.ss>
427
    Order deny,allow
428
    Deny from all
429
    Allow from 127.0.0.1
430
</Files>
431
432
# Deny access to IIS configuration
433
<Files web.config>
434
    Order deny,allow
435
    Deny from all
436
</Files>
437
438
# Deny access to YAML configuration files which might include sensitive information
439
<Files ~ "\.ya?ml$">
440
    Order allow,deny
441
    Deny from all
442
</Files>
443
444
# Route errors to static pages automatically generated by SilverStripe
445
ErrorDocument 404 /assets/error-404.html
446
ErrorDocument 500 /assets/error-500.html
447
448
<IfModule mod_rewrite.c>
449
450
    # Turn off index.php handling requests to the homepage fixes issue in apache >=2.4
451
    <IfModule mod_dir.c>
452
        DirectoryIndex disabled
453
        DirectorySlash On
454
    </IfModule>
455
456
    SetEnv HTTP_MOD_REWRITE On
457
    RewriteEngine On
458
    $baseClause
459
    $cgiClause
460
461
    # Deny access to potentially sensitive files and folders
462
    RewriteRule ^vendor(/|$) - [F,L,NC]
463
    RewriteRule ^\.env - [F,L,NC]
464
    RewriteRule silverstripe-cache(/|$) - [F,L,NC]
465
    RewriteRule composer\.(json|lock) - [F,L,NC]
466
    RewriteRule (error|silverstripe|debug)\.log - [F,L,NC]
467
468
    # Process through SilverStripe if no file with the requested name exists.
469
    # Pass through the original path as a query parameter, and retain the existing parameters.
470
    # Try finding framework in the vendor folder first
471
    RewriteCond %{REQUEST_URI} ^(.*)$
472
    RewriteCond %{REQUEST_FILENAME} !-f
473
    RewriteRule .* index.php
474
</IfModule>
475
TEXT;
476
477
        if (file_exists('.htaccess')) {
478
            $htaccess = file_get_contents('.htaccess');
479
480
            if (strpos($htaccess, '### SILVERSTRIPE START ###') === false
481
                && strpos($htaccess, '### SILVERSTRIPE END ###') === false
482
            ) {
483
                $htaccess .= "\n### SILVERSTRIPE START ###\n### SILVERSTRIPE END ###\n";
484
            }
485
486
            if (strpos($htaccess, '### SILVERSTRIPE START ###') !== false
487
                && strpos($htaccess, '### SILVERSTRIPE END ###') !== false
488
            ) {
489
                $start = substr($htaccess, 0, strpos($htaccess, '### SILVERSTRIPE START ###'))
490
                    . "### SILVERSTRIPE START ###\n";
491
                $end = "\n" . substr($htaccess, strpos($htaccess, '### SILVERSTRIPE END ###'));
492
            }
493
        }
494
495
        $this->writeToFile('.htaccess', $start . $rewrite . $end);
496
    }
497
498
    /**
499
     * Writes basic configuration to the web.config for IIS
500
     * so that rewriting capability can be use.
501
     */
502
    public function createWebConfig()
503
    {
504
        $content = <<<TEXT
505
<?xml version="1.0" encoding="utf-8"?>
506
<configuration>
507
    <system.webServer>
508
        <security>
509
            <requestFiltering>
510
                <hiddenSegments applyToWebDAV="false">
511
                    <add segment="silverstripe-cache" />
512
                    <add segment="vendor" />
513
                    <add segment="composer.json" />
514
                    <add segment="composer.lock" />
515
                </hiddenSegments>
516
                <fileExtensions allowUnlisted="true" >
517
                    <add fileExtension=".ss" allowed="false"/>
518
                    <add fileExtension=".yml" allowed="false"/>
519
                </fileExtensions>
520
            </requestFiltering>
521
        </security>
522
        <rewrite>
523
            <rules>
524
                <rule name="SilverStripe Clean URLs" stopProcessing="true">
525
                    <match url="^(.*)$" />
526
                    <conditions>
527
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
528
                    </conditions>
529
                    <action type="Rewrite" url="index.php" appendQueryString="true" />
530
                </rule>
531
            </rules>
532
        </rewrite>
533
    </system.webServer>
534
</configuration>
535
TEXT;
536
537
        $this->writeToFile('web.config', $content);
538
    }
539
540
    public function checkRewrite()
541
    {
542
        $token = new ParameterConfirmationToken('flush', new HTTPRequest('GET', '/'));
543
        $params = http_build_query($token->params());
544
545
        $destinationURL = str_replace('install.php', '', $_SERVER['SCRIPT_NAME']) .
546
            ($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params");
547
548
        echo <<<HTML
549
<li id="ModRewriteResult">Testing...</li>
550
<script>
551
    if (typeof $ == 'undefined') {
552
        document.getElementById('ModeRewriteResult').innerHTML = "I can't run jQuery ajax to set rewriting; I will redirect you to the homepage to see if everything is working.";
553
        setTimeout(function() {
554
            window.location = "$destinationURL";
555
        }, 10000);
556
    } else {
557
        $.ajax({
558
            method: 'get',
559
            url: 'InstallerTest/testrewrite',
560
            complete: function(response) {
561
                var r = response.responseText.replace(/[^A-Z]?/g,"");
562
                if (r === "OK") {
563
                    $('#ModRewriteResult').html("Friendly URLs set up successfully; I am now redirecting you to your SilverStripe site...")
564
                    setTimeout(function() {
565
                        window.location = "$destinationURL";
566
                    }, 2000);
567
                } else {
568
                    $('#ModRewriteResult').html("Friendly URLs are not working. This is most likely because a rewrite module isn't configured "
569
                        + "correctly on your site. You may need to get your web host or server administrator to do this for you: "
570
                        + "<ul>"
571
                        + "<li><strong>mod_rewrite</strong> or other rewrite module is enabled on your web server</li>"
572
                        + "<li><strong>AllowOverride All</strong> is set for the directory where SilverStripe is installed</li>"
573
                        + "</ul>");
574
                }
575
            }
576
        });
577
    }
578
</script>
579
<noscript>
580
    <li><a href="$destinationURL">Click here</a> to check friendly URLs are working. If you get a 404 then something is wrong.</li>
581
</noscript>
582
HTML;
583
    }
584
585
    /**
586
     * Show an installation status message.
587
     * The output differs depending on whether this is CLI or web based
588
     *
589
     * @param string $msg
590
     */
591
    public function statusMessage($msg)
592
    {
593
        echo "<li>$msg</li>\n";
594
        flush();
595
    }
596
597
    /**
598
     * @param $config
599
     */
600
    protected function sendInstallStats($config)
601
    {
602
        // Try to determine the database version from the helper
603
        $dbType = $config['db']['type'];
604
        $helper = $this->getDatabaseConfigurationHelper($dbType);
605
        if ($helper) {
0 ignored issues
show
introduced by
$helper is of type SilverStripe\Dev\Install...baseConfigurationHelper, thus it always evaluated to true.
Loading history...
606
            $databaseVersion = $dbType . ': ' . $helper->getDatabaseVersion($config['db']);
607
        } else {
608
            $databaseVersion = $dbType;
609
        }
610
611
        $args = http_build_query(array_filter([
612
            'SilverStripe' => $config['version'],
613
            'PHP' => phpversion(),
614
            'Database' => $databaseVersion,
615
            'WebServer' => $this->findWebserver(),
616
            'ID' => empty($_SESSION['StatsID']) ? null : $_SESSION['StatsID']
617
        ]));
618
        $url = "http://ss2stat.silverstripe.com/Installation/add?{$args}";
619
        @$_SESSION['StatsID'] = file_get_contents($url);
620
    }
621
}
622