Completed
Push — fix-install-db ( e20e35...3c35d2 )
by Sam
08:21
created

CoreKernel::bootDatabase()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 0
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core;
4
5
use InvalidArgumentException;
6
use Monolog\Handler\StreamHandler;
7
use Monolog\Logger;
8
use Psr\Log\LoggerInterface;
9
use SilverStripe\Config\Collections\CachedConfigCollection;
10
use SilverStripe\Control\Director;
11
use SilverStripe\Control\HTTPResponse;
12
use SilverStripe\Control\HTTPResponse_Exception;
13
use SilverStripe\Core\Cache\ManifestCacheFactory;
14
use SilverStripe\Core\Config\ConfigLoader;
15
use SilverStripe\Core\Config\CoreConfigFactory;
16
use SilverStripe\Core\Injector\Injector;
17
use SilverStripe\Core\Injector\InjectorLoader;
18
use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator;
19
use SilverStripe\Core\Manifest\ClassLoader;
20
use SilverStripe\Core\Manifest\ClassManifest;
21
use SilverStripe\Core\Manifest\ModuleLoader;
22
use SilverStripe\Core\Manifest\ModuleManifest;
23
use SilverStripe\Dev\DebugView;
24
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
25
use SilverStripe\Logging\ErrorHandler;
26
use SilverStripe\ORM\DB;
27
use SilverStripe\View\ThemeManifest;
28
use SilverStripe\View\ThemeResourceLoader;
29
30
/**
31
 * Simple Kernel container
32
 */
33
class CoreKernel implements Kernel
34
{
35
    /**
36
     * @var Kernel
37
     */
38
    protected $nestedFrom = null;
39
40
    /**
41
     * @var Injector
42
     */
43
    protected $container = null;
44
45
    /**
46
     * @var string
47
     */
48
    protected $enviroment = null;
49
50
    /**
51
     * @var ClassLoader
52
     */
53
    protected $classLoader = null;
54
55
    /**
56
     * @var ModuleLoader
57
     */
58
    protected $moduleLoader = null;
59
60
    /**
61
     * @var ConfigLoader
62
     */
63
    protected $configLoader = null;
64
65
    /**
66
     * @var InjectorLoader
67
     */
68
    protected $injectorLoader = null;
69
70
    /**
71
     * @var ThemeResourceLoader
72
     */
73
    protected $themeResourceLoader = null;
74
75
    protected $basePath = null;
76
77
    /**
78
     * Create a new kernel for this application
79
     *
80
     * @param string $basePath Path to base dir for this application
81
     */
82
    public function __construct($basePath)
83
    {
84
        $this->basePath = $basePath;
85
86
        // Initialise the dependency injector as soon as possible, as it is
87
        // subsequently used by some of the following code
88
        $injectorLoader = InjectorLoader::inst();
89
        $injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class));
90
        $injectorLoader->pushManifest($injector);
91
        $this->setInjectorLoader($injectorLoader);
92
93
        // Manifest cache factory
94
        $manifestCacheFactory = $this->buildManifestCacheFactory();
95
96
        // Class loader
97
        $classLoader = ClassLoader::inst();
98
        $classLoader->pushManifest(new ClassManifest($basePath, $manifestCacheFactory));
99
        $this->setClassLoader($classLoader);
100
101
        // Module loader
102
        $moduleLoader = ModuleLoader::inst();
103
        $moduleManifest = new ModuleManifest($basePath, $manifestCacheFactory);
104
        $moduleLoader->pushManifest($moduleManifest);
105
        $this->setModuleLoader($moduleLoader);
106
107
        // Config loader
108
        // @todo refactor CoreConfigFactory
109
        $configFactory = new CoreConfigFactory($manifestCacheFactory);
110
        $configManifest = $configFactory->createRoot();
111
        $configLoader = ConfigLoader::inst();
112
        $configLoader->pushManifest($configManifest);
113
        $this->setConfigLoader($configLoader);
114
115
        // Load template manifest
