Issues (37)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/MSSQLDatabase.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace SilverStripe\MSSQL;
4
5
use SilverStripe\Core\Config\Configurable;
6
use SilverStripe\Core\Injector\Injectable;
7
use SilverStripe\Core\ClassInfo;
8
use SilverStripe\ORM\ArrayList;
9
use SilverStripe\ORM\Connect\Database;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\DB;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\ORM\PaginatedList;
14
use SilverStripe\ORM\Queries\SQLSelect;
15
16
/**
17
 * Microsoft SQL Server 2008+ connector class.
18
 */
19
class MSSQLDatabase extends Database
20
{
21
    use Configurable;
22
    use Injectable;
23
24
    /**
25
     * Words that will trigger an error if passed to a SQL Server fulltext search
26
     */
27
    public static $noiseWords = array('about', '1', 'after', '2', 'all', 'also', '3', 'an', '4', 'and', '5', 'another', '6', 'any', '7', 'are', '8', 'as', '9', 'at', '0', 'be', '$', 'because', 'been', 'before', 'being', 'between', 'both', 'but', 'by', 'came', 'can', 'come', 'could', 'did', 'do', 'does', 'each', 'else', 'for', 'from', 'get', 'got', 'has', 'had', 'he', 'have', 'her', 'here', 'him', 'himself', 'his', 'how', 'if', 'in', 'into', 'is', 'it', 'its', 'just', 'like', 'make', 'many', 'me', 'might', 'more', 'most', 'much', 'must', 'my', 'never', 'no', 'now', 'of', 'on', 'only', 'or', 'other', 'our', 'out', 'over', 're', 'said', 'same', 'see', 'should', 'since', 'so', 'some', 'still', 'such', 'take', 'than', 'that', 'the', 'their', 'them', 'then', 'there', 'these', 'they', 'this', 'those', 'through', 'to', 'too', 'under', 'up', 'use', 'very', 'want', 'was', 'way', 'we', 'well', 'were', 'what', 'when', 'where', 'which', 'while', 'who', 'will', 'with', 'would', 'you', 'your', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
28
29
    /**
30
     * Transactions will work with FreeTDS, but not entirely with sqlsrv driver on Windows with MARS enabled.
31
     * TODO:
32
     * - after the test fails with open transaction, the transaction should be rolled back,
33
     *   otherwise other tests will break claiming that transaction is still open.
34
     * - figure out SAVEPOINTS
35
     * - READ ONLY transactions
36
     */
37
    protected $supportsTransactions = true;
38
39
    /**
40
     * Cached flag to determine if full-text is enabled. This is set by
41
     * {@link MSSQLDatabase::fullTextEnabled()}
42
     *
43
     * @var boolean
44
     */
45
    protected $fullTextEnabled = null;
46
47
    /**
48
     * @var bool
49
     */
50
    protected $transactionNesting = 0;
51
52
    /**
53
     * Set the default collation of the MSSQL nvarchar fields that we create.
54
     * We don't apply this to the database as a whole, so that we can use unicode collations.
55
     *
56
     * @param string $collation
57
     */
58
    public static function set_collation($collation)
59
    {
60
        static::config()->set('collation', $collation);
61
    }
62
63
    /**
64
     * The default collation of the MSSQL nvarchar fields that we create.
65
     * We don't apply this to the database as a whole, so that we can use
66
     * unicode collations.
67
     *
68
     * @return string
69
     */
70
    public static function get_collation()
71
    {
72
        return static::config()->get('collation');
73
    }
74
75
    /**
76
     * Connect to a MS SQL database.
77
     * @param array $parameters An map of parameters, which should include:
78
     *  - server: The server, eg, localhost
79
     *  - username: The username to log on with
80
     *  - password: The password to log on with
81
     *  - database: The database to connect to
82
     *  - windowsauthentication: Set to true to use windows authentication
83
     *    instead of username/password
84
     */
85
    public function connect($parameters)
86
    {
87
        parent::connect($parameters);
88
89
        // Configure the connection
90
        $this->query('SET QUOTED_IDENTIFIER ON');
91
        $this->query('SET TEXTSIZE 2147483647');
92
    }
93
94
    /**
95
     * Checks whether the current SQL Server version has full-text
96
     * support installed and full-text is enabled for this database.
97
     *
98
     * @return boolean
99
     */
100
    public function fullTextEnabled()
101
    {
102
        if ($this->fullTextEnabled === null) {
103
            $this->fullTextEnabled = $this->updateFullTextEnabled();
104
        }
105
        return $this->fullTextEnabled;
106
    }
107
108
    /**
109
     * Checks whether the current SQL Server version has full-text
110
     * support installed and full-text is enabled for this database.
111
     *
112
     * @return boolean
113
     */
114
    protected function updateFullTextEnabled()
115
    {
116
        // Check if installed
117
        $isInstalled = $this->query("SELECT fulltextserviceproperty('isfulltextinstalled')")->value();
118
        if (!$isInstalled) {
119
            return false;
120
        }
121
122
        // Check if current database is enabled
123
        $database = $this->getSelectedDatabase();
124
        $enabledForDb = $this->preparedQuery(
125
            "SELECT is_fulltext_enabled FROM sys.databases WHERE name = ?",
126
            array($database)
127
        )->value();
128
        return $enabledForDb;
129
    }
130
131
    public function supportsCollations()
132
    {
133
        return true;
134
    }
135
136
    public function supportsTimezoneOverride()
137
    {
138
        return true;
139
    }
140
141
    public function getDatabaseServer()
142
    {
143
        return "sqlsrv";
144
    }
145
146
    public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR)
147
    {
148
        $this->fullTextEnabled = null;
149
150
        return parent::selectDatabase($name, $create, $errorLevel);
151
    }
152
153
    public function clearTable($table)
154
    {
155
        $this->query("TRUNCATE TABLE \"$table\"");
156
    }
157
158
    /**
159
     * SQL Server uses CURRENT_TIMESTAMP for the current date/time.
160
     */
161
    public function now()
162
    {
163
        return 'CURRENT_TIMESTAMP';
164
    }
165
166
    /**
167
     * Returns the database-specific version of the random() function
168
     */
169
    public function random()
170
    {
171
        return 'RAND()';
172
    }
173
174
    /**
175
     * The core search engine configuration.
176
     * Picks up the fulltext-indexed tables from the database and executes search on all of them.
177
     * Results are obtained as ID-ClassName pairs which is later used to reconstruct the DataObjectSet.
178
     *
179
     * @param array $classesToSearch computes all descendants and includes them. Check is done via WHERE clause.
180
     * @param string $keywords Keywords as a space separated string
181
     * @param int $start
182
     * @param int $pageLength
183
     * @param string $sortBy
184
     * @param string $extraFilter
185
     * @param bool $booleanSearch
186
     * @param string $alternativeFileFilter
187
     * @param bool $invertedMatch
188
     * @return PaginatedList DataObjectSet of result pages
189
     */
190
    public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false)
