This commit is contained in:
2025-11-25 17:25:40 +08:00
parent f093791c14
commit 833d125f64
56 changed files with 9428 additions and 4334 deletions
+140
View File
@@ -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
View File
Regular → Executable
+17
View File
@@ -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
View File
@@ -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();
+34 -123
View File
@@ -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;
}
}
?>
View File
Regular → Executable
+26 -231
View File
@@ -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
View File
@@ -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
View File
@@ -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']) . ' — публичная страница'
]);
}