Completed
Push — travis-lint ( d30aed...b19475 )
by Sam
13:31
created

DB::get_alternative_database_name()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 5
nop 0
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Core\Injector\Injector;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Control\Cookie;
9
use SilverStripe\Dev\Deprecation;
10
use SilverStripe\ORM\Connect\DBConnector;
11
use SilverStripe\ORM\Connect\DBSchemaManager;
12
use SilverStripe\ORM\Connect\Query;
13
use SilverStripe\ORM\Queries\SQLExpression;
14
use SilverStripe\ORM\Connect\Database;
15
use InvalidArgumentException;
16
use LogicException;
17
18
/**
19
 * Global database interface, complete with static methods.
20
 * Use this class for interacting with the database.
21
 */
22
class DB {
23
24
	/**
25
	 * This constant was added in SilverStripe 2.4 to indicate that SQL-queries
26
	 * should now use ANSI-compatible syntax.  The most notable affect of this
27
	 * change is that table and field names should be escaped with double quotes
28
	 * and not backticks
29
	 */
30
	const USE_ANSI_SQL = true;
31
32
33
	/**
34
	 * The global database connection.
35
	 * @var Database
36
	 */
37
	private static $connections = array();
38
39
	/**
40
	 * The last SQL query run.
41
	 * @var string
42
	 */
43
	public static $lastQuery;
44
45
	/**
46
	 * Internal flag to keep track of when db connection was attempted.
47
	 */
48
	private static $connection_attempted = false;
49
50
	/**
51
	 * Set the global database connection.
52
	 * Pass an object that's a subclass of SS_Database.  This object will be used when {@link DB::query()}
53
	 * is called.
54
	 *
55
	 * @param Database $connection The connecton object to set as the connection.
56
	 * @param string $name The name to give to this connection.  If you omit this argument, the connection
57
	 * will be the default one used by the ORM.  However, you can store other named connections to
58
	 * be accessed through DB::get_conn($name).  This is useful when you have an application that
59
	 * needs to connect to more than one database.
60
	 */
61
	public static function set_conn(Database $connection, $name = 'default') {
62
		self::$connections[$name] = $connection;
63
	}
64
65
	/**
66
	 * Get the global database connection.
67
	 *
68
	 * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
69
	 * the default connection is returned.
70
	 * @return Database
71
	 */
72
	public static function get_conn($name = 'default') {
73
		if(isset(self::$connections[$name])) {
74
			return self::$connections[$name];
75
		}
76
		return null;
77
	}
78
79
	/**
80
	 * @deprecated since version 4.0 Use DB::get_conn instead
81
	 * @todo PSR-2 standardisation will probably un-deprecate this
82
	 */
83
	public static function getConn($name = 'default') {
84
		Deprecation::notice('4.0', 'Use DB::get_conn instead');
85
		return self::get_conn($name);
86
	}
87
88
	/**
89
	 * Retrieves the schema manager for the current database
90
	 *
91
	 * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
92
	 * the default connection is returned.
93
	 * @return DBSchemaManager
94
	 */
95
	public static function get_schema($name = 'default') {
96
		$connection = self::get_conn($name);
97
		if($connection) {
98
			return $connection->getSchemaManager();
99
		}
100
		return null;
101
	}
102
103
	/**
104
	 * Builds a sql query with the specified connection
105
	 *
106
	 * @param SQLExpression $expression The expression object to build from
107
	 * @param array $parameters Out parameter for the resulting query parameters
108
	 * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
109
	 * the default connection is returned.
110
	 * @return string The resulting SQL as a string
111
	 */
112
	public static function build_sql(SQLExpression $expression, &$parameters, $name = 'default') {
113
		$connection = self::get_conn($name);
114
		if($connection) {
115
			return $connection->getQueryBuilder()->buildSQL($expression, $parameters);
116
		} else {
117
			$parameters = array();
118
			return null;
119
		}
120
	}
121
122
	/**
123
	 * Retrieves the connector object for the current database
124
	 *
125
	 * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
126
	 * the default connection is returned.
127
	 * @return DBConnector
128
	 */
129
	public static function get_connector($name = 'default') {
130
		$connection = self::get_conn($name);
131
		if($connection) {
132
			return $connection->getConnector();
133
		}
134
		return null;
135
	}
136
137
	/**
138
	 * Set an alternative database in a browser cookie,
139
	 * with the cookie lifetime set to the browser session.
140
	 * This is useful for integration testing on temporary databases.
141
	 *
142
	 * There is a strict naming convention for temporary databases to avoid abuse:
143
	 * <prefix> (default: 'ss_') + tmpdb + <7 digits>
144
	 * As an additional security measure, temporary databases will
145
	 * be ignored in "live" mode.
146
	 *
147
	 * Note that the database will be set on the next request.
148
	 * Set it to null to revert to the main database.
149
	 * @param string $name
150
	 */
151
	public static function set_alternative_database_name($name = null) {
152
		// Skip if CLI
153
		if(Director::is_cli()) {
154
			return;
155
		}
156
		if($name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name 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...
157
			if(!self::valid_alternative_database_name($name)) {
158
				throw new InvalidArgumentException(sprintf(
159
					'Invalid alternative database name: "%s"',
160
					$name
161
				));
162
			}
163
164
			$key = Config::inst()->get('SilverStripe\\Security\\Security', 'token');
165
			if(!$key) {
166
				throw new LogicException('"Security.token" not found, run "sake dev/generatesecuretoken"');
167
			}
168
			if(!function_exists('mcrypt_encrypt')) {
169
				throw new LogicException('DB::set_alternative_database_name() requires the mcrypt PHP extension');
170
			}
171
172
			$key = md5($key); // Ensure key is correct length for chosen cypher
173
			$ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CFB);
174
			$iv = mcrypt_create_iv($ivSize);
175
			$encrypted = mcrypt_encrypt(
176
				MCRYPT_RIJNDAEL_256, $key, $name, MCRYPT_MODE_CFB, $iv
177
			);
178
179
			// Set to browser session lifetime, and restricted to HTTP access only
180
			Cookie::set("alternativeDatabaseName", base64_encode($encrypted), 0, null, null, false, true);
181
			Cookie::set("alternativeDatabaseNameIv", base64_encode($iv), 0, null, null, false, true);
182
		} else {
183
			Cookie::force_expiry("alternativeDatabaseName", null, null, false, true);
184
			Cookie::force_expiry("alternativeDatabaseNameIv", null, null, false, true);
185
		}
186
	}
