Passed
Push — phpunit8 ( f24111 )
by Sam
08:30
created

Installer::createWebConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 36
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 33
nc 1
nop 0
dl 0
loc 36
rs 9.392
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\Convert;
11
use SilverStripe\Core\CoreKernel;
12
use SilverStripe\Core\EnvironmentLoader;
13
use SilverStripe\Core\Kernel;
14
use SilverStripe\Core\Path;
15
use SilverStripe\ORM\DatabaseAdmin;
16
use SilverStripe\Security\DefaultAdminService;
17
use SilverStripe\Security\Security;
18
use SilverStripe\Control\Middleware\URLSpecialsMiddleware\SessionEnvTypeSwitcher;
19
20
/**
21
 * This installer doesn't use any of the fancy SilverStripe stuff in case it's unsupported.
22
 */
23
class Installer
24
{
25
    use InstallEnvironmentAware;
26
    use SessionEnvTypeSwitcher;
27
28
    /**
29
     * Errors during install
30
     *
31
     * @var array
32
     */
33
    protected $errors = [];
34
35
    /**
36
     * value='' attribute placeholder for password fields
37
     */
38
    const PASSWORD_PLACEHOLDER = '********';
39
40
    public function __construct($basePath = null)
41
    {
42
        $this->initBaseDir($basePath);
43
    }
44
45
    /**
46
     * Installer error
47
     *
48
     * @param string $message
49
     */
50
    protected function error($message = null)
51
    {
52
        $this->errors[] = $message;
53
    }
54
55
    protected function installHeader()
56
    {
57
        $clientPath = RESOURCES_DIR . (PUBLIC_DIR
58
            ? '/vendor/silverstripe/framework/src/Dev/Install/client'
59
            : '/silverstripe/framework/src/Dev/Install/client');
60
        ?>
61
        <html>
62
        <head>
63
            <meta charset="utf-8"/>
64
            <title>Installing SilverStripe...</title>
65
            <link rel="stylesheet" type="text/css" href="<?=$clientPath; ?>/styles/install.css"/>
66
            <script src="//code.jquery.com/jquery-1.7.2.min.js"></script>
67
        </head>
68
        <body>
69
        <div class="install-header">
70
            <div class="inner">
71
                <div class="brand">
72
                    <h1>SilverStripe</h1>
73
                </div>
74
            </div>
75
        </div>
76
77
        <div id="Navigation">&nbsp;</div>
78
        <div class="clear"><!-- --></div>
79
80
        <div class="main">
81
            <div class="inner">
82
                <h2>Installing SilverStripe...</h2>
83
84
                <p>I am now running through the installation steps (this should take about 30 seconds)</p>
85
86
                <p>If you receive a fatal error, refresh this page to continue the installation</p>
87
                <ul>
88
        <?php
89
    }
90
91
    public function install($config)
92
    {
93
        // Render header
94
        $this->installHeader();
95
        $isIIS = $this->isIIS();
96
        $isApache = $this->isApache();
97
        $projectDir = $this->getProjectDir();
98
        $projectSrcDir = $this->getProjectSrcDir();
99
100
        flush();
101
102
        // Send install stats
103
        if (!empty($config['stats'])) {
104
            $this->sendInstallStats($config);
105
        }
106
107
        // Cleanup _config.php
108
        $basePath = $this->getBaseDir();
109
        $appConfigPath = $basePath . "{$projectDir}/_config.php";
110
        if (file_exists($appConfigPath)) {
111
            // Truncate the contents of _config instead of deleting it - we can't re-create it because Windows handles
112
            // permissions slightly differently to UNIX based filesystems - it takes the permissions from the parent
113
            // directory instead of retaining them
114
            $fh = fopen($appConfigPath, 'wb');
115
            fclose($fh);
116
        }
117
118
        // Write all files
119
        $this->writeIndexPHP();
120
        $this->writeConfigPHP($config);
121
        $this->writeConfigYaml($config);
122
        $this->writeConfigEnv($config);
123
124
        // Write other stuff
125
        if (!$this->checkModuleExists('cms')) {
126
            $rootURLControllerPath = $basePath . "{$projectSrcDir}/RootURLController.php";
127
            $this->writeToFile($rootURLControllerPath, <<<PHP
128
<?php
129
130
use SilverStripe\\Control\\Controller;
131
132
class RootURLController extends Controller
133
{
134
    public function index()
135
    {
136
        echo "<html>Your site is now set up. Start adding controllers to app/src to get started.</html>";
137
    }
138
}
139
PHP
140
            );
141
        }
142
143
        // Write the appropriate web server configuration file for rewriting support
144
        if ($this->hasRewritingCapability()) {
145
            if ($isApache) {
146
                $this->createHtaccess();
147
            } elseif ($isIIS) {
148
                $this->createWebConfig();
149
            }
150
        }
151
152
        // Build request
153
        $request = HTTPRequestBuilder::createFromEnvironment();
154
155
        // Install kernel (fix to dev)
156
        $kernel = new CoreKernel(Path::normalise($basePath));
157
        $kernel->setEnvironment(Kernel::DEV);
158
        $app = new HTTPApplication($kernel);
159
160
        // Build db within HTTPApplication
161
        $app->execute($request, function (HTTPRequest $request) use ($config) {
162
            // Suppress cookie errors on install
163
            Cookie::config()->set('report_errors', false);
164
165
            // Start session and execute
166
            $request->getSession()->init($request);
167
168
            // Output status
169
            $this->statusMessage("Building database schema...");
170
171
            // Setup DB
172
            $dbAdmin = new DatabaseAdmin();
173
            $dbAdmin->setRequest($request);
174
            $dbAdmin->pushCurrent();
175
            $dbAdmin->doInit();
176
            $dbAdmin->doBuild(true);
177
178
            // Create default administrator user and group in database
179
            // (not using Security::setDefaultAdmin())
180
            $username = $config['admin']['username'];
181
            $password = $config['admin']['password'];
182
            $adminMember = DefaultAdminService::singleton()
183
                ->findOrCreateAdmin(
184
                    $username,
185
                    _t('SilverStripe\\Security\\DefaultAdminService.DefaultAdminFirstname', 'Default Admin')
186
                );
187
            $adminMember->Email = $username;
188
            $adminMember->Password = $password;
189
            $adminMember->PasswordEncryption = Security::config()->get('encryption_algorithm');
190
191
            try {
192
                $this->statusMessage('Creating default CMS admin account...');
193
                $adminMember->write();
194
            } catch (Exception $e) {
195
                $this->statusMessage(
196
                    sprintf('Warning: Default CMS admin account could not be created (error: %s)', $e->getMessage())
197
                );
198
            }
199
200
            $request->getSession()->set('username', $username);
201
            $request->getSession()->set('password', $password);
202
            $request->getSession()->save($request);
203
        }, true);
204
205
        // Check result of install
206
        if (!$this->errors) {
207
            // switch the session to Dev mode so that
208
            // flush does not require authentication
209
            // for the first time after installation
210
            $request['isDev'] = '1';
211
            $this->setSessionEnvType($request);
212
            unset($request['isDev']);
213
            $request->getSession()->save($request);
214
215
            if (isset($_SERVER['HTTP_HOST']) && $this->hasRewritingCapability()) {
216
                $this->statusMessage("Checking that friendly URLs work...");
217
                $this->checkRewrite();
218
            } else {
219
                $params = http_build_query($request->getVars() + ['flush' => '']);
220
221
                $destinationURL = 'index.php/' .
222
                    ($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params");
223
224
                echo <<<HTML
225
                <li>SilverStripe successfully installed; I am now redirecting you to your SilverStripe site...</li>
226
                <script>
227
                    setTimeout(function() {
228
                        window.location = "$destinationURL";
229
                    }, 2000);
230
                </script>
231
                <noscript>
232
                <li><a href="$destinationURL">Click here to access your site.</a></li>
233
                </noscript>
234
HTML;
235
            }
236
        } else {
237
            // Output all errors
238
            $this->statusMessage('Encountered ' . count($this->errors) . ' errors during install:');
239
            echo "<ul>";
240
            foreach ($this->errors as $error) {
241
                $this->statusMessage($error);
242
            }
243
            echo "</ul>";
244
            $this->statusMessage('Please <a href="install.php">Click here</a> to return to the installer.');
245
        }
246
247
        return $this->errors;
248
    }
249
250
    protected function writeIndexPHP()
251
    {
252
        $content = <<<'PHP'
253
<?php
254
255
use SilverStripe\Control\HTTPApplication;
256
use SilverStripe\Control\HTTPRequestBuilder;
257
use SilverStripe\Core\CoreKernel;
258
259
// Find autoload.php
260
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
261
    require __DIR__ . '/vendor/autoload.php';
262
} elseif (file_exists(__DIR__ . '/../vendor/autoload.php')) {
263
    require __DIR__ . '/../vendor/autoload.php';
264
} else {
265
    echo "autoload.php not found";
266
    die;
267
}
268
269
// Build request and detect flush
270
$request = HTTPRequestBuilder::createFromEnvironment();
271
272
// Default application
273
$kernel = new CoreKernel(BASE_PATH);
274
$app = new HTTPApplication($kernel);
275
$response = $app->handle($request);
276
$response->output();
277
PHP;
278
        $path = $this->getPublicDir() . 'index.php';
279
        $this->writeToFile($path, $content, true);
280
    }
281
282
    /**
283
     * Write all .env files
284
     *
285
     * @param $config
286
     */
287
    protected function writeConfigEnv($config)
288
    {
289
        if (!$config['usingEnv']) {
290
            return;
291
        }
292
293
        $path = $this->getBaseDir() . '.env';
294
        $vars = [];
295
296
        // Retain existing vars
297
        $env = new EnvironmentLoader();
298
        if (file_exists($path)) {
299
            $vars = $env->loadFile($path) ?: [];
300
        }
301
302
        // Set base URL
303
        if (!isset($vars['SS_BASE_URL']) && isset($_SERVER['HTTP_HOST'])) {
304
            $vars['SS_BASE_URL'] = 'http://' . $_SERVER['HTTP_HOST'] . BASE_URL;
305
        }
306
307
        // Set DB env
308
        if (empty($config['db']['database'])) {
309
            $vars['SS_DATABASE_CHOOSE_NAME'] = true;
310
        } else {
311
            $vars['SS_DATABASE_NAME'] = $config['db']['database'];
312
        }
313
        $vars['SS_DATABASE_CLASS'] = $config['db']['type'];
314
        if (isset($config['db']['server'])) {
315
            $vars['SS_DATABASE_SERVER'] = $config['db']['server'];
316
        }
317
        if (isset($config['db']['username'])) {
318
            $vars['SS_DATABASE_USERNAME'] = $config['db']['username'];
319
        }
320
        if (isset($config['db']['password'])) {
321
            $vars['SS_DATABASE_PASSWORD'] = $config['db']['password'];
322
        }
323
        if (isset($config['db']['path'])) {
324
            $vars['SS_DATABASE_PATH'] = $config['db']['path'];
325
            // sqlite compat
326
            $vars['SS_SQLITE_DATABASE_PATH'] = $config['db']['path'];
327
        }
328
        if (isset($config['db']['key'])) {
329
            $vars['SS_DATABASE_KEY'] = $config['db']['key'];
330
            // sqlite compat
331
            $vars['SS_SQLITE_DATABASE_KEY'] = $config['db']['key'];
332
        }
333
334
        // Write all env vars
335
        $lines = [
336
            '# Generated by SilverStripe Installer'
337
        ];
338
        ksort($vars);
339
        foreach ($vars as $key => $value) {
340
            $lines[] = $key . '="' . addcslashes($value, '"') . '"';
341
        }
342
343
        $this->writeToFile('.env', implode("\n", $lines));
344
345
        // Re-load env vars for installer access
346
        $env->loadFile($path);
347
    }
348
349
    /**
350
     * Write all *.php files
351
     *
352
     * @param array $config
353
     */
354
    protected function writeConfigPHP($config)
355
    {
356
        $configPath = $this->getProjectDir() . DIRECTORY_SEPARATOR . "_config.php";
357
        if ($config['usingEnv']) {
358
            $this->writeToFile($configPath, "<?php\n ");
359
            return;
360
        }
361
362
        // Create databaseConfig
363
        $lines = [];
364
        foreach ($config['db'] as $key => $value) {
365
            $lines[] = sprintf(
366
                "    '%s' => '%s'",
367
                addslashes($key),
368
                addslashes($value)
369
            );
370
        }
371
        $databaseConfigContent = implode(",\n", $lines);
372
        $this->writeToFile($configPath, <<<PHP
373
<?php
374
375
use SilverStripe\\ORM\\DB;
376
377
DB::setConfig([
378
{$databaseConfigContent}
379
]);
380
381
PHP
382
        );
383
    }
384
385
    /**
386
     * Write yml files
387
     *
388
     * @param array $config
389
     */
390
    protected function writeConfigYaml($config)
391
    {
392
        // Escape user input for safe insertion into PHP file
393
        $locale = $this->ymlString($config['locale']);
394
        $projectDir = $this->getProjectDir();
395
396
        // Set either specified, or no theme
397
        if ($config['theme'] && $config['theme'] !== 'tutorial') {
398
            $theme = $this->ymlString($config['theme']);
399
            $themeYML = <<<YML
400
    - '\$public'
401
    - '$theme'
402
    - '\$default'
403
YML;
404
        } else {
405
            $themeYML = <<<YML
406
    - '\$public'
407
    - '\$default'
408
YML;
409
        }
410
411
        // Write theme.yml
412
        $this->writeToFile("{$projectDir}/_config/theme.yml", <<<YML
413
---
414
Name: mytheme
415
---
416
SilverStripe\\View\\SSViewer:
417
  themes:
418
$themeYML
419
SilverStripe\\i18n\\i18n:
420
  default_locale: '$locale'
421
YML
422
        );
423
    }
424
425
    /**
426
     * Escape yml string
427
     *
428
     * @param string $string
429
     * @return mixed
430
     */
431
    protected function ymlString($string)
432
    {
433
        // just escape single quotes using ''
434
        return str_replace("'", "''", $string);
435
    }
436
437
    /**
438
     * Write file to given location
439
     *
440
     * @param string $filename
441
     * @param string $content
442
     * @param bool $absolute If $filename is absolute path set to true
443
     * @return bool
444
     */
445
    public function writeToFile($filename, $content, $absolute = false)
446
    {
447
        // Get absolute / relative paths by either combining or removing base from path
448
        list($absolutePath, $relativePath) = $absolute
449
            ? [
450
                $filename,
451
                substr($filename, strlen($this->getBaseDir()))]
452
            : [
453
                $this->getBaseDir() . $filename,
454
                $filename
455
            ];
456
        $this->statusMessage("Setting up $relativePath");
457
458
        if ((@$fh = fopen($absolutePath, 'wb')) && fwrite($fh, $content) && fclose($fh)) {
459
            // Set permissions to writable
460
            @chmod($absolutePath, 0775);
461
            return true;
462
        }
463
        $this->error("Couldn't write to file $relativePath");
464
        return false;
465
    }
466
467
    /**
468
     * Ensure root .htaccess is setup
469
     */
470
    public function createHtaccess()
471
    {
472
        $start = "### SILVERSTRIPE START ###\n";
473
        $end = "\n### SILVERSTRIPE END ###";
474
475
        $base = dirname($_SERVER['SCRIPT_NAME']);
476
        $base = Convert::slashes($base, '/');
477
478
        if ($base != '.') {
479
            $baseClause = "RewriteBase '$base'\n";
480
        } else {
481
            $baseClause = "";
482
        }
483
        if (strpos(strtolower(php_sapi_name()), "cgi") !== false) {
484
            $cgiClause = "RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n";
485
        } else {
486
            $cgiClause = "";
487
        }
488
        $rewrite = <<<TEXT
489
# Deny access to templates (but allow from localhost)
490
<Files *.ss>
491
    Order deny,allow
492
    Deny from all
493
    Allow from 127.0.0.1
494
</Files>
495
496
# Deny access to IIS configuration
497
<Files web.config>
498
    Order deny,allow
499
    Deny from all
500
</Files>
501
502
# Deny access to YAML configuration files which might include sensitive information
503
<Files ~ "\.ya?ml$">
504
    Order allow,deny
505
    Deny from all
506
</Files>
507
508
# Route errors to static pages automatically generated by SilverStripe
509
ErrorDocument 404 /assets/error-404.html
510
ErrorDocument 500 /assets/error-500.html
511
512
<IfModule mod_rewrite.c>
513
514
    # Turn off index.php handling requests to the homepage fixes issue in apache >=2.4
515
    <IfModule mod_dir.c>
516
        DirectoryIndex disabled
517
        DirectorySlash On
518
    </IfModule>
519
520
    SetEnv HTTP_MOD_REWRITE On
521
    RewriteEngine On
522
    $baseClause
523
    $cgiClause
524
525
    # Deny access to potentially sensitive files and folders
526
    RewriteRule ^vendor(/|$) - [F,L,NC]
527
    RewriteRule ^\.env - [F,L,NC]
528
    RewriteRule silverstripe-cache(/|$) - [F,L,NC]
529
    RewriteRule composer\.(json|lock) - [F,L,NC]
530
    RewriteRule (error|silverstripe|debug)\.log - [F,L,NC]
531
532
    # Process through SilverStripe if no file with the requested name exists.
533
    # Pass through the original path as a query parameter, and retain the existing parameters.
534
    # Try finding framework in the vendor folder first
535
    RewriteCond %{REQUEST_URI} ^(.*)$
536
    RewriteCond %{REQUEST_FILENAME} !-f
537
    RewriteRule .* index.php
538
</IfModule>
539
TEXT;
540
541
        $htaccessPath = $this->getPublicDir() . '.htaccess';
542
        if (file_exists($htaccessPath)) {
543
            $htaccess = file_get_contents($htaccessPath);
544
545
            if (strpos($htaccess, '### SILVERSTRIPE START ###') === false
546
                && strpos($htaccess, '### SILVERSTRIPE END ###') === false
547
            ) {
548
                $htaccess .= "\n### SILVERSTRIPE START ###\n### SILVERSTRIPE END ###\n";
549
            }
550
551
            if (strpos($htaccess, '### SILVERSTRIPE START ###') !== false
552
                && strpos($htaccess, '### SILVERSTRIPE END ###') !== false
553
            ) {
554
                $start = substr($htaccess, 0, strpos($htaccess, '### SILVERSTRIPE START ###'))
555
                    . "### SILVERSTRIPE START ###\n";
556
                $end = "\n" . substr($htaccess, strpos($htaccess, '### SILVERSTRIPE END ###'));
557
            }
558
        }
559
560
        $this->writeToFile($htaccessPath, $start . $rewrite . $end, true);
561
    }
562
563
    /**
564
     * Writes basic configuration to the web.config for IIS
565
     * so that rewriting capability can be use.
566
     */
567
    public function createWebConfig()
568
    {
569
        $content = <<<TEXT
570
<?xml version="1.0" encoding="utf-8"?>
571
<configuration>
572
    <system.webServer>
573
        <security>
574
            <requestFiltering>
575
                <hiddenSegments applyToWebDAV="false">
576
                    <add segment="silverstripe-cache" />
577
                    <add segment="composer.json" />
578
                    <add segment="composer.lock" />
579
                </hiddenSegments>
580
                <fileExtensions allowUnlisted="true" >
581
                    <add fileExtension=".ss" allowed="false"/>
582
                    <add fileExtension=".yml" allowed="false"/>
583
                </fileExtensions>
584
            </requestFiltering>
585
        </security>
586
        <rewrite>
587
            <rules>
588
                <rule name="SilverStripe Clean URLs" stopProcessing="true">
589
                    <match url="^(.*)$" />
590
                    <conditions>
591
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
592
                    </conditions>
593
                    <action type="Rewrite" url="index.php" appendQueryString="true" />
594
                </rule>
595
            </rules>
596
        </rewrite>
597
    </system.webServer>
598
</configuration>
599
TEXT;
600
601
        $path = $this->getPublicDir() . 'web.config';
602
        $this->writeToFile($path, $content, true);
603
    }
604
605
    public function checkRewrite()
606
    {
607
        $params = http_build_query(['flush' => '']);
608
609
        $destinationURL = rtrim(BASE_URL, '/') . '/' . (
610
            $this->checkModuleExists('cms')
611
                ? "home/successfullyinstalled?$params"
612
                : "?$params"
613
        );
614
615
        // phpcs:disable
616
        echo <<<HTML
617
<li id="ModRewriteResult">Testing...</li>
618
<script>
619
    if (typeof $ == 'undefined') {
620
        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.";
621
        setTimeout(function() {
622
            window.location = "$destinationURL";
623
        }, 10000);
624
    } else {
625
        $.ajax({
626
            method: 'get',
627
            url: 'InstallerTest/testrewrite',
628
            complete: function(response) {
629
                var r = response.responseText.replace(/[^A-Z]?/g,"");
630
                if (r === "OK") {
631
                    $('#ModRewriteResult').html("Friendly URLs set up successfully; I am now redirecting you to your SilverStripe site...")
632
                    setTimeout(function() {
633
                        window.location = "$destinationURL";
634
                    }, 2000);
635
                } else {
636
                    $('#ModRewriteResult').html("Friendly URLs are not working. This is most likely because a rewrite module isn't configured "
637
                        + "correctly on your site. You may need to get your web host or server administrator to do this for you: "
638
                        + "<ul>"
639
                        + "<li><strong>mod_rewrite</strong> or other rewrite module is enabled on your web server</li>"
640
                        + "<li><strong>AllowOverride All</strong> is set for the directory where SilverStripe is installed</li>"
641
                        + "</ul>");
642
                }
643
            }
644
        });
645
    }
646
</script>
647
<noscript>
648
    <li><a href="$destinationURL">Click here</a> to check friendly URLs are working. If you get a 404 then something is wrong.</li>
649
</noscript>
650
HTML;
651
        // phpcs:enable
652
    }
653
654
    /**
655
     * Show an installation status message.
656
     * The output differs depending on whether this is CLI or web based
657
     *
658
     * @param string $msg
659
     */
660
    public function statusMessage($msg)
661
    {
662
        echo "<li>$msg</li>\n";
663
        flush();
664
    }
665
666
    /**
667
     * @param $config
668
     */
669
    protected function sendInstallStats($config)
670
    {
671
        // Try to determine the database version from the helper
672
        $dbType = $config['db']['type'];
673
        $helper = $this->getDatabaseConfigurationHelper($dbType);
674
        if ($helper) {
675
            $databaseVersion = $dbType . ': ' . $helper->getDatabaseVersion($config['db']);
676
        } else {
677
            $databaseVersion = $dbType;
678
        }
679
680
        $args = http_build_query(array_filter([
681
            'SilverStripe' => $config['version'],
682
            'PHP' => phpversion(),
683
            'Database' => $databaseVersion,
684
            'WebServer' => $this->findWebserver(),
685
            'ID' => empty($_SESSION['StatsID']) ? null : $_SESSION['StatsID']
686
        ]));
687
        $url = "http://ss2stat.silverstripe.com/Installation/add?{$args}";
688
        @$_SESSION['StatsID'] = file_get_contents($url);
689
    }
690
}
691