SQLite3Database::datetimeDifferenceClause()   D
last analyzed

Complexity

Conditions 9
Paths 144

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 29
rs 4.6666
cc 9
eloc 18
nc 144
nop 2
1
<?php
2
3
namespace SilverStripe\SQLite;
4
5
use Convert;
6
use File;
7
use SilverStripe\ORM\DataList;
8
use SilverStripe\ORM\ArrayList;
9
use SilverStripe\ORM\Connect\SS_Database;
10
use Config;
11
use Deprecation;
12
use PaginatedList;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\Queries\SQLSelect;
15
16
17
/**
18
 * SQLite database controller class
19
 *
20
 * @package SQLite3
21
 */
22
class SQLite3Database extends SS_Database
23
{
24
25
    /**
26
     * Database schema manager object
27
     *
28
     * @var SQLite3SchemaManager
29
     */
30
    protected $schemaManager = null;
31
32
    /*
33
     * This holds the parameters that the original connection was created with,
34
     * so we can switch back to it if necessary (used for unit tests)
35
     *
36
     * @var array
37
     */
38
    protected $parameters;
39
40
    /*
41
     * if we're on a In-Memory db
42
     *
43
     * @var boolean
44
     */
45
    protected $livesInMemory = false;
46
47
    /**
48
     * List of default pragma values
49
     *
50
     * @todo Migrate to SS config
51
     *
52
     * @var array
53
     */
54
    public static $default_pragma = array(
55
        'encoding' => '"UTF-8"',
56
        'locking_mode' => 'NORMAL'
57
    );
58
59
60
    /**
61
     * Extension used to distinguish between sqllite database files and other files.
62
     * Required to handle multiple databases.
63
     *
64
     * @return string
65
     */
66
    public static function database_extension()
67
    {
68
        return Config::inst()->get('SilverStripe\\SQLite\\SQLite3Database', 'database_extension');
69
    }
70
71
    /**
72
     * Check if a database name has a valid extension
73
     *
74
     * @param string $name
75
     * @return boolean
76
     */
77
    public static function is_valid_database_name($name)
78
    {
79
        $extension = self::database_extension();
80
        if (empty($extension)) {
81
            return true;
82
        }
83
84
        return substr_compare($name, $extension, -strlen($extension), strlen($extension)) === 0;
85
    }
86
87
    /**
88
     * Connect to a SQLite3 database.
89
     * @param array $parameters An map of parameters, which should include:
90
     *  - database: The database to connect to, with the correct file extension (.sqlite)
91
     *  - path: the path to the SQLite3 database file
92
     *  - key: the encryption key (needs testing)
93
     *  - memory: use the faster In-Memory database for unit tests
94
     */
95
    public function connect($parameters)
96
    {
97
        if (!empty($parameters['memory'])) {
98
            Deprecation::notice(
99
                '1.4.0',
100
                "\$databaseConfig['memory'] is deprecated. Use \$databaseConfig['path'] = ':memory:' instead.",
101
                Deprecation::SCOPE_GLOBAL
102
            );
103
            unset($parameters['memory']);
104
            $parameters['path'] = ':memory:';
105
        }
106
107
        //We will store these connection parameters for use elsewhere (ie, unit tests)
108
        $this->parameters = $parameters;
109
        $this->schemaManager->flushCache();
110
111
        // Ensure database name is set
112
        if (empty($parameters['database'])) {
113
            $parameters['database'] = 'database' . self::database_extension();
114
        }
115
        $dbName = $parameters['database'];
116
        if (!self::is_valid_database_name($dbName)) {
117
            // If not using the correct file extension for database files then the
118
            // results of SQLite3SchemaManager::databaseList will be unpredictable
119
            $extension = self::database_extension();
120
            Deprecation::notice('3.2', "SQLite3Database now expects a database file with extension \"$extension\". Behaviour may be unpredictable otherwise.");
121
        }
122
123
        // use the very lightspeed SQLite In-Memory feature for testing
124
        if ($this->getLivesInMemory()) {
125
            $file = ':memory:';
126
        } else {
127
            // Ensure path is given
128
            if (empty($parameters['path'])) {
129
                $parameters['path'] = ASSETS_PATH . '/.sqlitedb';
130
            }
131
132
            //assumes that the path to dbname will always be provided:
133
            $file = $parameters['path'] . '/' . $dbName;
134
            if (!file_exists($parameters['path'])) {
135
                SQLiteDatabaseConfigurationHelper::create_db_dir($parameters['path']);
136
                SQLiteDatabaseConfigurationHelper::secure_db_dir($parameters['path']);
137
            }
138
        }
139
140
        // 'path' and 'database' are merged into the full file path, which
141
        // is the format that connectors such as PDOConnector expect
142
        $parameters['filepath'] = $file;
143
144
        // Ensure that driver is available (required by PDO)
145
        if (empty($parameters['driver'])) {
146
            $parameters['driver'] = $this->getDatabaseServer();
147
        }
148
149
        $this->connector->connect($parameters, true);
150
151
        foreach (self::$default_pragma as $pragma => $value) {
152
            $this->setPragma($pragma, $value);
153
        }
154
155
        if (empty(self::$default_pragma['locking_mode'])) {
156
            self::$default_pragma['locking_mode'] = $this->getPragma('locking_mode');
157
        }
158
    }
159
160
    /**
161
     * Retrieve parameters used to connect to this SQLLite database
162
     *
163
     * @return array
164
     */
165
    public function getParameters()
166
    {
167
        return $this->parameters;
168
    }
169
170
    public function getLivesInMemory()
171
    {
172
        return isset($this->parameters['path']) && $this->parameters['path'] === ':memory:';
173
    }
174
175
    public function supportsCollations()
176
    {
177
        return true;
178
    }
179
180
    public function supportsTimezoneOverride()
181
    {
182
        return false;
183
    }
184
185
    /**
186
     * Execute PRAGMA commands.
187
     *
188
     * @param string $pragma name
189
     * @param string $value to set
190
     */
191
    public function setPragma($pragma, $value)
192
    {
193
        $this->query("PRAGMA $pragma = $value");
194
    }
195
196
    /**
197
     * Gets pragma value.
198
     *
199
     * @param string $pragma name
200
     * @return string the pragma value
201
     */
202
    public function getPragma($pragma)
203
    {
204
        return $this->query("PRAGMA $pragma")->value();
205
    }
206
207
    public function getDatabaseServer()
208
    {
209
        return "sqlite";
210
    }
211
212
    public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR)
