Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 159
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
mssqlnative
0.00% covered (danger)
0.00%
0 / 159
0.00% covered (danger)
0.00%
0 / 15
6320
0.00% covered (danger)
0.00%
0 / 1
 sql_connect
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 sql_server_info
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
72
 sql_buffer_nested_transactions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 _sql_transaction
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 sql_query
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
272
 _sql_query_limit
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 sql_affectedrows
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 sql_fetchrow
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
132
 sql_last_inserted_id
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 sql_freeresult
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 _sql_error
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 _sql_close
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 _sql_report
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
72
 mssqlnative_num_rows
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 mssqlnative_set_query_options
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14/**
15* This is the MS SQL Server Native database abstraction layer.
16* PHP mssql native driver required.
17* @author Chris Pucci
18*
19*/
20
21namespace phpbb\db\driver;
22
23class mssqlnative extends \phpbb\db\driver\mssql_base
24{
25    var $m_insert_id = null;
26    var $query_options = array();
27    var $connect_error = '';
28
29    /** @var string|false Last error result or false if no last error set */
30    private $last_error_result = false;
31
32    /**
33    * {@inheritDoc}
34    */
35    function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false)
36    {
37        // Test for driver support, to avoid suppressed fatal error
38        if (!function_exists('sqlsrv_connect'))
39        {
40            $this->connect_error = 'Native MS SQL Server driver for PHP is missing or needs to be updated. Version 1.1 or later is required to install phpBB3. You can download the driver from: http://www.microsoft.com/sqlserver/2005/en/us/PHP-Driver.aspx';
41            return $this->sql_error('');
42        }
43
44        //set up connection variables
45        $this->persistency = $persistency;
46        $this->user = $sqluser;
47        $this->dbname = $database;
48        $port_delimiter = (defined('PHP_OS') && substr(PHP_OS, 0, 3) === 'WIN') ? ',' : ':';
49        $this->server = $sqlserver . (($port) ? $port_delimiter . $port : '');
50
51        //connect to database
52        $this->db_connect_id = sqlsrv_connect($this->server, array(
53            'Database' => $this->dbname,
54            'UID' => $this->user,
55            'PWD' => $sqlpassword,
56            'CharacterSet' => 'UTF-8'
57        ));
58
59        return ($this->db_connect_id) ? $this->db_connect_id : $this->sql_error('');
60    }
61
62    /**
63    * {@inheritDoc}
64    */
65    function sql_server_info($raw = false, $use_cache = true)
66    {
67        global $cache;
68
69        if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('mssql_version')) === false)
70        {
71            $arr_server_info = sqlsrv_server_info($this->db_connect_id);
72            $this->sql_server_version = $arr_server_info['SQLServerVersion'];
73
74            if (!empty($cache) && $use_cache)
75            {
76                $cache->put('mssql_version', $this->sql_server_version);
77            }
78        }
79
80        if ($raw)
81        {
82            return $this->sql_server_version;
83        }
84
85        return ($this->sql_server_version) ? 'MSSQL<br />' . $this->sql_server_version : 'MSSQL';
86    }
87
88    /**
89    * {@inheritDoc}
90    */
91    function sql_buffer_nested_transactions()
92    {
93        return true;
94    }
95
96    /**
97    * {@inheritDoc}
98    */
99    protected function _sql_transaction(string $status = 'begin'): bool
100    {
101        switch ($status)
102        {
103            case 'begin':
104                return sqlsrv_begin_transaction($this->db_connect_id);
105
106            case 'commit':
107                return sqlsrv_commit($this->db_connect_id);
108
109            case 'rollback':
110                return sqlsrv_rollback($this->db_connect_id);
111        }
112        return true;
113    }
114
115    /**
116    * {@inheritDoc}
117    */
118    function sql_query($query = '', $cache_ttl = 0)
119    {
120        if ($query != '')
121        {
122            global $cache;
123
124            if ($this->debug_sql_explain)
125            {
126                $this->sql_report('start', $query);
127            }
128            else if ($this->debug_load_time)
129            {
130                $this->curtime = microtime(true);
131            }
132
133            $this->last_query_text = $query;
134            $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false;
135            $this->sql_add_num_queries($this->query_result);
136
137            if ($this->query_result === false)
138            {
139                try
140                {
141                    $this->query_result = @sqlsrv_query($this->db_connect_id, $query, array(), $this->query_options);
142                }
143                catch (\Error $e)
144                {
145                    // Do nothing as SQL driver will report the error
146                }
147
148                if ($this->query_result === false)
149                {
150                    $this->sql_error($query);
151                }
152
153                // Reset options for the next query
154                $this->query_options = [];
155
156                if ($this->debug_sql_explain)
157                {
158                    $this->sql_report('stop', $query);
159                }
160                else if ($this->debug_load_time)
161                {
162                    $this->sql_time += microtime(true) - $this->curtime;
163                }
164
165                if (!$this->query_result)
166                {
167                    return false;
168                }
169
170                $safe_query_id = $this->clean_query_id($this->query_result);
171
172                if ($cache && $cache_ttl)
173                {
174                    $this->open_queries[$safe_query_id] = $this->query_result;
175                    $this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
176                }
177                else if (strpos($query, 'SELECT') === 0)
178                {
179                    $this->open_queries[$safe_query_id] = $this->query_result;
180                }
181            }
182            else if ($this->debug_sql_explain)
183            {
184                $this->sql_report('fromcache', $query);
185            }
186        }
187        else
188        {
189            return false;
190        }
191        return $this->query_result;
192    }
193
194    /**
195     * {@inheritDoc}
196     */
197    protected function _sql_query_limit(string $query, int $total, int $offset = 0, int $cache_ttl = 0)
198    {
199        $this->query_result = false;
200
201        // total == 0 means all results - not zero results
202        if ($offset == 0 && $total !== 0)
203        {
204            if (strpos($query, "SELECT") === false)
205            {
206                $query = "TOP {$total} " . $query;
207            }
208            else
209            {
210                $query = preg_replace('/SELECT(\s*DISTINCT)?/Dsi', 'SELECT$1 TOP '.$total, $query);
211            }
212        }
213        else if ($offset > 0)
214        {
215            $query = preg_replace('/SELECT(\s*DISTINCT)?/Dsi', 'SELECT$1 TOP(10000000) ', $query);
216            $query = 'SELECT *
217                    FROM (SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3
218                    FROM (SELECT 1 AS line2, sub1.* FROM (' . $query . ') AS sub1) as sub2) AS sub3';
219
220            if ($total > 0)
221            {
222                $query .= ' WHERE line3 BETWEEN ' . ($offset+1) . ' AND ' . ($offset + $total);
223            }
224            else
225            {
226                $query .= ' WHERE line3 > ' . $offset;
227            }
228        }
229
230        $result = $this->sql_query($query, $cache_ttl);
231
232        return $result;
233    }
234
235    /**
236    * {@inheritDoc}
237    */
238    function sql_affectedrows()
239    {
240        return ($this->db_connect_id) ? @sqlsrv_rows_affected($this->query_result) : false;
241    }
242
243    /**
244    * {@inheritDoc}
245    */
246    function sql_fetchrow($query_id = false)
247    {
248        global $cache;
249
250        if ($query_id === false)
251        {
252            $query_id = $this->query_result;
253        }
254
255        $safe_query_id = $this->clean_query_id($query_id);
256        if ($cache && $cache->sql_exists($safe_query_id))
257        {
258            return $cache->sql_fetchrow($safe_query_id);
259        }
260
261        if (!$query_id)
262        {
263            return false;
264        }
265
266        $row = sqlsrv_fetch_array($query_id, SQLSRV_FETCH_ASSOC);
267
268        if ($row)
269        {
270            foreach ($row as $key => $value)
271            {
272                $row[$key] = ($value === ' ' || $value === null) ? '' : $value;
273            }
274
275            // remove helper values from LIMIT queries
276            if (isset($row['line2']))
277            {
278                unset($row['line2'], $row['line3']);
279            }
280        }
281        return ($row !== null) ? $row : false;
282    }
283
284    /**
285     * {@inheritdoc}
286     */
287    public function sql_last_inserted_id()
288    {
289        $result_id = @sqlsrv_query($this->db_connect_id, 'SELECT @@IDENTITY');
290
291        if ($result_id)
292        {
293            $row = sqlsrv_fetch_array($result_id);
294            $id = isset($row[0]) ? (int) $row[0] : false;
295            sqlsrv_free_stmt($result_id);
296            return $id;
297        }
298        else
299        {
300            return false;
301        }
302    }
303
304    /**
305    * {@inheritDoc}
306    */
307    function sql_freeresult($query_id = false)
308    {
309        global $cache;
310
311        if ($query_id === false)
312        {
313            $query_id = $this->query_result;
314        }
315
316        $safe_query_id = $this->clean_query_id($query_id);
317        if ($cache && $cache->sql_exists($safe_query_id))
318        {
319            $cache->sql_freeresult($safe_query_id);
320        }
321        else if (isset($this->open_queries[$safe_query_id]))
322        {
323            unset($this->open_queries[$safe_query_id]);
324            sqlsrv_free_stmt($query_id);
325        }
326    }
327
328    /**
329    * {@inheritDoc}
330    */
331    protected function _sql_error(): array
332    {
333        if (function_exists('sqlsrv_errors'))
334        {
335            $errors = @sqlsrv_errors(SQLSRV_ERR_ERRORS);
336            $error_message = '';
337            $code = 0;
338
339            if ($errors != null)
340            {
341                foreach ($errors as $error)
342                {
343                    $error_message .= "SQLSTATE: " . $error['SQLSTATE'] . "\n";
344                    $error_message .= "code: " . $error['code'] . "\n";
345                    $code = $error['code'];
346                    $error_message .= "message: " . $error['message'] . "\n";
347                }
348                $this->last_error_result = $error_message;
349                $error = $this->last_error_result;
350            }
351            else
352            {
353                $error = $this->last_error_result ?: '';
354            }
355
356            $error = array(
357                'message'    => $error,
358                'code'        => $code,
359            );
360        }
361        else
362        {
363            $error = array(
364                'message'    => $this->connect_error,
365                'code'        => '',
366            );
367        }
368
369        return $error;
370    }
371
372    /**
373     * {@inheritDoc}
374     */
375    protected function _sql_close(): bool
376    {
377        return @sqlsrv_close($this->db_connect_id);
378    }
379
380    /**
381    * {@inheritDoc}
382    */
383    protected function _sql_report(string $mode, string $query = ''): void
384    {
385        switch ($mode)
386        {
387            case 'start':
388                $html_table = false;
389                @sqlsrv_query($this->db_connect_id, 'SET SHOWPLAN_TEXT ON;');
390                if ($result = @sqlsrv_query($this->db_connect_id, $query))
391                {
392                    sqlsrv_next_result($result);
393                    while ($row = sqlsrv_fetch_array($result))
394                    {
395                        $html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
396                    }
397                    sqlsrv_free_stmt($result);
398                }
399                @sqlsrv_query($this->db_connect_id, 'SET SHOWPLAN_TEXT OFF;');
400
401                if ($html_table)
402                {
403                    $this->html_hold .= '</table>';
404                }
405            break;
406
407            case 'fromcache':
408                $endtime = explode(' ', microtime());
409                $endtime = $endtime[0] + $endtime[1];
410
411                $result = @sqlsrv_query($this->db_connect_id, $query);
412                if ($result)
413                {
414                    while ($void = sqlsrv_fetch_array($result))
415                    {
416                        // Take the time spent on parsing rows into account
417                    }
418                    sqlsrv_free_stmt($result);
419                }
420
421                $splittime = explode(' ', microtime());
422                $splittime = $splittime[0] + $splittime[1];
423
424                $this->sql_report('record_fromcache', $query, $endtime, $splittime);
425
426            break;
427        }
428    }
429
430    /**
431    * Utility method used to retrieve number of rows
432    * Emulates mysql_num_rows
433    * Used in acp_database.php -> write_data_mssqlnative()
434    * Requires a static or keyset cursor to be definde via
435    * mssqlnative_set_query_options()
436    */
437    function mssqlnative_num_rows($res)
438    {
439        if ($res !== false)
440        {
441            return sqlsrv_num_rows($res);
442        }
443        else
444        {
445            return false;
446        }
447    }
448
449    /**
450    * Allows setting mssqlnative specific query options passed to sqlsrv_query as 4th parameter.
451    */
452    function mssqlnative_set_query_options($options)
453    {
454        $this->query_options = $options;
455    }
456}