187
188
	/**
189
	 * Get the name of the database in use
190
	 */
191
	public static function get_alternative_database_name() {
192
		$name = Cookie::get("alternativeDatabaseName");
193
		$iv = Cookie::get("alternativeDatabaseNameIv");
194
195
		if($name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name 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...
196
			$key = Config::inst()->get('SilverStripe\\Security\\Security', 'token');
197
			if(!$key) {
198
				throw new LogicException('"Security.token" not found, run "sake dev/generatesecuretoken"');
199
			}
200
			if(!function_exists('mcrypt_encrypt')) {
201
				throw new LogicException('DB::set_alternative_database_name() requires the mcrypt PHP extension');
202
			}
203
			$key = md5($key); // Ensure key is correct length for chosen cypher
204
			$decrypted = mcrypt_decrypt(
205
				MCRYPT_RIJNDAEL_256, $key, base64_decode($name), MCRYPT_MODE_CFB, base64_decode($iv)
206
			);
207
			return (self::valid_alternative_database_name($decrypted)) ? $decrypted : false;
208
		} else {
209
			return false;
210
		}
211
	}
212
213
	/**
214
	 * Determines if the name is valid, as a security
215
	 * measure against setting arbitrary databases.
216
	 *
217
	 * @param  String $name
218
	 * @return Boolean
219
	 */
220
	public static function valid_alternative_database_name($name) {
221
		if(Director::isLive()) return false;
222
223
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
224
		$pattern = strtolower(sprintf('/^%stmpdb\d{7}$/', $prefix));
225
		return (bool)preg_match($pattern, $name);
226
	}
227
228
	/**
229
	 * Connect to a database.
230
	 *
231
	 * Given the database configuration, this method will create the correct
232
	 * subclass of {@link SS_Database}.
233
	 *
234
	 * @param array $databaseConfig A map of options. The 'type' is the name of the
235
	 * subclass of SS_Database to use. For the rest of the options, see the specific class.
236
	 * @param string $label identifier for the connection
237
	 * @return Database
238
	 */
