Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

DatabaseAdmin::lastBuilt()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
// Include the DB class
3
require_once("model/DB.php");
4
5
/**
6
 * DatabaseAdmin class
7
 *
8
 * Utility functions for administrating the database. These can be accessed
9
 * via URL, e.g. http://www.yourdomain.com/db/build.
10
 *
11
 * @package framework
12
 * @subpackage model
13
 */
14
class DatabaseAdmin extends Controller {
15
16
	/// SECURITY ///
17
	private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
18
		'index',
19
		'build',
20
		'cleanup',
21
		'import'
22
	);
23
24 View Code Duplication
	public function init() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
25
		parent::init();
26
27
		// We allow access to this controller regardless of live-status or ADMIN permission only
28
		// if on CLI or with the database not ready. The latter makes it less errorprone to do an
29
		// initial schema build without requiring a default-admin login.
30
		// Access to this controller is always allowed in "dev-mode", or of the user is ADMIN.
31
		$isRunningTests = (class_exists('SapphireTest', false) && SapphireTest::is_running_test());
32
		$canAccess = (
33
			Director::isDev()
34
			|| !Security::database_is_ready()
35
			// We need to ensure that DevelopmentAdminTest can simulate permission failures when running
36
			// "dev/tests" from CLI.
37
			|| (Director::is_cli() && !$isRunningTests)
38
			|| Permission::check("ADMIN")
39
		);
40
		if(!$canAccess) {
41
			return Security::permissionFailure($this,
42
				"This page is secured and you need administrator rights to access it. " .
43
				"Enter your credentials below and we will send you right along.");
44
		}
45
	}
46
47
	/**
48
	 * Get the data classes, grouped by their root class
49
	 *
50
	 * @return array Array of data classes, grouped by their root class
51
	 */
52
	public function groupedDataClasses() {
53
		// Get all root data objects
54
		$allClasses = get_declared_classes();
55
		foreach($allClasses as $class) {
56
			if(get_parent_class($class) == "DataObject")
57
				$rootClasses[$class] = array();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$rootClasses was never initialized. Although not strictly required by PHP, it is generally a good practice to add $rootClasses = 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...
58
		}
59
60
		// Assign every other data object one of those
61
		foreach($allClasses as $class) {
62
			if(!isset($rootClasses[$class]) && is_subclass_of($class, "DataObject")) {
63
				foreach($rootClasses as $rootClass => $dummy) {
64
					if(is_subclass_of($class, $rootClass)) {
65
						$rootClasses[$rootClass][] = $class;
0 ignored issues
show
Bug introduced by
The variable $rootClasses does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
66
						break;
67
					}
68
				}
69
			}
70
		}
71
		return $rootClasses;
72
	}
73
74
75
	/**
76
	 * When we're called as /dev/build, that's actually the index. Do the same
77
	 * as /dev/build/build.
78
	 */
79
	public function index() {
80
		return $this->build();
81
	}
82
83
	/**
84
	 * Updates the database schema, creating tables & fields as necessary.
85
	 */
86
	public function build() {
87
		// The default time limit of 30 seconds is normally not enough
88
		increase_time_limit_to(600);
89
90
		// Get all our classes
91
		SS_ClassLoader::instance()->getManifest()->regenerate();
92
93
		$url = $this->getReturnURL();
94
		if($url) {
95
			echo "<p>Setting up the database; you will be returned to your site shortly....</p>";
96
			$this->doBuild(true);
97
			echo "<p>Done!</p>";
98
			$this->redirect($url);
99
		} else {
100
			$quiet = $this->request->requestVar('quiet') !== null;
101
			$fromInstaller = $this->request->requestVar('from_installer') !== null;
102
			$populate = $this->request->requestVar('dont_populate') === null;
103
			$this->doBuild($quiet || $fromInstaller, $populate);
104
		}
105
	}
106
107
	/**
108
	 * Gets the url to return to after build
109
	 *
110
	 * @return string|null
111
	 */
112
	protected function getReturnURL() {
113
		$url = $this->request->getVar('returnURL');
114
115
		// Check that this url is a site url
116
		if(empty($url) || !Director::is_site_url($url)) {
117
			return null;
118
		}
119
120
		// Convert to absolute URL
121
		return Director::absoluteURL($url, true);
122
	}
