Skip to content

Commit dcd037e

Browse files
committed
feat(api): added backup for authenticated users via API, closes #2891
1 parent cc39fc1 commit dcd037e

File tree

7 files changed

+207
-29
lines changed

7 files changed

+207
-29
lines changed

docs/openapi.yaml

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,60 @@ paths:
4949
application/json:
5050
schema: {}
5151
example: []
52+
'/api/v3.0/backup/{type}':
53+
get:
54+
tags:
55+
- 'Endpoints with Authentication'
56+
operationId: createBackup
57+
parameters:
58+
- name: type
59+
in: path
60+
description: 'The backup type. Can be "data" or "logs".'
61+
required: true
62+
schema:
63+
type: string
64+
responses:
65+
'200':
66+
description: 'The current backup as a file.'
67+
headers:
68+
Accept-Language:
69+
description: 'The language code for the login.'
70+
schema:
71+
type: string
72+
x-pmf-token:
73+
description: 'phpMyFAQ client API Token, generated in admin backend'
74+
schema:
75+
type: string
76+
content:
77+
application/octet-stream:
78+
schema:
79+
type: string
80+
'400':
81+
description: 'If the backup type is wrong'
82+
headers:
83+
Accept-Language:
84+
description: 'The language code for the login.'
85+
schema:
86+
type: string
87+
x-pmf-token:
88+
description: 'phpMyFAQ client API Token, generated in admin backend'
89+
schema:
90+
type: string
91+
content:
92+
application/octet-stream:
93+
schema:
94+
type: string
95+
'401':
96+
description: 'If the user is not authenticated and/or does not have sufficient permissions.'
97+
headers:
98+
Accept-Language:
99+
description: 'The language code for the login.'
100+
schema:
101+
type: string
102+
x-pmf-token:
103+
description: 'phpMyFAQ client API Token, generated in admin backend'
104+
schema:
105+
type: string
52106
/api/v3.0/categories:
53107
get:
54108
tags:
@@ -820,14 +874,11 @@ paths:
820874
application/json:
821875
schema:
822876
required:
823-
- language
824877
- category-id
825878
- question
826879
- author
827880
- email
828881
properties:
829-
language:
830-
type: string
831882
category-id:
832883
type: integer
833884
question:
@@ -837,7 +888,7 @@ paths:
837888
email:
838889
type: string
839890
type: object
840-
example: "{\n \"language\": \"de\",\n \"category-id\": \"1\",\n \"question\": \"Is this the world we created?\",\n \"author\": \"Freddie Mercury\",\n \"email\": \"[email protected]\"\n }"
891+
example: "{\n \"category-id\": \"1\",\n \"question\": \"Is this the world we created?\",\n \"author\": \"Freddie Mercury\",\n \"email\": \"[email protected]\"\n }"
841892
responses:
842893
'201':
843894
description: 'Used to add a new question in one existing category.'

phpmyfaq/admin/backup.export.php

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,33 +50,15 @@
5050
$user = CurrentUser::getCurrentUser($faqConfig);
5151

