Skip to content

Commit 2a76722

Browse files
feat: add experimental feature to import record data from CSV file
1 parent e0cdd85 commit 2a76722

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+500
-71
lines changed

composer.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpmyfaq/.htaccess

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ RewriteRule admin/api/faq/delete admin/api/index.php
185185
RewriteRule admin/api/faq/permissions admin/api/index.php
186186
RewriteRule admin/api/faq/search admin/api/index.php
187187
RewriteRule admin/api/faq/sticky admin/api/index.php
188+
RewriteRule admin/api/faq/import admin/api/index.php
188189
RewriteRule admin/api/faqs admin/api/index.php
189190
RewriteRule admin/api/group/data admin/api/index.php
190191
RewriteRule admin/api/group/groups admin/api/index.php
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Stuff for importing records via csv-file
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public License,
5+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
6+
* obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* @package phpMyFAQ
9+
* @author Thorsten Rinne <[email protected]>
10+
* @author Jan Harms <[email protected]>
11+
* @copyright 2022-2024 phpMyFAQ Team
12+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
13+
* @link https://www.phpmyfaq.de
14+
* @since 2024-01-13
15+
*/
16+
17+
import { addElement } from '../../../../assets/src/utils';
18+
19+
export const handleUploadCSVForm = async () => {
20+
const submitButton = document.getElementById('submitButton');
21+
if (submitButton) {
22+
submitButton.addEventListener('click', async (event) => {
23+
const fileInput = document.getElementById('fileInputCSVUpload');
24+
const form = document.getElementById('uploadCSVFileForm');
25+
const csrf = form.getAttribute('data-pmf-csrf');
26+
const file = fileInput.files[0];
27+
event.preventDefault();
28+
const formData = new FormData();
29+
formData.append('file', file);
30+
formData.append('csrf', csrf);
31+
try {
32+
const response = await fetch('./api/faq/import', {
33+
method: 'POST',
34+
body: formData
35+
});
36+
if (response.ok) {
37+
const jsonResponse = await response.json();
38+
document.getElementById('divImportColumns').insertAdjacentElement(
39+
'beforebegin',
40+
addElement('div', {classList: 'alert alert-success alert-dismissible fade show', innerText: jsonResponse.success})
41+
);
42+
fileInput.value = null;
43+
}
44+
if (response.status === 400) {
45+
const jsonResponse = await response.json();
46+
document.getElementById('divImportColumns').insertAdjacentElement(
47+
'beforebegin',
48+
addElement('div', {classList: 'alert alert-danger alert-dismissible fade show', innerText: jsonResponse.error})
49+
);
50+
fileInput.value = null;
51+
} else {
52+
const errorResponse = await response.json();
53+
throw new Error('Network response was not ok: ' + JSON.stringify(errorResponse));
54+
}
55+
} catch (error) {
56+
if (error.storedAll === false) {
57+
console.log(error.messages);
58+
error.messages.forEach(message => {
59+
document.getElementById('divImportColumns').insertAdjacentElement(
60+
'beforebegin',
61+
addElement('div', {classList: 'alert alert-danger alert-dismissible fade show', innerText: message})
62+
);
63+
});
64+
}
65+
}
66+
});
67+
}
68+
};
69+

phpmyfaq/admin/assets/src/content/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './attachment-upload';
22
export * from './attachments';
33
export * from './category';
44
export * from './comment';
5+
export * from './csvimport';
56
export * from './editor';
67
export * from './faqs';
78
export * from './faqs.autocomplete';

phpmyfaq/admin/assets/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
renderEditor,
3939
handleStickyFaqs,
4040
handleCategoryDelete,
41+
handleUploadCSVForm,
4142
} from './content';
4243
import { handleUserList, handleUsers } from './user';
4344
import { handleGroups } from './group';
@@ -110,4 +111,6 @@ document.addEventListener('DOMContentLoaded', async () => {
110111

111112
// Configuration -> Elasticsearch configuration
112113
await handleElasticsearch();
114+
115+
await handleUploadCSVForm();
113116
});