123
124
	/**
125
	 * Check if database needs to be built, and build it if it does.
126
	 */
127
	public static function autoBuild() {
128
		$dataClasses = ClassInfo::subclassesFor('DataObject');
129
		$lastBuilt = self::lastBuilt();
130
		foreach($dataClasses as $class) {
0 ignored issues
show
Bug introduced by
The expression $dataClasses of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
131
			if(filemtime(getClassFile($class)) > $lastBuilt) {
132
				$da = new DatabaseAdmin();
133
				$da->doBuild(true);
134
				return;
135
			}
136
		}
137
	}
138
139
	/**
140
	 * Build the default data, calling requireDefaultRecords on all
141
	 * DataObject classes
142
	 */
143
	public function buildDefaults() {
144
		$dataClasses = ClassInfo::subclassesFor('DataObject');
145
		array_shift($dataClasses);
146
		foreach($dataClasses as $dataClass){
147
			singleton($dataClass)->requireDefaultRecords();
148
			print "Defaults loaded for $dataClass<br/>";
149
		}
150
	}
151
152
	/**
153
	 * Returns the timestamp of the time that the database was last built
154
	 *
155
	 * @return string Returns the timestamp of the time that the database was
156
	 *                last built
157
	 */
158
	public static function lastBuilt() {
159
		$file = TEMP_FOLDER . '/database-last-generated-' .
160
			str_replace(array('\\','/',':'), '.' , Director::baseFolder());
161
162
		if(file_exists($file)) {
163
			return filemtime($file);
164
		}
165
	}
166
167
168
	/**
169
	 * Updates the database schema, creating tables & fields as necessary.
170
	 *
171
	 * @param boolean $quiet Don't show messages
172
	 * @param boolean $populate Populate the database, as well as setting up its schema
173
	 */
174
	public function doBuild($quiet = false, $populate = true, $testMode = false) {
175
		if($quiet) {
176
			DB::quiet();
177
		} else {
178
			$conn = DB::get_conn();
179
			// Assumes database class is like "MySQLDatabase" or "MSSQLDatabase" (suffixed with "Database")
180
			$dbType = substr(get_class($conn), 0, -8);
181
			$dbVersion = $conn->getVersion();
182
			$databaseName = (method_exists($conn, 'currentDatabase')) ? $conn->getSelectedDatabase() : "";
183
184
			if(Director::is_cli()) {
185
				echo sprintf("\n\nBuilding database %s using %s %s\n\n", $databaseName, $dbType, $dbVersion);
186
			} else {
187
				echo sprintf("<h2>Building database %s using %s %s</h2>", $databaseName, $dbType, $dbVersion);
188
			}
189
		}
190
191
		// Set up the initial database
192
		if(!DB::is_active()) {
193
			if(!$quiet) {
194
				echo '<p><b>Creating database</b></p>';
195
			}
196
197
			// Load parameters from existing configuration
198
			global $databaseConfig;
199
			if(empty($databaseConfig) && empty($_REQUEST['db'])) {
200
				user_error("No database configuration available", E_USER_ERROR);
201
			}
202
			$parameters = (!empty($databaseConfig)) ? $databaseConfig : $_REQUEST['db'];
203
204
			// Check database name is given
205
			if(empty($parameters['database'])) {
206
				user_error("No database name given; please give a value for \$databaseConfig['database']",
207
							E_USER_ERROR);
208
			}
209
			$database = $parameters['database'];
210
211
			// Establish connection and create database in two steps
212
			unset($parameters['database']);
213
			DB::connect($parameters);
214
			DB::create_database($database);
215
		}
216
217
		// Build the database.  Most of the hard work is handled by DataObject
218
		$dataClasses = ClassInfo::subclassesFor('DataObject');
219
		array_shift($dataClasses);
220
221
		if(!$quiet) {
222
			if(Director::is_cli()) echo "\nCREATING DATABASE TABLES\n\n";
223
			else echo "\n<p><b>Creating database tables</b></p>\n\n";
224
		}
225
226
		// Initiate schema update
227
		$dbSchema = DB::get_schema();
228
		$dbSchema->schemaUpdate(function() use($dataClasses, $testMode, $quiet){
229
			foreach($dataClasses as $dataClass) {
230
				// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
231
				if(!class_exists($dataClass)) continue;
232
233
				// Check if this class should be excluded as per testing conventions
234
				$SNG = singleton($dataClass);
235
				if(!$testMode && $SNG instanceof TestOnly) continue;
236
237
				// Log data
238
				if(!$quiet) {
239
					if(Director::is_cli()) echo " * $dataClass\n";
240
					else echo "<li>$dataClass</li>\n";
241
				}
242
243
				// Instruct the class to apply its schema to the database
244
				$SNG->requireTable();
245
			}
246
		});
247
		ClassInfo::reset_db_cache();
248
249
		if($populate) {
250
			if(!$quiet) {
251
				if(Director::is_cli()) echo "\nCREATING DATABASE RECORDS\n\n";
252
				else echo "\n<p><b>Creating database records</b></p>\n\n";
253
			}
254
255
			foreach($dataClasses as $dataClass) {
256
				// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
257
				// Test_ indicates that it's the data class is part of testing system
258
				if(strpos($dataClass,'Test_') === false && class_exists($dataClass)) {
259
					if(!$quiet) {
260
						if(Director::is_cli()) echo " * $dataClass\n";
261
						else echo "<li>$dataClass</li>\n";
262
					}
263
264
					singleton($dataClass)->requireDefaultRecords();
265
				}
266
			}
267
		}
268
269
		touch(TEMP_FOLDER
270
			. '/database-last-generated-'
271
			. str_replace(array('\\', '/', ':'), '.', Director::baseFolder())
272
		);
273
274
		if(isset($_REQUEST['from_installer'])) {
275
			echo "OK";
276
		}
277
278
		if(!$quiet) {
279
			echo (Director::is_cli()) ? "\n Database build completed!\n\n" :"<p>Database build completed!</p>";
280
		}
281
282
		ClassInfo::reset_db_cache();
283
	}
