Completed
Push — master ( a136b8...6cd9be )
by Ingo
07:21
created

DatabaseAdmin::init()   C

Complexity

Conditions 7
Paths 24

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 13
nc 24
nop 0
dl 0
loc 25
rs 6.7272
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 25 and the first side effect is on line 17.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace SilverStripe\ORM;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Core\ClassInfo;
8
use SilverStripe\Core\Manifest\ClassLoader;
9
use SilverStripe\Dev\SapphireTest;
10
use SilverStripe\Dev\TestOnly;
11
use SilverStripe\Dev\Deprecation;
12
use SilverStripe\Versioned\Versioned;
13
use SilverStripe\Security\Security;
14
use SilverStripe\Security\Permission;
15
16
// Include the DB class
17
require_once("DB.php");
18
19
/**
20
 * DatabaseAdmin class
21
 *
22
 * Utility functions for administrating the database. These can be accessed
23
 * via URL, e.g. http://www.yourdomain.com/db/build.
24
 */
25
class DatabaseAdmin extends Controller
26
{
27
28
    /// SECURITY ///
29
    private static $allowed_actions = array(
30
        'index',
31
        'build',
32
        'cleanup',
33
        'import'
34
    );
35
36
    /**
37
     * Obsolete classname values that should be remapped in dev/build
38
     */
39
    private static $classname_value_remapping = [
40
        'File' => 'SilverStripe\\Assets\\File',
41
        'Image' => 'SilverStripe\\Assets\\Image',
42
        'Folder' => 'SilverStripe\\Assets\\Folder',
43
        'Group' => 'SilverStripe\\Security\\Group',
44
        'LoginAttempt' => 'SilverStripe\\Security\\LoginAttempt',
45
        'Member' => 'SilverStripe\\Security\\Member',
46
        'MemberPassword' => 'SilverStripe\\Security\\MemberPassword',
47
        'Permission' => 'SilverStripe\\Security\\Permission',
48
        'PermissionRole' => 'SilverStripe\\Security\\PermissionRole',
49
        'PermissionRoleCode' => 'SilverStripe\\Security\\PermissionRoleCode',
50
        'RememberLoginHash' => 'SilverStripe\\Security\\RememberLoginHash',
51
    ];
52
53
    protected function init()
54
    {
55
        parent::init();
56
57
        // We allow access to this controller regardless of live-status or ADMIN permission only
58
        // if on CLI or with the database not ready. The latter makes it less errorprone to do an
59
        // initial schema build without requiring a default-admin login.
60
        // Access to this controller is always allowed in "dev-mode", or of the user is ADMIN.
61
        $isRunningTests = (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test());
62
        $canAccess = (
63
            Director::isDev()
64
            || !Security::database_is_ready()
65
            // We need to ensure that DevelopmentAdminTest can simulate permission failures when running
66
            // "dev/tests" from CLI.
67
            || (Director::is_cli() && !$isRunningTests)
68
            || Permission::check("ADMIN")
69
        );
70
        if (!$canAccess) {
71
            Security::permissionFailure(
72
                $this,
73
                "This page is secured and you need administrator rights to access it. " .
74
                "Enter your credentials below and we will send you right along."
75
            );
76
        }
77
    }
78
79
    /**
80
     * Get the data classes, grouped by their root class
81
     *
82
     * @return array Array of data classes, grouped by their root class
83
     */
84
    public function groupedDataClasses()
85
    {
86
        // Get all root data objects
87
        $allClasses = get_declared_classes();
88
        $rootClasses = [];
89
        foreach ($allClasses as $class) {
90
            if (get_parent_class($class) == 'SilverStripe\ORM\DataObject') {
91
                $rootClasses[$class] = array();
92
            }
93
        }
94
95
        // Assign every other data object one of those
96
        foreach ($allClasses as $class) {
97
            if (!isset($rootClasses[$class]) && is_subclass_of($class, 'SilverStripe\ORM\DataObject')) {
98
                foreach ($rootClasses as $rootClass => $dummy) {
99
                    if (is_subclass_of($class, $rootClass)) {
100
                        $rootClasses[$rootClass][] = $class;
101
                        break;
102
                    }
103
                }
104
            }
105
        }
106
        return $rootClasses;
107
    }
108
109
110
    /**
111
     * When we're called as /dev/build, that's actually the index. Do the same
112
     * as /dev/build/build.
113
     */
114
    public function index()
115
    {
116
        return $this->build();
117
    }
118
119
    /**
120
     * Updates the database schema, creating tables & fields as necessary.
121
     */
122
    public function build()
123
    {
124
        // The default time limit of 30 seconds is normally not enough
125
        increase_time_limit_to(600);
126
127
        // Get all our classes
128
        ClassLoader::instance()->getManifest()->regenerate();
129
130
        $url = $this->getReturnURL();
131
        if ($url) {
132
            echo "<p>Setting up the database; you will be returned to your site shortly....</p>";
133
            $this->doBuild(true);
134
            echo "<p>Done!</p>";
135
            $this->redirect($url);
136
        } else {
137
            $quiet = $this->request->requestVar('quiet') !== null;
138
            $fromInstaller = $this->request->requestVar('from_installer') !== null;
139
            $populate = $this->request->requestVar('dont_populate') === null;
140
            $this->doBuild($quiet || $fromInstaller, $populate);
141
        }
142
    }
143
144
    /**
145
     * Gets the url to return to after build
146
     *
147
     * @return string|null
148
     */
149
    protected function getReturnURL()
150
    {
151
        $url = $this->request->getVar('returnURL');
152
153
        // Check that this url is a site url
154
        if (empty($url) || !Director::is_site_url($url)) {
155
            return null;
156
        }
157
158
        // Convert to absolute URL
159
        return Director::absoluteURL($url, true);
160
    }
161
162
    /**
163
     * Build the default data, calling requireDefaultRecords on all
164
     * DataObject classes
165
     */
166
    public function buildDefaults()
167
    {
168
        $dataClasses = ClassInfo::subclassesFor('SilverStripe\ORM\DataObject');
169
        array_shift($dataClasses);
170
        foreach ($dataClasses as $dataClass) {
171
            singleton($dataClass)->requireDefaultRecords();
172
            print "Defaults loaded for $dataClass<br/>";
173
        }
174
    }
175
176
    /**
177
     * Returns the timestamp of the time that the database was last built
178
     *
179
     * @return string Returns the timestamp of the time that the database was
180
     *                last built
181
     */
182
    public static function lastBuilt()
183
    {
184
        $file = TEMP_FOLDER
185
            . '/database-last-generated-'
186
            . str_replace(array('\\','/',':'), '.', Director::baseFolder());
187
188
        if (file_exists($file)) {
189
            return filemtime($file);
190
        }
191
        return null;
192
    }
193
194
195
    /**
196
     * Updates the database schema, creating tables & fields as necessary.
197
     *
198
     * @param boolean $quiet Don't show messages
199
     * @param boolean $populate Populate the database, as well as setting up its schema
200
     * @param bool $testMode
201
     */
202
    public function doBuild($quiet = false, $populate = true, $testMode = false)
0 ignored issues
show
Coding Style introduced by
doBuild uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
203
    {
204
        if ($quiet) {
205
            DB::quiet();
206
        } else {
207
            $conn = DB::get_conn();
208
            // Assumes database class is like "MySQLDatabase" or "MSSQLDatabase" (suffixed with "Database")
209
            $dbType = substr(get_class($conn), 0, -8);
210
            $dbVersion = $conn->getVersion();
211
            $databaseName = (method_exists($conn, 'currentDatabase')) ? $conn->getSelectedDatabase() : "";
212
213
            if (Director::is_cli()) {
214
                echo sprintf("\n\nBuilding database %s using %s %s\n\n", $databaseName, $dbType, $dbVersion);
215
            } else {
216
                echo sprintf("<h2>Building database %s using %s %s</h2>", $databaseName, $dbType, $dbVersion);
217
            }
218
        }
219
220
        // Set up the initial database
221
        if (!DB::is_active()) {
222
            if (!$quiet) {
223
                echo '<p><b>Creating database</b></p>';
224
            }
225
226
            // Load parameters from existing configuration
227
            global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
228
            if (empty($databaseConfig) && empty($_REQUEST['db'])) {
229
                user_error("No database configuration available", E_USER_ERROR);
230
            }
231
            $parameters = (!empty($databaseConfig)) ? $databaseConfig : $_REQUEST['db'];
232
233
            // Check database name is given
234
            if (empty($parameters['database'])) {
235
                user_error(
236
                    "No database name given; please give a value for \$databaseConfig['database']",
237
                    E_USER_ERROR
238
                );
239
            }
240
            $database = $parameters['database'];
241
242
            // Establish connection and create database in two steps
243
            unset($parameters['database']);
244
            DB::connect($parameters);
245
            DB::create_database($database);
246
        }
247
248
        // Build the database.  Most of the hard work is handled by DataObject
249
        $dataClasses = ClassInfo::subclassesFor('SilverStripe\ORM\DataObject');
250
        array_shift($dataClasses);
251
252
        if (!$quiet) {
253
            if (Director::is_cli()) {
254
                echo "\nCREATING DATABASE TABLES\n\n";
255
            } else {
256
                echo "\n<p><b>Creating database tables</b></p>\n\n";
257
            }
258
        }
259
260
        // Initiate schema update
261
        $dbSchema = DB::get_schema();
262
        $dbSchema->schemaUpdate(function () use ($dataClasses, $testMode, $quiet) {
263
            foreach ($dataClasses as $dataClass) {
264
                // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
265
                if (!class_exists($dataClass)) {
266
                    continue;
267
                }
268
269
                // Check if this class should be excluded as per testing conventions
270
                $SNG = singleton($dataClass);
271
                if (!$testMode && $SNG instanceof TestOnly) {
272
                    continue;
273
                }
274
275
                // Log data
276
                if (!$quiet) {
277
                    if (Director::is_cli()) {
278
                        echo " * $dataClass\n";
279
                    } else {
280
                        echo "<li>$dataClass</li>\n";
281
                    }
282
                }
283
284
                // Instruct the class to apply its schema to the database
285
                $SNG->requireTable();
286
            }
287
        });
288
        ClassInfo::reset_db_cache();
289
290
        if ($populate) {
291
            if (!$quiet) {
292
                if (Director::is_cli()) {
293
                    echo "\nCREATING DATABASE RECORDS\n\n";
294
                } else {
295
                    echo "\n<p><b>Creating database records</b></p>\n\n";
296
                }
297
            }
298
299
            foreach ($dataClasses as $dataClass) {
300
                // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
301
                // Test_ indicates that it's the data class is part of testing system
302
                if (strpos($dataClass, 'Test_') === false && class_exists($dataClass)) {
303
                    if (!$quiet) {
304
                        if (Director::is_cli()) {
305
                            echo " * $dataClass\n";
306
                        } else {
307
                            echo "<li>$dataClass</li>\n";
308
                        }
309
                    }
310
311
                    singleton($dataClass)->requireDefaultRecords();
312
                }
313
            }
314
315
            // Remap obsolete class names
316
            $schema = DataObject::getSchema();
317
            foreach ($this->config()->classname_value_remapping as $oldClassName => $newClassName) {
318
                $baseDataClass = $schema->baseDataClass($newClassName);
319
                $badRecordCount = DataObject::get($baseDataClass)
320
                    ->filter(["ClassName" => $oldClassName ])
321
                    ->count();
322
                if ($badRecordCount > 0) {
323
                    if (Director::is_cli()) {
324
                        echo " * Correcting $badRecordCount obsolete classname values for $newClassName\n";
325
                    } else {
326
                        echo "<li>Correcting $badRecordCount obsolete classname values for $newClassName</li>\n";
327
                    }
328
                    $table = $schema->baseDataTable($baseDataClass);
329
330
                    $updateQuery = "UPDATE \"$table%s\" SET \"ClassName\" = ? WHERE \"ClassName\" = ?";
331
                    $updateQueries = [sprintf($updateQuery, '')];
332
333
                    // Remap versioned table ClassName values as well
334
                    $class = singleton($newClassName);
335
                    if ($class->has_extension(Versioned::class)) {
336
                        if ($class->hasStages()) {
337
                            $updateQueries[] = sprintf($updateQuery, '_Live');
338
                        }
339
                        $updateQueries[] = sprintf($updateQuery, '_Versions');
340
                    }
341
342
                    foreach ($updateQueries as $query) {
343
                        DB::prepared_query($query, [$newClassName, $oldClassName]);
344
                    }
345
                }
346
            }
347
        }
348
349
        touch(TEMP_FOLDER
350
            . '/database-last-generated-'
351
            . str_replace(array('\\', '/', ':'), '.', Director::baseFolder()));
352
353
        if (isset($_REQUEST['from_installer'])) {
354
            echo "OK";
355
        }
356
357
        if (!$quiet) {
358
            echo (Director::is_cli()) ? "\n Database build completed!\n\n" :"<p>Database build completed!</p>";
359
        }
360
361
        ClassInfo::reset_db_cache();
362
    }
363
364
    /**
365
     * Remove invalid records from tables - that is, records that don't have
366
     * corresponding records in their parent class tables.
367
     */
368
    public function cleanup()
369
    {
370
        $baseClasses = [];
371
        foreach (ClassInfo::subclassesFor(DataObject::class) as $class) {
372
            if (get_parent_class($class) == DataObject::class) {
373
                $baseClasses[] = $class;
374
            }
375
        }
376
377
        $schema = DataObject::getSchema();
378
        foreach ($baseClasses as $baseClass) {
379
            // Get data classes
380
            $baseTable = $schema->baseDataTable($baseClass);
381
            $subclasses = ClassInfo::subclassesFor($baseClass);
382
            unset($subclasses[0]);
383
            foreach ($subclasses as $k => $subclass) {
384
                if (!DataObject::getSchema()->classHasTable($subclass)) {
385
                    unset($subclasses[$k]);
386
                }
387
            }
388
389
            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...
390
                $records = DB::query("SELECT * FROM \"$baseTable\"");
391
392
393
                foreach ($subclasses as $subclass) {
394
                    $subclassTable = $schema->tableName($subclass);
395
                    $recordExists[$subclass] =
0 ignored issues
show
Coding Style Comprehensibility introduced by
$recordExists was never initialized. Although not strictly required by PHP, it is generally a good practice to add $recordExists = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
396
                        DB::query("SELECT \"ID\" FROM \"$subclassTable\"")->keyedColumn();
397
                }
398
399
                foreach ($records as $record) {
400
                    foreach ($subclasses as $subclass) {
401
                        $subclassTable = $schema->tableName($subclass);
402
                        $id = $record['ID'];
403
                        if (($record['ClassName'] != $subclass)
404
                            && (!is_subclass_of($record['ClassName'], $subclass))
405
                            && isset($recordExists[$subclass][$id])
406
                        ) {
407
                            $sql = "DELETE FROM \"$subclassTable\" WHERE \"ID\" = ?";
408
                            echo "<li>$sql [{$id}]</li>";
409
                            DB::prepared_query($sql, [$id]);
410
                        }
411
                    }
412
                }
413
            }
414
        }
415
    }
416
}
417