116
        $themeResourceLoader = ThemeResourceLoader::inst();
117
        $themeResourceLoader->addSet('$default', new ThemeManifest(
118
            $basePath,
119
            project(),
120
            $manifestCacheFactory
121
        ));
122
        $this->setThemeResourceLoader($themeResourceLoader);
123
    }
124
125
    public function getEnvironment()
126
    {
127
        // Check set
128
        if ($this->enviroment) {
129
            return $this->enviroment;
130
        }
131
132
        // Check saved session
133
        $env = $this->sessionEnvironment();
134
        if ($env) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $env of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
135
            return $env;
136
        }
137
138
        // Check getenv
139
        if ($env = getenv('SS_ENVIRONMENT_TYPE')) {
140
            return $env;
141
        }
142
143
        return self::LIVE;
144
    }
145
146
    /**
147
     * Check or update any temporary environment specified in the session.
148
     *
149
     * @return null|string
150
     */
151
    protected function sessionEnvironment()
152
    {
153
        // Check isDev in querystring
154 View Code Duplication
        if (isset($_GET['isDev'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
155
            if (isset($_SESSION)) {
156
                unset($_SESSION['isTest']); // In case we are changing from test mode
157
                $_SESSION['isDev'] = $_GET['isDev'];
158
            }
159
            return self::DEV;
160
        }
161
162
        // Check isTest in querystring
163 View Code Duplication
        if (isset($_GET['isTest'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
164
            if (isset($_SESSION)) {
165
                unset($_SESSION['isDev']); // In case we are changing from dev mode
166
                $_SESSION['isTest'] = $_GET['isTest'];
167
            }
168
            return self::TEST;
169
        }
170
171
        // Check session
172
        if (!empty($_SESSION['isDev'])) {
173
            return self::DEV;
174
        }
175
        if (!empty($_SESSION['isTest'])) {
176
            return self::TEST;
177
        }
178
179
        // no session environment
180
        return null;
181
    }
182
183
    public function boot($flush = false)
184
    {
185
        $this->bootPHP();
186
        $this->bootManifests($flush);
187
        $this->bootErrorHandling();
188
        $this->bootDatabaseEnvVars();
189
        $this->bootConfigs();
190
        $this->bootDatabaseGlobals();
191
        $this->validateDatabase();
192
    }
193
194
    /**
195
     * Include all _config.php files
196
     */
197
    protected function bootConfigs()
198
    {
199
        // After loading all other app manifests, include _config.php files
200
        $this->getModuleLoader()->getManifest()->activateConfig();
201
    }
202
203
    /**
204
     * Load default database configuration from the $database and $databaseConfig globals
205
     */
206
    protected function bootDatabaseGlobals()
207
    {
208
        // Now that configs have been loaded, we can check global for database config
209
        global $databaseConfig;
210
        global $database;
211
212
        $prefix = getenv('SS_DATABASE_PREFIX') ?: 'SS_';
213
214
        // Case 1: $databaseConfig global exists. Merge $database in as needed
215
        if (!empty($databaseConfig)) {
216
            if (!empty($database)) {
217
                $databaseConfig['database'] = $prefix . $database;
218
            }
219
220
            // Only set it if its valid, otherwise ignore $databaseConfig entirely
221
            if (!empty($databaseConfig['database'])) {
222
                DB::setConfig($databaseConfig);
223
                return;
224
            }
225
        }
226
227
        // Case 2: $database merged into existing config
228
        if (!empty($database)) {
229
            $existing = DB::getConfig();
230
            $existing['database'] = $prefix . $database;
231
            DB::setConfig($existing);
232
        }
233
    }
234
235
    /**
236
     * Load default database configuration from environment variable
237
     */
238
    protected function bootDatabaseEnvVars()
239
    {
240
        // Set default database config
241
        $databaseConfig = $this->getDatabaseConfig();
242
        $databaseConfig['database'] = $this->getDatabaseName();
243
        DB::setConfig($databaseConfig);
244
    }
245
246
    /**
247
     * Check that the database configuration is valid, throwing an HTTPResponse_Exception if it's not
248
     *
249
     * @throws HTTPResponse_Exception
250
     */
251
    protected function validateDatabase()
252
    {
253
        $databaseConfig = DB::getConfig();
254
        // Gracefully fail if no DB is configured
255
        if (empty($databaseConfig['database'])) {
256
            $this->detectLegacyEnvironment();
257
            $this->redirectToInstaller();
258
        }
259
    }
260
261
    /**
262
     * Check if there's a legacy _ss_environment.php file
263
     *
264
     * @throws HTTPResponse_Exception
265
     */
266
    protected function detectLegacyEnvironment()
267
    {
268
        // Is there an _ss_environment.php file?
269
        if (!file_exists($this->basePath . '/_ss_environment.php') &&
270
            !file_exists(dirname($this->basePath) . '/_ss_environment.php')
271
        ) {
272
            return;
273
        }
274
275
        // Build error response
276
        $dv = new DebugView();
277
        $body =
278
            $dv->renderHeader() .
279
            $dv->renderInfo(
280
                "Configuraton Error",
281
                Director::absoluteBaseURL()
0 ignored issues
show
Security Bug introduced by
It seems like \SilverStripe\Control\Director::absoluteBaseURL() targeting SilverStripe\Control\Director::absoluteBaseURL() can also be of type false; however, SilverStripe\Dev\DebugView::renderInfo() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
282
            ) .
283
            $dv->renderParagraph(
284
                'You need to replace your _ss_environment.php file with a .env file, or with environment variables.<br><br>'
285
                . 'See the <a href="https://docs.silverstripe.org/en/4/getting_started/environment_management/">'
286
                . 'Environment Management</a> docs for more information.'
287
            ) .
288
            $dv->renderFooter();
289
290
        // Raise error
291
        $response = new HTTPResponse($body, 500);
292
        throw new HTTPResponse_Exception($response);
293
    }
294
295
    /**
296
     * If missing configuration, redirect to install.php
297
     */
298
    protected function redirectToInstaller()
299
    {
300
        // Error if installer not available
301
        if (!file_exists($this->basePath . '/install.php')) {
302
            throw new HTTPResponse_Exception(
303
                'SilverStripe Framework requires a $databaseConfig defined.',
304
                500
305
            );
306
        }
307
308
        // Redirect to installer
309
        $response = new HTTPResponse();
310
        $response->redirect(Director::absoluteURL('install.php'));
0 ignored issues
show
Security Bug introduced by
It seems like \SilverStripe\Control\Di...oluteURL('install.php') targeting SilverStripe\Control\Director::absoluteURL() can also be of type false; however, SilverStripe\Control\HTTPResponse::redirect() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
311
        throw new HTTPResponse_Exception($response);
312
    }
313
314
    /**
315
     * Load database config from environment
316
     *
317
     * @return array
318
     */
319
    protected function getDatabaseConfig()
320
    {
321
322
        /** @skipUpgrade */
323
        $databaseConfig = [
324
            "type" => getenv('SS_DATABASE_CLASS') ?: 'MySQLDatabase',
325
            "server" => getenv('SS_DATABASE_SERVER') ?: 'localhost',
326
            "username" => getenv('SS_DATABASE_USERNAME') ?: null,
327
            "password" => getenv('SS_DATABASE_PASSWORD') ?: null,
328
        ];
329
330
        // Set the port if called for
331
        $dbPort = getenv('SS_DATABASE_PORT');
332
        if ($dbPort) {
333
            $databaseConfig['port'] = $dbPort;
334
        }
335
336
        // Set the timezone if called for
337
        $dbTZ = getenv('SS_DATABASE_TIMEZONE');
338
        if ($dbTZ) {
339
            $databaseConfig['timezone'] = $dbTZ;
340
        }
341
342
        // For schema enabled drivers:
343
        $dbSchema = getenv('SS_DATABASE_SCHEMA');
344
        if ($dbSchema) {
345
            $databaseConfig["schema"] = $dbSchema;
346
        }
347
348
        // For SQlite3 memory databases (mainly for testing purposes)
349
        $dbMemory = getenv('SS_DATABASE_MEMORY');
350
        if ($dbMemory) {
351
            $databaseConfig["memory"] = $dbMemory;
352
        }
353
354
        // Allow database adapters to handle their own configuration
355
        DatabaseAdapterRegistry::autoconfigure();
356
        return $databaseConfig;
357
    }
358
359
    /**
360
     * Get name of database
361
     *
362
     * @return string
363
     */
364
    protected function getDatabaseName()
365
    {
366
        $prefix = getenv('SS_DATABASE_PREFIX') ?: 'SS_';
367
368
        // Check globals
369
        global $database;
370
        if (!empty($database)) {
371
            return $prefix.$database;
372
        }
373
        global $databaseConfig;
374
        if (!empty($databaseConfig['database'])) {
375
            return $databaseConfig['database']; // Note: Already includes prefix
376
        }
377
378
        // Check environment
379
        $database = getenv('SS_DATABASE_NAME');
380
        if ($database) {
381
            return $prefix.$database;
382
        }
383
384
        // Auto-detect name
385
        $chooseName = getenv('SS_DATABASE_CHOOSE_NAME');
386
        if ($chooseName) {
387
            // Find directory to build name from
388
            $loopCount = (int)$chooseName;
389
            $databaseDir = $this->basePath;
390
            for ($i = 0; $i < $loopCount-1; $i++) {
391
                $databaseDir = dirname($databaseDir);
392
            }
393
394
            // Build name
395
            $database = str_replace('.', '', basename($databaseDir));
396
            return $prefix.$database;
397
        }
398
399
        // no DB name (may be optional for some connectors)
400
        return null;
401
    }
402
403
    /**
404
     * Initialise PHP with default variables
405
     */
406
    protected function bootPHP()
407
    {
408
        if ($this->getEnvironment() === self::LIVE) {
409
            // limited to fatal errors and warnings in live mode
410
            error_reporting(E_ALL & ~(E_DEPRECATED | E_STRICT | E_NOTICE));
411
        } else {
412
            // Report all errors in dev / test mode
413
            error_reporting(E_ALL | E_STRICT);
414
        }
415
416
        /**
417
         * Ensure we have enough memory
418
         */
419
        Environment::increaseMemoryLimitTo('64M');
420
421
        // Ensure we don't run into xdebug's fairly conservative infinite recursion protection limit
422
        if (function_exists('xdebug_enable')) {
423
            $current = ini_get('xdebug.max_nesting_level');
424
            if ((int)$current < 200) {
425
                ini_set('xdebug.max_nesting_level', 200);
426
            }
427
        }
428
429
        /**
430
         * Set default encoding
431
         */
432
        mb_http_output('UTF-8');
433
        mb_internal_encoding('UTF-8');
434
        mb_regex_encoding('UTF-8');
435
436
        /**
437
         * Enable better garbage collection
438
         */
439
        gc_enable();
440
    }
441
442
    /**
443
     * @return ManifestCacheFactory
444
     */
445
    protected function buildManifestCacheFactory()
446
    {
447
        return new ManifestCacheFactory([
448
            'namespace' => 'manifestcache',
449
            'directory' => TempFolder::getTempFolder($this->basePath),
450
        ]);
451
    }
452
453
    /**
454
     * @return bool
455
     */
456
    protected function getIncludeTests()
457
    {
458
        return false;
459
    }
460
461
    /**
462
     * Boot all manifests
463
     *
464
     * @param bool $flush
465
     */
466
    protected function bootManifests($flush)
467
    {
468
        // Setup autoloader
469
        $this->getClassLoader()->init($this->getIncludeTests(), $flush);
470
471
        // Find modules
472
        $this->getModuleLoader()->init($this->getIncludeTests(), $flush);
473
474
        // Flush config
475
        if ($flush) {
476
            $config = $this->getConfigLoader()->getManifest();
477
            if ($config instanceof CachedConfigCollection) {
478
                $config->setFlush(true);
479
            }
480
        }
481
482
        // Find default templates
483
        $defaultSet = $this->getThemeResourceLoader()->getSet('$default');
484
        if ($defaultSet instanceof ThemeManifest) {
485
            $defaultSet->init($this->getIncludeTests(), $flush);
486
        }
487
    }
488
489
    /**
490
     * Turn on error handling
491
     */
492
    protected function bootErrorHandling()
493
    {
494
        // Register error handler
495
        $errorHandler = Injector::inst()->get(ErrorHandler::class);
496
        $errorHandler->start();
497
498
        // Register error log file
499
        $errorLog = getenv('SS_ERROR_LOG');
500
        if ($errorLog) {
501
            $logger = Injector::inst()->get(LoggerInterface::class);
502
            if ($logger instanceof Logger) {
503
                $logger->pushHandler(new StreamHandler($this->basePath . '/' . $errorLog, Logger::WARNING));
504
            } else {
505
                user_error("SS_ERROR_LOG setting only works with Monolog, you are using another logger", E_USER_WARNING);
506
            }
507
        }
508
    }
509
510
    public function shutdown()
511
    {
512
    }
513
514
    public function nest()
515
    {
516
        // Clone this kernel, nesting config / injector manifest containers
517
        $kernel = clone $this;
518
        $kernel->setConfigLoader($this->configLoader->nest());
519
        $kernel->setInjectorLoader($this->injectorLoader->nest());
520
        return $kernel;
521
    }
522
523
    public function activate()
524
    {
525
        $this->configLoader->activate();
526
        $this->injectorLoader->activate();
527
528
        // Self register
529
        $this->getInjectorLoader()
530
            ->getManifest()
531
            ->registerService($this, Kernel::class);
532
        return $this;
533
    }
534
535
    public function getNestedFrom()
536
    {
537
        return $this->nestedFrom;
538
    }
539
540
    public function getContainer()
541
    {
542
        return $this->getInjectorLoader()->getManifest();
543
    }
544
545
    public function setInjectorLoader(InjectorLoader $injectorLoader)
546
    {
547
        $this->injectorLoader = $injectorLoader;
548
        $injectorLoader
549
            ->getManifest()
550
            ->registerService($this, Kernel::class);
551
        return $this;
552
    }
553
554
    public function getInjectorLoader()
555
    {
556
        return $this->injectorLoader;
557
    }
558
559
    public function getClassLoader()
560
    {
561
        return $this->classLoader;
562
    }
563
564
    public function setClassLoader(ClassLoader $classLoader)
565
    {
566
        $this->classLoader = $classLoader;
567
        return $this;
568
    }
569
570
    public function getModuleLoader()
571
    {
572
        return $this->moduleLoader;
573
    }
574
575
    public function setModuleLoader(ModuleLoader $moduleLoader)
576
    {
577
        $this->moduleLoader = $moduleLoader;
578
        return $this;
579
    }
580
581
    public function setEnvironment($environment)
582
    {
583
        if (!in_array($environment, [self::DEV, self::TEST, self::LIVE, null])) {
584
            throw new InvalidArgumentException(
585
                "Director::set_environment_type passed '$environment'.  It should be passed dev, test, or live"
586
            );
587
        }
588
        $this->enviroment = $environment;
589
        return $this;
590
    }
591
592
    public function getConfigLoader()
593
    {
594
        return $this->configLoader;
595
    }
596
597
    public function setConfigLoader($configLoader)
598
    {
599
        $this->configLoader = $configLoader;
600
        return $this;
601
    }
602
603
    public function getThemeResourceLoader()
604
    {
605
        return $this->themeResourceLoader;
606
    }
607
608
    public function setThemeResourceLoader($themeResourceLoader)
609
    {
610
        $this->themeResourceLoader = $themeResourceLoader;
611
        return $this;
612
    }
613
}
614