213
    {
214
        if (!$this->schemaManager->databaseExists($name)) {
215
            // Check DB creation permisson
216
            if (!$create) {
217
                if ($errorLevel !== false) {
218
                    user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel);
219
                }
220
                // Unselect database
221
                $this->connector->unloadDatabase();
222
                return false;
223
            }
224
            $this->schemaManager->createDatabase($name);
225
        }
226
227
        // Reconnect using the existing parameters
228
        $parameters = $this->parameters;
229
        $parameters['database'] = $name;
230
        $this->connect($parameters);
231
        return true;
232
    }
233
234
    public function now()
235
    {
236
        return "datetime('now', 'localtime')";
237
    }
238
239
    public function random()
240
    {
241
        return 'random()';
242
    }
243
244
    /**
245
     * The core search engine configuration.
246
     * @todo There is a fulltext search for SQLite making use of virtual tables, the fts3 extension and the
247
     * MATCH operator
248
     * there are a few issues with fts:
249
     * - shared cached lock doesn't allow to create virtual tables on versions prior to 3.6.17
250
     * - there must not be more than one MATCH operator per statement
251
     * - the fts3 extension needs to be available
252
     * for now we use the MySQL implementation with the MATCH()AGAINST() uglily replaced with LIKE
253
     *
254
     * @param array $classesToSearch
255
     * @param string $keywords Keywords as a space separated string
256
     * @param int $start
257
     * @param int $pageLength
258
     * @param string $sortBy
259
     * @param string $extraFilter
260
     * @param bool $booleanSearch
261
     * @param string $alternativeFileFilter
262
     * @param bool $invertedMatch
263
     * @return PaginatedList DataObjectSet of result pages
264
     */
