This commit is contained in:
287
common.php
287
common.php
@ -4,6 +4,13 @@
|
||||
|
||||
require_once __DIR__ . '/lib/text_sanitize.php';
|
||||
|
||||
/** IANA timezone for all human-readable timestamps (CET/CEST). */
|
||||
function qdb_app_timezone(): string {
|
||||
return 'Europe/Berlin';
|
||||
}
|
||||
|
||||
date_default_timezone_set(qdb_app_timezone());
|
||||
|
||||
/**
|
||||
* Values loaded from the project .env file (see qdb_load_dotenv).
|
||||
*
|
||||
@ -374,11 +381,273 @@ function rbac_client_filter(
|
||||
define('QDB_SOURCE_LANGUAGE', 'de');
|
||||
|
||||
function qdb_ensure_source_language(PDO $pdo): void {
|
||||
if (qdb_column_exists($pdo, 'language', 'enabled')) {
|
||||
$pdo->prepare("INSERT INTO language (languageCode, name, enabled) VALUES (:lc, :n, 1)
|
||||
ON CONFLICT(languageCode) DO UPDATE SET enabled = 1")
|
||||
->execute([':lc' => QDB_SOURCE_LANGUAGE, ':n' => 'German']);
|
||||
return;
|
||||
}
|
||||
$pdo->prepare("INSERT INTO language (languageCode, name) VALUES (:lc, :n)
|
||||
ON CONFLICT(languageCode) DO NOTHING")
|
||||
->execute([':lc' => QDB_SOURCE_LANGUAGE, ':n' => 'German']);
|
||||
}
|
||||
|
||||
/** @return list<array{languageCode:string,name:string,enabled:int}> */
|
||||
function qdb_load_languages(PDO $pdo): array {
|
||||
qdb_ensure_source_language($pdo);
|
||||
$hasEnabled = qdb_column_exists($pdo, 'language', 'enabled');
|
||||
if ($hasEnabled) {
|
||||
$rows = $pdo->query(
|
||||
'SELECT languageCode, name, COALESCE(enabled, 1) AS enabled
|
||||
FROM language ORDER BY languageCode'
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} else {
|
||||
$rows = $pdo->query('SELECT languageCode, name FROM language ORDER BY languageCode')
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as &$row) {
|
||||
$row['enabled'] = 1;
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
if (!array_filter($rows, fn($l) => ($l['languageCode'] ?? '') === QDB_SOURCE_LANGUAGE)) {
|
||||
array_unshift($rows, [
|
||||
'languageCode' => QDB_SOURCE_LANGUAGE,
|
||||
'name' => 'German',
|
||||
'enabled' => 1,
|
||||
]);
|
||||
}
|
||||
foreach ($rows as &$row) {
|
||||
if (($row['languageCode'] ?? '') === QDB_SOURCE_LANGUAGE) {
|
||||
$row['enabled'] = 1;
|
||||
} else {
|
||||
$row['enabled'] = (int)($row['enabled'] ?? 1) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function qdb_set_language_enabled(PDO $pdo, string $languageCode, bool $enabled): void {
|
||||
$languageCode = trim($languageCode);
|
||||
if ($languageCode === '') {
|
||||
throw new InvalidArgumentException('languageCode required');
|
||||
}
|
||||
if ($languageCode === QDB_SOURCE_LANGUAGE && !$enabled) {
|
||||
throw new InvalidArgumentException('German (de) cannot be disabled');
|
||||
}
|
||||
qdb_ensure_source_language($pdo);
|
||||
if (!qdb_column_exists($pdo, 'language', 'enabled')) {
|
||||
throw new RuntimeException('Language enablement is not available on this database');
|
||||
}
|
||||
$pdo->prepare('UPDATE language SET enabled = :en WHERE languageCode = :lc')
|
||||
->execute([':en' => $enabled ? 1 : 0, ':lc' => $languageCode]);
|
||||
if ($pdo->query('SELECT changes()')->fetchColumn() == 0) {
|
||||
throw new InvalidArgumentException('Language not found');
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<array{questionnaireID:string,languageCode:string}> */
|
||||
function qdb_questionnaire_language_disables(PDO $pdo): array {
|
||||
if (!qdb_table_exists($pdo, 'questionnaire_language_disable')) {
|
||||
return [];
|
||||
}
|
||||
return $pdo->query(
|
||||
'SELECT questionnaireID, languageCode
|
||||
FROM questionnaire_language_disable
|
||||
ORDER BY questionnaireID, languageCode'
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
function qdb_questionnaire_disabled_language_codes(PDO $pdo, string $questionnaireID): array {
|
||||
if (!qdb_table_exists($pdo, 'questionnaire_language_disable')) {
|
||||
return [];
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT languageCode FROM questionnaire_language_disable
|
||||
WHERE questionnaireID = :qid ORDER BY languageCode'
|
||||
);
|
||||
$stmt->execute([':qid' => $questionnaireID]);
|
||||
return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), 'languageCode');
|
||||
}
|
||||
|
||||
function qdb_set_questionnaire_language_disabled(
|
||||
PDO $pdo,
|
||||
string $questionnaireID,
|
||||
string $languageCode,
|
||||
bool $disabled
|
||||
): void {
|
||||
$questionnaireID = trim($questionnaireID);
|
||||
$languageCode = trim($languageCode);
|
||||
if ($questionnaireID === '' || $languageCode === '') {
|
||||
throw new InvalidArgumentException('questionnaireID and languageCode required');
|
||||
}
|
||||
if ($languageCode === QDB_SOURCE_LANGUAGE) {
|
||||
throw new InvalidArgumentException('German (de) cannot be disabled per questionnaire');
|
||||
}
|
||||
if (!qdb_table_exists($pdo, 'questionnaire_language_disable')) {
|
||||
throw new RuntimeException('Per-questionnaire language settings are not available on this database');
|
||||
}
|
||||
$exists = $pdo->prepare('SELECT 1 FROM questionnaire WHERE questionnaireID = :id LIMIT 1');
|
||||
$exists->execute([':id' => $questionnaireID]);
|
||||
if (!$exists->fetchColumn()) {
|
||||
throw new InvalidArgumentException('Questionnaire not found');
|
||||
}
|
||||
if ($disabled) {
|
||||
$pdo->prepare(
|
||||
'INSERT INTO questionnaire_language_disable (questionnaireID, languageCode)
|
||||
VALUES (:qid, :lc) ON CONFLICT(questionnaireID, languageCode) DO NOTHING'
|
||||
)->execute([':qid' => $questionnaireID, ':lc' => $languageCode]);
|
||||
return;
|
||||
}
|
||||
$pdo->prepare(
|
||||
'DELETE FROM questionnaire_language_disable
|
||||
WHERE questionnaireID = :qid AND languageCode = :lc'
|
||||
)->execute([':qid' => $questionnaireID, ':lc' => $languageCode]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Language codes selectable in the app for global UI or a questionnaire.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function qdb_available_language_codes(PDO $pdo, ?string $questionnaireID = null): array {
|
||||
$codes = [];
|
||||
foreach (qdb_load_languages($pdo) as $lang) {
|
||||
if ((int)($lang['enabled'] ?? 1) === 1) {
|
||||
$codes[] = $lang['languageCode'];
|
||||
}
|
||||
}
|
||||
if ($questionnaireID !== null && $questionnaireID !== '') {
|
||||
$disabled = qdb_questionnaire_disabled_language_codes($pdo, $questionnaireID);
|
||||
if ($disabled) {
|
||||
$blocked = array_fill_keys($disabled, true);
|
||||
$codes = array_values(array_filter($codes, fn($code) => !isset($blocked[$code])));
|
||||
}
|
||||
}
|
||||
if (!in_array(QDB_SOURCE_LANGUAGE, $codes, true)) {
|
||||
array_unshift($codes, QDB_SOURCE_LANGUAGE);
|
||||
}
|
||||
sort($codes);
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/** Delete non-source translations for app UI, one questionnaire, or everything. */
|
||||
function qdb_delete_translations_for_scope(
|
||||
PDO $pdo,
|
||||
string $scope,
|
||||
?string $questionnaireID = null,
|
||||
?string $languageCode = null
|
||||
): int {
|
||||
$scope = trim($scope);
|
||||
if (!in_array($scope, ['app', 'questionnaire', 'all'], true)) {
|
||||
throw new InvalidArgumentException('scope must be app, questionnaire, or all');
|
||||
}
|
||||
if ($scope === 'questionnaire' && trim((string)$questionnaireID) === '') {
|
||||
throw new InvalidArgumentException('questionnaireID required for questionnaire scope');
|
||||
}
|
||||
$languageCode = $languageCode !== null ? trim($languageCode) : '';
|
||||
if ($languageCode === QDB_SOURCE_LANGUAGE) {
|
||||
throw new InvalidArgumentException('German (de) source translations cannot be bulk-deleted');
|
||||
}
|
||||
|
||||
$languageCodes = [];
|
||||
if ($languageCode !== '') {
|
||||
$languageCodes = [$languageCode];
|
||||
} else {
|
||||
foreach (qdb_load_languages($pdo) as $lang) {
|
||||
if (($lang['languageCode'] ?? '') !== QDB_SOURCE_LANGUAGE) {
|
||||
$languageCodes[] = $lang['languageCode'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$languageCodes) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$deleted = 0;
|
||||
|
||||
if ($scope === 'app' || $scope === 'all') {
|
||||
$appKeys = array_column(qdb_app_ui_string_entries_for_website($pdo), 'entityId');
|
||||
if ($appKeys) {
|
||||
$ph = implode(',', array_fill(0, count($appKeys), '?'));
|
||||
$stmt = $pdo->prepare(
|
||||
"DELETE FROM string_translation
|
||||
WHERE stringKey IN ($ph) AND languageCode = :lang"
|
||||
);
|
||||
foreach ($languageCodes as $lc) {
|
||||
$stmt->execute(array_merge($appKeys, [$lc]));
|
||||
$deleted += (int)$stmt->rowCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope === 'questionnaire' || $scope === 'all') {
|
||||
$questionnaireIDs = [];
|
||||
if ($scope === 'questionnaire') {
|
||||
$questionnaireIDs = [trim((string)$questionnaireID)];
|
||||
} else {
|
||||
$questionnaireIDs = array_column(
|
||||
$pdo->query('SELECT questionnaireID FROM questionnaire ORDER BY orderIndex, name')->fetchAll(PDO::FETCH_ASSOC),
|
||||
'questionnaireID'
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($questionnaireIDs as $qnID) {
|
||||
$lists = qdb_translation_entry_lists($pdo, $qnID);
|
||||
$entries = array_merge($lists['stringEntries'], $lists['contentEntries']);
|
||||
$questionIDs = [];
|
||||
$answerOptionIDs = [];
|
||||
$stringKeys = [];
|
||||
foreach ($entries as $entry) {
|
||||
if (($entry['type'] ?? '') === 'question') {
|
||||
$questionIDs[] = $entry['entityId'];
|
||||
} elseif (($entry['type'] ?? '') === 'answer_option') {
|
||||
$answerOptionIDs[] = $entry['entityId'];
|
||||
} elseif (($entry['type'] ?? '') === 'string') {
|
||||
$stringKeys[] = $entry['entityId'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($questionIDs) {
|
||||
$ph = implode(',', array_fill(0, count($questionIDs), '?'));
|
||||
$stmt = $pdo->prepare(
|
||||
"DELETE FROM question_translation
|
||||
WHERE questionID IN ($ph) AND languageCode = ?"
|
||||
);
|
||||
foreach ($languageCodes as $lc) {
|
||||
$stmt->execute(array_merge($questionIDs, [$lc]));
|
||||
$deleted += (int)$stmt->rowCount();
|
||||
}
|
||||
}
|
||||
if ($answerOptionIDs) {
|
||||
$ph = implode(',', array_fill(0, count($answerOptionIDs), '?'));
|
||||
$stmt = $pdo->prepare(
|
||||
"DELETE FROM answer_option_translation
|
||||
WHERE answerOptionID IN ($ph) AND languageCode = ?"
|
||||
);
|
||||
foreach ($languageCodes as $lc) {
|
||||
$stmt->execute(array_merge($answerOptionIDs, [$lc]));
|
||||
$deleted += (int)$stmt->rowCount();
|
||||
}
|
||||
}
|
||||
if ($stringKeys) {
|
||||
$ph = implode(',', array_fill(0, count($stringKeys), '?'));
|
||||
$stmt = $pdo->prepare(
|
||||
"DELETE FROM string_translation
|
||||
WHERE stringKey IN ($ph) AND languageCode = ?"
|
||||
);
|
||||
foreach ($languageCodes as $lc) {
|
||||
$stmt->execute(array_merge($stringKeys, [$lc]));
|
||||
$deleted += (int)$stmt->rowCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
function qdb_upsert_source_translation(PDO $pdo, string $type, string $id, string $text): void {
|
||||
qdb_put_translation($pdo, $type, $id, QDB_SOURCE_LANGUAGE, $text);
|
||||
}
|
||||
@ -972,6 +1241,21 @@ function qdb_validate_stable_key(string $s, string $label = 'Key'): string {
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/** Question keys omitted from results tables and CSV exports (e.g. last-page warnings). */
|
||||
function qdb_results_excluded_question_keys(): array {
|
||||
return ['data_final_warning'];
|
||||
}
|
||||
|
||||
/** Whether a question row should be hidden from results tables and CSV exports. */
|
||||
function qdb_question_is_results_excluded(array $qRow): bool {
|
||||
if (($qRow['type'] ?? '') === 'last_page') {
|
||||
return true;
|
||||
}
|
||||
$cfg = json_decode($qRow['configJson'] ?? '{}', true) ?: [];
|
||||
$key = qdb_question_key($cfg, (string)($qRow['defaultText'] ?? ''));
|
||||
return in_array($key, qdb_results_excluded_question_keys(), true);
|
||||
}
|
||||
|
||||
/** Resolve app lookup key for a question row. */
|
||||
function qdb_question_key(array $config, string $defaultText): string {
|
||||
$key = trim((string)($config['questionKey'] ?? ''));
|
||||
@ -1078,6 +1362,9 @@ function qdb_results_export_columns(array $questions, string $qnID): array
|
||||
{
|
||||
$columns = [];
|
||||
foreach ($questions as $q) {
|
||||
if (qdb_question_is_results_excluded($q)) {
|
||||
continue;
|
||||
}
|
||||
$cfg = json_decode($q['configJson'] ?? '{}', true) ?: [];
|
||||
$type = $q['type'] ?? '';
|
||||
if ($type === 'glass_scale_question') {
|
||||
|
||||
Reference in New Issue
Block a user