5252
if ($user->perm->hasPermission($user->getUserId(), PermissionType::BACKUP->value)) {
53-
$tables = $faqConfig->getDb()->getTableNames(Database::getTablePrefix());
54-
$tableNames = '';
55-
5653
$dbHelper = new DatabaseHelper($faqConfig);
5754
$backup = new Backup($faqConfig, $dbHelper);
5855

5956
switch ($action) {
6057
case 'backup_content':
61-
foreach ($tables as $table) {
62-
if (
63-
(Database::getTablePrefix() . 'faqadminlog' === trim((string) $table)) || (Database::getTablePrefix(
64-
) . 'faqsessions' === trim((string) $table))
65-
) {
66-
continue;
67-
}
68-
$tableNames .= $table . ' ';
69-
}
58+
$tableNames = $backup->getBackupTableNames(BackupType::BACKUP_TYPE_DATA);
7059
break;
7160
case 'backup_logs':
72-
foreach ($tables as $table) {
73-
if (
74-
(Database::getTablePrefix() . 'faqadminlog' === trim((string) $table)) || (Database::getTablePrefix(
75-
) . 'faqsessions' === trim((string) $table))
76-
) {
77-
$tableNames .= $table . ' ';
78-
}
79-
}
61+
$tableNames = $backup->getBackupTableNames(BackupType::BACKUP_TYPE_LOGS);
8062
break;
8163
}
8264

phpmyfaq/src/api-routes.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
use phpMyFAQ\Controller\Api\AttachmentController;
19+
use phpMyFAQ\Controller\Api\BackupController;
1920
use phpMyFAQ\Controller\Api\CategoryController;
2021
use phpMyFAQ\Controller\Api\CommentController;
2122
use phpMyFAQ\Controller\Api\FaqController;
@@ -56,6 +57,10 @@
5657
'api.attachments',
5758
new Route("v{$apiVersion}/attachments/{recordId}", ['_controller' => [AttachmentController::class, 'list']])
5859
);
60+
$routes->add(
61+
'api.backup',
62+
new Route("v{$apiVersion}/backup/{type}", ['_controller' => [BackupController::class, 'download']])
63+
);
5964
$routes->add(
6065
'api.categories',
6166
new Route("v{$apiVersion}/categories", ['_controller' => [CategoryController::class, 'list']])

phpmyfaq/src/phpMyFAQ/Administration/Backup.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use phpMyFAQ\Configuration;
2121
use phpMyFAQ\Database;
2222
use phpMyFAQ\Database\DatabaseHelper;
23+
use phpMyFAQ\Enums\BackupType;
2324
use SodiumException;
2425

2526
/**
@@ -106,6 +107,38 @@ public function generateBackupQueries(string $tableNames): string
106107
return $backup;
107108
}
108109

110+
public function getBackupTableNames(BackupType $type): string
111+
{
112+
$tables = $this->configuration->getDb()->getTableNames(Database::getTablePrefix());
113+
$tableNames = '';
114+
115+
switch ($type) {
116+
case BackupType::BACKUP_TYPE_DATA:
117+
foreach ($tables as $table) {
118+
if (
119+
(Database::getTablePrefix() . 'faqadminlog' === trim((string) $table)) ||
120+
(Database::getTablePrefix() . 'faqsessions' === trim((string) $table))
121+
) {
122+
continue;
123+
}
124+
$tableNames .= $table . ' ';
125+
}
126+
break;
127+
case BackupType::BACKUP_TYPE_LOGS:
128+
foreach ($tables as $table) {
129+
if (
130+
(Database::getTablePrefix() . 'faqadminlog' === trim((string) $table)) ||
131+
(Database::getTablePrefix() . 'faqsessions' === trim((string) $table))
132+
) {
133+
$tableNames .= $table . ' ';
134+
}
135+
}
136+
break;
137+
}
138+
139+
return $tableNames;
140+
}
141+
109142
/**
110143
* Returns the backup file header
111144
* @return string[]

phpmyfaq/src/phpMyFAQ/Controller/Api/AttachmentController.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,20 @@ class AttachmentController extends AbstractController
6969
)]
7070
public function list(Request $request): JsonResponse
7171
{
72-
$jsonResponse = new JsonResponse();
73-
$faqConfig = Configuration::getConfigurationInstance();
74-
7572
$recordId = Filter::filterVar($request->get('recordId'), FILTER_VALIDATE_INT);
7673
$attachments = [];
7774
$result = [];
7875

7976
try {
80-
$attachments = AttachmentFactory::fetchByRecordId($faqConfig, $recordId);
77+
$attachments = AttachmentFactory::fetchByRecordId($this->configuration, $recordId);
8178
} catch (AttachmentException) {
8279
$result = [];
8380
}
8481

8582
foreach ($attachments as $attachment) {
8683
$result[] = [
8784
'filename' => $attachment->getFilename(),
88-
'url' => $faqConfig->getDefaultUrl() . $attachment->buildUrl(),
85+
'url' => $this->configuration->getDefaultUrl() . $attachment->buildUrl(),
8986
];
9087
}
9188

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace phpMyFAQ\Controller\Api;
4+
5+
use OpenApi\Attributes as OA;
6+
use phpMyFAQ\Administration\Backup;
7+
use phpMyFAQ\Controller\AbstractController;
8+
use phpMyFAQ\Core\Exception;
9+
use phpMyFAQ\Database\DatabaseHelper;
10+
use phpMyFAQ\Enums\BackupType;
11+
use phpMyFAQ\Enums\PermissionType;
12+
use phpMyFAQ\Filter;
13+
use SodiumException;
14+
use Symfony\Component\HttpFoundation\HeaderUtils;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
18+
class BackupController extends AbstractController
19+
{
20+
/**
21+
* @throws Exception
22+
*/
23+
#[OA\Get(
24+
path: '/api/v3.0/backup/{type}',
25+
operationId: 'createBackup',
26+
tags: ['Endpoints with Authentication'],
27+
)]
28+
#[OA\Header(
29+
header: 'Accept-Language',
30+
description: 'The language code for the login.',
31+
schema: new OA\Schema(type: 'string')
32+
)]
33+
#[OA\Header(
34+
header: 'x-pmf-token',
35+
description: 'phpMyFAQ client API Token, generated in admin backend',
36+
schema: new OA\Schema(type: 'string')
37+
)]
38+
#[OA\Parameter(
39+
name: 'type',
40+
description: 'The backup type. Can be "data" or "logs".',
41+
in: 'path',
42+
required: true,
43+
schema: new OA\Schema(type: 'string')
44+
)]
45+
#[OA\Response(
46+
response: 200,
47+
description: 'The current backup as a file.',
48+
content: new OA\MediaType(
49+
mediaType: 'application/octet-stream',
50+
schema: new OA\Schema(type: 'string')
51+
)
52+
)]
53+
#[OA\Response(
54+
response: 400,
55+
description: 'If the backup type is wrong',
56+
content: new OA\MediaType(
57+
mediaType: 'application/octet-stream',
58+
schema: new OA\Schema(type: 'string')
59+
)
60+
)]
61+
#[OA\Response(
62+
response: 401,
63+
description: 'If the user is not authenticated and/or does not have sufficient permissions.'
64+
)]
65+
public function download(Request $request): Response
66+
{
67+
$this->userHasPermission(PermissionType::BACKUP);
68+
69+
$type = Filter::filterVar($request->get('type'), FILTER_SANITIZE_SPECIAL_CHARS);
70+
71+
switch ($type) {
72+
case 'data':
73+
$backupType = BackupType::BACKUP_TYPE_DATA;
74+
break;
75+
case 'logs':
76+
$backupType = BackupType::BACKUP_TYPE_LOGS;
77+
break;
78+
default:
79+
return new Response('Invalid backup type.', Response::HTTP_BAD_REQUEST);
80+
}
81+
82+
$dbHelper = new DatabaseHelper($this->configuration);
83+
$backup = new Backup($this->configuration, $dbHelper);
84+
$tableNames = $backup->getBackupTableNames($backupType);
85+
$backupQueries = $backup->generateBackupQueries($tableNames);
86+
87+
try {
88+
$backupFileName = $backup->createBackup($backupType->value, $backupQueries);
89+
90+
$response = new Response($backupQueries);
91+
92+
$disposition = HeaderUtils::makeDisposition(
93+
HeaderUtils::DISPOSITION_ATTACHMENT,
94+
urlencode($backupFileName)
95+
);
96+
97+
$response->headers->set('Content-Type', 'application/octet-stream');
98+
$response->headers->set('Content-Disposition', $disposition);
99+
$response->setStatusCode(Response::HTTP_OK);
100+
return $response->send();
101+
} catch (SodiumException) {
102+
return new Response('An error occurred while creating the backup.', Response::HTTP_INTERNAL_SERVER_ERROR);
103+
}
104+
}
105+
}

phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
use phpMyFAQ\Configuration;
2121
use phpMyFAQ\Database;
22+
use phpMyFAQ\Enums\PermissionType;
2223
use phpMyFAQ\User\CurrentUser;
2324

2425
/**
@@ -105,6 +106,10 @@ public function hasPermission(int $userId, mixed $right): bool
105106
$right = $this->getRightId($right);
106107
}
107108

109+
if ($right instanceof PermissionType) {
110+
$right = $this->getRightId($right->value);
111+
}
112+
108113
// check user right and group right
109114
if ($this->checkUserGroupRight($userId, $right)) {
110115
return true;

0 commit comments

Comments
 (0)