Skip to content

Commit

Permalink
feat(api): added backup for authenticated users via API, closes #2891
Browse files Browse the repository at this point in the history
  • Loading branch information
thorsten committed Mar 23, 2024
1 parent cc39fc1 commit dcd037e
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 29 deletions.
59 changes: 55 additions & 4 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,60 @@ paths:
application/json:
schema: {}
example: []
'/api/v3.0/backup/{type}':
get:
tags:
- 'Endpoints with Authentication'
operationId: createBackup
parameters:
- name: type
in: path
description: 'The backup type. Can be "data" or "logs".'
required: true
schema:
type: string
responses:
'200':
description: 'The current backup as a file.'
headers:
Accept-Language:
description: 'The language code for the login.'
schema:
type: string
x-pmf-token:
description: 'phpMyFAQ client API Token, generated in admin backend'
schema:
type: string
content:
application/octet-stream:
schema:
type: string
'400':
description: 'If the backup type is wrong'
headers:
Accept-Language:
description: 'The language code for the login.'
schema:
type: string
x-pmf-token:
description: 'phpMyFAQ client API Token, generated in admin backend'
schema:
type: string
content:
application/octet-stream:
schema:
type: string
'401':
description: 'If the user is not authenticated and/or does not have sufficient permissions.'
headers:
Accept-Language:
description: 'The language code for the login.'
schema:
type: string
x-pmf-token:
description: 'phpMyFAQ client API Token, generated in admin backend'
schema:
type: string
/api/v3.0/categories:
get:
tags:
Expand Down Expand Up @@ -820,14 +874,11 @@ paths:
application/json:
schema:
required:
- language
- category-id
- question
- author
- email
properties:
language:
type: string
category-id:
type: integer
question:
Expand All @@ -837,7 +888,7 @@ paths:
email:
type: string
type: object
example: "{\n \"language\": \"de\",\n \"category-id\": \"1\",\n \"question\": \"Is this the world we created?\",\n \"author\": \"Freddie Mercury\",\n \"email\": \"freddie.mercury@example.org\"\n }"
example: "{\n \"category-id\": \"1\",\n \"question\": \"Is this the world we created?\",\n \"author\": \"Freddie Mercury\",\n \"email\": \"freddie.mercury@example.org\"\n }"
responses:
'201':
description: 'Used to add a new question in one existing category.'
Expand Down
22 changes: 2 additions & 20 deletions phpmyfaq/admin/backup.export.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,33 +50,15 @@
$user = CurrentUser::getCurrentUser($faqConfig);

