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