Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
PdoSqlsrv
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
2 / 2
8
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createTables
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3/**
4 * The phpMyFAQ instances a database class with CREATE TABLE statements for MS SQL Server.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2025 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2025-10-26
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Instance\Database;
21
22use phpMyFAQ\Configuration;
23use phpMyFAQ\Instance\Database;
24
25/**
26 * Class Sqlsrv
27 *
28 * @package phpMyFAQ\Instance\Database
29 */
30class PdoSqlsrv extends Database implements DriverInterface
31{
32    /** @var array<string, string> */
33    private array $createTableStatements = [
34        'faqadminlog' => 'CREATE TABLE %sfaqadminlog (
35            id INTEGER NOT NULL,
36            time INTEGER NOT NULL,
37            usr INTEGER NOT NULL,
38            text NVARCHAR(4000) NOT NULL,
39            hash VARCHAR(64) NULL,
40            previous_hash VARCHAR(64) NULL,
41            ip NVARCHAR(64) NOT NULL,
42            PRIMARY KEY (id))',
43        'faqattachment' => 'CREATE TABLE %sfaqattachment (
44            id INTEGER NOT NULL,
45            record_id INTEGER NOT NULL,
46            record_lang NVARCHAR(5) NOT NULL,
47            real_hash CHAR(32) NOT NULL,
48            virtual_hash CHAR(32) NOT NULL,
49            password_hash CHAR(40) NULL,
50            filename NVARCHAR(255) NOT NULL,
51            filesize INTEGER NOT NULL,
52            encrypted INTEGER NOT NULL DEFAULT 0,
53            mime_type NVARCHAR(255) NULL,
54            PRIMARY KEY (id))',
55        'faqattachment_file' => 'CREATE TABLE %sfaqattachment_file (
56            virtual_hash CHAR(32) NOT NULL,
57            contents NVARCHAR(MAX) NOT NULL,
58            PRIMARY KEY (virtual_hash))',
59        'faqbackup' => 'CREATE TABLE %sfaqbackup (
60            id INT(11) NOT NULL,
61            filename VARCHAR(255) NOT NULL,
62            authkey VARCHAR(255) NOT NULL,
63            authcode VARCHAR(255) NOT NULL,
64            created timestamp NOT NULL,
65            PRIMARY KEY (id))',
66        'faqbookmarks' => 'CREATE TABLE %sfaqbookmarks (
67            userid INT(11) DEFAULT NULL,
68            faqid INT(11) DEFAULT NULL)',
69        'faqcaptcha' => 'CREATE TABLE %sfaqcaptcha (
70            id NVARCHAR(6) NOT NULL,
71            useragent NVARCHAR(255) NOT NULL,
72            language NVARCHAR(5) NOT NULL,
73            ip NVARCHAR(64) NOT NULL,
74            captcha_time INTEGER NOT NULL,
75            PRIMARY KEY (id))',
76        'faqcategories' => 'CREATE TABLE %sfaqcategories (
77            id INTEGER NOT NULL,
78            lang NVARCHAR(5) NOT NULL,
79            parent_id INTEGER NOT NULL,
80            name NVARCHAR(255) NOT NULL,
81            description NVARCHAR(255) DEFAULT NULL,
82            user_id INTEGER NOT NULL,
83            group_id INTEGER NOT NULL DEFAULT -1,
84            active INTEGER NULL DEFAULT 1,
85            image NVARCHAR(255) DEFAULT NULL,
86            show_home SMALLINT DEFAULT NULL,
87            PRIMARY KEY (id, lang))',
88        'faqcategoryrelations' => 'CREATE TABLE %sfaqcategoryrelations (
89            category_id INTEGER NOT NULL,
90            category_lang NVARCHAR(5) NOT NULL,
91            record_id INTEGER NOT NULL,
92            record_lang NVARCHAR(5) NOT NULL,
93            PRIMARY KEY (category_id, category_lang, record_id, record_lang))',
94        'faqcategoryrelations_idx' => 'CREATE INDEX idx_records ON %sfaqcategoryrelations (record_id, record_lang)',
95        'faqcategory_group' => 'CREATE TABLE %sfaqcategory_group (
96            category_id INTEGER NOT NULL,
97            group_id INTEGER NOT NULL,
98            PRIMARY KEY (category_id, group_id))',
99        'faqcategory_news' => 'CREATE TABLE %sfaqcategory_news (
100            category_id INTEGER NOT NULL,
101            news_id INTEGER NOT NULL,
102            PRIMARY KEY (category_id, news_id))',
103        'faqcategory_order' => 'CREATE TABLE %sfaqcategory_order (
104            category_id INTEGER NOT NULL,
105            parent_id INTEGER DEFAULT NULL,
106            position INTEGER NOT NULL,
107            PRIMARY KEY (category_id))',
108        'faqcategory_user' => 'CREATE TABLE %sfaqcategory_user (
109            category_id INTEGER NOT NULL,
110            user_id INTEGER NOT NULL,
111            PRIMARY KEY (category_id, user_id))',
112        'faqchanges' => 'CREATE TABLE %sfaqchanges (
113            id INTEGER NOT NULL,
114            beitrag SMALLINT NOT NULL,
115            lang NVARCHAR(5) NOT NULL,
116            revision_id INTEGER NOT NULL DEFAULT 0,
117            usr INTEGER NOT NULL ,
118            datum INTEGER NOT NULL,
119            what NVARCHAR(MAX) DEFAULT NULL,
120            PRIMARY KEY (id, lang))',
121        'faqcomments' => 'CREATE TABLE %sfaqcomments (
122            id_comment INTEGER NOT NULL,
123            id INTEGER NOT NULL,
124            type NVARCHAR(10) NOT NULL,
125            usr NVARCHAR(255) NOT NULL,
126            email NVARCHAR(255) NOT NULL,
127            comment NVARCHAR(MAX) NOT NULL,
128            datum NVARCHAR(64) NOT NULL,
129            helped NVARCHAR(MAX) DEFAULT NULL,
130            PRIMARY KEY (id_comment))',
131        'faqconfig' => 'CREATE TABLE %sfaqconfig (
132            config_name NVARCHAR(255) NOT NULL default \'\',
133            config_value TEXT DEFAULT NULL,
134            PRIMARY KEY (config_name))',
135        'faqdata' => 'CREATE TABLE %sfaqdata (
136            id INTEGER NOT NULL,
137            lang NVARCHAR(5) NOT NULL,
138            solution_id INTEGER NOT NULL,
139            revision_id INTEGER NOT NULL DEFAULT 0,
140            active char(3) NOT NULL,
141            sticky INTEGER NOT NULL,
142            keywords NVARCHAR(MAX) DEFAULT NULL,
143            thema NVARCHAR(MAX) NOT NULL,
144            content NVARCHAR(MAX) DEFAULT NULL,
145            author NVARCHAR(255) NOT NULL,
146            email NVARCHAR(255) NOT NULL,
147            comment char(1) default \'y\',
148            updated NVARCHAR(15) NOT NULL,
149            date_start NVARCHAR(14) NOT NULL DEFAULT \'00000000000000\',
150            date_end NVARCHAR(14) NOT NULL DEFAULT \'99991231235959\',
151            created DATETIME DEFAULT CURRENT_TIMESTAMP,
152            notes NVARCHAR(MAX) DEFAULT NULL,
153            sticky_order INTEGER DEFAULT NULL,
154            PRIMARY KEY (id, lang))',
155        'faqdata_revisions' => 'CREATE TABLE %sfaqdata_revisions (
156            id INTEGER NOT NULL,
157            lang NVARCHAR(5) NOT NULL,
158            solution_id INTEGER NOT NULL,
159            revision_id INTEGER NOT NULL DEFAULT 0,
160            active char(3) NOT NULL,
161            sticky INTEGER NOT NULL,
162            keywords NVARCHAR(MAX) DEFAULT NULL,
163            thema NVARCHAR(MAX) NOT NULL,
164            content NVARCHAR(MAX) DEFAULT NULL,
165            author NVARCHAR(255) NOT NULL,
166            email NVARCHAR(255) NOT NULL,
167            comment char(1) default \'y\',
168            updated NVARCHAR(15) NOT NULL,
169            date_start NVARCHAR(14) NOT NULL DEFAULT \'00000000000000\',
170            date_end NVARCHAR(14) NOT NULL DEFAULT \'99991231235959\',
171            created DATETIME DEFAULT CURRENT_TIMESTAMP,
172            notes NVARCHAR(MAX) DEFAULT NULL,
173            sticky_order INTEGER DEFAULT NULL,
174            PRIMARY KEY (id, lang, solution_id, revision_id))',
175        'faqdata_group' => 'CREATE TABLE %sfaqdata_group (
176            record_id INTEGER NOT NULL,
177            group_id INTEGER NOT NULL,
178            PRIMARY KEY (record_id, group_id))',
179        'faqdata_tags' => 'CREATE TABLE %sfaqdata_tags (
180            record_id INTEGER NOT NULL,
181            tagging_id INTEGER NOT NULL,
182            PRIMARY KEY (record_id, tagging_id))',
183        'faqdata_user' => 'CREATE TABLE %sfaqdata_user (
184            record_id INTEGER NOT NULL,
185            user_id INTEGER NOT NULL,
186            PRIMARY KEY (record_id, user_id))',
187        'faqforms' => 'CREATE TABLE %sfaqforms (
188            form_id INTEGER NOT NULL,
189            input_id INTEGER NOT NULL,
190            input_type NVARCHAR(1000) NOT NULL,
191            input_label NVARCHAR(500) NOT NULL,
192            input_active INTEGER NOT NULL,
193            input_required INTEGER NOT NULL,
194            input_lang NVARCHAR(11) NOT NULL)',
195        'faqglossary' => 'CREATE TABLE %sfaqglossary (
196            id INTEGER NOT NULL ,
197            lang NVARCHAR(5) NOT NULL ,
198            item NVARCHAR(255) NOT NULL ,
199            definition NVARCHAR(MAX) NOT NULL,
200            PRIMARY KEY (id, lang))',
201        'faqgroup' => 'CREATE TABLE %sfaqgroup (
202            group_id INTEGER NOT NULL,
203            name NVARCHAR(25) NULL,
204            description NVARCHAR(MAX) NULL,
205            auto_join INTEGER NULL,
206            PRIMARY KEY(group_id))',
207        'faqgroup_idx' => 'CREATE UNIQUE INDEX idx_name ON %sfaqgroup (name)',
208        'faqgroup_right' => 'CREATE TABLE %sfaqgroup_right (
209            group_id INTEGER NOT NULL,
210            right_id INTEGER NOT NULL,
211            PRIMARY KEY(group_id, right_id))',
212        'faqinstances' => 'CREATE TABLE %sfaqinstances (
213            id INT NOT NULL,
214            url NVARCHAR(255) NOT NULL,
215            instance NVARCHAR(255) NOT NULL,
216            comment NVARCHAR(MAX) NULL,
217            created DATETIME NOT NULL,
218            modified DATETIME NOT NULL,
219            PRIMARY KEY (id))',
220        'faqinstances_config' => 'CREATE TABLE %sfaqinstances_config (
221            instance_id INT NOT NULL,
222            config_name NVARCHAR(255) NOT NULL default \'\',
223            config_value NVARCHAR(255) DEFAULT NULL,
224            PRIMARY KEY (instance_id, config_name))',
225        'faqnews' => 'CREATE TABLE %sfaqnews (
226            id INTEGER NOT NULL,
227            lang NVARCHAR(5) NOT NULL,
228            header NVARCHAR(255) NOT NULL,
229            artikel NVARCHAR(MAX) NOT NULL,
230            datum NVARCHAR(14) NOT NULL,
231            author_name  NVARCHAR(255) NULL,
232            author_email NVARCHAR(255) NULL,
233            active char(1) default \'y\',
234            comment char(1) default \'n\',
235            link NVARCHAR(255) DEFAULT NULL,
236            linktitel NVARCHAR(255) DEFAULT NULL,
237            target NVARCHAR(255) NOT NULL,
238            PRIMARY KEY (id))',
239        'faqcustompages' => 'CREATE TABLE %sfaqcustompages (
240            id INT NOT NULL,
241            lang VARCHAR(5) NOT NULL,
242            page_title VARCHAR(255) NOT NULL,
243            slug VARCHAR(255) NOT NULL,
244            content NVARCHAR(MAX) NOT NULL,
245            author_name VARCHAR(255) NOT NULL,
246            author_email VARCHAR(255) NOT NULL,
247            active CHAR(1) NOT NULL DEFAULT \'n\',
248            created DATETIME NOT NULL DEFAULT GETDATE(),
249            updated DATETIME NULL,
250            seo_title VARCHAR(60) NULL,
251            seo_description VARCHAR(160) NULL,
252            seo_robots VARCHAR(50) NOT NULL DEFAULT \'index,follow\',
253            PRIMARY KEY (id, lang))',
254        'faqquestions' => 'CREATE TABLE %sfaqquestions (
255            id INTEGER NOT NULL,
256            lang NVARCHAR(5) NOT NULL,
257            username NVARCHAR(100) NOT NULL,
258            email NVARCHAR(100) NOT NULL,
259            category_id INTEGER NOT NULL,
260            question NVARCHAR(MAX) NOT NULL,
261            created NVARCHAR(20) NOT NULL,
262            is_visible char(1) default \'Y\',
263            answer_id INTEGER NOT NULL DEFAULT 0,
264            PRIMARY KEY (id))',
265        'faqright' => 'CREATE TABLE %sfaqright (
266            right_id INTEGER NOT NULL,
267            name NVARCHAR(50) NULL,
268            description NVARCHAR(MAX) NULL,
269            for_users INTEGER NULL DEFAULT 1,
270            for_groups INTEGER NULL DEFAULT 1,
271            for_sections INTEGER NULL DEFAULT 1,
272            PRIMARY KEY (right_id))',
273        'faqsearches' => 'CREATE TABLE %sfaqsearches (
274            id INTEGER NOT NULL ,
275            lang NVARCHAR(5) NOT NULL ,
276            searchterm NVARCHAR(255) NOT NULL ,
277            searchdate DATETIME,
278            PRIMARY KEY (id, lang))',
279        'faqseo' => 'CREATE TABLE %sfaqseo (
280            id INT NOT NULL,
281            type VARCHAR(32) NOT NULL,
282            reference_id INT NOT NULL,
283            reference_language VARCHAR(5) NOT NULL,
284            title TEXT NULL,
285            description TEXT NULL,
286            slug TEXT NULL,
287            created DATE NOT NULL DEFAULT GETDATE(),
288            PRIMARY KEY (id))',
289        'faqsessions' => 'CREATE TABLE %sfaqsessions (
290            sid INTEGER NOT NULL,
291            user_id INTEGER NOT NULL,
292            ip NVARCHAR(64) NOT NULL,
293            time INTEGER NOT NULL,
294            PRIMARY KEY (sid))',
295        'faqsessions_idx' => 'CREATE INDEX idx_time ON %sfaqsessions (time)',
296        'faqsearches_searchterm_idx' => 'CREATE INDEX idx_faqsearches_searchterm ON %sfaqsearches (searchterm)',
297        'faqsearches_date_term_idx' => 'CREATE INDEX idx_faqsearches_date_term ON %sfaqsearches (searchdate, searchterm)',
298        'faqsearches_date_term_lang_idx' => 'CREATE INDEX idx_faqsearches_date_term_lang ON %sfaqsearches (searchdate, searchterm, lang)',
299        'faqstopwords' => 'CREATE TABLE %sfaqstopwords (
300            id INTEGER NOT NULL,
301            lang NVARCHAR(5) NOT NULL,
302            stopword NVARCHAR(64) NOT NULL,
303            PRIMARY KEY (id, lang))',
304        'faqtags' => 'CREATE TABLE %sfaqtags (
305            tagging_id INTEGER NOT NULL,
306            tagging_name NVARCHAR(255) NOT NULL ,
307            PRIMARY KEY (tagging_id, tagging_name))',
308        'faquser' => 'CREATE TABLE %sfaquser (
309            user_id INTEGER NOT NULL,
310            login NVARCHAR(128) NOT NULL,
311            session_id NVARCHAR(150) NULL,
312            session_timestamp INTEGER NULL,
313            ip NVARCHAR(64) NULL,
314            account_status NVARCHAR(50) NULL,
315            last_login NVARCHAR(14) NULL,
316            auth_source NVARCHAR(100) NULL,
317            member_since NVARCHAR(14) NULL,
318            remember_me NVARCHAR(150) NULL,
319            success INTEGER NULL DEFAULT 1,
320            is_superadmin INTEGER NULL DEFAULT 0,
321            login_attempts INTEGER NULL DEFAULT 0,
322            refresh_token TEXT NULL DEFAULT NULL,
323            access_token TEXT NULL DEFAULT NULL,
324            code_verifier VARCHAR(255) NULL DEFAULT NULL,
325            jwt TEXT NULL DEFAULT NULL,
326            webauthnkeys TEXT NULL DEFAULT NULL,
327            PRIMARY KEY (user_id))',
328        'faquserdata' => 'CREATE TABLE %sfaquserdata (
329            user_id INTEGER NOT NULL,
330            last_modified NVARCHAR(14) NULL,
331            display_name NVARCHAR(128) NULL,
332            email NVARCHAR(128) NULL,
333            keycloak_sub NVARCHAR(255) NULL,
334            is_visible INTEGER NULL DEFAULT 0,
335            twofactor_enabled INTEGER NULL DEFAULT 0,
336            secret NVARCHAR(128) NULL DEFAULT NULL)',
337        'faquserlogin' => 'CREATE TABLE %sfaquserlogin (
338            login NVARCHAR(128) NOT NULL,
339            pass NVARCHAR(80) NULL,
340            domain NVARCHAR(255) NULL,
341            PRIMARY KEY (login))',
342        'faquser_group' => 'CREATE TABLE %sfaquser_group (
343            user_id INTEGER NOT NULL,
344            group_id INTEGER NOT NULL,
345            PRIMARY KEY (user_id, group_id))',
346        'faquser_right' => 'CREATE TABLE %sfaquser_right (
347            user_id INTEGER NOT NULL,
348            right_id INTEGER NOT NULL,
349            PRIMARY KEY (user_id, right_id))',
350        'faqvisits' => 'CREATE TABLE %sfaqvisits (
351            id INTEGER NOT NULL,
352            lang NVARCHAR(5) NOT NULL,
353            visits INTEGER NOT NULL,
354            last_visit INTEGER NOT NULL,
355            PRIMARY KEY (id, lang))',
356        'faqvoting' => 'CREATE TABLE %sfaqvoting (
357            id INTEGER NOT NULL,
358            artikel SMALLINT NOT NULL,
359            vote SMALLINT NOT NULL,
360            usr SMALLINT NOT NULL,
361            datum NVARCHAR(20) DEFAULT \'\',
362            ip NVARCHAR(15) DEFAULT \'\',
363            PRIMARY KEY (id))',
364        'faqchat_messages' => 'CREATE TABLE %sfaqchat_messages (
365            id INTEGER IDENTITY(1,1) NOT NULL,
366            sender_id INTEGER NOT NULL,
367            recipient_id INTEGER NOT NULL,
368            message NVARCHAR(MAX) NOT NULL,
369            is_read INTEGER NOT NULL DEFAULT 0,
370            created_at DATETIME DEFAULT GETDATE(),
371            PRIMARY KEY (id))',
372        'faqchat_messages_idx_sender' => 'CREATE INDEX idx_chat_sender ON %sfaqchat_messages (sender_id)',
373        'faqchat_messages_idx_recipient' => 'CREATE INDEX idx_chat_recipient ON %sfaqchat_messages (recipient_id)',
374        'faqchat_messages_idx_conversation' => 'CREATE INDEX idx_chat_conversation ON %sfaqchat_messages (sender_id, recipient_id)',
375        'faqchat_messages_idx_created' => 'CREATE INDEX idx_chat_created ON %sfaqchat_messages (created_at)',
376    ];
377
378    /**
379     * Constructor.
380     */
381    public function __construct(Configuration $configuration)
382    {
383        $this->configuration = $configuration;
384    }
385
386    /**
387     * Executes all CREATE TABLE and CREATE INDEX statements.
388     */
389    public function createTables(string $prefix = '', ?string $schema = null): bool
390    {
391        if ($schema !== null && $schema !== '') {
392            if (!preg_match('/^[A-Za-z0-9_]+$/', $schema)) {
393                return false;
394            }
395
396            $schemaLiteral = str_replace(search: "'", replace: "''", subject: $schema);
397            $schemaIdentifier = sprintf('[%s]', str_replace(search: ']', replace: ']]', subject: $schema));
398
399            if (!$this->configuration->getDb()->query(sprintf(
400                "IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '%s') EXEC('CREATE SCHEMA %s')",
401                $schemaLiteral,
402                $schemaIdentifier,
403            ))) {
404                return false;
405            }
406        }
407
408        foreach ($this->createTableStatements as $createTableStatement) {
409            $result = $this->configuration->getDb()->query(sprintf($createTableStatement, $prefix));
410
411            if (!$result) {
412                return false;
413            }
414        }
415
416        return true;
417    }
418}