191
    {
192
        $start = (int)$start;
193
        $pageLength = (int)$pageLength;
194
        $results = new ArrayList();
195
196
        if (!$this->fullTextEnabled()) {
197
            return new PaginatedList($results);
198
        }
199
        if (!in_array(substr($sortBy, 0, 9), array('"Relevanc', 'Relevance'))) {
200
            user_error("Non-relevance sort not supported.", E_USER_ERROR);
201
        }
202
203
        $allClassesToSearch = array();
204
        foreach ($classesToSearch as $class) {
205
            $allClassesToSearch = array_merge($allClassesToSearch, array_values(ClassInfo::dataClassesFor($class)));
206
        }
207
        $allClassesToSearch = array_unique($allClassesToSearch);
208
209
        //Get a list of all the tables and columns we'll be searching on:
210
        $fulltextColumns = $this->query('EXEC sp_help_fulltext_columns');
211
        $queries = array();
212
213
        // Sort the columns back into tables.
214
        $tables = array();
215
        foreach ($fulltextColumns as $column) {
216
            // Skip extension tables.
217
            if (substr($column['TABLE_NAME'], -5) == '_Live' || substr($column['TABLE_NAME'], -9) == '_versions') {
218
                continue;
219
            }
220
221
            // Add the column to table.
222
            $table = &$tables[$column['TABLE_NAME']];
223
            if (!$table) {
224
                $table = array($column['FULLTEXT_COLUMN_NAME']);
225
            } else {
226
                array_push($table, $column['FULLTEXT_COLUMN_NAME']);
227
            }
228
        }
229
230
        // Create one query per each table, $columns not used. We want just the ID and the ClassName of the object from this query.
231
        foreach ($tables as $tableName => $columns) {
232
            $class = DataObject::getSchema()->tableClass($tableName);
233
            $join = $this->fullTextSearchMSSQL($tableName, $keywords);
234
            if (!$join) {
235
                return new PaginatedList($results);
236
            } // avoid "Null or empty full-text predicate"
237
238
            // Check if we need to add ShowInSearch
239
            $where = null;
240
            if ($class === 'SilverStripe\\CMS\\Model\\SiteTree') {
241
                $where = array("\"$tableName\".\"ShowInSearch\"!=0");
242
            } elseif ($class === 'SilverStripe\\Assets\\File') {
243
                // File.ShowInSearch was added later, keep the database driver backwards compatible
244
                // by checking for its existence first
245
                $fields = $this->getSchemaManager()->fieldList($tableName);
246
                if (array_key_exists('ShowInSearch', $fields)) {
247
                    $where = array("\"$tableName\".\"ShowInSearch\"!=0");
248
                }
249
            }
250
251
            $queries[$tableName] = DataList::create($class)->where($where)->dataQuery()->query();
252
            $queries[$tableName]->setOrderBy(array());
253
254
            // Join with CONTAINSTABLE, a full text searcher that includes relevance factor
255
            $queries[$tableName]->setFrom(array("\"$tableName\" INNER JOIN $join AS \"ft\" ON \"$tableName\".\"ID\"=\"ft\".\"KEY\""));
256
            // Join with the base class if needed, as we want to test agains the ClassName
257
            if ($tableName != $tableName) {
258
                $queries[$tableName]->setFrom("INNER JOIN \"$tableName\" ON  \"$tableName\".\"ID\"=\"$tableName\".\"ID\"");
259
            }
260
261
            $queries[$tableName]->setSelect(array("\"$tableName\".\"ID\""));
262
            $queries[$tableName]->selectField("'$tableName'", 'Source');
263
            $queries[$tableName]->selectField('Rank', 'Relevance');
264
            if ($extraFilter) {
265
                $queries[$tableName]->addWhere($extraFilter);
266
            }
267
            if (count($allClassesToSearch)) {
268
                $classesPlaceholder = DB::placeholders($allClassesToSearch);
269
                $queries[$tableName]->addWhere(array(
270
                    "\"$tableName\".\"ClassName\" IN ($classesPlaceholder)" =>
271
                    $allClassesToSearch
272
                ));
273
            }
274
            // Reset the parameters that would get in the way
275
        }
276
277
        // Generate SQL
278
        $querySQLs = array();
279
        $queryParameters = array();
280
        foreach ($queries as $query) {
281
            /** @var SQLSelect $query */
282
            $querySQLs[] = $query->sql($parameters);
283
            $queryParameters = array_merge($queryParameters, $parameters);
284
        }
285
286
        // Unite the SQL
287
        $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy";
288
289
        // Perform the search
290
        $result = $this->preparedQuery($fullQuery, $queryParameters);
291
292
        // Regenerate DataObjectSet - watch out, numRecords doesn't work on sqlsrv driver on Windows.
293
        $current = -1;
294
        $objects = array();
295
        foreach ($result as $row) {
296
            $current++;
297
298
            // Select a subset for paging
299
            if ($current >= $start && $current < $start + $pageLength) {
300
                $objects[] = DataObject::get_by_id($row['Source'], $row['ID']);
301
            }
302
        }
303
304
        if (isset($objects)) {
305
            $results = new ArrayList($objects);
306
        } else {
307
            $results = new ArrayList();
308
        }
309
        $list = new PaginatedList($results);
310
        $list->setPageStart($start);
311
        $list->setPageLength($pageLength);
312
        $list->setTotalItems($current+1);
313
        return $list;
314
    }