239
	public static function connect($databaseConfig, $label = 'default') {
240
241
		// This is used by the "testsession" module to test up a test session using an alternative name
242
		if($name = self::get_alternative_database_name()) {
243
			$databaseConfig['database'] = $name;
244
		}
245
246
		if(!isset($databaseConfig['type']) || empty($databaseConfig['type'])) {
247
			user_error("DB::connect: Not passed a valid database config", E_USER_ERROR);
248
		}
249
250
		self::$connection_attempted = true;
251
252
		$dbClass = $databaseConfig['type'];
253
254
		// Using Injector->create allows us to use registered configurations
255
		// which may or may not map to explicit objects
256
		$conn = Injector::inst()->create($dbClass);
257
		$conn->connect($databaseConfig);
258
259
		self::set_conn($conn, $label);
260
261
		return $conn;
262
	}
263
264
	/**
265
	 * Returns true if a database connection has been attempted.
266
	 * In particular, it lets the caller know if we're still so early in the execution pipeline that
267
	 * we haven't even tried to connect to the database yet.
268
	 */
269
	public static function connection_attempted() {
270
		return self::$connection_attempted;
271
	}
272
273
	/**
274
	 * Execute the given SQL query.
275
	 * @param string $sql The SQL query to execute
276
	 * @param int $errorLevel The level of error reporting to enable for the query
277
	 * @return Query
278
	 */
279
	public static function query($sql, $errorLevel = E_USER_ERROR) {
280
		self::$lastQuery = $sql;
281
282
		return self::get_conn()->query($sql, $errorLevel);
283
	}
284
285
	/**
286
	 * Helper function for generating a list of parameter placeholders for the
287
	 * given argument(s)
288
	 *
289
	 * @param array|integer $input An array of items needing placeholders, or a
290
	 * number to specify the number of placeholders
291
	 * @param string $join The string to join each placeholder together with
292
	 * @return string|null Either a list of placeholders, or null
293
	 */
294
	public static function placeholders($input, $join = ', ') {
295
		if(is_array($input)) {
296
			$number = count($input);
297
		} elseif(is_numeric($input)) {
298
			$number = intval($input);
299
		} else {
300
			return null;
301
		}
302
		if($number === 0) return null;
303
		return implode($join, array_fill(0, $number, '?'));
304
	}
305
306
	/**
307
	 * @param string $sql The parameterised query
308
	 * @param array $parameters The parameters to inject into the query
309
	 *
310
	 * @return string
311
	 */
312
	public static function inline_parameters($sql, $parameters) {
313
		$segments = preg_split('/\?/', $sql);
314
		$joined = '';
315
		$inString = false;
316
		$numSegments = count($segments);
317
		for($i = 0; $i < $numSegments; $i++) {
318
			$input = $segments[$i];
319
			// Append next segment
320
			$joined .= $segments[$i];
321
			// Don't add placeholder after last segment
322
			if($i === $numSegments - 1) {
323
				break;
324
			}
325
			// check string escape on previous fragment
326
			// Remove escaped backslashes, count them!
327
			$input = preg_replace('/\\\\\\\\/', '', $input);
328
			// Count quotes
329
			$totalQuotes = substr_count($input, "'"); // Includes double quote escaped quotes
330
			$escapedQuotes = substr_count($input, "\\'");
331
			if((($totalQuotes - $escapedQuotes) % 2) !== 0) {
332
				$inString = !$inString;
333
			}
334
			// Append placeholder replacement
335
			if($inString) {
336
				// Literal question mark
337
				$joined .= '?';
338
				continue;
339
			}
340
341
			// Encode and insert next parameter
342
			$next = array_shift($parameters);
343
			if(is_array($next) && isset($next['value'])) {
344
				$next = $next['value'];
345
			}
346
			if (is_bool($next)) {
347
				$value = $next ? '1' : '0';
348
			}
349
			elseif (is_int($next)) {
350
				$value = $next;
351
			}
352
			else {
353
				$value = DB::is_active() ? Convert::raw2sql($next, true) : $next;
354
			}
355
			$joined .= $value;
356
		}
357
		return $joined;
358
	}
359
360
	/**
361
	 * Execute the given SQL parameterised query with the specified arguments
362
	 *
363
	 * @param string $sql The SQL query to execute. The ? character will denote parameters.
364
	 * @param array $parameters An ordered list of arguments.
365
	 * @param int $errorLevel The level of error reporting to enable for the query
366
	 * @return Query
367
	 */
