CVE-2022-3608 (PHP, published 2022-10-19T13:15Z)
Description: value: Cross-site Scripting (XSS) - Stored in GitHub repository thorsten/phpmyfaq prior to 3.2.0-alpha.
Severity: nan (CVSS v3 base score: 8.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/thorsten/phpmyfaq
Files changed:
CHANGELOG.md
composer.json
composer.lock
phpmyfaq/admin/backup.export.php
phpmyfaq/admin/backup.import.php
phpmyfaq/assets/themes/default/scss/_mixins.scss
phpmyfaq/assets/themes/default/scss/_theme.scss
phpmyfaq/assets/themes/default/scss/layout/_startpage.scss
phpmyfaq/setup/update.php
phpmyfaq/src/phpMyFAQ/Backup.php
phpmyfaq/src/phpMyFAQ/Database.php
phpmyfaq/src/phpMyFAQ/Database/DatabaseHelper.php
phpmyfaq/src/phpMyFAQ/Export.php
phpmyfaq/src/phpMyFAQ/Filesystem.php
phpmyfaq/src/phpMyFAQ/Helper/CategoryHelper.php
phpmyfaq/src/phpMyFAQ/Instance/Database/Mysqli.php
phpmyfaq/src/phpMyFAQ/Instance/Database/Pgsql.php
phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlite3.php
phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlsrv.php
phpmyfaq/src/phpMyFAQ/Instance/Setup.php
tests/phpMyFAQ/ApiTest.php
tests/phpMyFAQ/BackupTest.php
tests/phpMyFAQ/Database/DatabaseHelperTest.php
Commit message: feat(backup): added verification of backup files


Vulnerable code:
    "content-hash": "dba4f03bccb10ea790582b909d306ac5",
            "version": "v7.17.0",
                "url": "https://github.com/elastic/elasticsearch-php.git",
                "reference": "1890f9d7fde076b5a3ddcf579a802af05b2e781b"
                "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/1890f9d7fde076b5a3ddcf579a802af05b2e781b",
                "reference": "1890f9d7fde076b5a3ddcf579a802af05b2e781b",
            "support": {
                "issues": "https://github.com/elastic/elasticsearch-php/issues",
                "source": "https://github.com/elastic/elasticsearch-php/tree/v7.17.0"
            },
            "time": "2022-02-03T13:40:04+00:00"
    $tables = $tableNames = $faqConfig->getDb()->getTableNames(Database::getTablePrefix());
    $tablePrefix = (Database::getTablePrefix() !== '') ? Database::getTablePrefix() . '.phpmyfaq' : 'phpmyfaq';
    $majorVersion = substr($faqConfig->getVersion(), 0, 3);
    $text[] = '-- pmf' . $majorVersion . ': ' . $tableNames;
    $text[] = '-- DO NOT REMOVE THE FIRST LINE!';
    $text[] = '-- pmftableprefix: ' . Database::getTablePrefix();
    $text[] = '-- DO NOT REMOVE THE LINES ABOVE!';
    $text[] = '-- Otherwise this backup will be broken.';

            $header = sprintf(
                'Content-Disposition: attachment; filename=%s',
                urlencode(
                    sprintf(
                        '%s-data.%s.sql',
                        $tablePrefix,
                        date('Y-m-d-H-i-s')
                    )
                )
            );
            $httpHelper->addExtraHeader($header);
            foreach (explode(' ', $tableNames) as $table) {
                echo implode("\r\n", $text);
                if ('' !== $table) {
                    $text = $dbHelper->buildInsertQueries('SELECT * FROM ' . $table, $table);
                }
            $header = sprintf(
                'Content-Disposition: attachment; filename=%s',
                urlencode(
                    sprintf(
                        '%s-logs.%s.sql',
                        $tablePrefix,
                        date('Y-m-d-H-i-s')
                    )
                )
            );
            $httpHelper->addExtraHeader($header);
            foreach (explode(' ', $tableNames) as $table) {
                echo implode("\r\n", $text);
                if ('' !== $table) {
                    $text = $dbHelper->buildInsertQueries('SELECT * FROM ' . $table, $table);
                }
            <?= $PMF_LANG['ad_csv_rest'] ?>
                $PMF_LANG['ad_csv_no'],
            printf("<p>%s</p>\n", $PMF_LANG['ad_csv_prepare']);
            printf("<p>%s</p>\n", $PMF_LANG['ad_csv_process']);
                $PMF_LANG['ad_csv_of'],
                $PMF_LANG['ad_csv_suc']
    echo $PMF_LANG['err_NotAuth'];
        $faqConfig->add('security.enableSignInWithMicrosoft', false);
    public static function errorPage(string $method)
    public static function setTablePrefix(string $tablePrefix)
 * @package phpMyFAQ
 * @author Thorsten Rinne <thorsten@phpmyfaq.de>
 * @author Matteo Scaramuccia <matteo@phpmyfaq.de>
 * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
 * @link https://www.phpmyfaq.de
 * @since 2012-04-12
    /**
     * @var Configuration
     */
    private $config;
     * @return array
    /** @var Faq */
    protected $faq = null;
    /** @var Category */
    protected $category = null;
    /** @var Configuration */
    protected $config = null;
    public static function create(Faq $faq, Category $category, Configuration $config, string $mode = 'pdf')
        switch ($mode) {
            case 'json':
                return new Json($faq, $category, $config);
            case 'pdf':
                return new Pdf($faq, $category, $config);
            case 'html5':
                return new Html5($faq, $category, $config);
            default:
                throw new Exception('Export not implemented!');
        }
    private $rootPath;
    private $path;
    private $folders = [];
    public function __construct($rootPath = '')
            $this->rootPath = dirname(dirname(__DIR__));
    public function createDirectory(string $pathname, $mode = 0777, $recursive = false): bool
        $key = 1;
            $decks .= '<div class="card mb-4"><a href="' . $category['url'] . '">';
                $category['name'] . '" src="' . $category['image'] . '" />';
            $decks .= '</a>' .
                '<div class="card-body">' .
                '<h4 class="card-title text-center">' .
                '<a href="' . $category['url'] . '">' . $category['name'] . '</a>' .
                '</h4>' .
                '<p class="card-text">' . $category['description'] . '</p>' .
                '</div>' .
                '</div>';
            if ($key % 2 === 0) {
                $decks .= '<div class="w-100 d-none d-sm-block d-md-none"></div>';
            }
            if ($key % 3 === 0) {
                $decks .= '<div class="w-100 d-none d-md-block d-lg-none"></div>';
            }
            if ($key % 4 === 0) {
                $decks .= '<div class="w-100 d-none d-lg-block d-xl-block"></div>';
            }
            $key++;
    private $rootDir;
    public function setRootDir($rootDir)
     * @return array
    public function checkDirs(array $dirs)
     * @param  array  $data   Array with database credentials
     * @param  string $folder Folder
    public function createDatabaseFile(array $data, $folder = '/config')
     * @param  array  $data   Array with LDAP credentials
     * @param  string $folder Folder
    public function createLdapFile(array $data, string $folder = '/config')
     * @param  array  $data   Array with LDAP credentials
     * @param  string $folder Folder
    public function createElasticsearchFile(array $data, string $folder = '/config')


Fixed code:
- added verification of backup files (Thorsten)
    "ext-sodium": "*",
    "content-hash": "c4432f3947b3f562463e6633dd744fe6",
            "version": "v7.17.1",
                "url": "git@github.com:elastic/elasticsearch-php.git",
                "reference": "f1b8918f411b837ce5f6325e829a73518fd50367"
                "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/f1b8918f411b837ce5f6325e829a73518fd50367",
                "reference": "f1b8918f411b837ce5f6325e829a73518fd50367",
            "time": "2022-09-30T12:28:55+00:00"
        "ext-sodium": "*",
use phpMyFAQ\Backup;
    $tables = $faqConfig->getDb()->getTableNames(Database::getTablePrefix());

    $backup = new Backup($faqConfig, $dbHelper);

            $backupQueries = $backup->generateBackupQueries($tableNames);
            try {
                $backupFileName = $backup->createBackup(Backup::BACKUP_TYPE_DATA, $backupQueries);
                $header = sprintf('Content-Disposition: attachment; filename=%s', urlencode($backupFileName));
                $httpHelper->addExtraHeader($header);

                echo $backupQueries;
            } catch (SodiumException $e) {
                // Handle exception
            $backupQueries = $backup->generateBackupQueries($tableNames);
            try {
                $backupFileName = $backup->createBackup(Backup::BACKUP_TYPE_LOGS, $backupQueries);
                $header = sprintf('Content-Disposition: attachment; filename=%s', urlencode($backupFileName));
                $httpHelper->addExtraHeader($header);

                echo $backupQueries;
            } catch (SodiumException $e) {
                // Handle exception
use phpMyFAQ\Backup;
use phpMyFAQ\Translation;
            <?= Translation::get('ad_csv_rest') ?>
        $dbHelper = new DatabaseHelper($faqConfig);
        $backup = new Backup($faqConfig, $dbHelper);


        $fileName = $_FILES['userfile']['name'];

        try {
            $verification = $backup->verifyBackup(file_get_contents($_FILES['userfile']['tmp_name']), $fileName);
            if ($verification) {
                $ok = 1;
            } else {
                $ok = 0;
            }
        } catch (SodiumException $e) {
            echo 'This file cannot be verified.<br>';
            $ok = 0;
        }

                Translation::get('ad_csv_no'),
            printf("<p>%s</p>\n", Translation::get('ad_csv_prepare'));

            printf("<p>%s</p>\n", Translation::get('ad_csv_process'));


                Translation::get('ad_csv_of'),
                Translation::get('ad_csv_suc')
    echo Translation::get('err_NotAuth');
@mixin text-shadow($string: 0 1px 3px rgba(0, 0, 0, 0.25)) {
  text-shadow: $string;
}
@mixin box-shadow($string) {
  -webkit-box-shadow: $string;
  -moz-box-shadow: $string;
  box-shadow: $string;
}

@mixin box-sizing($type: border-box) {
  -webkit-box-sizing: $type;
  -moz-box-sizing: $type;
  box-sizing: $type;
}

@mixin border-radius($radius: 5px) {
  -webkit-border-radius: $radius;
  -moz-border-radius: $radius;
  -ms-border-radius: $radius;
  -o-border-radius: $radius;
  border-radius: $radius;

  -moz-background-clip: padding;
  -webkit-background-clip: padding-box;
  background-clip: padding-box;
}
@mixin border-radiuses($topright: 0, $bottomright: 0, $bottomleft: 0, $topleft: 0) {
  -webkit-border-top-right-radius: $topright;
  -webkit-border-bottom-right-radius: $bottomright;
  -webkit-border-bottom-left-radius: $bottomleft;
  -webkit-border-top-left-radius: $topleft;

  -moz-border-radius-topright: $topright;
  -moz-border-radius-bottomright: $bottomright;
  -moz-border-radius-bottomleft: $bottomleft;
  -moz-border-radius-topleft: $topleft;

  border-top-right-radius: $topright;
  border-bottom-right-radius: $bottomright;
  border-bottom-left-radius: $bottomleft;
  border-top-left-radius: $topleft;

  -moz-background-clip: padding;
  -webkit-background-clip: padding-box;
  background-clip: padding-box;
}

@mixin opacity($opacity: 0.5) {
  -webkit-opacity: $opacity;
  -moz-opacity: $opacity;
  opacity: $opacity;
}

@mixin gradient($startColor: #eee, $endColor: white) {
  background-color: $startColor;
  background: -webkit-gradient(linear, left top, left bottom, from($startColor), to($endColor));
  background: -webkit-linear-gradient(top, $startColor, $endColor);
  background: -moz-linear-gradient(top, $startColor, $endColor);
  background: -ms-linear-gradient(top, $startColor, $endColor);
  background: -o-linear-gradient(top, $startColor, $endColor);
}
@mixin horizontal-gradient($startColor: #eee, $endColor: white) {
  background-color: $startColor;
  background-image: -webkit-gradient(linear, left top, right top, from($startColor), to($endColor));
  background-image: -webkit-linear-gradient(left, $startColor, $endColor);
  background-image: -moz-linear-gradient(left, $startColor, $endColor);
  background-image: -ms-linear-gradient(left, $startColor, $endColor);
  background-image: -o-linear-gradient(left, $startColor, $endColor);
}

@mixin animation($name, $duration: 300ms, $delay: 0, $ease: ease) {
  -webkit-animation: $name $duration $delay $ease;
  -moz-animation: $name $duration $delay $ease;
  -ms-animation: $name $duration $delay $ease;
}

@mixin transition($transition) {
  -webkit-transition: $transition;
  -moz-transition: $transition;
  -ms-transition: $transition;
  -o-transition: $transition;
}
@mixin transform($string) {
  -webkit-transform: $string;
  -moz-transform: $string;
  -ms-transform: $string;
  -o-transform: $string;
}
@mixin scale($factor) {
  -webkit-transform: scale($factor);
  -moz-transform: scale($factor);
  -ms-transform: scale($factor);
  -o-transform: scale($factor);
}
@mixin rotate($deg) {
  -webkit-transform: rotate($deg);
  -moz-transform: rotate($deg);
  -ms-transform: rotate($deg);
  -o-transform: rotate($deg);
}
@mixin skew($deg, $deg2) {
  -webkit-transform: skew($deg, $deg2);
  -moz-transform: skew($deg, $deg2);
  -ms-transform: skew($deg, $deg2);
  -o-transform: skew($deg, $deg2);
}
@mixin translate($x, $y: 0) {
  -webkit-transform: translate($x, $y);
  -moz-transform: translate($x, $y);
  -ms-transform: translate($x, $y);
  -o-transform: translate($x, $y);
}
@mixin translate3d($x, $y: 0, $z: 0) {
  -webkit-transform: translate3d($x, $y, $z);
  -moz-transform: translate3d($x, $y, $z);
  -ms-transform: translate3d($x, $y, $z);
  -o-transform: translate3d($x, $y, $z);
}
@mixin perspective($value: 1000) {
  -webkit-perspective: $value;
  -moz-perspective: $value;
  -ms-perspective: $value;
  perspective: $value;
}
@mixin transform-origin($x: center, $y: center) {
  -webkit-transform-origin: $x $y;
  -moz-transform-origin: $x $y;
  -ms-transform-origin: $x $y;
  -o-transform-origin: $x $y;
}

@mixin reset-box-sizing($size: content-box) {
  &,
  *,
  *:before,
  *:after {
    @include box-sizing($size);
  }
}

@mixin truncate($max-width: 250px) {
  max-width: $max-width;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@mixin background-size($string: contain) {
  -webkit-background-size: $string;
  -moz-background-size: $string;
  -o-background-size: $string;
  background-size: $string;
}

@mixin placeholder($color: #999) {
  &::-webkit-input-placeholder {
    /* WebKit browsers */
    color: $color;
  }
  &:-moz-placeholder {
    /* Mozilla Firefox 4 to 18 */
    color: $color;
  }
  &::-moz-placeholder {
    /* Mozilla Firefox 19+ */
    color: $color;
  }
  &:-ms-input-placeholder {
    /* Internet Explorer 10+ */
    color: $color;
  }
}
$color-primary: #fd7e14;
$color-green: #75c181;
$color-red: #f77b6b;
$color-blue: #58bbee;
$color-orange: #f88c30;
$color-pink: #ea5395;
$color-purple: #8a40a7;

$text-color: #494d55;
$text-color-secondary: lighten($text-color, 10%);
$text-grey: lighten($text-color-secondary, 25%);

$grey: lighten($text-color-secondary, 25%);
$light-grey: #c3c3c3;
$dark-grey: #666;
$black: #000;
$smoky-white: #f5f5f5;
$smoky-grey: #f9f9fb;
$divider: #f0f0f0;

$new: #60a823;
$error: #e65348;
$facebook: #3b5998;
$twitter: #55acee;
$google: #dd4b39;
$github: #444;

        $faqConfig->add('security.enableSignInWithMicrosoft', false);

        // New backup
        $query[] = 'CREATE TABLE ' . $prefix . 'faqbackup (
            id INT(11) NOT NULL,
            filename VARCHAR(255) NOT NULL,
            authkey VARCHAR(255) NOT NULL,
            authcode VARCHAR(255) NOT NULL,
            created timestamp NOT NULL,
            PRIMARY KEY (id))';

<?php

/**
 * Provides methods for phpMyFAQ backups
 *
 * This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
 * obtain one at https://mozilla.org/MPL/2.0/.
 *
 * @package   phpMyFAQ
 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
 * @copyright 2022 phpMyFAQ Team
 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
 * @link      https://www.phpmyfaq.de
 * @since     2022-10-08
 */

namespace phpMyFAQ;

use phpMyFAQ\Core\Exception;
use phpMyFAQ\Database\DatabaseHelper;
use SodiumException;

/**
 * Class Backup
 *
 * @package phpMyFAQ
 */
class Backup
{
    public const BACKUP_TYPE_DATA = 'data';
    public const BACKUP_TYPE_LOGS = 'logs';

    /** @var Configuration */
    private Configuration $config;

    /** @var DatabaseHelper */
    private DatabaseHelper $databaseHelper;

    /**
     * Constructor.
     *
     * @param Configuration  $config
     * @param DatabaseHelper $databaseHelper
     */
    public function __construct(Configuration $config, DatabaseHelper $databaseHelper)
    {
        $this->config = $config;
        $this->databaseHelper = $databaseHelper;
    }

    /**
     * @param string $backupType
     * @param string $backupFile
     * @return string
     * @throws SodiumException
     */
    public function createBackup(string $backupType, string $backupFile): string
    {
        $backupDate = date('Y-m-d-H-i-s');

        $fileNamePrefix = (Database::getTablePrefix() !== '') ? Database::getTablePrefix() . '.phpmyfaq' : 'phpmyfaq';
        $fileName = sprintf('%s-%s.%s.sql', $fileNamePrefix, $backupType, $backupDate);

        $authKey = sodium_crypto_auth_keygen();
        $authCode = sodium_crypto_auth($backupFile, $authKey);

        $query = sprintf(
            "INSERT INTO %sfaqbackup (id, filename, authkey, authcode, created) VALUES (%d, '%s', '%s', '%s', '%s')",
            Database::getTablePrefix(),
            $this->config->getDb()->nextId(Database::getTablePrefix() . 'faqbackup', 'id'),
            $this->config->getDb()->escape($fileName),
            $this->config->getDb()->escape(sodium_bin2hex($authKey)),
            $this->config->getDb()->escape(sodium_bin2hex($authCode)),
            $backupDate
        );

        $this->config->getDb()->query($query);

        return $fileName;
    }

    /**
     * @param string $backup
     * @param string $backupFileName
     * @return bool
     * @throws SodiumException
     */
    public function verifyBackup(string $backup, string $backupFileName): bool
    {
        $query = sprintf(
            "SELECT id, filename, authkey, authcode, created FROM %sfaqbackup WHERE filename = '%s'",
            Database::getTablePrefix(),
            $this->config->getDb()->escape($backupFileName),
        );

        $result = $this->config->getDb()->query($query);

        if ($this->config->getDb()->numRows($result) > 0) {
            $row = $this->config->getDb()->fetchObject($result);

            return sodium_crypto_auth_verify(
                sodium_hex2bin($row->authcode),
                $backup,
                sodium_hex2bin($row->authkey)
            );
        }

        return false;
    }

    /**
     * @param string $tableNames
     * @return string
     */
    public function generateBackupQueries(string $tableNames): string
    {
        $backup = implode("\r\n", $this->getBackupHeader($tableNames));

        foreach (explode(' ', $tableNames) as $table) {
            if ('' !== $table) {
                $backup .= implode(
                    "\r\n",
                    $this->databaseHelper->buildInsertQueries('SELECT * FROM ' . $table, $table)
                );
            }
        }

        return $backup;
    }

    /**
     * Returns the backup file header
     * @param string $tableNames
     * @return string[]
     */
    private function getBackupHeader(string $tableNames): array
    {
        return [
            sprintf('-- pmf%s: %s', substr($this->config->getVersion(), 0, 3), $tableNames),
            '-- DO NOT REMOVE THE FIRST LINE!',
            '-- pmftableprefix: ' . Database::getTablePrefix(),
            '-- DO NOT REMOVE THE LINES ABOVE!',
            '-- Otherwise this backup will be broken.'
        ];
    }
}
    public static function errorPage(string $method): void
                <link href="assets/dist/styles.css" rel="stylesheet">
                <script src="assets/dist/frontend.js"></script>
    public static function setTablePrefix(string $tablePrefix): void
 * @package   phpMyFAQ
 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
 * @author    Matteo Scaramuccia <matteo@phpmyfaq.de>
 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
 * @link      https://www.phpmyfaq.de
 * @since     2012-04-12
    /** @var Configuration */
    private Configuration $config;
     * @return string[]
                if (is_int($key)) {
                    continue; // Fix for SQLite3
                }
    /** @var Faq|null */
    protected ?Faq $faq = null;
    /** @var Category|null */
    protected ?Category $category = null;
    /** @var Configuration|null */
    protected ?Configuration $config = null;
    public static function create(Faq $faq, Category $category, Configuration $config, string $mode = 'pdf'): mixed
        return match ($mode) {
            'json' => new Json($faq, $category, $config),
            'pdf' => new Pdf($faq, $category, $config),
            'html5' => new Html5($faq, $category, $config),
            default => throw new Exception('Export not implemented!'),
        };
    private string $rootPath;
    private string $path;
    private array $folders = [];
    public function __construct(string $rootPath = '')
            $this->rootPath = dirname(__DIR__, 2);
    public function createDirectory(string $pathname, int $mode = 0777, bool $recursive = false): bool
            $decks .= '<div class="phpmyfaq-startpage-item phpmyfaq-startpage-item-primary col-lg-4 col-6">';
            $decks .= ' <div class="phpmyfaq-startpage-item-inner">';
            $decks .= '   <div class="icon-holder">';
                    $category['name'] . '" src="' . $category['image'] . '" />';
            $decks .= '     </div>';
            $decks .= '     <h3 class="title">' . $category['name'] . '</h3>';
            $decks .= '     <p class="intro">' . $category['description'] . '</p>';
            $decks .= '     <a class="link" href="' . $category['url'] . '"><span></span></a>';
            $decks .= ' </div>';
            $decks .= '</div>';
        'faqbackup' => 'CREATE TABLE %sfaqbackup (
            id INT(11) NOT NULL,
            filename VARCHAR(255) NOT NULL,
            authkey VARCHAR(255) NOT NULL,
            authcode VARCHAR(255) NOT NULL,
            created timestamp NOT NULL,
            PRIMARY KEY (id))',

        'faqbackup' => 'CREATE TABLE %sfaqbackup (
            id INT(11) NOT NULL,
            filename VARCHAR(255) NOT NULL,
            authkey VARCHAR(255) NOT NULL,
            authcode VARCHAR(255) NOT NULL,
            created timestamp NOT NULL,
            PRIMARY KEY (id))',

        'faqbackup' => 'CREATE TABLE %sfaqbackup (
            id INT(11) NOT NULL,
            filename VARCHAR(255) NOT NULL,
            authkey VARCHAR(255) NOT NULL,
            authcode VARCHAR(255) NOT NULL,
            created timestamp NOT NULL,
            PRIMARY KEY (id))',

        'faqbackup' => 'CREATE TABLE %sfaqbackup (
            id INT(11) NOT NULL,
            filename VARCHAR(255) NOT NULL,
            authkey VARCHAR(255) NOT NULL,
            authcode VARCHAR(255) NOT NULL,
            created timestamp NOT NULL,
            PRIMARY KEY (id))',

    private string $rootDir;
    public function setRootDir(string $rootDir): void
     * @return string[]
    public function checkDirs(array $dirs): array
     * @param  int[]|string[] $data   Array with database credentials
     * @param  string         $folder Folder
    public function createDatabaseFile(array $data, string $folder = '/config'): int|bool
     * @param  int[]|string[] $data   Array with LDAP credentials
     * @param  string         $folder Folder
    public function createLdapFile(array $data, string $folder = '/config'): int|bool
     * @param  int[]|string[] $data   Array with Elasticsearch credentials
     * @param  string         $folder Folder
    public function createElasticsearchFile(array $data, string $folder = '/config'): int|bool
     * @throws Core\Exception
     * @throws Core\Exception
<?php

namespace phpMyFAQ;

use phpMyFAQ\Database\DatabaseHelper;
use phpMyFAQ\Database\Sqlite3;
use PHPUnit\Framework\TestCase;
use SodiumException;

/**
 * Class BackupTest
 *
 * @testdox Backup should
 * @package phpMyFAQ
 */
class BackupTest extends TestCase
{
    /** @var Configuration */
    protected Configuration $configuration;

    /** @var DatabaseHelper */
    protected DatabaseHelper $databaseHelper;

    /** @var Backup */
    protected Backup $backup;

    protected function setUp(): void
    {
        parent::setUp();

        $dbHandle = new Sqlite3();
        $dbHandle->connect(PMF_TEST_DIR . '/test.db', '', '');

        $this->configuration = new Configuration($dbHandle);
        $this->configuration->config['main.currentVersion'] = System::getVersion();

        $this->databaseHelper = new DatabaseHelper($this->configuration);

        $this->backup = new Backup($this->configuration, $this->databaseHelper);
    }

    /**
     * @testdox create a complete backup file
     * @throws SodiumException
     */
    public function testCreateBackup(): void
    {
        $tableNames = 'faqconfig faqinstances';
        $backupQueries = $this->backup->generateBackupQueries($tableNames);
        $dataBackup = $this->backup->createBackup(Backup::BACKUP_TYPE_DATA, $backupQueries);
        $expected = 'phpmyfaq-data.' . date('Y-m-d-H-i-s') . '.sql';

        $this->assertEquals($expected, $dataBackup);

        $tableNames = 'faqadminlog faqsessions';
        $backupQueries = $this->backup->generateBackupQueries($tableNames);
        $logsBackup = $this->backup->createBackup(Backup::BACKUP_TYPE_LOGS, $backupQueries);
        $expected = 'phpmyfaq-logs.' . date('Y-m-d-H-i-s') . '.sql';

        $this->assertEquals($expected, $logsBackup);
    }

    /**
     * @throws SodiumException
     */
    public function testVerifyBackup(): void
    {
        $tableNames = 'faqconfig faqinstances';
        $backupQueries = $this->backup->generateBackupQueries($tableNames);
        $dataBackup = $this->backup->createBackup(Backup::BACKUP_TYPE_DATA, $backupQueries);

        $result = $this->backup->verifyBackup($backupQueries, $dataBackup);

        $this->assertTrue($result);
    }

    /**
     * @testdox generates correct INSERT queries for the backup
     */
    public function testGenerateBackupQueries(): void
    {
        $tableNames = 'faqconfig faqinstances';
        $queries = $this->backup->generateBackupQueries($tableNames);

        $this->assertStringContainsString('DO NOT REMOVE THE FIRST LINE!', $queries);
    }
}
<?php

namespace phpMyFAQ\Database;

use phpMyFAQ\Configuration;
use phpMyFAQ\System;
use PHPUnit\Framework\TestCase;

/**
 * Class DatabaseHelperTest
 *
 * @testdox DatabaseHelper should
 * @package phpMyFAQ
 */
class DatabaseHelperTest extends TestCase
{
    /** @var DatabaseHelper */
    private DatabaseHelper $databaseHelper;

    protected function setUp(): void
    {
        parent::setUp();

        $dbHandle = new Sqlite3();
        $dbHandle->connect(PMF_TEST_DIR . '/test.db', '', '');
        $dbHandle->query(
            'CREATE TABLE faqtest (name VARCHAR(255) NOT NULL, testvalue VARCHAR(255) DEFAULT NULL, PRIMARY KEY (name))'
        );
        $dbHandle->query("INSERT INTO faqtest (name,testvalue) VALUES ('foo','bar')");
        $dbHandle->query("INSERT INTO faqtest (name,testvalue) VALUES ('bar','baz')");

        $configuration = new Configuration($dbHandle);
        $configuration->config['main.currentVersion'] = System::getVersion();

        $this->databaseHelper = new DatabaseHelper($configuration);
    }

    protected function tearDown(): void
    {
        parent::tearDown();

        $dbHandle = new Sqlite3();
        $dbHandle->connect(PMF_TEST_DIR . '/test.db', '', '');
        $dbHandle->query('DROP TABLE faqtest');
    }

    /**
     * @testdox create the correct INSERT queries
     */
    public function testBuildInsertQueries(): void
    {
        $table = 'faqtest';
        $queries = $this->databaseHelper->buildInsertQueries('SELECT * FROM ' . $table, $table);

        $expected = [
            "\r\n-- Table: faqtest",
            "INSERT INTO faqtest (name,testvalue) VALUES ('foo','bar');",
            "INSERT INTO faqtest (name,testvalue) VALUES ('bar','baz');"
        ];

        $this->assertEquals($expected, $queries);
    }
}