continue
This commit is contained in:
Executable
+140
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/User.php';
|
||||
|
||||
class AdminController extends BaseController {
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->requireAdmin();
|
||||
}
|
||||
|
||||
|
||||
public function users() {
|
||||
$userModel = new User($this->pdo);
|
||||
$users = $userModel->findAll();
|
||||
|
||||
$this->render('admin/users', [
|
||||
'users' => $users,
|
||||
'page_title' => 'Управление пользователями'
|
||||
]);
|
||||
}
|
||||
|
||||
public function toggleUserStatus($user_id) {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Неверный метод запроса или токен безопасности";
|
||||
$this->redirect('/admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user_id == $_SESSION['user_id']) {
|
||||
$_SESSION['error'] = "Нельзя изменить статус собственного аккаунта";
|
||||
$this->redirect('/admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$userModel = new User($this->pdo);
|
||||
$user = $userModel->findById($user_id);
|
||||
|
||||
if (!$user) {
|
||||
$_SESSION['error'] = "Пользователь не найден";
|
||||
$this->redirect('/admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$newStatus = $user['is_active'] ? 0 : 1;
|
||||
if ($userModel->updateStatus($user_id, $newStatus)) {
|
||||
$_SESSION['success'] = "Статус пользователя обновлен";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при обновлении статуса";
|
||||
}
|
||||
|
||||
$this->redirect('/admin/users');
|
||||
}
|
||||
|
||||
public function deleteUser($user_id) {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Неверный метод запроса или токен безопасности";
|
||||
$this->redirect('/admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user_id == $_SESSION['user_id']) {
|
||||
$_SESSION['error'] = "Нельзя удалить собственный аккаунт";
|
||||
$this->redirect('/admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$userModel = new User($this->pdo);
|
||||
$user = $userModel->findById($user_id);
|
||||
|
||||
if (!$user) {
|
||||
$_SESSION['error'] = "Пользователь не найден";
|
||||
$this->redirect('/admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($userModel->delete($user_id)) {
|
||||
$_SESSION['success'] = "Пользователь успешно удален";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при удалении пользователя";
|
||||
}
|
||||
|
||||
$this->redirect('/admin/users');
|
||||
}
|
||||
|
||||
public function addUser() {
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$error = "Ошибка безопасности";
|
||||
} else {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$password_confirm = $_POST['password_confirm'] ?? '';
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$display_name = trim($_POST['display_name'] ?? '');
|
||||
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$error = 'Имя пользователя и пароль обязательны';
|
||||
} elseif ($password !== $password_confirm) {
|
||||
$error = 'Пароли не совпадают';
|
||||
} elseif (strlen($password) < 6) {
|
||||
$error = 'Пароль должен быть не менее 6 символов';
|
||||
} else {
|
||||
$userModel = new User($this->pdo);
|
||||
if ($userModel->findByUsername($username)) {
|
||||
$error = 'Имя пользователя уже занято';
|
||||
} elseif (!empty($email) && $userModel->findByEmail($email)) {
|
||||
$error = 'Email уже используется';
|
||||
} else {
|
||||
$data = [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'email' => $email ?: null,
|
||||
'display_name' => $display_name ?: $username,
|
||||
'is_active' => $is_active
|
||||
];
|
||||
|
||||
if ($userModel->create($data)) {
|
||||
$success = 'Пользователь успешно создан';
|
||||
// Очищаем поля формы
|
||||
$_POST = [];
|
||||
} else {
|
||||
$error = 'Ошибка при создании пользователя';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->render('admin/add_user', [
|
||||
'error' => $error,
|
||||
'success' => $success,
|
||||
'page_title' => 'Добавление пользователя'
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
Regular → Executable
Regular → Executable
+17
@@ -24,6 +24,23 @@ class BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
protected function requireAdmin() {
|
||||
if (!is_logged_in()) {
|
||||
$this->redirect('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
global $pdo;
|
||||
$userModel = new User($pdo);
|
||||
$user = $userModel->findById($_SESSION['user_id']);
|
||||
|
||||
if (!$user || $user['id'] != 1) { // Предполагаем, что администратор имеет ID = 1
|
||||
$_SESSION['error'] = "У вас нет прав администратора";
|
||||
$this->redirect('/dashboard');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
protected function jsonResponse($data) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
|
||||
Regular → Executable
+85
-58
@@ -21,13 +21,9 @@ class BookController extends BaseController {
|
||||
$this->requireLogin();
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$series = $seriesModel->findByUser($_SESSION['user_id']);
|
||||
|
||||
// Возвращаем типы редакторов для выбора
|
||||
$editor_types = [
|
||||
'markdown' => 'Markdown редактор',
|
||||
'html' => 'HTML редактор (TinyMCE)'
|
||||
];
|
||||
|
||||
$error = '';
|
||||
$cover_error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
@@ -46,25 +42,38 @@ class BookController extends BaseController {
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'genre' => trim($_POST['genre'] ?? ''),
|
||||
'user_id' => $_SESSION['user_id'],
|
||||
'editor_type' => $_POST['editor_type'] ?? 'markdown',
|
||||
'series_id' => !empty($_POST['series_id']) ? (int)$_POST['series_id'] : null,
|
||||
'sort_order_in_series' => !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : null,
|
||||
'published' => isset($_POST['published']) ? 1 : 0
|
||||
];
|
||||
|
||||
if ($bookModel->create($data)) {
|
||||
$_SESSION['success'] = "Книга успешно создана";
|
||||
$new_book_id = $this->pdo->lastInsertId();
|
||||
$this->redirect("/books/{$new_book_id}/edit");
|
||||
|
||||
// Обработка загрузки обложки
|
||||
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$cover_result = handleCoverUpload($_FILES['cover_image'], $new_book_id);
|
||||
if ($cover_result['success']) {
|
||||
$bookModel->updateCover($new_book_id, $cover_result['filename']);
|
||||
} else {
|
||||
$cover_error = $cover_result['error'];
|
||||
// Сохраняем ошибку в сессии, чтобы показать после редиректа
|
||||
$_SESSION['cover_error'] = $cover_error;
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['success'] = "Книга успешно создана" . ($cover_error ? ", но возникла ошибка с обложкой: " . $cover_error : "");
|
||||
$this->redirect("/books/{$new_book_id}/edit");
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при создании книги";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->render('books/create', [
|
||||
'series' => $series,
|
||||
'editor_types' => $editor_types,
|
||||
'selected_editor' => 'markdown', // по умолчанию
|
||||
'error' => $error,
|
||||
'cover_error' => $cover_error,
|
||||
'page_title' => 'Создание новой книги'
|
||||
]);
|
||||
}
|
||||
@@ -82,11 +91,6 @@ class BookController extends BaseController {
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$series = $seriesModel->findByUser($_SESSION['user_id']);
|
||||
|
||||
// Типы редакторов для выбора
|
||||
$editor_types = [
|
||||
'markdown' => 'Markdown редактор',
|
||||
'html' => 'HTML редактор (TinyMCE)'
|
||||
];
|
||||
|
||||
$error = '';
|
||||
$cover_error = '';
|
||||
@@ -98,30 +102,17 @@ class BookController extends BaseController {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
if (empty($title)) {
|
||||
$error = "Название книги обязательно";
|
||||
} else {
|
||||
$old_editor_type = $book['editor_type'];
|
||||
$new_editor_type = $_POST['editor_type'] ?? 'markdown';
|
||||
$editor_changed = ($old_editor_type !== $new_editor_type);
|
||||
|
||||
} else {
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'genre' => trim($_POST['genre'] ?? ''),
|
||||
'user_id' => $_SESSION['user_id'],
|
||||
'editor_type' => $new_editor_type,
|
||||
'series_id' => !empty($_POST['series_id']) ? (int)$_POST['series_id'] : null,
|
||||
'sort_order_in_series' => !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : null,
|
||||
'published' => isset($_POST['published']) ? 1 : 0
|
||||
];
|
||||
|
||||
// Обработка смены редактора (прежде чем обновлять книгу)
|
||||
if ($editor_changed) {
|
||||
$conversion_success = $bookModel->convertChaptersContent($id, $old_editor_type, $new_editor_type);
|
||||
if (!$conversion_success) {
|
||||
$_SESSION['warning'] = "Внимание: не удалось автоматически сконвертировать содержание всех глав.";
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка обложки
|
||||
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$cover_result = handleCoverUpload($_FILES['cover_image'], $id);
|
||||
@@ -142,9 +133,6 @@ class BookController extends BaseController {
|
||||
|
||||
if ($success) {
|
||||
$success_message = "Книга успешно обновлена";
|
||||
if ($editor_changed) {
|
||||
$success_message .= ". Содержание глав сконвертировано в новый формат.";
|
||||
}
|
||||
$_SESSION['success'] = $success_message;
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
} else {
|
||||
@@ -162,7 +150,6 @@ class BookController extends BaseController {
|
||||
'book' => $book,
|
||||
'series' => $series,
|
||||
'chapters' => $chapters,
|
||||
'editor_types' => $editor_types,
|
||||
'error' => $error,
|
||||
'cover_error' => $cover_error,
|
||||
'page_title' => 'Редактирование книги'
|
||||
@@ -193,6 +180,69 @@ class BookController extends BaseController {
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
|
||||
public function deleteAll() {
|
||||
$this->requireLogin();
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
$bookModel = new Book($this->pdo);
|
||||
|
||||
// Получаем все книги пользователя
|
||||
$books = $bookModel->findByUser($user_id);
|
||||
if (empty($books)) {
|
||||
$_SESSION['info'] = "У вас нет книг для удаления";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
$deleted_count = 0;
|
||||
$deleted_covers = 0;
|
||||
|
||||
foreach ($books as $book) {
|
||||
// Удаляем обложку если она есть
|
||||
if (!empty($book['cover_image'])) {
|
||||
$cover_path = COVERS_PATH . $book['cover_image'];
|
||||
if (file_exists($cover_path) && unlink($cover_path)) {
|
||||
$deleted_covers++;
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем главы книги
|
||||
$stmt = $this->pdo->prepare("DELETE FROM chapters WHERE book_id = ?");
|
||||
$stmt->execute([$book['id']]);
|
||||
|
||||
// Удаляем саму книгу
|
||||
$stmt = $this->pdo->prepare("DELETE FROM books WHERE id = ? AND user_id = ?");
|
||||
$stmt->execute([$book['id'], $user_id]);
|
||||
|
||||
$deleted_count++;
|
||||
}
|
||||
|
||||
$this->pdo->commit();
|
||||
|
||||
$message = "Все книги успешно удалены ($deleted_count книг";
|
||||
if ($deleted_covers > 0) {
|
||||
$message .= ", удалено $deleted_covers обложек";
|
||||
}
|
||||
$message .= ")";
|
||||
|
||||
$_SESSION['success'] = $message;
|
||||
} catch (Exception $e) {
|
||||
$this->pdo->rollBack();
|
||||
error_log("Ошибка при массовом удалении: " . $e->getMessage());
|
||||
$_SESSION['error'] = "Произошла ошибка при удалении книг: " . $e->getMessage();
|
||||
}
|
||||
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
public function viewPublic($share_token) {
|
||||
$bookModel = new Book($this->pdo);
|
||||
$book = $bookModel->findByShareToken($share_token);
|
||||
@@ -216,29 +266,6 @@ class BookController extends BaseController {
|
||||
]);
|
||||
}
|
||||
|
||||
public function normalizeContent($id) {
|
||||
$this->requireLogin();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$_SESSION['error'] = "Неверный метод запроса";
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
}
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
}
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$bookModel = new Book($this->pdo);
|
||||
if (!$bookModel->userOwnsBook($id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой книге";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
if ($bookModel->normalizeBookContent($id)) {
|
||||
$_SESSION['success'] = "Контент глав успешно нормализован";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при нормализации контента";
|
||||
}
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
}
|
||||
|
||||
public function regenerateToken($id) {
|
||||
$this->requireLogin();
|
||||
|
||||
Regular → Executable
+34
-123
@@ -3,7 +3,6 @@
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/Chapter.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
|
||||
class ChapterController extends BaseController {
|
||||
|
||||
@@ -94,11 +93,29 @@ class ChapterController extends BaseController {
|
||||
|
||||
// Проверяем права доступа к главе
|
||||
if (!$chapterModel->userOwnsChapter($id, $user_id)) {
|
||||
if (isset($_POST['autosave']) && $_POST['autosave'] === 'true') {
|
||||
// Для AJAX запросов возвращаем JSON
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Доступ запрещен']);
|
||||
exit;
|
||||
}
|
||||
$_SESSION['error'] = "У вас нет доступа к этой главе";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
$chapter = $chapterModel->findById($id);
|
||||
|
||||
// Дополнительная проверка - глава должна существовать
|
||||
if (!$chapter) {
|
||||
if (isset($_POST['autosave']) && $_POST['autosave'] === 'true') {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Глава не найдена']);
|
||||
exit;
|
||||
}
|
||||
$_SESSION['error'] = "Глава не найдена";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
$book = $bookModel->findById($chapter['book_id']);
|
||||
$error = '';
|
||||
|
||||
@@ -119,6 +136,20 @@ class ChapterController extends BaseController {
|
||||
'status' => $status
|
||||
];
|
||||
|
||||
// Если это запрос автосейва, возвращаем JSON ответ
|
||||
if (isset($_POST['autosave']) && $_POST['autosave'] === 'true') {
|
||||
if ($chapterModel->update($id, $data)) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
} else {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Ошибка при сохранении']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Обычный POST запрос (сохранение формы)
|
||||
if ($chapterModel->update($id, $data)) {
|
||||
$_SESSION['success'] = "Глава успешно обновлена";
|
||||
$this->redirect("/books/{$chapter['book_id']}/chapters");
|
||||
@@ -174,23 +205,12 @@ class ChapterController extends BaseController {
|
||||
|
||||
public function preview() {
|
||||
$this->requireLogin();
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
$Parsedown = new ParsedownExtra();
|
||||
|
||||
$content = $_POST['content'] ?? '';
|
||||
$title = $_POST['title'] ?? 'Предпросмотр';
|
||||
$editor_type = $_POST['editor_type'] ?? 'markdown';
|
||||
|
||||
// Обрабатываем контент в зависимости от типа редактора
|
||||
if ($editor_type == 'markdown') {
|
||||
// Нормализуем Markdown перед преобразованием
|
||||
$normalized_content = $this->normalizeMarkdownContent($content);
|
||||
$html_content = $Parsedown->text($normalized_content);
|
||||
} else {
|
||||
// Для HTML редактора нормализуем контент
|
||||
$normalized_content = $this->normalizeHtmlContent($content);
|
||||
$html_content = $normalized_content;
|
||||
}
|
||||
// Просто используем HTML как есть
|
||||
$html_content = $content;
|
||||
|
||||
$this->render('chapters/preview', [
|
||||
'content' => $html_content,
|
||||
@@ -199,114 +219,5 @@ class ChapterController extends BaseController {
|
||||
]);
|
||||
}
|
||||
|
||||
private function normalizeMarkdownContent($markdown) {
|
||||
// Нормализация Markdown - убеждаемся, что есть пустые строки между абзацами
|
||||
$lines = explode("\n", $markdown);
|
||||
$normalized = [];
|
||||
$inParagraph = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = trim($line);
|
||||
|
||||
if (empty($trimmed)) {
|
||||
// Пустая строка - конец абзаца
|
||||
if ($inParagraph) {
|
||||
$normalized[] = '';
|
||||
$inParagraph = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем, не является ли строка началом списка
|
||||
if (preg_match('/^[\*\-\+] /', $line) || preg_match('/^\d+\./', $line)) {
|
||||
if ($inParagraph) {
|
||||
$normalized[] = ''; // Завершаем предыдущий абзац
|
||||
$inParagraph = false;
|
||||
}
|
||||
$normalized[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем, не является ли строка началом цитаты
|
||||
if (preg_match('/^> /', $line) || preg_match('/^— /', $line)) {
|
||||
if ($inParagraph) {
|
||||
$normalized[] = ''; // Завершаем предыдущий абзац
|
||||
$inParagraph = false;
|
||||
}
|
||||
$normalized[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем, не является ли строка заголовком
|
||||
if (preg_match('/^#+ /', $line)) {
|
||||
if ($inParagraph) {
|
||||
$normalized[] = ''; // Завершаем предыдущий абзац
|
||||
$inParagraph = false;
|
||||
}
|
||||
$normalized[] = $line;
|
||||
$normalized[] = ''; // Пустая строка после заголовка
|
||||
continue;
|
||||
}
|
||||
|
||||
// Непустая строка - часть абзаца
|
||||
if (!$inParagraph && !empty($normalized) && end($normalized) !== '') {
|
||||
// Добавляем пустую строку перед новым абзацем
|
||||
$normalized[] = '';
|
||||
}
|
||||
|
||||
$normalized[] = $line;
|
||||
$inParagraph = true;
|
||||
}
|
||||
|
||||
return implode("\n", $normalized);
|
||||
}
|
||||
|
||||
// И метод для нормализации HTML контента
|
||||
private function normalizeHtmlContent($html) {
|
||||
// Оборачиваем текст без тегов в <p>
|
||||
if (!preg_match('/<[^>]+>/', $html) && trim($html) !== '') {
|
||||
$lines = explode("\n", trim($html));
|
||||
$wrapped = [];
|
||||
$inParagraph = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = trim($line);
|
||||
|
||||
if (empty($trimmed)) {
|
||||
if ($inParagraph) {
|
||||
$wrapped[] = '</p>';
|
||||
$inParagraph = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем на начало списка
|
||||
if (preg_match('/^[\*\-\+] /', $trimmed) || preg_match('/^\d+\./', $trimmed)) {
|
||||
if ($inParagraph) {
|
||||
$wrapped[] = '</p>';
|
||||
$inParagraph = false;
|
||||
}
|
||||
// Обрабатываем списки отдельно
|
||||
$wrapped[] = '<ul><li>' . htmlspecialchars($trimmed) . '</li></ul>';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$inParagraph) {
|
||||
$wrapped[] = '<p>' . htmlspecialchars($trimmed);
|
||||
$inParagraph = true;
|
||||
} else {
|
||||
$wrapped[] = htmlspecialchars($trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
if ($inParagraph) {
|
||||
$wrapped[] = '</p>';
|
||||
}
|
||||
|
||||
return implode("\n", $wrapped);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
?>
|
||||
Regular → Executable
Regular → Executable
+26
-231
@@ -4,7 +4,6 @@ require_once 'controllers/BaseController.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'models/Chapter.php';
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
|
||||
use PhpOffice\PhpWord\PhpWord;
|
||||
use PhpOffice\PhpWord\IOFactory;
|
||||
@@ -69,20 +68,20 @@ class ExportController extends BaseController {
|
||||
}
|
||||
|
||||
private function handleExport($book, $chapters, $is_public, $author_name, $format) {
|
||||
$Parsedown = new ParsedownExtra();
|
||||
|
||||
|
||||
switch ($format) {
|
||||
case 'pdf':
|
||||
$this->exportPDF($book, $chapters, $is_public, $author_name, $Parsedown);
|
||||
$this->exportPDF($book, $chapters, $is_public, $author_name);
|
||||
break;
|
||||
case 'docx':
|
||||
$this->exportDOCX($book, $chapters, $is_public, $author_name, $Parsedown);
|
||||
$this->exportDOCX($book, $chapters, $is_public, $author_name);
|
||||
break;
|
||||
case 'html':
|
||||
$this->exportHTML($book, $chapters, $is_public, $author_name, $Parsedown);
|
||||
$this->exportHTML($book, $chapters, $is_public, $author_name);
|
||||
break;
|
||||
case 'txt':
|
||||
$this->exportTXT($book, $chapters, $is_public, $author_name, $Parsedown);
|
||||
$this->exportTXT($book, $chapters, $is_public, $author_name);
|
||||
break;
|
||||
default:
|
||||
$_SESSION['error'] = "Неверный формат экспорта";
|
||||
@@ -94,7 +93,7 @@ class ExportController extends BaseController {
|
||||
}
|
||||
|
||||
function exportPDF($book, $chapters, $is_public, $author_name) {
|
||||
global $Parsedown;
|
||||
|
||||
|
||||
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
|
||||
|
||||
@@ -200,11 +199,9 @@ class ExportController extends BaseController {
|
||||
|
||||
// Контент главы
|
||||
$pdf->SetFont('dejavusans', '', 11);
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$htmlContent = $Parsedown->text($chapter['content']);
|
||||
} else {
|
||||
$htmlContent = $chapter['content'];
|
||||
}
|
||||
|
||||
$htmlContent = $chapter['content'];
|
||||
|
||||
$pdf->writeHTML($htmlContent, true, false, true, false, '');
|
||||
|
||||
$pdf->Ln(8);
|
||||
@@ -223,8 +220,7 @@ class ExportController extends BaseController {
|
||||
}
|
||||
|
||||
function exportDOCX($book, $chapters, $is_public, $author_name) {
|
||||
global $Parsedown;
|
||||
|
||||
|
||||
$phpWord = new PhpWord();
|
||||
|
||||
// Стили документа
|
||||
@@ -263,11 +259,8 @@ class ExportController extends BaseController {
|
||||
|
||||
// Описание
|
||||
if (!empty($book['description'])) {
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$descriptionParagraphs = $this->markdownToParagraphs($book['description']);
|
||||
} else {
|
||||
$descriptionParagraphs = $this->htmlToParagraphs($book['description']);
|
||||
}
|
||||
|
||||
$descriptionParagraphs = $this->htmlToParagraphs($book['description']);
|
||||
|
||||
foreach ($descriptionParagraphs as $paragraph) {
|
||||
if (!empty(trim($paragraph))) {
|
||||
@@ -303,14 +296,11 @@ class ExportController extends BaseController {
|
||||
$section->addText($chapter['title'], ['bold' => true, 'size' => 14]);
|
||||
$section->addTextBreak(1);
|
||||
|
||||
// Получаем очищенный текст и разбиваем на абзацы в зависимости от типа редактора
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$cleanContent = $this->cleanMarkdown($chapter['content']);
|
||||
$paragraphs = $this->markdownToParagraphs($cleanContent);
|
||||
} else {
|
||||
$cleanContent = strip_tags($chapter['content']);
|
||||
$paragraphs = $this->htmlToParagraphs($chapter['content']);
|
||||
}
|
||||
// Получаем очищенный текст и разбиваем на абзацы
|
||||
|
||||
$cleanContent = strip_tags($chapter['content']);
|
||||
$paragraphs = $this->htmlToParagraphs($chapter['content']);
|
||||
|
||||
|
||||
// Добавляем каждый абзац
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
@@ -342,7 +332,6 @@ class ExportController extends BaseController {
|
||||
}
|
||||
|
||||
function exportHTML($book, $chapters, $is_public, $author_name) {
|
||||
global $Parsedown;
|
||||
|
||||
$html = '<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
@@ -520,11 +509,7 @@ class ExportController extends BaseController {
|
||||
|
||||
if (!empty($book['description'])) {
|
||||
$html .= '<div class="book-description">';
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$html .= nl2br(htmlspecialchars($book['description']));
|
||||
} else {
|
||||
$html .= $book['description'];
|
||||
}
|
||||
$html .= $book['description'];
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
@@ -546,15 +531,7 @@ class ExportController extends BaseController {
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
$html .= '<div class="chapter">';
|
||||
$html .= '<div class="chapter-title" id="chapter-' . $chapter['id'] . '" name="chapter-' . $chapter['id'] . '">' . htmlspecialchars($chapter['title']) . '</div>';
|
||||
|
||||
// Обрабатываем контент в зависимости от типа редактора
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$htmlContent = $Parsedown->text($chapter['content']);
|
||||
} else {
|
||||
$htmlContent = $chapter['content'];
|
||||
}
|
||||
|
||||
$html .= '<div class="chapter-content">' . $htmlContent . '</div>';
|
||||
$html .= '<div class="chapter-content">' . $chapter['content']. '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
if ($index < count($chapters) - 1) {
|
||||
@@ -589,13 +566,8 @@ class ExportController extends BaseController {
|
||||
if (!empty($book['description'])) {
|
||||
$content .= "ОПИСАНИЕ:\n";
|
||||
|
||||
// Обрабатываем описание в зависимости от типа редактора
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$descriptionText = $this->cleanMarkdown($book['description']);
|
||||
} else {
|
||||
$descriptionText = strip_tags($book['description']);
|
||||
}
|
||||
|
||||
// Обрабатываем описание
|
||||
$descriptionText = strip_tags($book['description']);
|
||||
$content .= wordwrap($descriptionText, 144) . "\n\n";
|
||||
}
|
||||
|
||||
@@ -616,14 +588,9 @@ class ExportController extends BaseController {
|
||||
$content .= $chapter['title'] . "\n";
|
||||
$content .= str_repeat("-", 60) . "\n\n";
|
||||
|
||||
// Получаем очищенный текст в зависимости от типа редактора
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$cleanContent = $this->cleanMarkdown($chapter['content']);
|
||||
$paragraphs = $this->markdownToParagraphs($cleanContent);
|
||||
} else {
|
||||
$cleanContent = strip_tags($chapter['content']);
|
||||
$paragraphs = $this->htmlToPlainTextParagraphs($cleanContent);
|
||||
}
|
||||
// Получаем очищенный текст
|
||||
$cleanContent = strip_tags($chapter['content']);
|
||||
$paragraphs = $this->htmlToPlainTextParagraphs($cleanContent);
|
||||
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
if (!empty(trim($paragraph))) {
|
||||
@@ -648,180 +615,7 @@ class ExportController extends BaseController {
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Функция для преобразования Markdown в чистый текст с форматированием абзацев
|
||||
function markdownToPlainText($markdown) {
|
||||
// Обрабатываем диалоги (заменяем - на —)
|
||||
$markdown = preg_replace('/^- (.+)$/m', "— $1", $markdown);
|
||||
|
||||
// Убираем Markdown разметку, но сохраняем переносы строк
|
||||
$text = $markdown;
|
||||
|
||||
// Убираем заголовки
|
||||
$text = preg_replace('/^#+\s+/m', '', $text);
|
||||
|
||||
// Убираем жирный и курсив
|
||||
$text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text);
|
||||
$text = preg_replace('/\*(.*?)\*/', '$1', $text);
|
||||
$text = preg_replace('/__(.*?)__/', '$1', $text);
|
||||
$text = preg_replace('/_(.*?)_/', '$1', $text);
|
||||
|
||||
// Убираем зачеркивание
|
||||
$text = preg_replace('/~~(.*?)~~/', '$1', $text);
|
||||
|
||||
// Убираем код (встроенный)
|
||||
$text = preg_replace('/`(.*?)`/', '$1', $text);
|
||||
|
||||
// Убираем блоки кода (сохраняем содержимое)
|
||||
$text = preg_replace('/```.*?\n(.*?)```/s', '$1', $text);
|
||||
|
||||
// Убираем ссылки
|
||||
$text = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $text);
|
||||
|
||||
// Обрабатываем списки - заменяем маркеры на *
|
||||
$text = preg_replace('/^[\*\-+]\s+/m', '* ', $text);
|
||||
$text = preg_replace('/^\d+\.\s+/m', '* ', $text);
|
||||
|
||||
// Обрабатываем цитаты
|
||||
$text = preg_replace('/^>\s+/m', '', $text);
|
||||
|
||||
return $text;
|
||||
}
|
||||
// Функция для разбивки Markdown на абзацы с сохранением структуры
|
||||
function markdownToParagraphs($markdown) {
|
||||
// Нормализуем переносы строк
|
||||
$text = str_replace(["\r\n", "\r"], "\n", $markdown);
|
||||
|
||||
// Обрабатываем диалоги (заменяем - на —)
|
||||
$text = preg_replace('/^- (.+)$/m', "— $1", $text);
|
||||
|
||||
// Разбиваем на строки
|
||||
$lines = explode("\n", $text);
|
||||
$paragraphs = [];
|
||||
$currentParagraph = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmedLine = trim($line);
|
||||
|
||||
// Пустая строка - конец абзаца
|
||||
if (empty($trimmedLine)) {
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
$currentParagraph = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Диалог (начинается с —) всегда начинает новый абзац
|
||||
if (str_starts_with($trimmedLine, '—')) {
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
}
|
||||
$currentParagraph = $trimmedLine;
|
||||
$paragraphs[] = $currentParagraph;
|
||||
$currentParagraph = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Заголовки (начинаются с #) всегда начинают новый абзац
|
||||
if (str_starts_with($trimmedLine, '#')) {
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
}
|
||||
$currentParagraph = preg_replace('/^#+\s+/', '', $trimmedLine);
|
||||
$paragraphs[] = $currentParagraph;
|
||||
$currentParagraph = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Обычный текст - добавляем к текущему абзацу
|
||||
if (!empty($currentParagraph)) {
|
||||
$currentParagraph .= ' ' . $trimmedLine;
|
||||
} else {
|
||||
$currentParagraph = $trimmedLine;
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем последний абзац
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
}
|
||||
|
||||
return $paragraphs;
|
||||
}
|
||||
|
||||
// Функция для очистки Markdown разметки
|
||||
function cleanMarkdown($markdown) {
|
||||
$text = $markdown;
|
||||
|
||||
// Убираем заголовки
|
||||
$text = preg_replace('/^#+\s+/m', '', $text);
|
||||
|
||||
// Убираем жирный и курсив
|
||||
$text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text);
|
||||
$text = preg_replace('/\*(.*?)\*/', '$1', $text);
|
||||
$text = preg_replace('/__(.*?)__/', '$1', $text);
|
||||
$text = preg_replace('/_(.*?)_/', '$1', $text);
|
||||
|
||||
// Убираем зачеркивание
|
||||
$text = preg_replace('/~~(.*?)~~/', '$1', $text);
|
||||
|
||||
// Убираем код
|
||||
$text = preg_replace('/`(.*?)`/', '$1', $text);
|
||||
$text = preg_replace('/```.*?\n(.*?)```/s', '$1', $text);
|
||||
|
||||
// Убираем ссылки
|
||||
$text = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $text);
|
||||
|
||||
// Обрабатываем списки - убираем маркеры
|
||||
$text = preg_replace('/^[\*\-+]\s+/m', '', $text);
|
||||
$text = preg_replace('/^\d+\.\s+/m', '', $text);
|
||||
|
||||
// Обрабатываем цитаты
|
||||
$text = preg_replace('/^>\s+/m', '', $text);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
// Функция для форматирования текста с сохранением абзацев и диалогов
|
||||
function formatPlainText($text) {
|
||||
$lines = explode("\n", $text);
|
||||
$formatted = [];
|
||||
$in_paragraph = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
if (empty($line)) {
|
||||
if ($in_paragraph) {
|
||||
$formatted[] = ''; // Пустая строка для разделения абзацев
|
||||
$in_paragraph = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Диалоги начинаются с —
|
||||
if (str_starts_with($line, '—')) {
|
||||
if ($in_paragraph) {
|
||||
$formatted[] = ''; // Разделяем абзацы перед диалогом
|
||||
}
|
||||
$formatted[] = $line;
|
||||
$formatted[] = ''; // Пустая строка после диалога
|
||||
$in_paragraph = false;
|
||||
} else {
|
||||
// Обычный текст
|
||||
$formatted[] = $line;
|
||||
$in_paragraph = true;
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", array_filter($formatted, function($line) {
|
||||
return $line !== '' || !empty($line);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// // Новая функция для разбивки HTML на абзацы
|
||||
// Функция для разбивки HTML на абзацы
|
||||
function htmlToParagraphs($html) {
|
||||
// Убираем HTML теги и нормализуем пробелы
|
||||
$text = strip_tags($html);
|
||||
@@ -837,6 +631,7 @@ class ExportController extends BaseController {
|
||||
|
||||
return $paragraphs;
|
||||
}
|
||||
|
||||
function htmlToPlainTextParagraphs($html) {
|
||||
// Убираем HTML теги
|
||||
$text = strip_tags($html);
|
||||
|
||||
Regular → Executable
+120
-4
@@ -3,7 +3,6 @@
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/Series.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
|
||||
class SeriesController extends BaseController {
|
||||
|
||||
@@ -178,17 +177,134 @@ class SeriesController extends BaseController {
|
||||
$total_chapters += $book_stats['chapter_count'] ?? 0;
|
||||
}
|
||||
|
||||
$Parsedown = new ParsedownExtra();
|
||||
|
||||
$this->render('series/view_public', [
|
||||
'series' => $series,
|
||||
'books' => $books,
|
||||
'author' => $author,
|
||||
'total_words' => $total_words,
|
||||
'total_chapters' => $total_chapters,
|
||||
'Parsedown' => $Parsedown,
|
||||
'page_title' => $series['title'] . ' — серия книг'
|
||||
]);
|
||||
}
|
||||
|
||||
public function addBook($series_id) {
|
||||
$this->requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$bookModel = new Book($this->pdo);
|
||||
|
||||
if (!$seriesModel->userOwnsSeries($series_id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой серии";
|
||||
$this->redirect('/series');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
$book_id = (int)($_POST['book_id'] ?? 0);
|
||||
$sort_order = (int)($_POST['sort_order'] ?? 0);
|
||||
|
||||
if (!$book_id) {
|
||||
$_SESSION['error'] = "Выберите книгу";
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
// Проверяем, что книга принадлежит пользователю
|
||||
if (!$bookModel->userOwnsBook($book_id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой книге";
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
// Добавляем книгу в серию
|
||||
if ($bookModel->updateSeriesInfo($book_id, $series_id, $sort_order)) {
|
||||
$_SESSION['success'] = "Книга добавлена в серию";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при добавлении книги в серию";
|
||||
}
|
||||
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
}
|
||||
|
||||
public function removeBook($series_id, $book_id) {
|
||||
$this->requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$_SESSION['error'] = "Неверный метод запроса";
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$bookModel = new Book($this->pdo);
|
||||
|
||||
if (!$seriesModel->userOwnsSeries($series_id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой серии";
|
||||
$this->redirect('/series');
|
||||
}
|
||||
|
||||
// Проверяем, что книга принадлежит пользователю
|
||||
if (!$bookModel->userOwnsBook($book_id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой книге";
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
// Удаляем книгу из серии
|
||||
if ($bookModel->removeFromSeries($book_id)) {
|
||||
$_SESSION['success'] = "Книга удалена из серии";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при удалении книги из серии";
|
||||
}
|
||||
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
public function updateBookOrder($series_id) {
|
||||
$this->requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$_SESSION['error'] = "Неверный метод запроса";
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$bookModel = new Book($this->pdo);
|
||||
|
||||
if (!$seriesModel->userOwnsSeries($series_id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой серии";
|
||||
$this->redirect('/series');
|
||||
}
|
||||
|
||||
$order_data = $_POST['order'] ?? [];
|
||||
|
||||
if (empty($order_data)) {
|
||||
$_SESSION['error'] = "Нет данных для обновления";
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
|
||||
// Обновляем порядок книг
|
||||
if ($bookModel->reorderSeriesBooks($series_id, $order_data)) {
|
||||
$_SESSION['success'] = "Порядок книг обновлен";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при обновлении порядка книг";
|
||||
}
|
||||
|
||||
$this->redirect("/series/{$series_id}/edit");
|
||||
}
|
||||
}
|
||||
?>
|
||||
Regular → Executable
+1
-3
@@ -3,7 +3,6 @@
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/User.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
|
||||
class UserController extends BaseController {
|
||||
|
||||
@@ -101,7 +100,7 @@ class UserController extends BaseController {
|
||||
$total_chapters += $book_stats['chapter_count'] ?? 0;
|
||||
}
|
||||
|
||||
$Parsedown = new ParsedownExtra();
|
||||
|
||||
|
||||
$this->render('user/view_public', [
|
||||
'user' => $user,
|
||||
@@ -109,7 +108,6 @@ class UserController extends BaseController {
|
||||
'total_books' => $total_books,
|
||||
'total_words' => $total_words,
|
||||
'total_chapters' => $total_chapters,
|
||||
'Parsedown' => $Parsedown,
|
||||
'page_title' => ($user['display_name'] ?: $user['username']) . ' — публичная страница'
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user