if ($user->perm->hasPermission($user->getUserId(), PermissionType::BACKUP->value)) {
$tables = $faqConfig->getDb()->getTableNames(Database::getTablePrefix());
$tableNames = '';

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

switch ($action) {
case 'backup_content':
foreach ($tables as $table) {
if (
(Database::getTablePrefix() . 'faqadminlog' === trim((string) $table)) || (Database::getTablePrefix(
) . 'faqsessions' === trim((string) $table))
) {
continue;
}
$tableNames .= $table . ' ';
}
$tableNames = $backup->getBackupTableNames(BackupType::BACKUP_TYPE_DATA);
break;
case 'backup_logs':
foreach ($tables as $table) {
if (
(Database::getTablePrefix() . 'faqadminlog' === trim((string) $table)) || (Database::getTablePrefix(
) . 'faqsessions' === trim((string) $table))
) {
$tableNames .= $table . ' ';
}
}
$tableNames = $backup->getBackupTableNames(BackupType::BACKUP_TYPE_LOGS);
break;
}

Expand Down
5 changes: 5 additions & 0 deletions phpmyfaq/src/api-routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

use phpMyFAQ\Controller\Api\AttachmentController;
use phpMyFAQ\Controller\Api\BackupController;
use phpMyFAQ\Controller\Api\CategoryController;
use phpMyFAQ\Controller\Api\CommentController;
use phpMyFAQ\Controller\Api\FaqController;
Expand Down Expand Up @@ -56,6 +57,10 @@
'api.attachments',
new Route("v{$apiVersion}/attachments/{recordId}", ['_controller' => [AttachmentController::class, 'list']])
);
$routes->add(
'api.backup',
new Route("v{$apiVersion}/backup/{type}", ['_controller' => [BackupController::class, 'download']])
);
$routes->add(
'api.categories',
new Route("v{$apiVersion}/categories", ['_controller' => [CategoryController::class, 'list']])
Expand Down
33 changes: 33 additions & 0 deletions phpmyfaq/src/phpMyFAQ/Administration/Backup.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use phpMyFAQ\Configuration;
use phpMyFAQ\Database;
use phpMyFAQ\Database\DatabaseHelper;
use phpMyFAQ\Enums\BackupType;
use SodiumException;

/**
Expand Down Expand Up @@ -106,6 +107,38 @@ public function generateBackupQueries(string $tableNames): string
return $backup;
}

public function getBackupTableNames(BackupType $type): string
{
$tables = $this->configuration->getDb()->getTableNames(Database::getTablePrefix());
$tableNames = '';

switch ($type) {
case BackupType::BACKUP_TYPE_DATA:
foreach ($tables as $table) {
if (
(Database::getTablePrefix() . 'faqadminlog' === trim((string) $table)) ||
(Database::getTablePrefix() . 'faqsessions' === trim((string) $table))
) {
continue;
}
$tableNames .= $table . ' ';
}
break;
case BackupType::BACKUP_TYPE_LOGS:
foreach ($tables as $table) {
if (
(Database::getTablePrefix() . 'faqadminlog' === trim((string) $table)) ||
(Database::getTablePrefix() . 'faqsessions' === trim((string) $table))
) {
$tableNames .= $table . ' ';
}
}
break;
}

return $tableNames;
}

/**
* Returns the backup file header
* @return string[]
Expand Down
7 changes: 2 additions & 5 deletions phpmyfaq/src/phpMyFAQ/Controller/Api/AttachmentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,20 @@ class AttachmentController extends AbstractController
)]
public function list(Request $request): JsonResponse
{
$jsonResponse = new JsonResponse();
$faqConfig = Configuration::getConfigurationInstance();

$recordId = Filter::filterVar($request->get('recordId'), FILTER_VALIDATE_INT);
$attachments = [];
$result = [];

try {
$attachments = AttachmentFactory::fetchByRecordId($faqConfig, $recordId);
$attachments = AttachmentFactory::fetchByRecordId($this->configuration, $recordId);
} catch (AttachmentException) {
$result = [];
}

foreach ($attachments as $attachment) {
$result[] = [
'filename' => $attachment->getFilename(),
'url' => $faqConfig->getDefaultUrl() . $attachment->buildUrl(),
'url' => $this->configuration->getDefaultUrl() . $attachment->buildUrl(),
];
}

Expand Down
105 changes: 105 additions & 0 deletions phpmyfaq/src/phpMyFAQ/Controller/Api/BackupController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace phpMyFAQ\Controller\Api;

use OpenApi\Attributes as OA;
use phpMyFAQ\Administration\Backup;
use phpMyFAQ\Controller\AbstractController;
use phpMyFAQ\Core\Exception;
use phpMyFAQ\Database\DatabaseHelper;
use phpMyFAQ\Enums\BackupType;
use phpMyFAQ\Enums\PermissionType;
use phpMyFAQ\Filter;
use SodiumException;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class BackupController extends AbstractController
{
/**
* @throws Exception
*/
#[OA\Get(
path: '/api/v3.0/backup/{type}',
operationId: 'createBackup',
tags: ['Endpoints with Authentication'],
)]
#[OA\Header(
header: 'Accept-Language',
description: 'The language code for the login.',
schema: new OA\Schema(type: 'string')
)]
#[OA\Header(
header: 'x-pmf-token',
description: 'phpMyFAQ client API Token, generated in admin backend',
schema: new OA\Schema(type: 'string')
)]
#[OA\Parameter(
name: 'type',
description: 'The backup type. Can be "data" or "logs".',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[OA\Response(
response: 200,
description: 'The current backup as a file.',
content: new OA\MediaType(
mediaType: 'application/octet-stream',
schema: new OA\Schema(type: 'string')
)
)]
#[OA\Response(
response: 400,
description: 'If the backup type is wrong',
content: new OA\MediaType(
mediaType: 'application/octet-stream',
schema: new OA\Schema(type: 'string')
)
)]
#[OA\Response(
response: 401,
description: 'If the user is not authenticated and/or does not have sufficient permissions.'
)]
public function download(Request $request): Response
{
$this->userHasPermission(PermissionType::BACKUP);

$type = Filter::filterVar($request->get('type'), FILTER_SANITIZE_SPECIAL_CHARS);

switch ($type) {
case 'data':
$backupType = BackupType::BACKUP_TYPE_DATA;
break;
case 'logs':
$backupType = BackupType::BACKUP_TYPE_LOGS;
break;
default:
return new Response('Invalid backup type.', Response::HTTP_BAD_REQUEST);
}

$dbHelper = new DatabaseHelper($this->configuration);
$backup = new Backup($this->configuration, $dbHelper);
$tableNames = $backup->getBackupTableNames($backupType);
$backupQueries = $backup->generateBackupQueries($tableNames);

try {
$backupFileName = $backup->createBackup($backupType->value, $backupQueries);

$response = new Response($backupQueries);

$disposition = HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
urlencode($backupFileName)
);

$response->headers->set('Content-Type', 'application/octet-stream');
$response->headers->set('Content-Disposition', $disposition);
$response->setStatusCode(Response::HTTP_OK);
return $response->send();
} catch (SodiumException) {
return new Response('An error occurred while creating the backup.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}
5 changes: 5 additions & 0 deletions phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

use phpMyFAQ\Configuration;
use phpMyFAQ\Database;
use phpMyFAQ\Enums\PermissionType;
use phpMyFAQ\User\CurrentUser;

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

if ($right instanceof PermissionType) {
$right = $this->getRightId($right->value);
}

// check user right and group right
if ($this->checkUserGroupRight($userId, $right)) {
return true;
Expand Down

0 comments on commit dcd037e

Please sign in to comment.