315
316
    /**
317
     * Allow auto-increment primary key editing on the given table.
318
     * Some databases need to enable this specially.
319
     *
320
     * @param string $table The name of the table to have PK editing allowed on
321
     * @param bool $allow True to start, false to finish
322
     */
323
    public function allowPrimaryKeyEditing($table, $allow = true)
324
    {
325
        $this->query("SET IDENTITY_INSERT \"$table\" " . ($allow ? "ON" : "OFF"));
326
    }
327
328
    /**
329
     * Returns a SQL fragment for querying a fulltext search index
330
     *
331
     * @param string $tableName specific - table name
332
     * @param string $keywords The search query
333
     * @param array $fields The list of field names to search on, or null to include all
334
     * @return string Clause, or null if keyword set is empty or the string with JOIN clause to be added to SQL query
335
     */
336
    public function fullTextSearchMSSQL($tableName, $keywords, $fields = null)
337
    {
338
        // Make sure we are getting an array of fields
339
        if (isset($fields) && !is_array($fields)) {
340
            $fields = array($fields);
341
        }
342
343
        // Strip unfriendly characters, SQLServer "CONTAINS" predicate will crash on & and | and ignore others anyway.
344
        if (function_exists('mb_ereg_replace')) {
345
            $keywords = mb_ereg_replace('[^\w\s]', '', trim($keywords));
346
        } else {
347
            $keywords = $this->escapeString(str_replace(array('&', '|', '!', '"', '\''), '', trim($keywords)));
348
        }
349
350
        // Remove stopwords, concat with ANDs
351
        $keywordList = explode(' ', $keywords);
352
        $keywordList = $this->removeStopwords($keywordList);
353
354
        // remove any empty values from the array
355
        $keywordList = array_filter($keywordList);
356
        if (empty($keywordList)) {
357
            return null;
358
        }
359
360
        $keywords = implode(' AND ', $keywordList);
361
        if ($fields) {
362
            $fieldNames = '"' . implode('", "', $fields) . '"';
363
        } else {
364
            $fieldNames = "*";
365
        }
366
367
        return "CONTAINSTABLE(\"$tableName\", ($fieldNames), '$keywords')";
368
    }
