Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.25% covered (success)
80.25%
130 / 162
40.91% covered (danger)
40.91%
9 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
Sqlsrv
80.25% covered (success)
80.25%
130 / 162
40.91% covered (danger)
40.91%
9 / 22
72.84
0.00% covered (danger)
0.00%
0 / 1
 connect
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 escape
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetchArray
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 fetchRow
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fetchAll
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 error
80.00% covered (success)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 fetchObject
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 numRows
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 log
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTableStatus
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 query
81.82% covered (success)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
4.10
 queryPrepared
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 affectedRows
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 nextId
88.89% covered (success)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 clientVersion
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 serverVersion
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getTableNames
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
1
 close
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 lastInsertId
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 now
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setConnectionOptions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 formatErrors
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3/**
4 * The phpMyFAQ\Database\Sqlsrv class provides methods and functions for SQL Server Driver
5 * for PHP from Microsoft for Microsoft SQL Server 2012 or later.
6 *
7 * This Source Code Form is subject to the terms of the Mozilla Public License,
8 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
9 * obtain one at https://mozilla.org/MPL/2.0/.
10 *
11 * @package   phpMyFAQ
12 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
13 * @copyright 2009-2026 phpMyFAQ Team
14 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
15 * @link      https://www.phpmyfaq.de
16 * @since     2009-02-18
17 */
18
19declare(strict_types=1);
20
21namespace phpMyFAQ\Database;
22
23use Exception;
24use phpMyFAQ\Database;
25use SensitiveParameter;
26
27/**
28 * Class Sqlsrv
29 *
30 * @package phpMyFAQ\Database
31 * @deprecated Use PDO instead. Will be removed in the v5.0 release.
32 */
33class Sqlsrv implements DatabaseDriver
34{
35    /** @var string[] Tables */
36    public array $tableNames = [];
37
38    /** @var resource|bool */
39    private $conn = false;
40
41    /** The query log string. */
42    private string $sqlLog = '';
43
44    /** @var resource|false|null The last query result for tracking affected rows. */
45    private mixed $lastResult = null;
46
47    /**
48     * Connection options array.
49     *
50     * @var array<string, mixed>
51     */
52    private array $connectionOptions = [];
53
54    /**
55     * Connects to the database.
56     * This function connects to a MySQL database
57     *
58     * @param string $host A string specifying the name of the server to which a connection is being established
59     * @param string $user Specifies the User ID to be used when connecting with SQL Server Authentication
60     * @param string $password Specifies the password associated with the User ID to be used when connecting with
61     *                         SQL Server Authentication
62     * @param string $database Specifies the name of the database in use for the connection being established
63     * @return bool|null true, if connected, otherwise false
64     */
65    public function connect(
66        string $host,
67        #[SensitiveParameter]
68        string $user,
69        #[SensitiveParameter]
70        string $password,
71        string $database = '',
72        ?int $port = null,
73    ): ?bool {
74        $this->setConnectionOptions($user, $password, $database);
75
76        $this->conn = sqlsrv_connect($host . ', ' . (string) $port, $this->connectionOptions);
77        if (!$this->conn) {
78            Database::errorPage($this->formatErrors(sqlsrv_errors() ?? []));
79            die();
80        }
81
82        return true;
83    }
84
85    /**
86     * Escapes a string for use in a query.
87     *
88     * @param string $string String
89     */
90    public function escape(string $string): string
91    {
92        return str_replace(search: "'", replace: "''", subject: $string);
93    }
94
95    /**
96     * Fetch a result row as an assoc array.
97     *
98     * @param mixed $result Resultset
99     */
100    public function fetchArray(mixed $result): ?array
101    {
102        if (!is_resource($result)) {
103            return [];
104        }
105
106        $fetchedData = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC);
107
108        return is_array($fetchedData) ? $fetchedData : [];
109    }
110
111    /**
112     * Fetch a result row.
113     */
114    public function fetchRow(mixed $result): mixed
115    {
116        $row = $this->fetchArray($result);
117
118        return $row[0] ?? null;
119    }
120
121    /**
122     * Fetches a complete result as an object.
123     *
124     * @param mixed $result Resultset
125     * @throws Exception
126     * @return list<\stdClass>|null
127     */
128    public function fetchAll(mixed $result): ?array
129    {
130        $ret = [];
131        if (false === $result) {
132            throw new Exception('Error while fetching result: ' . $this->error());
133        }
134
135        while (true) {
136            $row = $this->fetchObject($result);
137            if (!$row instanceof \stdClass) {
138                break;
139            }
140
141            $ret[] = $row;
142        }
143
144        return $ret;
145    }
146
147    /**
148     * Returns the error string.
149     */
150    public function error(): string
151    {
152        $errors = sqlsrv_errors();
153
154        if (null !== $errors) {
155            $firstError = $errors[0] ?? [];
156            return (string) ($firstError['SQLSTATE'] ?? '') . ': ' . (string) ($firstError['message'] ?? '');
157        }
158
159        return '';
160    }
161
162    /**
163     * Fetch a result row as an object.
164     *
165     * @param mixed $result Results
166     *
167     * @return \stdClass|false|null
168     */
169    public function fetchObject(mixed $result): mixed
170    {
171        if (!is_resource($result)) {
172            return false;
173        }
174
175        /* @mago-expect lint:inline-variable-return - the variable carries the @var type for mago analyze */
176        /** @var \stdClass|false|null $row */
177        $row = sqlsrv_fetch_object($result);
178
179        return $row;
180    }
181
182    /**
183     * Number of rows in a result.
184     *
185     * @param mixed $result Resultset
186     */
187    public function numRows(mixed $result): int
188    {
189        if (!is_resource($result)) {
190            return 0;
191        }
192
193        $numRows = sqlsrv_num_rows($result);
194
195        return is_int($numRows) ? $numRows : 0;
196    }
197
198    /**
199     * Logs the queries.
200     */
201    public function log(): string
202    {
203        return $this->sqlLog;
204    }
205
206    /**
207     * This function returns the table status.
208     *
209     * @param string $prefix Table prefix
210     */
211    public function getTableStatus(string $prefix = ''): array
212    {
213        $tables = [];
214        $query = "
215            SELECT
216                obj.name AS table_name,
217                idx.rows AS table_rows
218            FROM
219                sysobjects obj, sysindexes idx
220            WHERE
221                    idx.id = OBJECT_ID(obj.name)
222                AND idx.indid < 2
223                AND obj.xtype = 'U'
224            ORDER BY obj.name";
225        $result = $this->query($query);
226
227        while (true) {
228            $row = $this->fetchObject($result);
229            if (!$row instanceof \stdClass) {
230                break;
231            }
232
233            $tables[$row->table_name] = $row->table_rows;
234        }
235
236        return $tables;
237    }
238
239    /**
240     * This function sends a query to the database.
241     *
242     * @return resource|bool
243     */
244    public function query(string $query, int $offset = 0, int $rowcount = 0): mixed
245    {
246        $this->sqlLog .= $query;
247
248        if (!is_resource($this->conn)) {
249            return false;
250        }
251
252        $options = ['Scrollable' => SQLSRV_CURSOR_KEYSET];
253
254        if (0 < $rowcount) {
255            $query .= sprintf(' OFFSET %d ROWS FETCH NEXT %d ROWS ONLY', $offset, $rowcount);
256        }
257
258        $result = sqlsrv_query($this->conn, $query, [], $options);
259
260        if (!$result) {
261            $this->sqlLog .= $this->error();
262        }
263
264        $this->lastResult = $result;
265
266        return $result;
267    }
268
269    /**
270     * Sends a parameterized query; `?` placeholders are bound natively by sqlsrv.
271     *
272     * @param array<int, string|int|float|null> $params
273     */
274    public function queryPrepared(string $query, array $params): mixed
275    {
276        $this->sqlLog .= $query;
277
278        if (!is_resource($this->conn)) {
279            return false;
280        }
281
282        $result = sqlsrv_query($this->conn, $query, $params, ['Scrollable' => SQLSRV_CURSOR_KEYSET]);
283
284        if (!$result) {
285            $this->sqlLog .= $this->error();
286        }
287
288        $this->lastResult = $result;
289
290        return $result;
291    }
292
293    /**
294     * Returns the number of rows affected by the last INSERT, UPDATE, or DELETE query.
295     */
296    public function affectedRows(): int
297    {
298        if ($this->lastResult === false || $this->lastResult === null) {
299            return 0;
300        }
301
302        $rows = sqlsrv_rows_affected($this->lastResult);
303        return $rows === false ? 0 : $rows;
304    }
305
306    /**
307     * Returns the next ID of a table.
308     *
309     * @param string $table the name of the table
310     * @param string $column    the name of the ID column
311     */
312    public function nextId(string $table, string $column): int
313    {
314        $select = sprintf('
315           SELECT 
316               max(%s) as current_id
317           FROM 
318               %s', $column, $table);
319
320        $result = $this->query($select);
321        if (!is_resource($result)) {
322            return 1;
323        }
324
325        sqlsrv_fetch($result);
326
327        $fieldIndex = 0;
328        $currentId = sqlsrv_get_field($result, $fieldIndex);
329
330        return (is_numeric($currentId) ? (int) $currentId : 0) + 1;
331    }
332
333    /**
334     * Returns the library version string.
335     */
336    public function clientVersion(): string
337    {
338        if (!is_resource($this->conn)) {
339            return '';
340        }
341
342        $clientInfo = sqlsrv_client_info($this->conn);
343        if (!is_array($clientInfo)) {
344            return '';
345        }
346
347        return (string) ($clientInfo['DriverODBCVer'] ?? '') . ' ' . (string) ($clientInfo['DriverVer'] ?? '');
348    }
349
350    /**
351     * Returns the library version string.
352     */
353    public function serverVersion(): string
354    {
355        if (!is_resource($this->conn)) {
356            return '';
357        }
358
359        $serverInfo = sqlsrv_server_info($this->conn);
360
361        return (string) ($serverInfo['SQLServerVersion'] ?? '');
362    }
363
364    /**
365     * Returns an array with all table names.
366     *
367     * @todo Have to be refactored because of https://github.com/thorsten/phpMyFAQ/issues/965
368     *
369     * @param string $prefix Table prefix
370     */
371    public function getTableNames(string $prefix = ''): array
372    {
373        return $this->tableNames = [
374            $prefix . 'faqadminlog',
375            $prefix . 'faqattachment',
376            $prefix . 'faqattachment_file',
377            $prefix . 'faqbackup',
378            $prefix . 'faqbookmarks',
379            $prefix . 'faqcaptcha',
380            $prefix . 'faqcategories',
381            $prefix . 'faqcategoryrelations',
382            $prefix . 'faqcategory_group',
383            $prefix . 'faqcategory_news',
384            $prefix . 'faqcategory_order',
385            $prefix . 'faqcategory_user',
386            $prefix . 'faqchanges',
387            $prefix . 'faqchat_messages',
388            $prefix . 'faqcomments',
389            $prefix . 'faqconfig',
390            $prefix . 'faqcustompages',
391            $prefix . 'faqdata',
392            $prefix . 'faqdata_group',
393            $prefix . 'faqdata_revisions',
394            $prefix . 'faqdata_tags',
395            $prefix . 'faqdata_user',
396            $prefix . 'faqforms',
397            $prefix . 'faqglossary',
398            $prefix . 'faqgroup',
399            $prefix . 'faqgroup_right',
400            $prefix . 'faqinstances',
401            $prefix . 'faqinstances_config',
402            $prefix . 'faqnews',
403            $prefix . 'faqquestions',
404            $prefix . 'faqright',
405            $prefix . 'faqsearches',
406            $prefix . 'faqseo',
407            $prefix . 'faqsessions',
408            $prefix . 'faqstopwords',
409            $prefix . 'faqtags',
410            $prefix . 'faquser',
411            $prefix . 'faquserdata',
412            $prefix . 'faquserlogin',
413            $prefix . 'faquser_group',
414            $prefix . 'faquser_right',
415            $prefix . 'faqvisits',
416            $prefix . 'faqvoting',
417        ];
418    }
419
420    /**
421     * Closes the connection to the database.
422     */
423    public function close(): void
424    {
425        if (is_resource($this->conn)) {
426            sqlsrv_close($this->conn);
427        }
428    }
429
430    /**
431     * Returns the ID of the last inserted row.
432     */
433    public function lastInsertId(): int|string
434    {
435        if (!is_resource($this->conn)) {
436            return 0;
437        }
438
439        $query = 'SELECT SCOPE_IDENTITY() AS id';
440        $result = sqlsrv_query($this->conn, $query);
441        if ($result === false) {
442            return 0;
443        }
444
445        sqlsrv_fetch($result);
446
447        $fieldIndex = 0;
448        return (int) sqlsrv_get_field($result, $fieldIndex);
449    }
450
451    public function now(): string
452    {
453        return 'GETDATE()';
454    }
455
456    /**
457     * Sets the connection options.
458     *
459     * @param string $user Specifies the User ID to be used when connecting with SQL Server Authentication
460     * @param string $password Specifies the password associated with the User ID to be used when connecting with
461     *                         SQL Server Authentication
462     * @param string $database Specifies the name of the database in use for the connection being established
463     */
464    private function setConnectionOptions(string $user, #[SensitiveParameter] string $password, string $database): void
465    {
466        $this->connectionOptions = [
467            'UID' => $user,
468            'PWD' => $password,
469            'Database' => $database,
470            'CharacterSet' => 'UTF-8',
471            'TrustServerCertificate' => true, // even trust self-signed certificates
472        ];
473    }
474
475    /**
476     * Formats the error output
477     */
478    private function formatErrors(array $errors): string
479    {
480        $error = '<h3>SQL Error:</h3>MS SQL Error information: <br/>';
481        foreach ($errors as $entry) {
482            $entry = is_array($entry) ? $entry : [];
483            $error .= sprintf(
484                'SQLSTATE: %s<br/>Code: %s<br/>Message: %s<br/>',
485                (string) ($entry['SQLSTATE'] ?? ''),
486                (string) ($entry['code'] ?? ''),
487                (string) ($entry['message'] ?? ''),
488            );
489        }
490
491        return $error;
492    }
493}