265
    public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC",
266
        $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false
267
    ) {
268
        $keywords = $this->escapeString(str_replace(array('*', '+', '-', '"', '\''), '', $keywords));
269
        $htmlEntityKeywords = htmlentities(utf8_decode($keywords));
270
271
        $pageClass = 'SilverStripe\\CMS\\Model\\SiteTree';
272
		$fileClass = 'File';
273
274
        $extraFilters = array($pageClass => '', $fileClass => '');
275
276
        if ($extraFilter) {
277
            $extraFilters[$pageClass] = " AND $extraFilter";
278
279
            if ($alternativeFileFilter) {
280
                $extraFilters[$fileClass] = " AND $alternativeFileFilter";
281
            } else {
282
                $extraFilters[$fileClass] = $extraFilters[$pageClass];
283
            }
284
        }
285
286
        // Always ensure that only pages with ShowInSearch = 1 can be searched
287
        $extraFilters[$pageClass] .= ' AND ShowInSearch <> 0';
288
        // File.ShowInSearch was added later, keep the database driver backwards compatible
289
        // by checking for its existence first
290
        if (File::singleton()->db('ShowInSearch')) {
291
            $extraFilters[$fileClass] .= " AND ShowInSearch <> 0";
292
        }
293
294
        $limit = $start . ", " . (int) $pageLength;
295
296
        $notMatch = $invertedMatch ? "NOT " : "";
297
        if ($keywords) {
298
            $match[$pageClass] = "
0 ignored issues
show
Coding Style Comprehensibility introduced by
$match was never initialized. Although not strictly required by PHP, it is generally a good practice to add $match = 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...
299
				(Title LIKE '%$keywords%' OR MenuTitle LIKE '%$keywords%' OR Content LIKE '%$keywords%' OR MetaDescription LIKE '%$keywords%' OR
300
				Title LIKE '%$htmlEntityKeywords%' OR MenuTitle LIKE '%$htmlEntityKeywords%' OR Content LIKE '%$htmlEntityKeywords%' OR MetaDescription LIKE '%$htmlEntityKeywords%')
301
			";
302
            $fileClassSQL = Convert::raw2sql($fileClass);
303
            $match[$fileClass] = "(Name LIKE '%$keywords%' OR Title LIKE '%$keywords%') AND ClassName = '$fileClassSQL'";
304
305
            // We make the relevance search by converting a boolean mode search into a normal one
306
            $relevanceKeywords = $keywords;
307
            $htmlEntityRelevanceKeywords = $htmlEntityKeywords;
308
            $relevance[$pageClass] = "(Title LIKE '%$relevanceKeywords%' OR MenuTitle LIKE '%$relevanceKeywords%' OR Content LIKE '%$relevanceKeywords%' OR MetaDescription LIKE '%$relevanceKeywords%') + (Title LIKE '%$htmlEntityRelevanceKeywords%' OR MenuTitle LIKE '%$htmlEntityRelevanceKeywords%' OR Content LIKE '%$htmlEntityRelevanceKeywords%' OR MetaDescription LIKE '%$htmlEntityRelevanceKeywords%')";
0 ignored issues
show
Coding Style Comprehensibility introduced by
$relevance was never initialized. Although not strictly required by PHP, it is generally a good practice to add $relevance = 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...
309
            $relevance[$fileClass] = "(Name LIKE '%$relevanceKeywords%' OR Title LIKE '%$relevanceKeywords%')";
310
        } else {
311
            $relevance[$pageClass] = $relevance[$fileClass] = 1;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$relevance was never initialized. Although not strictly required by PHP, it is generally a good practice to add $relevance = 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...
312
            $match[$pageClass] = $match[$fileClass] = "1 = 1";
0 ignored issues
show
Coding Style Comprehensibility introduced by
$match was never initialized. Although not strictly required by PHP, it is generally a good practice to add $match = 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...
313
        }
314
315
        // Generate initial queries
316
        $queries = array();
317
        foreach ($classesToSearch as $class) {
318
            $queries[$class] = DataList::create($class)
319
                ->where($notMatch . $match[$class] . $extraFilters[$class])
320
                ->dataQuery()
321
                ->query();
322
        }
323
324
        // Make column selection lists
325
        $select = array(
326
            $pageClass => array(
327
                "\"ClassName\"",
328
                "\"ID\"",
329
                "\"ParentID\"",
330
                "\"Title\"",
331
                "\"URLSegment\"",
332
                "\"Content\"",
333
                "\"LastEdited\"",
334
                "\"Created\"",
335
                "NULL AS \"Name\"",
336
                "\"CanViewType\"",
337
                $relevance[$pageClass] . " AS Relevance"
338
            ),
339
            $fileClass => array(
340
                "\"ClassName\"",
341
                "\"ID\"",
342
                "NULL AS \"ParentID\"",
343
                "\"Title\"",
344
                "NULL AS \"URLSegment\"",
345
                "NULL AS \"Content\"",
346
                "\"LastEdited\"",
347
                "\"Created\"",
348
                "\"Name\"",
349
                "NULL AS \"CanViewType\"",
350
                $relevance[$fileClass] . " AS Relevance"
351
            )
352
        );
353
354
        // Process queries
355
        foreach ($classesToSearch as $class) {
356
            // There's no need to do all that joining
357
            $queries[$class]->setFrom('"'.DataObject::getSchema()->baseDataTable($class).'"');
358
            $queries[$class]->setSelect(array());
359
            foreach ($select[$class] as $clause) {
360
                if (preg_match('/^(.*) +AS +"?([^"]*)"?/i', $clause, $matches)) {
361
                    $queries[$class]->selectField($matches[1], $matches[2]);
362
                } else {
363
                    $queries[$class]->selectField(str_replace('"', '', $clause));
364
                }
365
            }
366
367
            $queries[$class]->setOrderBy(array());
368
        }
369
370
        // Combine queries
371
        $querySQLs = array();
372
        $queryParameters = array();
373
        $totalCount = 0;
374
        foreach ($queries as $query) {
375
            /** @var SQLSelect $query */
376
            $querySQLs[] = $query->sql($parameters);
377
            $queryParameters = array_merge($queryParameters, $parameters);
378
            $totalCount += $query->unlimitedRowCount();
379
        }
380
381
        $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
382
        // Get records
383
        $records = $this->preparedQuery($fullQuery, $queryParameters);
384
385
        foreach ($records as $record) {
386
            $objects[] = new $record['ClassName']($record);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$objects was never initialized. Although not strictly required by PHP, it is generally a good practice to add $objects = 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...
387
        }
388
389
        if (isset($objects)) {
390
            $doSet = new ArrayList($objects);
391
        } else {
392
            $doSet = new ArrayList();
393
        }
394
        $list = new PaginatedList($doSet);
395
        $list->setPageStart($start);
396
        $list->setPageLength($pageLength);
397
        $list->setTotalItems($totalCount);
398
        return $list;
399
    }