369
370
    /**
371
     * Remove stopwords that would kill a MSSQL full-text query
372
     *
373
     * @param array $keywords
374
     *
375
     * @return array $keywords with stopwords removed
376
     */
377
    public function removeStopwords($keywords)
378
    {
379
        $goodKeywords = array();
380
        foreach ($keywords as $keyword) {
381
            if (in_array($keyword, self::$noiseWords)) {
382
                continue;
383
            }
384
            $goodKeywords[] = trim($keyword);
385
        }
386
        return $goodKeywords;
387
    }
388
389
    /**
390
     * Does this database support transactions?
391
     */
392
    public function supportsTransactions()
393
    {
394
        return $this->supportsTransactions;
395
    }
396
397
    /**
398
     * This is a quick lookup to discover if the database supports particular extensions
399
     * Currently, MSSQL supports no extensions
400
     *
401
     * @param array $extensions List of extensions to check for support of. The key of this array
402
     * will be an extension name, and the value the configuration for that extension. This
403
     * could be one of partitions, tablespaces, or clustering
404
     * @return boolean Flag indicating support for all of the above
405
     */
406
    public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering'))
407
    {
408
        if (isset($extensions['partitions'])) {
409
            return false;
410
        } elseif (isset($extensions['tablespaces'])) {
411
            return false;
412
        } elseif (isset($extensions['clustering'])) {
413
            return false;
414
        } else {
415
            return false;
416
        }
417
    }
418
419
    /**
420
     * Start transaction. READ ONLY not supported.
421
     *
422
     * @param bool $transactionMode
423
     * @param bool $sessionCharacteristics
424
     */
425
    public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
426
    {
427 View Code Duplication
        if ($this->transactionNesting > 0) {
428
            $this->transactionSavepoint('NESTEDTRANSACTION' . $this->transactionNesting);
429
        } elseif ($this->connector instanceof SQLServerConnector) {
430
            $this->connector->transactionStart();
431
        } else {
432
            $this->query('BEGIN TRANSACTION');
433
        }
434
        ++$this->transactionNesting;
435
    }
436
437
    public function transactionSavepoint($savepoint)