phpmyfaq/admin/csv.import.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/**
4+
* Frontend for importing records from a csv file.
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 <[email protected]>
12+
* @copyright 2003-2024 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 2003-02-24
16+
*/
17+
use phpMyFAQ\Session\Token;
18+
use phpMyFAQ\Template\TwigWrapper;
19+
use phpMyFAQ\Translation;
20+
21+
if (!defined('IS_VALID_PHPMYFAQ')) {
22+
http_response_code(400);
23+
exit();
24+
}
25+
26+
if ($user->perm->hasPermission($user->getUserId(), 'add_faq')) {
27+
$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates');
28+
$template = $twig->loadTemplate('./admin/content/csv.import.twig');
29+
30+
$templateVars = [
31+
'adminHeaderImport' => Translation::get('msgImportRecords'),
32+
'adminHeaderCSVImport' => Translation::get('msgImportCSVFile'),
33+
'adminBodyCSVImport' => Translation::get('msgImportCSVFileBody'),
34+
'adminImportLabel' => Translation::get('ad_csv_file'),
35+
'adminCSVImport' => Translation::get('msgImport'),
36+
'adminHeaderCSVImportColumns' => Translation::get('msgColumnStructure'),
37+
'categoryId' => Translation::get('ad_categ_categ'),
38+
'question' => Translation::get('ad_entry_topic'),
39+
'answer' => Translation::get('ad_entry_content'),
40+
'keywords' => Translation::get('ad_entry_keywords'),
41+
'author' => Translation::get('ad_entry_author'),
42+
'email' => Translation::get('ad_entry_email'),
43+
'languageCode' => Translation::get('msgLanguageCode'),
44+
'seperateWithCommas' => Translation::get('msgSeperateWithCommas'),
45+
'tags' => Translation::get('ad_entry_tags'),
46+
'msgImportRecordsColumnStructure' => Translation::get('msgImportRecordsColumnStructure'),
47+
'csrfToken' => Token::getInstance()->getTokenString('importfaqs'),
48+
'is_active' => Translation::get('ad_entry_active'),
49+
'is_sticky' => Translation::get('ad_entry_sticky'),
50+
'trueFalse' => Translation::get('msgCSVImportTrueOrFalse')
51+
];
52+
echo $template->render($templateVars);
53+
} else {
54+
require 'no-permission.php';
55+
}

phpmyfaq/admin/header.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@
100100
$secLevelEntries['statistics'] .= $adminHelper->addMenuEntry('viewlog', 'searchstats', 'ad_menu_searchstats', $action);
101101
$secLevelEntries['statistics'] .= $adminHelper->addMenuEntry('reports', 'reports', 'ad_menu_reports', $action);
102102

103-
$secLevelEntries['exports'] = $adminHelper->addMenuEntry('export', 'export', 'ad_menu_export', $action);
103+
$secLevelEntries['imports_exports'] = $adminHelper->addMenuEntry('add_faq', 'importcsv', 'msgImportRecords', $action);
104+
$secLevelEntries['imports_exports'] .= $adminHelper->addMenuEntry('export', 'export', 'ad_menu_export', $action);
104105

105106
$secLevelEntries['backup'] = $adminHelper->addMenuEntry('editconfig', 'backup', 'ad_menu_backup', $action);
106107

@@ -178,6 +179,7 @@
178179
$statisticsPage = true;
179180
break;
180181
case 'export':
182+
case 'importcsv':
181183
$exportsPage = true;
182184
break;
183185
case 'backup':
@@ -355,19 +357,19 @@
355357
</div>
356358
<?php endif; ?>
357359
<!-- Exports -->
358-
<?php if ($secLevelEntries['exports'] !== '') : ?>
360+
<?php if ($secLevelEntries['imports_exports'] !== '') : ?>
359361
<a class="nav-link <?= ($exportsPage) ? '' : 'collapsed' ?>" href="#" data-bs-toggle="collapse"
360362
data-bs-target="#collapseExports" aria-expanded="false" aria-controls="collapseExports">
361363
<div class="pmf-admin-nav-link-icon">
362364
<i aria-hidden="true" class="bi bi-archive h6"></i>
363365
</div>
364-
<?= Translation::get('admin_mainmenu_exports'); ?>
366+
<?= Translation::get('admin_mainmenu_imports_exports'); ?>
365367
<div class="pmf-admin-sidenav-collapse-arrow"><i class="bi bi-arrow-down"></i></div>
366368
</a>
367369
<div class="<?= ($exportsPage) ? '' : 'collapse' ?>" id="collapseExports"
368370
aria-labelledby="headingOne" data-bs-parent="#sidenavAccordion">
369371
<nav class="pmf-admin-sidenav-menu-nested nav">
370-
<?= $secLevelEntries['exports']; ?>
372+
<?= $secLevelEntries['imports_exports']; ?>
371373
</nav>
372374
</div>
373375
<?php endif; ?>

phpmyfaq/admin/index.php

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
* @link https://www.phpmyfaq.de
1919
* @since 2002-09-16
2020
*/
21-
2221
use phpMyFAQ\Administration\AdminLog;
2322
use phpMyFAQ\Attachment\AttachmentFactory;
2423
use phpMyFAQ\Configuration;
@@ -72,10 +71,10 @@
7271
//
7372
try {
7473
Translation::create()
75-
->setLanguagesDir(PMF_TRANSLATION_DIR)
76-
->setDefaultLanguage('en')
77-
->setCurrentLanguage($faqLangCode)
78-
->setMultiByteLanguage();
74+
->setLanguagesDir(PMF_TRANSLATION_DIR)
75+
->setDefaultLanguage('en')
76+
->setCurrentLanguage($faqLangCode)
77+
->setMultiByteLanguage();
7978
} catch (Exception $e) {
8079
echo '<strong>Error:</strong> ' . $e->getMessage();
8180
}
@@ -94,8 +93,8 @@
9493
// Initialize attachment factory
9594
//
9695
AttachmentFactory::init(
97-
$faqConfig->get('records.defaultAttachmentEncKey'),
98-
$faqConfig->get('records.enableAttachmentEncryption')
96+
$faqConfig->get('records.defaultAttachmentEncKey'),
97+
$faqConfig->get('records.enableAttachmentEncryption')
9998
);
10099