400
401
    /*
402
     * Does this database support transactions?
403
     */
404
    public function supportsTransactions()
405
    {
406
        return version_compare($this->getVersion(), '3.6', '>=');
407
    }
408
409
    public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering'))
410
    {
411
        if (isset($extensions['partitions'])) {
412
            return true;
413
        } elseif (isset($extensions['tablespaces'])) {
414
            return true;
415
        } elseif (isset($extensions['clustering'])) {
416
            return true;
417
        } else {
418
            return false;
419
        }
420
    }
421
422
    public function transactionStart($transaction_mode = false, $session_characteristics = false)
423
    {
424
        $this->query('BEGIN');
425
    }
426
427
    public function transactionSavepoint($savepoint)
428
    {
429
        $this->query("SAVEPOINT \"$savepoint\"");
430
    }
431
432
    public function transactionRollback($savepoint = false)
433
    {
434
        if ($savepoint) {
435
            $this->query("ROLLBACK TO $savepoint;");
436
        } else {
437
            $this->query('ROLLBACK;');
438
        }
439
    }
440
441
    public function transactionEnd($chain = false)
442
    {
443
        $this->query('COMMIT;');
444
    }
445
446
    public function clearTable($table)
447
    {
448
        $this->query("DELETE FROM \"$table\"");
449
    }