438
    {
439
        $this->query("SAVE TRANSACTION \"$savepoint\"");
440
    }
441
442
    public function transactionRollback($savepoint = false)
443
    {
444
        // Named transaction
445
        if ($savepoint) {
446
            $this->query("ROLLBACK TRANSACTION \"$savepoint\"");
447
            return true;
448
        }
449
450
        // Fail if transaction isn't available
451
        if (!$this->transactionNesting) {
452
            return false;
453
        }
454
455
        --$this->transactionNesting;
456 View Code Duplication
        if ($this->transactionNesting > 0) {
457
            $this->transactionRollback('NESTEDTRANSACTION' . $this->transactionNesting);
458
        } elseif ($this->connector instanceof SQLServerConnector) {
459
            $this->connector->transactionRollback();
460
        } else {
461
            $this->query('ROLLBACK TRANSACTION');
462
        }
463
        return true;
464
    }
465
466
    public function transactionEnd($chain = false)
467
    {
468
        // Fail if transaction isn't available
469
        if (!$this->transactionNesting) {
470
            return false;
471
        }
472
        --$this->transactionNesting;
473 View Code Duplication
        if ($this->transactionNesting <= 0) {
474
            $this->transactionNesting = 0;
475
            if ($this->connector instanceof SQLServerConnector) {
476
                $this->connector->transactionEnd();
477
            } else {
478
                $this->query('COMMIT TRANSACTION');
479
            }
480
        }
481
        return true;
482
    }
483
484
    /**
485
     * In error condition, set transactionNesting to zero
486
     */
487
    protected function resetTransactionNesting()
488
    {
489
        $this->transactionNesting = 0;
490
    }
491
492
    public function query($sql, $errorLevel = E_USER_ERROR)
493
    {
494
        $this->inspectQuery($sql);
495
        return parent::query($sql, $errorLevel);
496
    }
497
498
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
499
    {
500
        $this->inspectQuery($sql);
501
        return parent::preparedQuery($sql, $parameters, $errorLevel);
502
    }
503
504
    protected function inspectQuery($sql)
505
    {
506
        // Any DDL discards transactions.
507
        $isDDL = $this->getConnector()->isQueryDDL($sql);
508
509
        if ($isDDL) {
510
            $this->resetTransactionNesting();
511
        }
512
    }
513
514
    public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, $parameterised = false)
515
    {
516
        if ($exact) {
517
            $comp = ($negate) ? '!=' : '=';
518
        } else {
519
            $comp = 'LIKE';
520
            if ($negate) {
521
                $comp = 'NOT ' . $comp;
522
            }
523
        }
524
525
        // Field definitions are case insensitive by default,
526
        // change used collation for case sensitive searches.
527
        $collateClause = '';
528
        if ($caseSensitive === true) {
529
            if (self::get_collation()) {
530
                $collation = preg_replace('/_CI_/', '_CS_', self::get_collation());
531
            } else {
532
                $collation = 'Latin1_General_CS_AS';
533
            }
534
            $collateClause = ' COLLATE ' . $collation;
535
        } elseif ($caseSensitive === false) {
536
            if (self::get_collation()) {
537
                $collation = preg_replace('/_CS_/', '_CI_', self::get_collation());
538
            } else {
539
                $collation = 'Latin1_General_CI_AS';
540
            }
541
            $collateClause = ' COLLATE ' . $collation;
542
        }
543
544
        $clause = sprintf("%s %s %s", $field, $comp, $parameterised ? '?' : "'$value'");
545
        if ($collateClause) {
546
            $clause .= $collateClause;
547
        }
548
549
        return $clause;
550
    }
551
552
    /**
553
     * Function to return an SQL datetime expression for MSSQL
554
     * used for querying a datetime in a certain format
555
     *
556
     * @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
557
     * @param string $format to be used, supported specifiers:
558
     * %Y = Year (four digits)
559
     * %m = Month (01..12)
560
     * %d = Day (01..31)
561
     * %H = Hour (00..23)
562
     * %i = Minutes (00..59)
563
     * %s = Seconds (00..59)
564
     * %U = unix timestamp, can only be used on it's own
565
     * @return string SQL datetime expression to query for a formatted datetime
566
     */