368
	public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ERROR) {
369
		self::$lastQuery = $sql;
370
371
		return self::get_conn()->preparedQuery($sql, $parameters, $errorLevel);
372
	}
373
374
	/**
375
	 * Execute a complex manipulation on the database.
376
	 * A manipulation is an array of insert / or update sequences.  The keys of the array are table names,
377
	 * and the values are map containing 'command' and 'fields'.  Command should be 'insert' or 'update',
378
	 * and fields should be a map of field names to field values, including quotes.  The field value can
379
	 * also be a SQL function or similar.
380
	 *
381
	 * Example:
382
	 * <code>
383
	 * array(
384
	 *   // Command: insert
385
	 *   "table name" => array(
386
	 *      "command" => "insert",
387
	 *      "fields" => array(
388
	 *         "ClassName" => "'MyClass'", // if you're setting a literal, you need to escape and provide quotes
389
	 *         "Created" => "now()", // alternatively, you can call DB functions
390
	 *         "ID" => 234,
391
	 *       ),
392
	 *      "id" => 234 // an alternative to providing ID in the fields list
393
	 *    ),
394
	 *
395
	 *   // Command: update
396
	 *   "other table" => array(
397
	 *      "command" => "update",
398
	 *      "fields" => array(
399
	 *         "ClassName" => "'MyClass'",
400
	 *         "LastEdited" => "now()",
401
	 *       ),
402
	 *      "where" => "ID = 234",
403
	 *      "id" => 234 // an alternative to providing a where clause
404
	 *    ),
405
	 * )
406
	 * </code>
407
	 *
408
	 * You'll note that only one command on a given table can be called.
409
	 * That's a limitation of the system that's due to it being written for {@link DataObject::write()},
410
	 * which needs to do a single write on a number of different tables.
411
	 *
412
	 * @todo Update this to support paramaterised queries
413
	 *
414
	 * @param array $manipulation
415
	 */
416
	public static function manipulate($manipulation) {
417
		self::$lastQuery = $manipulation;
0 ignored issues
show
Documentation Bug introduced by
It seems like $manipulation of type array is incompatible with the declared type string of property $lastQuery.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
418
		self::get_conn()->manipulate($manipulation);
419
	}
420
421
	/**
422
	 * Get the autogenerated ID from the previous INSERT query.
423
	 *
424
	 * @param string $table
425
	 * @return int
426
	 */
427
	public static function get_generated_id($table) {
428
		return self::get_conn()->getGeneratedID($table);
429
	}
430
431
	/**
432
	 * Check if the connection to the database is active.
433
	 *
434
	 * @return boolean
435
	 */
436
	public static function is_active() {
437
		return ($conn = self::get_conn()) && $conn->isActive();
438
	}
439
440
	/**
441
	 * Create the database and connect to it. This can be called if the
442
	 * initial database connection is not successful because the database
443
	 * does not exist.
444
	 *
445
	 * @param string $database Name of database to create
446
	 * @return boolean Returns true if successful
447
	 */
448
	public static function create_database($database) {
449
		return self::get_conn()->selectDatabase($database, true);
450
	}
451
452
	/**
453
	 * Create a new table.
454
	 * @param string $table The name of the table
455
	 * @param array$fields A map of field names to field types
456
	 * @param array $indexes A map of indexes
457
	 * @param array $options An map of additional options.  The available keys are as follows:
458
	 *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine"
459
	 *     for MySQL.
460
	 *   - 'temporary' - If true, then a temporary table will be created
461
	 * @param array $advancedOptions Advanced creation options
462
	 * @return string The table name generated.  This may be different from the table name, for example with
463
	 * temporary tables.
464
	 */
465
	public static function create_table($table, $fields = null, $indexes = null, $options = null,
466
		$advancedOptions = null
467
	) {
468
		return self::get_schema()->createTable($table, $fields, $indexes, $options, $advancedOptions);
469
	}
470
471
	/**
472
	 * Create a new field on a table.
473
	 * @param string $table Name of the table.
474
	 * @param string $field Name of the field to add.
475
	 * @param string $spec The field specification, eg 'INTEGER NOT NULL'
476
	 */
477
	public static function create_field($table, $field, $spec) {
478
		return self::get_schema()->createField($table, $field, $spec);
479
	}