284
285
	/**
286
	 * Clear all data out of the database
287
	 *
288
	 * @deprecated since version 4.0
289
	 */
290
	public function clearAllData() {
291
		Deprecation::notice('4.0', 'Use DB::get_conn()->clearAllData() instead');
292
		DB::get_conn()->clearAllData();
293
	}
294
295
	/**
296
	 * Remove invalid records from tables - that is, records that don't have
297
	 * corresponding records in their parent class tables.
298
	 */
299
	public function cleanup() {
300
		$allClasses = get_declared_classes();
301
		foreach($allClasses as $class) {
302
			if(get_parent_class($class) == 'DataObject') {
303
				$baseClasses[] = $class;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$baseClasses was never initialized. Although not strictly required by PHP, it is generally a good practice to add $baseClasses = 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...
304
			}
305
		}
306
307
		foreach($baseClasses as $baseClass) {
0 ignored issues
show
Bug introduced by
The variable $baseClasses does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
308
			// Get data classes
309
			$subclasses = ClassInfo::subclassesFor($baseClass);
310
			unset($subclasses[0]);
311
			foreach($subclasses as $k => $subclass) {
0 ignored issues
show
Bug introduced by
The expression $subclasses of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
312
				if(DataObject::has_own_table($subclass)) {
313
					unset($subclasses[$k]);
314
				}
315
			}
316
317
			if($subclasses) {
318
				$records = DB::query("SELECT * FROM \"$baseClass\"");
319
320
321
				foreach($subclasses as $subclass) {
322
					$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...
323
						DB::query("SELECT \"ID\" FROM \"$subclass\"")->keyedColumn();
324
				}
325
326
				foreach($records as $record) {
327
					foreach($subclasses as $subclass) {
328
						$id = $record['ID'];
329
						if(($record['ClassName'] != $subclass) &&
330
							(!is_subclass_of($record['ClassName'], $subclass)) &&
331
								(isset($recordExists[$subclass][$id]))) {
332
							$sql = "DELETE FROM \"$subclass\" WHERE \"ID\" = $record[ID]";
333
							echo "<li>$sql";
334
							DB::query($sql);
335
						}
336
					}
337
				}
338
			}
339
		}
340
	}
341
342
}
343