450
451
    public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null,
452
        $parameterised = false
453
    ) {
454
        if ($exact && !$caseSensitive) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $caseSensitive of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
455
            $comp = ($negate) ? '!=' : '=';
456
        } else {
457
            if ($caseSensitive) {
458
                // GLOB uses asterisks as wildcards.
459
                // Replace them in search string, without replacing escaped percetage signs.
460
                $comp = 'GLOB';
461
                $value = preg_replace('/^%([^\\\\])/', '*$1', $value);
462
                $value = preg_replace('/([^\\\\])%$/', '$1*', $value);
463
                $value = preg_replace('/([^\\\\])%/', '$1*', $value);
464
            } else {
465
                $comp = 'LIKE';
466
            }
467
            if ($negate) {
468
                $comp = 'NOT ' . $comp;
469
            }
470
        }
471
472
        if ($parameterised) {
473
            return sprintf("%s %s ?", $field, $comp);
474
        } else {
475
            return sprintf("%s %s '%s'", $field, $comp, $value);
476
        }
477
    }
478
479
    public function formattedDatetimeClause($date, $format)
480
    {
481
        preg_match_all('/%(.)/', $format, $matches);
482
        foreach ($matches[1] as $match) {
483
            if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
484
                user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
485
            }
486
        }
487
488
        $translate = array(
489
            '/%i/' => '%M',
490
            '/%s/' => '%S',
491
            '/%U/' => '%s',
492
        );
493
        $format = preg_replace(array_keys($translate), array_values($translate), $format);
494
495
        $modifiers = array();
496
        if ($format == '%s' && $date != 'now') {
497
            $modifiers[] = 'utc';
498
        }
499
        if ($format != '%s' && $date == 'now') {
500
            $modifiers[] = 'localtime';
501
        }
502
503 View Code Duplication
        if (preg_match('/^now$/i', $date)) {
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...
504
            $date = "'now'";
505
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
506
            $date = "'$date'";
507
        }
508
509
        $modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'";
510
        return "strftime('$format', $date$modifier)";
511
    }
512
513
    public function datetimeIntervalClause($date, $interval)
514
    {
515
        $modifiers = array();
516
        if ($date == 'now') {
517
            $modifiers[] = 'localtime';
518
        }
519
520 View Code Duplication
        if (preg_match('/^now$/i', $date)) {
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...
521
            $date = "'now'";
522
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
523
            $date = "'$date'";
524
        }
525
526
        $modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'";
527
        return "datetime($date$modifier, '$interval')";
528
    }
529
530
    public function datetimeDifferenceClause($date1, $date2)
531
    {
532
        $modifiers1 = array();
533
        $modifiers2 = array();
534
535
        if ($date1 == 'now') {
536
            $modifiers1[] = 'localtime';
537
        }
538
        if ($date2 == 'now') {
539
            $modifiers2[] = 'localtime';
540
        }
541
542
        if (preg_match('/^now$/i', $date1)) {
543
            $date1 = "'now'";
544
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
545
            $date1 = "'$date1'";
546
        }
547
548
        if (preg_match('/^now$/i', $date2)) {
549
            $date2 = "'now'";
550
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
551
            $date2 = "'$date2'";
552
        }
553
554
        $modifier1 = empty($modifiers1) ? '' : ", '" . implode("', '", $modifiers1) . "'";
555
        $modifier2 = empty($modifiers2) ? '' : ", '" . implode("', '", $modifiers2) . "'";
556
557
        return "strftime('%s', $date1$modifier1) - strftime('%s', $date2$modifier2)";
558
    }
559
}
560