567
    public function formattedDatetimeClause($date, $format)
568
    {
569
        preg_match_all('/%(.)/', $format, $matches);
570
        foreach ($matches[1] as $match) {
571
            if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
572
                user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
573
            }
574
        }
575
576 View Code Duplication
        if (preg_match('/^now$/i', $date)) {
0 ignored issues
show
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...
577
            $date = "CURRENT_TIMESTAMP";
578
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
579
            $date = "'$date.000'";
580
        }
581
582
        if ($format == '%U') {
583
            return "DATEDIFF(s, '1970-01-01 00:00:00', DATEADD(hour, DATEDIFF(hour, GETDATE(), GETUTCDATE()), $date))";
584
        }
585
586
        $trans = array(
587
            'Y' => 'yy',
588
            'm' => 'mm',
589
            'd' => 'dd',
590
            'H' => 'hh',
591
            'i' => 'mi',
592
            's' => 'ss',
593
        );
594
595
        $strings = array();
596
        $buffer = $format;
597
        while (strlen($buffer)) {
598
            if (substr($buffer, 0, 1) == '%') {
599
                $f = substr($buffer, 1, 1);
600
                $flen = $f == 'Y' ? 4 : 2;
601
                $strings[] = "RIGHT('0' + CAST(DATEPART({$trans[$f]},$date) AS VARCHAR), $flen)";
602
                $buffer = substr($buffer, 2);
603
            } else {
604
                $pos = strpos($buffer, '%');
605
                if ($pos === false) {
606
                    $strings[] = $buffer;
607
                    $buffer = '';
608
                } else {
609
                    $strings[] = "'".substr($buffer, 0, $pos)."'";
610
                    $buffer = substr($buffer, $pos);
611
                }
612
            }
613
        }
614
615
        return '(' . implode(' + ', $strings) . ')';
616
    }
617
618
    /**
619
     * Function to return an SQL datetime expression for MSSQL.
620
     * used for querying a datetime addition
621
     *
622
     * @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
623
     * @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes, +1 YEAR
624
     * supported qualifiers:
625
     * - years
626
     * - months
627
     * - days
628
     * - hours
629
     * - minutes
630
     * - seconds
631
     * This includes the singular forms as well
632
     * @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of the addition
633
     */
634
    public function datetimeIntervalClause($date, $interval)
635
    {
636
        $trans = array(
637
            'year' => 'yy',
638
            'month' => 'mm',
639
            'day' => 'dd',
640
            'hour' => 'hh',
641
            'minute' => 'mi',
642
            'second' => 'ss',
643
        );
644
645
        $singularinterval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
646
647
        if (
648
            !($params = preg_match('/([-+]\d+) (\w+)/i', $singularinterval, $matches)) ||
649
            !isset($trans[strtolower($matches[2])])
650
        ) {
651
            user_error('datetimeIntervalClause(): invalid interval ' . $interval, E_USER_WARNING);
652
        }
653
654 View Code Duplication
        if (preg_match('/^now$/i', $date)) {
0 ignored issues
show
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...
655
            $date = "CURRENT_TIMESTAMP";
656
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
657
            $date = "'$date'";
658
        }
659
660
        return "CONVERT(VARCHAR, DATEADD(" . $trans[strtolower($matches[2])] . ", " . (int)$matches[1] . ", $date), 120)";
661
    }
662
663
    /**
664
     * Function to return an SQL datetime expression for MSSQL.
665
     * used for querying a datetime substraction
666
     *
667
     * @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
668
     * @param string $date2 to be substracted of $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
669
     * @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which is the result of the substraction
670
     */
671
    public function datetimeDifferenceClause($date1, $date2)
672
    {
673 View Code Duplication
        if (preg_match('/^now$/i', $date1)) {
674
            $date1 = "CURRENT_TIMESTAMP";
675
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
676
            $date1 = "'$date1'";
677
        }
678
679 View Code Duplication
        if (preg_match('/^now$/i', $date2)) {
680
            $date2 = "CURRENT_TIMESTAMP";
681
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
682
            $date2 = "'$date2'";
683
        }
684
685
        return "DATEDIFF(s, $date2, $date1)";
686
    }
687
}
688