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

DatabaseAdmin::buildDefaults()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 12
nop 0
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use BadMethodCallException;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\Core\Environment;
10
use SilverStripe\Core\Manifest\ClassLoader;
11
use SilverStripe\Dev\DevelopmentAdmin;
12
use SilverStripe\Dev\TestOnly;
13
use SilverStripe\Security\Permission;
14
use SilverStripe\Security\Security;
15
use SilverStripe\Versioned\Versioned;
16
17
/**
18
 * DatabaseAdmin class
19
 *
20
 * Utility functions for administrating the database. These can be accessed
21
 * via URL, e.g. http://www.yourdomain.com/db/build.
22
 */
23
class DatabaseAdmin extends Controller
24
{
25
26
    /// SECURITY ///
27
    private static $allowed_actions = array(
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
28
        'index',
29
        'build',
30
        'cleanup',
31
        'import'
32
    );
33
34
    /**
35
     * Obsolete classname values that should be remapped in dev/build
36
     */
37
    private static $classname_value_remapping = [
38
        'File' => 'SilverStripe\\Assets\\File',
39
        'Image' => 'SilverStripe\\Assets\\Image',
40
        'Folder' => 'SilverStripe\\Assets\\Folder',
41
        'Group' => 'SilverStripe\\Security\\Group',
42
        'LoginAttempt' => 'SilverStripe\\Security\\LoginAttempt',
43
        'Member' => 'SilverStripe\\Security\\Member',
44
        'MemberPassword' => 'SilverStripe\\Security\\MemberPassword',
45
        'Permission' => 'SilverStripe\\Security\\Permission',
46
        'PermissionRole' => 'SilverStripe\\Security\\PermissionRole',
47
        'PermissionRoleCode' => 'SilverStripe\\Security\\PermissionRoleCode',
48
        'RememberLoginHash' => 'SilverStripe\\Security\\RememberLoginHash',
49
    ];
50
51
    protected function init()
52
    {
53
        parent::init();
54
55
        // We allow access to this controller regardless of live-status or ADMIN permission only
56
        // if on CLI or with the database not ready. The latter makes it less errorprone to do an
57
        // initial schema build without requiring a default-admin login.
58
        // Access to this controller is always allowed in "dev-mode", or of the user is ADMIN.
59
        $allowAllCLI = DevelopmentAdmin::config()->get('allow_all_cli');
60
        $canAccess = (
61
            Director::isDev()
62
            || !Security::database_is_ready()
63
            // We need to ensure that DevelopmentAdminTest can simulate permission failures when running
64
            // "dev/tests" from CLI.
65
            || (Director::is_cli() && $allowAllCLI)
66
            || Permission::check("ADMIN")
67
        );
68
        if (!$canAccess) {
69
            Security::permissionFailure(
70
                $this,
71
                "This page is secured and you need administrator rights to access it. " .
72
                "Enter your credentials below and we will send you right along."
73
            );
74
        }
75
    }
76
77
    /**
78
     * Get the data classes, grouped by their root class
79
     *
80
     * @return array Array of data classes, grouped by their root class
81
     */
82
    public function groupedDataClasses()
83
    {
84
        // Get all root data objects
85
        $allClasses = get_declared_classes();
86
        $rootClasses = [];
87
        foreach ($allClasses as $class) {
88
            if (get_parent_class($class) == DataObject::class) {
89
                $rootClasses[$class] = array();
90
            }
91
        }
92
93
        // Assign every other data object one of those
94
        foreach ($allClasses as $class) {
95
            if (!isset($rootClasses[$class]) && is_subclass_of($class, DataObject::class)) {
96
                foreach ($rootClasses as $rootClass => $dummy) {
97
                    if (is_subclass_of($class, $rootClass)) {
98
                        $rootClasses[$rootClass][] = $class;
99
                        break;
100
                    }
101
                }
102
            }
103
        }
104
        return $rootClasses;
105
    }
106
107
108
    /**
109
     * When we're called as /dev/build, that's actually the index. Do the same
110
     * as /dev/build/build.
111
     */
112
    public function index()
113
    {
114
        return $this->build();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->build() targeting SilverStripe\ORM\DatabaseAdmin::build() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
115
    }
116
117
    /**
118
     * Updates the database schema, creating tables & fields as necessary.
119
     */
120
    public function build()
121
    {
122
        // The default time limit of 30 seconds is normally not enough
123
        Environment::increaseTimeLimitTo(600);
124
125
        // If this code is being run outside of a dev/build or without a ?flush query string param,
126
        // the class manifest hasn't been flushed, so do it here
127
        $request = $this->getRequest();
128
        if (!array_key_exists('flush', $request->getVars()) && strpos($request->getURL(), 'dev/build') !== 0) {
129
            ClassLoader::inst()->getManifest()->regenerate(false);
130
        }
131
132
        $url = $this->getReturnURL();
133
        if ($url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type null|string 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...
134
            echo "<p>Setting up the database; you will be returned to your site shortly....</p>";
135
            $this->doBuild(true);
136
            echo "<p>Done!</p>";
137
            $this->redirect($url);
138
        } else {
139
            $quiet = $this->request->requestVar('quiet') !== null;
140
            $fromInstaller = $this->request->requestVar('from_installer') !== null;
141
            $populate = $this->request->requestVar('dont_populate') === null;
142
            $this->doBuild($quiet || $fromInstaller, $populate);
143
        }
144
    }
145
146
    /**
147
     * Gets the url to return to after build
148
     *
149
     * @return string|null
150
     */
151
    protected function getReturnURL()
152
    {
153
        $url = $this->request->getVar('returnURL');
154
155
        // Check that this url is a site url
156
        if (empty($url) || !Director::is_site_url($url)) {
157
            return null;
158
        }
159
160
        // Convert to absolute URL
161
        return Director::absoluteURL($url, true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $relativeParent of SilverStripe\Control\Director::absoluteURL(). ( Ignorable by Annotation )

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

161
        return Director::absoluteURL($url, /** @scrutinizer ignore-type */ true);
Loading history...
162
    }
163
164
    /**
165
     * Build the default data, calling requireDefaultRecords on all
166
     * DataObject classes
167
     */
168
    public function buildDefaults()
169
    {
170
        $dataClasses = ClassInfo::subclassesFor(DataObject::class);
171
        array_shift($dataClasses);
172
173
        if (!Director::is_cli()) {
174
            echo "<ul>";
175
        }
176
177
        foreach ($dataClasses as $dataClass) {
178
            singleton($dataClass)->requireDefaultRecords();
179
            if (Director::is_cli()) {
180
                echo "Defaults loaded for $dataClass\n";
181
            } else {
182
                echo "<li>Defaults loaded for $dataClass</li>\n";
183
            }
184
        }
185
186
        if (!Director::is_cli()) {
187
            echo "</ul>";
188
        }
189
    }
190
191
    /**
192
     * Returns the timestamp of the time that the database was last built
193
     *
194
     * @return string Returns the timestamp of the time that the database was
195
     *                last built
196
     */
197
    public static function lastBuilt()
198
    {
199
        $file = TEMP_PATH
200
            . DIRECTORY_SEPARATOR
201
            . 'database-last-generated-'
202
            . str_replace(array('\\','/',':'), '.', Director::baseFolder());
203
204
        if (file_exists($file)) {
205
            return filemtime($file);
206
        }
207
        return null;
208
    }
209
210
211
    /**
212
     * Updates the database schema, creating tables & fields as necessary.
213
     *
214
     * @param boolean $quiet Don't show messages
215
     * @param boolean $populate Populate the database, as well as setting up its schema
216
     * @param bool $testMode
217
     */
218
    public function doBuild($quiet = false, $populate = true, $testMode = false)
219
    {
220
        if ($quiet) {
221
            DB::quiet();
222
        } else {
223
            $conn = DB::get_conn();
224
            // Assumes database class is like "MySQLDatabase" or "MSSQLDatabase" (suffixed with "Database")
225
            $dbType = substr(get_class($conn), 0, -8);
226
            $dbVersion = $conn->getVersion();
227
            $databaseName = $conn->getSelectedDatabase();
228
229
            if (Director::is_cli()) {
230
                echo sprintf("\n\nBuilding database %s using %s %s\n\n", $databaseName, $dbType, $dbVersion);
231
            } else {
232
                echo sprintf("<h2>Building database %s using %s %s</h2>", $databaseName, $dbType, $dbVersion);
233
            }
234
        }
235
236
        // Set up the initial database
237
        if (!DB::is_active()) {
238
            if (!$quiet) {
239
                echo '<p><b>Creating database</b></p>';
240
            }
241
242
            // Load parameters from existing configuration
243
            $databaseConfig = DB::getConfig();
244
            if (empty($databaseConfig) && empty($_REQUEST['db'])) {
245
                throw new BadMethodCallException("No database configuration available");
246
            }
247
            $parameters = (!empty($databaseConfig)) ? $databaseConfig : $_REQUEST['db'];
248
249
            // Check database name is given
250
            if (empty($parameters['database'])) {
251
                throw new BadMethodCallException(
252
                    "No database name given; please give a value for SS_DATABASE_NAME or set SS_DATABASE_CHOOSE_NAME"
253
                );
254
            }
255
            $database = $parameters['database'];
256
257
            // Establish connection and create database in two steps
258
            unset($parameters['database']);
259
            DB::connect($parameters);
260
            DB::create_database($database);
261
        }
262
263
        // Build the database.  Most of the hard work is handled by DataObject
264
        $dataClasses = ClassInfo::subclassesFor(DataObject::class);
265
        array_shift($dataClasses);
266
267
        if (!$quiet) {
268
            if (Director::is_cli()) {
269
                echo "\nCREATING DATABASE TABLES\n\n";
270
            } else {
271
                echo "\n<p><b>Creating database tables</b></p><ul>\n\n";
272
            }
273
        }
274
275
        // Initiate schema update
276
        $dbSchema = DB::get_schema();
277
        $dbSchema->schemaUpdate(function () use ($dataClasses, $testMode, $quiet) {
278
            $dataObjectSchema = DataObject::getSchema();
279
280
            foreach ($dataClasses as $dataClass) {
281
                // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
282
                if (!class_exists($dataClass)) {
283
                    continue;
284
                }
285
286
                // Check if this class should be excluded as per testing conventions
287
                $SNG = singleton($dataClass);
288
                if (!$testMode && $SNG instanceof TestOnly) {
289
                    continue;
290
                }
291
                $tableName = $dataObjectSchema->tableName($dataClass);
292
293
                // Log data
294
                if (!$quiet) {
295
                    if (Director::is_cli()) {
296
                        echo " * $tableName\n";
297
                    } else {
298
                        echo "<li>$tableName</li>\n";
299
                    }
300
                }
301
302
                // Instruct the class to apply its schema to the database
303
                $SNG->requireTable();
304
            }
305
        });
306
        ClassInfo::reset_db_cache();
307
308
        if (!$quiet && !Director::is_cli()) {
309
            echo "</ul>";
310
        }
311
312
        if ($populate) {
313
            if (!$quiet) {
314
                if (Director::is_cli()) {
315
                    echo "\nCREATING DATABASE RECORDS\n\n";
316
                } else {
317
                    echo "\n<p><b>Creating database records</b></p><ul>\n\n";
318
                }
319
            }
320
321
            foreach ($dataClasses as $dataClass) {
322
                // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
323
                // Test_ indicates that it's the data class is part of testing system
324
                if (strpos($dataClass, 'Test_') === false && class_exists($dataClass)) {
325
                    if (!$quiet) {
326
                        if (Director::is_cli()) {
327
                            echo " * $dataClass\n";
328
                        } else {
329
                            echo "<li>$dataClass</li>\n";
330
                        }
331
                    }
332
333
                    singleton($dataClass)->requireDefaultRecords();
334
                }
335
            }
336
337
            // Remap obsolete class names
338
            $schema = DataObject::getSchema();
339
            foreach ($this->config()->classname_value_remapping as $oldClassName => $newClassName) {
0 ignored issues
show
Bug Best Practice introduced by
The property classname_value_remapping does not exist on SilverStripe\Core\Config\Config_ForClass. Since you implemented __get, consider adding a @property annotation.
Loading history...
340
                $baseDataClass = $schema->baseDataClass($newClassName);
341
                $badRecordCount = DataObject::get($baseDataClass)
342
                    ->filter(["ClassName" => $oldClassName ])
343
                    ->count();
344
                if ($badRecordCount > 0) {
345
                    if (Director::is_cli()) {
346
                        echo " * Correcting $badRecordCount obsolete classname values for $newClassName\n";
347
                    } else {
348
                        echo "<li>Correcting $badRecordCount obsolete classname values for $newClassName</li>\n";
349
                    }
350
                    $table = $schema->baseDataTable($baseDataClass);
351
352
                    $updateQuery = "UPDATE \"$table%s\" SET \"ClassName\" = ? WHERE \"ClassName\" = ?";
353
                    $updateQueries = [sprintf($updateQuery, '')];
354
355
                    // Remap versioned table ClassName values as well
356
                    $class = singleton($newClassName);
357
                    if ($class->has_extension(Versioned::class)) {
358
                        if ($class->hasStages()) {
359
                            $updateQueries[] = sprintf($updateQuery, '_Live');
360
                        }
361
                        $updateQueries[] = sprintf($updateQuery, '_Versions');
362
                    }
363
364
                    foreach ($updateQueries as $query) {
365
                        DB::prepared_query($query, [$newClassName, $oldClassName]);
366
                    }
367
                }
368
            }
369
370
            if (!$quiet && !Director::is_cli()) {
371
                echo "</ul>";
372
            }
373
        }
374
375
        touch(TEMP_PATH
376
            . DIRECTORY_SEPARATOR
377
            . 'database-last-generated-'
378
            . str_replace(array('\\', '/', ':'), '.', Director::baseFolder()));
379
380
        if (isset($_REQUEST['from_installer'])) {
381
            echo "OK";
382
        }
383
384
        if (!$quiet) {
385
            echo (Director::is_cli()) ? "\n Database build completed!\n\n" :"<p>Database build completed!</p>";
386
        }
387
388
        ClassInfo::reset_db_cache();
389
    }
390
391
    /**
392
     * Remove invalid records from tables - that is, records that don't have
393
     * corresponding records in their parent class tables.
394
     */
395
    public function cleanup()
396
    {
397
        $baseClasses = [];
398
        foreach (ClassInfo::subclassesFor(DataObject::class) as $class) {
399
            if (get_parent_class($class) == DataObject::class) {
400
                $baseClasses[] = $class;
401
            }
402
        }
403
404
        $schema = DataObject::getSchema();
405
        foreach ($baseClasses as $baseClass) {
406
            // Get data classes
407
            $baseTable = $schema->baseDataTable($baseClass);
408
            $subclasses = ClassInfo::subclassesFor($baseClass);
409
            unset($subclasses[0]);
410
            foreach ($subclasses as $k => $subclass) {
411
                if (!DataObject::getSchema()->classHasTable($subclass)) {
412
                    unset($subclasses[$k]);
413
                }
414
            }
415
416
            if ($subclasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subclasses 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...
417
                $records = DB::query("SELECT * FROM \"$baseTable\"");
418
419
420
                foreach ($subclasses as $subclass) {
421
                    $subclassTable = $schema->tableName($subclass);
422
                    $recordExists[$subclass] =
423
                        DB::query("SELECT \"ID\" FROM \"$subclassTable\"")->keyedColumn();
424
                }
425
426
                foreach ($records as $record) {
427
                    foreach ($subclasses as $subclass) {
428
                        $subclassTable = $schema->tableName($subclass);
429
                        $id = $record['ID'];
430
                        if (($record['ClassName'] != $subclass)
431
                            && (!is_subclass_of($record['ClassName'], $subclass))
432
                            && isset($recordExists[$subclass][$id])
433
                        ) {
434
                            $sql = "DELETE FROM \"$subclassTable\" WHERE \"ID\" = ?";
435
                            echo "<li>$sql [{$id}]</li>";
436
                            DB::prepared_query($sql, [$id]);
437
                        }
438
                    }
439
                }
440
            }
441
        }
442
    }
443
}
444