480
481
	/**
482
	 * Generate the following table in the database, modifying whatever already exists
483
	 * as necessary.
484
	 *
485
	 * @param string $table The name of the table
486
	 * @param string $fieldSchema A list of the fields to create, in the same form as DataObject::$db
487
	 * @param string $indexSchema A list of indexes to create.  The keys of the array are the names of the index.
488
	 * The values of the array can be one of:
489
	 *   - true: Create a single column index on the field named the same as the index.
490
	 *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
491
	 *     control over the index.
492
	 * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
493
	 * @param string $options SQL statement to append to the CREATE TABLE call.
494
	 * @param array $extensions List of extensions
495
	 */
496
	public static function require_table($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
497
		$options = null, $extensions = null
498
	) {
499
		self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $fieldSchema defined by parameter $fieldSchema on line 496 can also be of type string; however, SilverStripe\ORM\Connect...Manager::requireTable() does only seem to accept array|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $indexSchema defined by parameter $indexSchema on line 496 can also be of type string; however, SilverStripe\ORM\Connect...Manager::requireTable() does only seem to accept array|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Documentation introduced by
$options is of type string|null, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $extensions defined by parameter $extensions on line 497 can also be of type null; however, SilverStripe\ORM\Connect...Manager::requireTable() does only seem to accept array|boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
500
	}
501
502
	/**
503
	 * Generate the given field on the table, modifying whatever already exists as necessary.
504
	 *
505
	 * @param string $table The table name.
506
	 * @param string $field The field name.
507
	 * @param string $spec The field specification.
508
	 */
509
	public static function require_field($table, $field, $spec) {
510
		self::get_schema()->requireField($table, $field, $spec);
511
	}
512
513
	/**
514
	 * Generate the given index in the database, modifying whatever already exists as necessary.
515
	 *
516
	 * @param string $table The table name.
517
	 * @param string $index The index name.
518
	 * @param string|boolean $spec The specification of the index. See requireTable() for more information.
519
	 */
520
	public static function require_index($table, $index, $spec) {
521
		self::get_schema()->requireIndex($table, $index, $spec);
522
	}
523
524
	/**
525
	 * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
526
	 *
527
	 * @param string $table The table name.
528
	 */
529
	public static function dont_require_table($table) {
530
		self::get_schema()->dontRequireTable($table);
531
	}
532
533
	/**
534
	 * See {@link SS_Database->dontRequireField()}.
535
	 *
536
	 * @param string $table The table name.
537
	 * @param string $fieldName The field name not to require
538
	 */
539
	public static function dont_require_field($table, $fieldName) {
540
		self::get_schema()->dontRequireField($table, $fieldName);
541
	}
542
543
	/**
544
	 * Checks a table's integrity and repairs it if necessary.
545
	 *
546
	 * @param string $table The name of the table.
547
	 * @return boolean Return true if the table has integrity after the method is complete.
548
	 */
549
	public static function check_and_repair_table($table) {
550
		return self::get_schema()->checkAndRepairTable($table);
551
	}
552
553
	/**
554
	 * Return the number of rows affected by the previous operation.
555
	 *
556
	 * @return integer The number of affected rows
557
	 */
558
	public static function affected_rows() {
559
		return self::get_conn()->affectedRows();
560
	}
561
562
	/**
563
	 * Returns a list of all tables in the database.
564
	 * The table names will be in lower case.
565
	 *
566
	 * @return array The list of tables
567
	 */
568
	public static function table_list() {
569
		return self::get_schema()->tableList();
570
	}
571
572
	/**
573
	 * Get a list of all the fields for the given table.
574
	 * Returns a map of field name => field spec.
575
	 *
576
	 * @param string $table The table name.
577
	 * @return array The list of fields
578
	 */
579
	public static function field_list($table) {
580
		return self::get_schema()->fieldList($table);
581
	}
582
583
	/**
584
	 * Enable supression of database messages.
585
	 */
586
	public static function quiet() {
587
		self::get_schema()->quiet();
588
	}
589
590
	/**
591
	 * Show a message about database alteration
592
	 *
593
	 * @param string $message to display
594
	 * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
595
	 */
596
	public static function alteration_message($message, $type = "") {
597
		self::get_schema()->alterationMessage($message, $type);
598
	}
599
600
}
601