101100
//
@@ -133,10 +132,10 @@
133132
$error = '';
134133
$faqusername = Filter::filterInput(INPUT_POST, 'faqusername', FILTER_SANITIZE_SPECIAL_CHARS);
135134
$faqpassword = Filter::filterInput(
136-
INPUT_POST,
137-
'faqpassword',
138-
FILTER_SANITIZE_SPECIAL_CHARS,
139-
FILTER_FLAG_NO_ENCODE_QUOTES
135+
INPUT_POST,
136+
'faqpassword',
137+
FILTER_SANITIZE_SPECIAL_CHARS,
138+
FILTER_FLAG_NO_ENCODE_QUOTES
140139
);
141140
$faqremember = Filter::filterInput(INPUT_POST, 'faqrememberme', FILTER_SANITIZE_SPECIAL_CHARS);
142141

@@ -207,10 +206,10 @@
207206
//
208207
$csrfToken = Filter::filterInput(INPUT_GET, 'csrf', FILTER_SANITIZE_SPECIAL_CHARS);
209208
if (
210-
$csrfToken &&
211-
Token::getInstance()->verifyToken('logout', $csrfToken) &&
212-
$action === 'logout' &&
213-
$user->isLoggedIn()
209+
$csrfToken &&
210+
Token::getInstance()->verifyToken('logout', $csrfToken) &&
211+
$action === 'logout' &&
212+
$user->isLoggedIn()
214213
) {
215214
$user->deleteFromSession(true);
216215
$ssoLogout = $faqConfig->get('security.ssoLogoutRedirect');
@@ -223,7 +222,7 @@
223222
//
224223
// Get current admin user and group id - default: -1
225224
//
226-
[ $currentAdminUser, $currentAdminGroups ] = CurrentUser::getCurrentUserGroupId($user);
225+
[$currentAdminUser, $currentAdminGroups] = CurrentUser::getCurrentUserGroupId($user);
227226

228227
// are we running a PMF export file request?
229228
switch ($action) {
@@ -284,6 +283,9 @@
284283
case 'stickyfaqs':
285284
require 'stickyfaqs.php';
286285
break;
286+
case 'importcsv':
287+
require 'csv.import.php';
288+
break;
287289
// functions for tags
288290
case 'tags':
289291
case 'delete-tag':
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
2+
<h1 class="h2">
3+
<i aria-hidden="true" class="bi bi-upload"></i>
4+
{{ adminHeaderImport }}
5+
</h1>
6+
</div>
7+
8+
<div class="card shadow mb-4" id="divImportColumns">
9+
<h5 class="card-header py-3">
10+
{{ adminHeaderCSVImportColumns }}
11+
</h5>
12+
<div class="card-body">
13+
<div class="row">
14+
<div class="col-md-12">
15+
<p>{{ msgImportRecordsColumnStructure }}</p>
16+
<ul>
17+
<li>{{ categoryId }} *</li>
18+
<li>{{ question }} *</li>
19+
<li>{{ answer }} *</li>
20+
<li>{{ keywords }} {{ seperateWithCommas }}</li>
21+
<li>{{ languageCode }} *</li>
22+
<li>{{ author }} *</li>
23+
<li>{{ email }} *</li>
24+
<li>{{ is_active }} {{ trueFalse }} *</li>
25+
<li>{{ is_sticky }} {{ trueFalse }} *</li>
26+
</ul>
27+
</div>
28+
</div>
29+
30+
</div>
31+
</div>
32+
33+
<div class="card shadow mb-4">
34+
<form id="uploadCSVFileForm" method="post" action="./api/faqs/import"
35+
enctype="multipart/form-data" data-pmf-csrf="{{ csrfToken }}">
36+
<h5 class="card-header py-3">
37+
{{ adminHeaderCSVImport }}
38+
</h5>
39+
<div class="card-body">
40+
<p>{{ adminBodyCSVImport }}</p>
41+
<div class="row">
42+
<label class="col-lg-4 col-form-label">{{ adminImportLabel }}:</label>
43+
<div class="col-lg-8">
44+
<input type="file" id="fileInputCSVUpload" name="userfile">
45+
</div>
46+
</div>
47+
<div class="form-row row">
48+
<div class="d-flex justify-content-center">
49+
<button class="btn btn-primary btn-lg m-2" type="submit" id="submitButton">
50+
<i aria-hidden="true" class="bi bi-upload"></i> {{ adminCSVImport }}
51+
</button>
52+
</div>
53+
</div>
54+
</div>
55+
</form>
56+
</div>
57+
58+
</div>

0 commit comments

Comments
 (0)