diff --git a/admin/index.php b/admin/index.php deleted file mode 100755 index e70c8ba..0000000 --- a/admin/index.php +++ /dev/null @@ -1,2 +0,0 @@ -findAll(); - -// Обработка действий -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Ошибка безопасности"; - } else { - $action = $_POST['action'] ?? ''; - $user_id = $_POST['user_id'] ?? null; - - if ($user_id && $user_id != $_SESSION['user_id']) { // Нельзя изменять себя - switch ($action) { - case 'toggle_active': - $user = $userModel->findById($user_id); - if ($user) { - $new_status = $user['is_active'] ? 0 : 1; - if ($userModel->updateStatus($user_id, $new_status)) { - $_SESSION['success'] = 'Статус пользователя обновлен'; - } else { - $_SESSION['error'] = 'Ошибка при обновлении статуса'; - } - } - break; - - case 'delete': - if ($userModel->delete($user_id)) { - $_SESSION['success'] = 'Пользователь удален'; - } else { - $_SESSION['error'] = 'Ошибка при удалении пользователя'; - } - break; - } - } else { - $_SESSION['error'] = 'Нельзя изменить собственный аккаунт'; - } - - redirect('users.php'); - } -} - -$page_title = "Управление пользователями"; -include '../views/header.php'; -?> - -
-

Управление пользователями

- - -
- - -
- - - -
- - -
- - -
-

Всего пользователей:

- ➕ Добавить пользователя -
- - -
-

Пользователи не найдены

-

Зарегистрируйте первого пользователя

- 📝 Добавить пользователя -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - -
IDИмя пользователяОтображаемое имяEmailДата регистрацииСтатусДействия
- - -
(Вы) - -
- - -
Вход: - -
- - - - - -
-
- - - - -
-
- - - - -
-
- - Текущий пользователь - -
-
- -
- - \ No newline at end of file diff --git a/book_delete.php b/book_delete.php deleted file mode 100755 index 0e7243a..0000000 --- a/book_delete.php +++ /dev/null @@ -1,47 +0,0 @@ -userOwnsBook($book_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой книге"; - redirect('books.php'); -} - -// Получаем информацию о книге перед удалением -$book = $bookModel->findById($book_id); -if (!empty($book['cover_image'])) { - $cover_path = COVERS_PATH . $book['cover_image']; - if (file_exists($cover_path)) { - unlink($cover_path); - } -} -// Удаляем книгу -if ($bookModel->delete($book_id, $user_id)) { - $_SESSION['success'] = "Книга «" . e($book['title']) . "» успешно удалена"; -} else { - $_SESSION['error'] = "Ошибка при удалении книги"; -} - -redirect('books.php'); -?> \ No newline at end of file diff --git a/book_delete_all.php b/book_delete_all.php deleted file mode 100755 index 6e9f5af..0000000 --- a/book_delete_all.php +++ /dev/null @@ -1,45 +0,0 @@ -findByUser($user_id); - -if (empty($books)) { - $_SESSION['error'] = "У вас нет книг для удаления"; - redirect('books.php'); -} - -$deleted_count = 0; -$error_count = 0; - -// Удаляем каждую книгу -foreach ($books as $book) { - if ($bookModel->delete($book['id'], $user_id)) { - $deleted_count++; - } else { - $error_count++; - } -} - -if ($error_count === 0) { - $_SESSION['success'] = "Все книги успешно удалены ($deleted_count книг)"; -} else { - $_SESSION['error'] = "Удалено $deleted_count книг, не удалось удалить $error_count книг"; -} - -redirect('books.php'); -?> \ No newline at end of file diff --git a/book_edit.php b/book_edit.php deleted file mode 100755 index f06cc70..0000000 --- a/book_edit.php +++ /dev/null @@ -1,413 +0,0 @@ -findById($book_id); - if (!$book || $book['user_id'] != $user_id) { - $_SESSION['error'] = "Книга не найдена или у вас нет доступа"; - redirect('books.php'); - } - $is_edit = true; -} - -// Обработка формы -$cover_error = ''; -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Ошибка безопасности"; - redirect($is_edit ? "book_edit.php?id=$book_id" : 'book_edit.php'); - } - - $title = trim($_POST['title'] ?? ''); - $description = trim($_POST['description'] ?? ''); - $genre = trim($_POST['genre'] ?? ''); - $editor_type = $_POST['editor_type'] ?? 'markdown'; - - if (empty($title)) { - $_SESSION['error'] = "Название книги обязательно"; - } else { - $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; - - if ($series_id && !$sort_order_in_series) { - $seriesModel = new Series($pdo); - $sort_order_in_series = $seriesModel->getNextSortOrder($series_id); - } - - $data = [ - 'title' => $title, - 'description' => $description, - 'genre' => $genre, - 'user_id' => $user_id, - 'series_id' => $series_id, - 'sort_order_in_series' => $sort_order_in_series, - 'editor_type' => $editor_type - ]; - $data['published'] = isset($_POST['published']) ? 1 : 0; - - // Проверяем, изменился ли тип редактора - $editor_changed = false; - $old_editor_type = null; - - if ($is_edit && $book['editor_type'] !== $editor_type) { - $editor_changed = true; - $old_editor_type = $book['editor_type']; - } - // Обработка загрузки обложки - if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) { - $cover_result = handleCoverUpload($_FILES['cover_image'], $book_id); - if ($cover_result['success']) { - $bookModel->updateCover($book_id, $cover_result['filename']); - // Обновляем данные книги - $book = $bookModel->findById($book_id); - } else { - $cover_error = $cover_result['error']; - } - } - - // Обработка удаления обложки - if (isset($_POST['delete_cover']) && $_POST['delete_cover'] == '1') { - $bookModel->deleteCover($book_id); - $book = $bookModel->findById($book_id); - } - - if ($is_edit) { - $success = $bookModel->update($book_id, $data); - - // Конвертируем контент глав, если изменился редактор - if ($success && $editor_changed) { - $conversion_success = $bookModel->convertChaptersContent($book_id, $old_editor_type, $editor_type); - if (!$conversion_success) { - $_SESSION['warning'] = "Книга обновлена, но возникли ошибки при конвертации содержания глав"; - } else { - $_SESSION['info'] = "Книга обновлена. Содержание глав сконвертировано в новый формат редактора."; - } - } - - $message = $success ? "Книга успешно обновлена" : "Ошибка при обновлении книги"; - } else { - $success = $bookModel->create($data); - $message = $success ? "Книга успешно создана" : "Ошибка при создании книги"; - - if ($success) { - $new_book_id = $pdo->lastInsertId(); - redirect("book_edit.php?id=$new_book_id"); - } - } - - if ($success) { - $_SESSION['success'] = $message; - redirect('books.php'); - } else { - $_SESSION['error'] = $message; - } - } -} - -$page_title = $is_edit ? "Редактирование книги" : "Создание новой книги"; -include 'views/header.php'; -?> - -
- - -
- - - - - - - - - - - - - - - - - - -
- - - -
-

Текущая обложка:

- Обложка -
- -
-
- - - - - Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 5MB. - Рекомендуемый размер: 300×450 пикселей. - - - -
- ❌ -
- -
- - - - -
- -
-
- -
- -
-
- -
- - - -

- Если контент глав отображается неправильно после смены редактора, можно нормализовать его структуру. -

-
- - - -
- - - -
- - - -
-

Публичная ссылка для чтения

-
- -
- - -
- - - - -
-

- Примечание: В публичном просмотре отображаются только главы со статусом "Опубликована" -

-
- - -
- - - - - -
-

Экспорт книги

-

Экспортируйте книгу в различные форматы:

- -
- - 📄 PDF - - - 📝 DOCX - - - 🌐 HTML - - - 📄 TXT - -
- -

- Примечание: Экспортируются все главы книги (включая черновики) -

-
- - - -
-

Главы этой книги

- - 📑 Все главы - -   - - ✏️ Добавить главу - - prepare("SELECT * FROM chapters WHERE book_id = ? ORDER BY sort_order, created_at"); - $stmt->execute([$book_id]); - $chapters = $stmt->fetchAll(); - - if ($chapters): ?> -
- - - - - - - - - - - - - - - - - - - -
НазваниеСтатусСловДействия
- - - - - - Редактировать - -
-
- -
-

В этой книге пока нет глав.

- - ✏️ Добавить первую главу - -
- -
- - - \ No newline at end of file diff --git a/book_regenerate_token.php b/book_regenerate_token.php deleted file mode 100755 index 6d84783..0000000 --- a/book_regenerate_token.php +++ /dev/null @@ -1,41 +0,0 @@ -userOwnsBook($book_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой книге"; - redirect('books.php'); -} - -// Генерируем новый токен -$new_token = $bookModel->generateNewShareToken($book_id); - -if ($new_token) { - $_SESSION['success'] = "Публичная ссылка обновлена"; -} else { - $_SESSION['error'] = "Ошибка при обновлении ссылки"; -} - -redirect("book_edit.php?id=$book_id"); -?> \ No newline at end of file diff --git a/books.php b/books.php deleted file mode 100755 index 64bff7a..0000000 --- a/books.php +++ /dev/null @@ -1,156 +0,0 @@ -findByUser($user_id); - -$page_title = "Мои книги"; -include 'views/header.php'; -?> - -

Мои книги

- - -
- - -
- - - -
- - -
- - -
-

Всего книг:

-
- ➕ Новая книга - - - -
-
- - -
-

У вас пока нет книг

-

Создайте свою первую книгу и начните писать!

- 📖 Создать первую книгу -
- -
- -
- -
- <?= e($book['title']) ?> -
- -
-

- - prepare("SELECT title FROM series WHERE id = ?"); - $series_stmt->execute([$book['series_id']]); - $series_title = $series_stmt->fetch()['title'] ?? ''; - ?> - -
- - 📚 Серия: - - (Книга ) - - -
- - -
- - ✏️ - - - 👁️ - - - 📑 - - - 📄 - -
- - - -
-
-

- - - -
- - -

- - -
-
- - Глав: | - Слов: - -
- -
-
- -
- - -

Удалить все книги?

-

Это действие удалит все ваши книги и все связанные с ними главы. Это действие нельзя отменить.

- -
- - - -
-
- - - - - \ No newline at end of file diff --git a/chapter_delete.php b/chapter_delete.php deleted file mode 100755 index 5073cca..0000000 --- a/chapter_delete.php +++ /dev/null @@ -1,43 +0,0 @@ -userOwnsChapter($chapter_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой главе"; - redirect('books.php'); -} - - -$chapter = $chapterModel->findById($chapter_id); -$book_id = $chapter['book_id']; - -// Удаляем главу -if ($chapterModel->delete($chapter_id)) { - $_SESSION['success'] = "Глава успешно удалена"; -} else { - $_SESSION['error'] = "Ошибка при удалении главы"; -} - -redirect("chapters.php?book_id=$book_id"); -?> \ No newline at end of file diff --git a/chapter_edit.php b/chapter_edit.php deleted file mode 100755 index 22a54da..0000000 --- a/chapter_edit.php +++ /dev/null @@ -1,386 +0,0 @@ -findById($chapter_id); - if (!$chapter || $chapter['user_id'] != $user_id) { - $_SESSION['error'] = "Глава не найдена или у вас нет доступа"; - redirect('books.php'); - } - $book_id = $chapter['book_id']; - $is_edit = true; -} - -if (!$book_id) { - $_SESSION['error'] = "Не указана книга"; - redirect('books.php'); -} - -if (!$bookModel->userOwnsBook($book_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой книге"; - redirect('books.php'); -} - -// Получаем информацию о книге -$book = $bookModel->findById($book_id); - -// Обработка формы -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Ошибка безопасности"; - redirect($is_edit ? "chapter_edit.php?id=$chapter_id" : "chapter_edit.php?book_id=$book_id"); - } - - // Обработка автосохранения - if (isset($_POST['autosave']) && $_POST['autosave'] === 'true') { - // Автосохранение работает только для существующих глав - // Если это не редактирование, игнорируем автосохранение - if (!$is_edit) { - - header('Content-Type: application/json'); - echo json_encode(['success' => false, 'message' => 'Автосохранение недоступно для новых глав']); - exit; - } - - $title = trim($_POST['title'] ?? ''); - $content = trim($_POST['content'] ?? ''); - $status = $_POST['status'] ?? 'draft'; - - if (empty($title)) { - header('Content-Type: application/json'); - echo json_encode(['success' => false, 'message' => 'Название главы обязательно']); - exit; - } - - $data = [ - 'title' => $title, - 'content' => $content, - 'status' => $status, - 'book_id' => $book_id - ]; - - $success = $chapterModel->update($chapter_id, $data); - - header('Content-Type: application/json'); - echo json_encode(['success' => $success]); - exit; - } - - // Обычная обработка формы (не автосохранение) - $title = trim($_POST['title'] ?? ''); - $content = trim($_POST['content'] ?? ''); - $status = $_POST['status'] ?? 'draft'; - - if (empty($title)) { - $_SESSION['error'] = "Название главы обязательно"; - } else { - $data = [ - 'title' => $title, - 'content' => $content, - 'status' => $status, - 'book_id' => $book_id - ]; - - if ($is_edit) { - $success = $chapterModel->update($chapter_id, $data); - $message = $success ? "Глава успешно обновлена" : "Ошибка при обновлении главы"; - } else { - $success = $chapterModel->create($data); - $message = $success ? "Глава успешно создана" : "Ошибка при создании главы"; - - if ($success) { - $new_chapter_id = $pdo->lastInsertId(); - redirect("chapter_edit.php?id=$new_chapter_id"); - } - } - - if ($success) { - $_SESSION['success'] = $message; - redirect("book_edit.php?id=$book_id"); - } else { - $_SESSION['error'] = $message; - } - } -} - -$page_title = $is_edit ? "Редактирование главы" : "Создание новой главы"; -include 'views/header.php'; -?> - -
-
- findByBook($book_id); - $current_index = null; - - // Находим индекс текущей главы - foreach ($chapters as $index => $chap) { - if ($chap['id'] == $chapter_id) { - $current_index = $index; - break; - } - } - - if ($current_index !== null && $current_index > 0): - $prev_chapter = $chapters[$current_index - 1]; - ?> - - ⬅️ Предыдущая: - - - - - - Следующая: ➡️ - - -
-
- - -

-

Книга:

- - -
- - -
- - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
- - - - ❌ Отмена - - - -
- - - - - -
- - ➕ Новая глава - - -
- - - -
-
- - - -
-
- findByBook($book_id); - $current_index = null; - - // Находим индекс текущей главы - foreach ($chapters as $index => $chap) { - if ($chap['id'] == $chapter_id) { - $current_index = $index; - break; - } - } - - if ($current_index !== null && $current_index > 0): - $prev_chapter = $chapters[$current_index - 1]; - ?> - - ⬅️ Предыдущая: - - - - - - Следующая: ➡️ - - -
-
- - - - - - - \ No newline at end of file diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..35e767f --- /dev/null +++ b/config/config.php @@ -0,0 +1,51 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +} catch(PDOException $e) { + error_log("DB Error: " . $e->getMessage()); + die("Ошибка подключения к базе данных"); +} + +// Добавляем константы для новых путей +define('CONTROLLERS_PATH', __DIR__ . '/../controllers/'); +define('VIEWS_PATH', __DIR__ . '/../views/'); +define('LAYOUTS_PATH', VIEWS_PATH . 'layouts/'); + +// Автозагрузка контроллеров +spl_autoload_register(function ($class_name) { + $controller_file = CONTROLLERS_PATH . $class_name . '.php'; + if (file_exists($controller_file)) { + require_once $controller_file; + } +}); +?> \ No newline at end of file diff --git a/controllers/AuthController.php b/controllers/AuthController.php new file mode 100644 index 0000000..2f6bdb9 --- /dev/null +++ b/controllers/AuthController.php @@ -0,0 +1,137 @@ +redirect('/dashboard'); + } + + $error = ''; + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $error = "Ошибка безопасности"; + } else { + $username = trim($_POST['username'] ?? ''); + $password = $_POST['password'] ?? ''; + + if (empty($username) || empty($password)) { + $error = 'Пожалуйста, введите имя пользователя и пароль'; + } else { + $userModel = new User($this->pdo); + $user = $userModel->findByUsername($username); + + if ($user && $userModel->verifyPassword($password, $user['password_hash'])) { + if (!$user['is_active']) { + $error = 'Ваш аккаунт деактивирован или ожидает активации администратором.'; + } else { + // Успешный вход + session_regenerate_id(true); + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['display_name'] = $user['display_name'] ?: $user['username']; + $_SESSION['avatar'] = $user['avatar'] ?? null; + + // Обновляем время последнего входа + $userModel->updateLastLogin($user['id']); + + $_SESSION['success'] = 'Добро пожаловать, ' . e($user['display_name'] ?: $user['username']) . '!'; + $this->redirect('/dashboard'); + } + } else { + $error = 'Неверное имя пользователя или пароль'; + } + } + } + } + + $this->render('auth/login', [ + 'error' => $error, + 'page_title' => 'Вход в систему' + ]); + } + + public function logout() { + // Очищаем все данные сессии + $_SESSION = []; + + if (ini_get("session.use_cookies")) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, + $params["path"], $params["domain"], + $params["secure"], $params["httponly"] + ); + } + + session_destroy(); + $this->redirect('/login'); + } + + public function register() { + // Если пользователь уже авторизован, перенаправляем на dashboard + if (is_logged_in()) { + $this->redirect('/dashboard'); + } + + $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'] ?? ''); + + // Валидация + if (empty($username) || empty($password)) { + $error = 'Имя пользователя и пароль обязательны'; + } elseif ($password !== $password_confirm) { + $error = 'Пароли не совпадают'; + } elseif (strlen($password) < 6) { + $error = 'Пароль должен быть не менее 6 символов'; + } else { + $userModel = new User($this->pdo); + + // Проверяем, не занят ли username + if ($userModel->findByUsername($username)) { + $error = 'Имя пользователя уже занято'; + } elseif ($email && $userModel->findByEmail($email)) { + $error = 'Email уже используется'; + } else { + $data = [ + 'username' => $username, + 'password' => $password, + 'email' => $email ?: null, + 'display_name' => $display_name ?: $username, + 'is_active' => 1 // Авто-активация для простоты + ]; + + if ($userModel->create($data)) { + $success = 'Регистрация успешна! Теперь вы можете войти в систему.'; + // Можно автоматически войти после регистрации + // $this->redirect('/login'); + } else { + $error = 'Ошибка при создании аккаунта'; + } + } + } + } + } + + $this->render('auth/register', [ + 'error' => $error, + 'success' => $success, + 'page_title' => 'Регистрация' + ]); + } +} +?> \ No newline at end of file diff --git a/controllers/BaseController.php b/controllers/BaseController.php new file mode 100644 index 0000000..572b033 --- /dev/null +++ b/controllers/BaseController.php @@ -0,0 +1,33 @@ +pdo = $pdo; + } + + protected function render($view, $data = []) { + extract($data); + include "views/$view.php"; + } + + protected function redirect($url) { + header("Location: " . SITE_URL . $url); + exit; + } + + protected function requireLogin() { + if (!is_logged_in()) { + $this->redirect('/login'); + } + } + + protected function jsonResponse($data) { + header('Content-Type: application/json'); + echo json_encode($data); + exit; + } +} +?> \ No newline at end of file diff --git a/controllers/BookController.php b/controllers/BookController.php new file mode 100644 index 0000000..f4377fe --- /dev/null +++ b/controllers/BookController.php @@ -0,0 +1,268 @@ +requireLogin(); + $user_id = $_SESSION['user_id']; + $bookModel = new Book($this->pdo); + $books = $bookModel->findByUser($user_id); + $this->render('books/index', [ + 'books' => $books, + 'page_title' => 'Мои книги' + ]); + } + + public function create() { + $this->requireLogin(); + $seriesModel = new Series($this->pdo); + $series = $seriesModel->findByUser($_SESSION['user_id']); + + // Возвращаем типы редакторов для выбора + $editor_types = [ + 'markdown' => 'Markdown редактор', + 'html' => 'HTML редактор (TinyMCE)' + ]; + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $_SESSION['error'] = "Ошибка безопасности"; + $this->redirect('/books/create'); + } + + $title = trim($_POST['title'] ?? ''); + if (empty($title)) { + $_SESSION['error'] = "Название книги обязательно"; + $this->redirect('/books/create'); + } + + $bookModel = new Book($this->pdo); + $data = [ + 'title' => $title, + '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"); + } else { + $_SESSION['error'] = "Ошибка при создании книги"; + } + } + + $this->render('books/create', [ + 'series' => $series, + 'editor_types' => $editor_types, + 'selected_editor' => 'markdown', // по умолчанию + 'page_title' => 'Создание новой книги' + ]); + } + + public function edit($id) { + $this->requireLogin(); + $bookModel = new Book($this->pdo); + $book = $bookModel->findById($id); + + if (!$book || $book['user_id'] != $_SESSION['user_id']) { + $_SESSION['error'] = "Книга не найдена или у вас нет доступа"; + $this->redirect('/books'); + } + + $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'] ?? '')) { + $error = "Ошибка безопасности"; + } else { + $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); + + $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); + if ($cover_result['success']) { + $bookModel->updateCover($id, $cover_result['filename']); + } else { + $cover_error = $cover_result['error']; + } + } + + // Удаление обложки + if (isset($_POST['delete_cover']) && $_POST['delete_cover'] == '1') { + $bookModel->deleteCover($id); + } + + // Обновление книги + $success = $bookModel->update($id, $data); + + if ($success) { + $success_message = "Книга успешно обновлена"; + if ($editor_changed) { + $success_message .= ". Содержание глав сконвертировано в новый формат."; + } + $_SESSION['success'] = $success_message; + $this->redirect("/books/{$id}/edit"); + } else { + $error = "Ошибка при обновлении книги"; + } + } + } + } + + // Получаем статистику по главам для отображения в шаблоне + $chapterModel = new Chapter($this->pdo); + $chapters = $chapterModel->findByBook($id); + + $this->render('books/edit', [ + 'book' => $book, + 'series' => $series, + 'chapters' => $chapters, + 'editor_types' => $editor_types, + 'error' => $error, + 'cover_error' => $cover_error, + 'page_title' => 'Редактирование книги' + ]); + } + + public function delete($id) { + $this->requireLogin(); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $_SESSION['error'] = "Неверный метод запроса"; + $this->redirect('/books'); + } + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $_SESSION['error'] = "Ошибка безопасности"; + $this->redirect('/books'); + } + $user_id = $_SESSION['user_id']; + $bookModel = new Book($this->pdo); + if (!$bookModel->userOwnsBook($id, $user_id)) { + $_SESSION['error'] = "У вас нет доступа к этой книге"; + $this->redirect('/books'); + } + if ($bookModel->delete($id, $user_id)) { + $_SESSION['success'] = "Книга успешно удалена"; + } else { + $_SESSION['error'] = "Ошибка при удалении книги"; + } + $this->redirect('/books'); + } + + public function viewPublic($share_token) { + $bookModel = new Book($this->pdo); + $book = $bookModel->findByShareToken($share_token); + if (!$book) { + http_response_code(404); + $this->render('errors/404'); + return; + } + $chapters = $bookModel->getPublishedChapters($book['id']); + + // Получаем информацию об авторе + $stmt = $this->pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?"); + $stmt->execute([$book['user_id']]); + $author = $stmt->fetch(PDO::FETCH_ASSOC); + + $this->render('books/view_public', [ + 'book' => $book, + 'chapters' => $chapters, + 'author' => $author, + 'page_title' => $book['title'] + ]); + } + + 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(); + 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'); + } + $new_token = $bookModel->generateNewShareToken($id); + if ($new_token) { + $_SESSION['success'] = "Ссылка успешно обновлена"; + } else { + $_SESSION['error'] = "Ошибка при обновлении ссылки"; + } + $this->redirect("/books/{$id}/edit"); + } +} +?> \ No newline at end of file diff --git a/controllers/ChapterController.php b/controllers/ChapterController.php new file mode 100644 index 0000000..bc57f2f --- /dev/null +++ b/controllers/ChapterController.php @@ -0,0 +1,199 @@ +requireLogin(); + + $user_id = $_SESSION['user_id']; + + $bookModel = new Book($this->pdo); + $chapterModel = new Chapter($this->pdo); + + // Проверяем права доступа к книге + if (!$bookModel->userOwnsBook($book_id, $user_id)) { + $_SESSION['error'] = "У вас нет доступа к этой книге"; + $this->redirect('/books'); + } + + // Получаем информацию о книге и главах + $book = $bookModel->findById($book_id); + $chapters = $chapterModel->findByBook($book_id); + + $this->render('chapters/index', [ + 'book' => $book, + 'chapters' => $chapters, + 'page_title' => "Главы книги: " . e($book['title']) + ]); + } + + public function create($book_id) { + $this->requireLogin(); + + $user_id = $_SESSION['user_id']; + + $bookModel = new Book($this->pdo); + $chapterModel = new Chapter($this->pdo); + + // Проверяем права доступа к книге + if (!$bookModel->userOwnsBook($book_id, $user_id)) { + $_SESSION['error'] = "У вас нет доступа к этой книге"; + $this->redirect('/books'); + } + + $book = $bookModel->findById($book_id); + $error = ''; + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $error = "Ошибка безопасности"; + } else { + $title = trim($_POST['title'] ?? ''); + $content = $_POST['content'] ?? ''; + $status = $_POST['status'] ?? 'draft'; + + if (empty($title)) { + $error = "Название главы обязательно"; + } else { + $data = [ + 'book_id' => $book_id, + 'title' => $title, + 'content' => $content, + 'status' => $status + ]; + + if ($chapterModel->create($data)) { + $_SESSION['success'] = "Глава успешно создана"; + $this->redirect("/books/{$book_id}/chapters"); + } else { + $error = "Ошибка при создании главы"; + } + } + } + } + + $this->render('chapters/create', [ + 'book' => $book, + 'error' => $error, + 'page_title' => "Новая глава для: " . e($book['title']) + ]); + } + + public function edit($id) { + $this->requireLogin(); + + $user_id = $_SESSION['user_id']; + + $chapterModel = new Chapter($this->pdo); + $bookModel = new Book($this->pdo); + + // Проверяем права доступа к главе + if (!$chapterModel->userOwnsChapter($id, $user_id)) { + $_SESSION['error'] = "У вас нет доступа к этой главе"; + $this->redirect('/books'); + } + + $chapter = $chapterModel->findById($id); + $book = $bookModel->findById($chapter['book_id']); + $error = ''; + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $error = "Ошибка безопасности"; + } else { + $title = trim($_POST['title'] ?? ''); + $content = $_POST['content'] ?? ''; + $status = $_POST['status'] ?? 'draft'; + + if (empty($title)) { + $error = "Название главы обязательно"; + } else { + $data = [ + 'title' => $title, + 'content' => $content, + 'status' => $status + ]; + + if ($chapterModel->update($id, $data)) { + $_SESSION['success'] = "Глава успешно обновлена"; + $this->redirect("/books/{$chapter['book_id']}/chapters"); + } else { + $error = "Ошибка при обновлении главы"; + } + } + } + } + + $this->render('chapters/edit', [ + 'chapter' => $chapter, + 'book' => $book, + 'error' => $error, + 'page_title' => "Редактирование главы: " . e($chapter['title']) + ]); + } + + public function delete($id) { + $this->requireLogin(); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $_SESSION['error'] = "Неверный метод запроса"; + $this->redirect('/books'); + } + + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $_SESSION['error'] = "Ошибка безопасности"; + $this->redirect('/books'); + } + + $user_id = $_SESSION['user_id']; + $chapterModel = new Chapter($this->pdo); + + // Проверяем права доступа + if (!$chapterModel->userOwnsChapter($id, $user_id)) { + $_SESSION['error'] = "У вас нет доступа к этой главе"; + $this->redirect('/books'); + } + + $chapter = $chapterModel->findById($id); + $book_id = $chapter['book_id']; + + // Удаляем главу + if ($chapterModel->delete($id)) { + $_SESSION['success'] = "Глава успешно удалена"; + } else { + $_SESSION['error'] = "Ошибка при удалении главы"; + } + + $this->redirect("/books/{$book_id}/chapters"); + } + + 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') { + $html_content = $Parsedown->text($content); + } else { + $html_content = $content; + } + + $this->render('chapters/preview', [ + 'content' => $html_content, + 'title' => $title, + 'page_title' => "Предпросмотр: " . e($title) + ]); + } +} +?> \ No newline at end of file diff --git a/controllers/DashboardController.php b/controllers/DashboardController.php new file mode 100644 index 0000000..b738410 --- /dev/null +++ b/controllers/DashboardController.php @@ -0,0 +1,52 @@ +requireLogin(); + + $user_id = $_SESSION['user_id']; + + $bookModel = new Book($this->pdo); + $chapterModel = new Chapter($this->pdo); + $seriesModel = new Series($this->pdo); + + // Получаем статистику + $books = $bookModel->findByUser($user_id); + $published_books = $bookModel->findByUser($user_id, true); + + $total_books = count($books); + $published_books_count = count($published_books); + + // Общее количество слов и глав + $total_words = 0; + $total_chapters = 0; + foreach ($books as $book) { + $stats = $bookModel->getBookStats($book['id']); + $total_words += $stats['total_words'] ?? 0; + $total_chapters += $stats['chapter_count'] ?? 0; + } + + // Последние книги + $recent_books = array_slice($books, 0, 5); + + // Серии + $series = $seriesModel->findByUser($user_id); + + $this->render('dashboard/index', [ + 'total_books' => $total_books, + 'published_books_count' => $published_books_count, + 'total_words' => $total_words, + 'total_chapters' => $total_chapters, + 'recent_books' => $recent_books, + 'series' => $series, + 'page_title' => 'Панель управления' + ]); + } +} +?> \ No newline at end of file diff --git a/controllers/ExportController.php b/controllers/ExportController.php new file mode 100644 index 0000000..da07a15 --- /dev/null +++ b/controllers/ExportController.php @@ -0,0 +1,883 @@ +requireLogin(); + + $user_id = $_SESSION['user_id']; + + $bookModel = new Book($this->pdo); + $chapterModel = new Chapter($this->pdo); + + $book = $bookModel->findById($book_id); + if (!$book || $book['user_id'] != $user_id) { + $_SESSION['error'] = "Доступ запрещен"; + $this->redirect('/books'); + } + + // Для автора - все главы + $chapters = $chapterModel->findByBook($book_id); + + // Получаем информацию об авторе + $author_name = $this->getAuthorName($book['user_id']); + + $this->handleExport($book, $chapters, false, $author_name, $format); + } + + public function exportShared($share_token, $format = 'pdf') { + $bookModel = new Book($this->pdo); + $chapterModel = new Chapter($this->pdo); + + $book = $bookModel->findByShareToken($share_token); + if (!$book) { + $_SESSION['error'] = "Книга не найдена"; + $this->redirect('/'); + } + + // Для публичного доступа - только опубликованные главы + $chapters = $bookModel->getPublishedChapters($book['id']); + + // Получаем информацию об авторе + $author_name = $this->getAuthorName($book['user_id']); + + $this->handleExport($book, $chapters, true, $author_name, $format); + } + + private function getAuthorName($user_id) { + $stmt = $this->pdo->prepare("SELECT display_name, username FROM users WHERE id = ?"); + $stmt->execute([$user_id]); + $author_info = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($author_info && $author_info['display_name'] != "") { + return $author_info['display_name']; + } elseif ($author_info) { + return $author_info['username']; + } + + return "Неизвестный автор"; + } + + 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); + break; + case 'docx': + $this->exportDOCX($book, $chapters, $is_public, $author_name, $Parsedown); + break; + case 'html': + $this->exportHTML($book, $chapters, $is_public, $author_name, $Parsedown); + break; + case 'txt': + $this->exportTXT($book, $chapters, $is_public, $author_name, $Parsedown); + break; + default: + $_SESSION['error'] = "Неверный формат экспорта"; + $redirect_url = $is_public ? + "/book/{$book['share_token']}" : + "/books/{$book['id']}/edit"; + $this->redirect($redirect_url); + } + } + + 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); + + // Устанавливаем метаданные документа + $pdf->SetCreator(APP_NAME); + $pdf->SetAuthor($author_name); + $pdf->SetTitle($book['title']); + $pdf->SetSubject($book['genre'] ?? ''); + + // Устанавливаем margins + $pdf->SetMargins(15, 25, 15); + $pdf->SetHeaderMargin(10); + $pdf->SetFooterMargin(10); + + // Устанавливаем авто разрыв страниц + $pdf->SetAutoPageBreak(TRUE, 15); + + // Добавляем страницу + $pdf->AddPage(); + + // Устанавливаем шрифт с поддержкой кириллицы + $pdf->SetFont('dejavusans', '', 12); + + // Заголовок книги + $pdf->SetFont('dejavusans', 'B', 18); + $pdf->Cell(0, 10, $book['title'], 0, 1, 'C'); + $pdf->Ln(2); + + // Автор + $pdf->SetFont('dejavusans', 'I', 14); + $pdf->Cell(0, 10, $author_name, 0, 1, 'C'); + $pdf->Ln(5); + + // Обложка книги + if (!empty($book['cover_image'])) { + $cover_path = COVERS_PATH . $book['cover_image']; + if (file_exists($cover_path)) { + list($width, $height) = getimagesize($cover_path); + $max_width = 80; + $ratio = $width / $height; + $new_height = $max_width / $ratio; + + $x = (210 - $max_width) / 2; + $pdf->Image($cover_path, $x, $pdf->GetY(), $max_width, $new_height, '', '', 'N', false, 300, '', false, false, 0, false, false, false); + $pdf->Ln($new_height + 5); + } + } + + // Жанр + if (!empty($book['genre'])) { + $pdf->SetFont('dejavusans', 'I', 12); + $pdf->Cell(0, 10, 'Жанр: ' . $book['genre'], 0, 1, 'C'); + $pdf->Ln(5); + } + + // Описание + if (!empty($book['description'])) { + $pdf->SetFont('dejavusans', '', 11); + $pdf->MultiCell(0, 6, $book['description'], 0, 'J'); + $pdf->Ln(10); + } + + // Интерактивное оглавление + $chapterLinks = []; + if (!empty($chapters)) { + $pdf->SetFont('dejavusans', 'B', 14); + $pdf->Cell(0, 10, 'Оглавление', 0, 1, 'C'); + $pdf->Ln(5); + + $toc_page = $pdf->getPage(); + + $pdf->SetFont('dejavusans', '', 11); + foreach ($chapters as $index => $chapter) { + $chapter_number = $index + 1; + $link = $pdf->AddLink(); + $chapterLinks[$chapter['id']] = $link; // Сохраняем ссылку для этой главы + $pdf->Cell(0, 6, "{$chapter_number}. {$chapter['title']}", 0, 1, 'L', false, $link); + } + $pdf->Ln(10); + } + + // Разделитель + $pdf->Line(15, $pdf->GetY(), 195, $pdf->GetY()); + $pdf->Ln(10); + + // Главы с закладками и правильными ссылками + foreach ($chapters as $index => $chapter) { + // Добавляем новую страницу для каждой главы + $pdf->AddPage(); + + // УСТАНАВЛИВАЕМ ЯКОРЬ ДЛЯ ССЫЛКИ + if (isset($chapterLinks[$chapter['id']])) { + $pdf->SetLink($chapterLinks[$chapter['id']]); + } + + // Устанавливаем закладку для этой главы + $pdf->Bookmark($chapter['title'], 0, 0, '', 'B', array(0, 0, 0)); + + // Название главы + $pdf->SetFont('dejavusans', 'B', 14); + $pdf->Cell(0, 8, $chapter['title'], 0, 1); + $pdf->Ln(2); + + // Контент главы + $pdf->SetFont('dejavusans', '', 11); + if ($book['editor_type'] == 'markdown') { + $htmlContent = $Parsedown->text($chapter['content']); + } else { + $htmlContent = $chapter['content']; + } + $pdf->writeHTML($htmlContent, true, false, true, false, ''); + + $pdf->Ln(8); + } + + // Футер с информацией + $pdf->SetY(-25); + $pdf->SetFont('dejavusans', 'I', 8); + $pdf->Cell(0, 6, 'Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), 0, 1, 'C'); + $pdf->Cell(0, 6, 'Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), 0, 1, 'C'); + + // Отправляем файл + $filename = cleanFilename($book['title']) . '.pdf'; + $pdf->Output($filename, 'D'); + exit; + } + + function exportDOCX($book, $chapters, $is_public, $author_name) { + global $Parsedown; + + $phpWord = new PhpWord(); + + // Стили документа + $phpWord->setDefaultFontName('Times New Roman'); + $phpWord->setDefaultFontSize(12); + + // Секция документа + $section = $phpWord->addSection(); + + // Заголовок книги + $section->addText($book['title'], ['bold' => true, 'size' => 16], ['alignment' => 'center']); + $section->addTextBreak(1); + + // Автор + $section->addText($author_name, ['italic' => true, 'size' => 14], ['alignment' => 'center']); + $section->addTextBreak(2); + + // Обложка книги + if (!empty($book['cover_image'])) { + $cover_path = COVERS_PATH . $book['cover_image']; + if (file_exists($cover_path)) { + $section->addImage($cover_path, [ + 'width' => 150, + 'height' => 225, + 'alignment' => 'center' + ]); + $section->addTextBreak(2); + } + } + + // Жанр + if (!empty($book['genre'])) { + $section->addText('Жанр: ' . $book['genre'], ['italic' => true], ['alignment' => 'center']); + $section->addTextBreak(1); + } + + // Описание + if (!empty($book['description'])) { + if ($book['editor_type'] == 'markdown') { + $descriptionParagraphs = $this->markdownToParagraphs($book['description']); + } else { + $descriptionParagraphs = $this->htmlToParagraphs($book['description']); + } + + foreach ($descriptionParagraphs as $paragraph) { + if (!empty(trim($paragraph))) { + $section->addText($paragraph); + } + } + $section->addTextBreak(2); + } + + // Интерактивное оглавление + if (!empty($chapters)) { + $section->addText('Оглавление', ['bold' => true, 'size' => 14], ['alignment' => 'center']); + $section->addTextBreak(1); + + foreach ($chapters as $index => $chapter) { + $chapter_number = $index + 1; + // Создаем гиперссылку на заголовок главы + $section->addLink("chapter_{$chapter['id']}", "{$chapter_number}. {$chapter['title']}", null, null, true); + $section->addTextBreak(1); + } + $section->addTextBreak(2); + } + + // Разделитель + $section->addPageBreak(); + + // Главы с закладками + foreach ($chapters as $index => $chapter) { + // Добавляем закладку для главы + $section->addBookmark("chapter_{$chapter['id']}"); + + // Заголовок главы + $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']); + } + + // Добавляем каждый абзац + foreach ($paragraphs as $paragraph) { + if (!empty(trim($paragraph))) { + $section->addText($paragraph); + $section->addTextBreak(1); + } + } + + // Добавляем разрыв страницы между главами (кроме последней) + if ($index < count($chapters) - 1) { + $section->addPageBreak(); + } + } + + // Футер + $section->addTextBreak(2); + $section->addText('Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), ['italic' => true, 'size' => 9]); + $section->addText('Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), ['italic' => true, 'size' => 9]); + + // Сохраняем и отправляем + $filename = cleanFilename($book['title']) . '.docx'; + header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + + $objWriter = IOFactory::createWriter($phpWord, 'Word2007'); + $objWriter->save('php://output'); + exit; + } + + function exportHTML($book, $chapters, $is_public, $author_name) { + global $Parsedown; + + $html = ' + + + + + ' . htmlspecialchars($book['title']) . ' + + + +
' . htmlspecialchars($book['title']) . '
+
' . htmlspecialchars($author_name) . '
'; + + if (!empty($book['genre'])) { + $html .= '
Жанр: ' . htmlspecialchars($book['genre']) . '
'; + } + + // Обложка книги + if (!empty($book['cover_image'])) { + $cover_url = COVERS_URL . $book['cover_image']; + $html .= '
'; + $html .= '' . htmlspecialchars($book['title']) . ''; + $html .= '
'; + } + + if (!empty($book['description'])) { + $html .= '
'; + if ($book['editor_type'] == 'markdown') { + $html .= nl2br(htmlspecialchars($book['description'])); + } else { + $html .= $book['description']; + } + $html .= '
'; + } + + // Интерактивное оглавление + if (!empty($chapters)) { + $html .= '
'; + $html .= '

Оглавление

'; + $html .= ''; + $html .= '
'; + } + + $html .= '
'; + + foreach ($chapters as $index => $chapter) { + $html .= '
'; + $html .= '
' . htmlspecialchars($chapter['title']) . '
'; + + // Обрабатываем контент в зависимости от типа редактора + if ($book['editor_type'] == 'markdown') { + $htmlContent = $Parsedown->text($chapter['content']); + } else { + $htmlContent = $chapter['content']; + } + + $html .= '
' . $htmlContent . '
'; + $html .= '
'; + + if ($index < count($chapters) - 1) { + $html .= '
'; + } + } + + $html .= ' + + '; + + $filename = cleanFilename($book['title']) . '.html'; + header('Content-Type: text/html; charset=utf-8'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + echo $html; + exit; + } + + function exportTXT($book, $chapters, $is_public, $author_name) { + $content = "=" . str_repeat("=", 80) . "=\n"; + $content .= str_pad($book['title'], 80, " ", STR_PAD_BOTH) . "\n"; + $content .= str_pad($author_name, 80, " ", STR_PAD_BOTH) . "\n"; + $content .= "=" . str_repeat("=", 80) . "=\n\n"; + + if (!empty($book['genre'])) { + $content .= "Жанр: " . $book['genre'] . "\n\n"; + } + + if (!empty($book['description'])) { + $content .= "ОПИСАНИЕ:\n"; + + // Обрабатываем описание в зависимости от типа редактора + if ($book['editor_type'] == 'markdown') { + $descriptionText = $this->cleanMarkdown($book['description']); + } else { + $descriptionText = strip_tags($book['description']); + } + + $content .= wordwrap($descriptionText, 144) . "\n\n"; + } + + // Оглавление + if (!empty($chapters)) { + $content .= "ОГЛАВЛЕНИЕ:\n"; + $content .= str_repeat("-", 60) . "\n"; + foreach ($chapters as $index => $chapter) { + $chapter_number = $index + 1; + $content .= "{$chapter_number}. {$chapter['title']}\n"; + } + $content .= "\n"; + } + + $content .= str_repeat("-", 144) . "\n\n"; + + foreach ($chapters as $index => $chapter) { + $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); + } + + foreach ($paragraphs as $paragraph) { + if (!empty(trim($paragraph))) { + $content .= wordwrap($paragraph, 144) . "\n\n"; + } + } + + if ($index < count($chapters) - 1) { + $content .= str_repeat("-", 144) . "\n\n"; + } + } + + $content .= "\n" . str_repeat("=", 144) . "\n"; + $content .= "Экспортировано из " . APP_NAME . " - " . date('d.m.Y H:i') . "\n"; + $content .= "Автор: " . $author_name . " | Всего глав: " . count($chapters) . " | Всего слов: " . array_sum(array_column($chapters, 'word_count')) . "\n"; + $content .= str_repeat("=", 144) . "\n"; + + $filename = cleanFilename($book['title']) . '.txt'; + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + echo $content; + 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 на абзацы + function htmlToParagraphs($html) { + // Убираем HTML теги и нормализуем пробелы + $text = strip_tags($html); + $text = preg_replace('/\s+/', ' ', $text); + + // Разбиваем на абзацы по точкам и переносам строк + $paragraphs = preg_split('/(?<=[.!?])\s+/', $text); + + // Фильтруем пустые абзацы + $paragraphs = array_filter($paragraphs, function($paragraph) { + return !empty(trim($paragraph)); + }); + + return $paragraphs; + } + function htmlToPlainTextParagraphs($html) { + // Убираем HTML теги + $text = strip_tags($html); + + // Заменяем HTML entities + $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // Нормализуем переносы строк + $text = str_replace(["\r\n", "\r"], "\n", $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 (!empty($currentParagraph)) { + $currentParagraph .= ' ' . $trimmedLine; + } else { + $currentParagraph = $trimmedLine; + } + } + + // Добавляем последний абзац + if (!empty($currentParagraph)) { + $paragraphs[] = $currentParagraph; + } + + return $paragraphs; + } +} +?> \ No newline at end of file diff --git a/controllers/SeriesController.php b/controllers/SeriesController.php new file mode 100644 index 0000000..2bc3e16 --- /dev/null +++ b/controllers/SeriesController.php @@ -0,0 +1,194 @@ +requireLogin(); + + $user_id = $_SESSION['user_id']; + $seriesModel = new Series($this->pdo); + $series = $seriesModel->findByUser($user_id); + + // Получаем статистику для каждой серии отдельно + foreach ($series as &$ser) { + $stats = $seriesModel->getSeriesStats($ser['id'], $user_id); + $ser['book_count'] = $stats['book_count'] ?? 0; + $ser['total_words'] = $stats['total_words'] ?? 0; + } + unset($ser); + + $this->render('series/index', [ + 'series' => $series, + 'page_title' => "Мои серии книг" + ]); + } + + public function create() { + $this->requireLogin(); + + $error = ''; + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $error = "Ошибка безопасности"; + } else { + $title = trim($_POST['title'] ?? ''); + $description = trim($_POST['description'] ?? ''); + + if (empty($title)) { + $error = "Название серии обязательно"; + } else { + $seriesModel = new Series($this->pdo); + $data = [ + 'title' => $title, + 'description' => $description, + 'user_id' => $_SESSION['user_id'] + ]; + + if ($seriesModel->create($data)) { + $_SESSION['success'] = "Серия успешно создана"; + $new_series_id = $this->pdo->lastInsertId(); + $this->redirect("/series/{$new_series_id}/edit"); + } else { + $error = "Ошибка при создании серии"; + } + } + } + } + + $this->render('series/create', [ + 'error' => $error, + 'page_title' => "Создание новой серии" + ]); + } + + public function edit($id) { + $this->requireLogin(); + + $user_id = $_SESSION['user_id']; + $seriesModel = new Series($this->pdo); + $series = $seriesModel->findById($id); + + if (!$series || !$seriesModel->userOwnsSeries($id, $user_id)) { + $_SESSION['error'] = "Серия не найдена или у вас нет доступа"; + $this->redirect('/series'); + } + + $error = ''; + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $error = "Ошибка безопасности"; + } else { + $title = trim($_POST['title'] ?? ''); + $description = trim($_POST['description'] ?? ''); + + if (empty($title)) { + $error = "Название серии обязательно"; + } else { + $data = [ + 'title' => $title, + 'description' => $description, + 'user_id' => $user_id + ]; + + if ($seriesModel->update($id, $data)) { + $_SESSION['success'] = "Серия успешно обновлена"; + $this->redirect('/series'); + } else { + $error = "Ошибка при обновлении серии"; + } + } + } + } + + // Получаем книги в серии + $bookModel = new Book($this->pdo); + $books_in_series = $bookModel->findBySeries($id); + + $this->render('series/edit', [ + 'series' => $series, + 'books_in_series' => $books_in_series, + 'error' => $error, + 'page_title' => "Редактирование серии: " . e($series['title']) + ]); + } + + public function delete($id) { + $this->requireLogin(); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $_SESSION['error'] = "Неверный метод запроса"; + $this->redirect('/series'); + } + + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $_SESSION['error'] = "Ошибка безопасности"; + $this->redirect('/series'); + } + + $user_id = $_SESSION['user_id']; + $seriesModel = new Series($this->pdo); + + if (!$seriesModel->userOwnsSeries($id, $user_id)) { + $_SESSION['error'] = "У вас нет доступа к этой серии"; + $this->redirect('/series'); + } + + if ($seriesModel->delete($id, $user_id)) { + $_SESSION['success'] = "Серия успешно удалена"; + } else { + $_SESSION['error'] = "Ошибка при удалении серии"; + } + + $this->redirect('/series'); + } + + public function viewPublic($id) { + $seriesModel = new Series($this->pdo); + $series = $seriesModel->findById($id); + + if (!$series) { + http_response_code(404); + $this->render('errors/404'); + return; + } + + // Получаем только опубликованные книги серии + $books = $seriesModel->getBooksInSeries($id, true); + + // Получаем информацию об авторе + $stmt = $this->pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?"); + $stmt->execute([$series['user_id']]); + $author = $stmt->fetch(PDO::FETCH_ASSOC); + + // Получаем статистику по опубликованным книгам + $bookModel = new Book($this->pdo); + $total_words = 0; + $total_chapters = 0; + + foreach ($books as $book) { + $book_stats = $bookModel->getBookStats($book['id'], true); + $total_words += $book_stats['total_words'] ?? 0; + $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'] . ' — серия книг' + ]); + } +} +?> \ No newline at end of file diff --git a/controllers/UserController.php b/controllers/UserController.php new file mode 100644 index 0000000..6306ce4 --- /dev/null +++ b/controllers/UserController.php @@ -0,0 +1,117 @@ +requireLogin(); + + $user_id = $_SESSION['user_id']; + $userModel = new User($this->pdo); + $user = $userModel->findById($user_id); + + $message = ''; + $avatar_error = ''; + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $message = "Ошибка безопасности"; + } else { + $display_name = trim($_POST['display_name'] ?? ''); + $email = trim($_POST['email'] ?? ''); + $bio = trim($_POST['bio'] ?? ''); + + // Обработка загрузки аватарки + if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) { + $avatar_result = handleAvatarUpload($_FILES['avatar'], $user_id); + if ($avatar_result['success']) { + $userModel->updateAvatar($user_id, $avatar_result['filename']); + // Обновляем данные пользователя + $user = $userModel->findById($user_id); + } else { + $avatar_error = $avatar_result['error']; + } + } + + // Обработка удаления аватарки + if (isset($_POST['delete_avatar']) && $_POST['delete_avatar'] == '1') { + deleteUserAvatar($user_id); + $user = $userModel->findById($user_id); + } + + // Обновляем основные данные + $data = [ + 'display_name' => $display_name, + 'email' => $email, + 'bio' => $bio + ]; + + if ($userModel->updateProfile($user_id, $data)) { + $_SESSION['display_name'] = $display_name ?: $user['username']; + $message = "Профиль обновлен"; + // Обновляем данные пользователя + $user = $userModel->findById($user_id); + } else { + $message = "Ошибка при обновлении профиля"; + } + } + } + + $this->render('user/profile', [ + 'user' => $user, + 'message' => $message, + 'avatar_error' => $avatar_error, + 'page_title' => "Мой профиль" + ]); + } + + public function updateProfile() { + $this->requireLogin(); + + // Эта функция обрабатывает AJAX или прямые POST запросы для обновления профиля + // Можно объединить с методом profile() или оставить отдельно для API-like операций + $this->profile(); // Перенаправляем на основной метод + } + + public function viewPublic($id) { + $userModel = new User($this->pdo); + $user = $userModel->findById($id); + + if (!$user) { + http_response_code(404); + $this->render('errors/404'); + return; + } + + $bookModel = new Book($this->pdo); + $books = $bookModel->findByUser($id, true); // только опубликованные + + // Получаем статистику автора + $total_books = count($books); + $total_words = 0; + $total_chapters = 0; + + foreach ($books as $book) { + $book_stats = $bookModel->getBookStats($book['id'], true); + $total_words += $book_stats['total_words'] ?? 0; + $total_chapters += $book_stats['chapter_count'] ?? 0; + } + + $Parsedown = new ParsedownExtra(); + + $this->render('user/view_public', [ + 'user' => $user, + 'books' => $books, + 'total_books' => $total_books, + 'total_words' => $total_words, + 'total_chapters' => $total_chapters, + 'Parsedown' => $Parsedown, + 'page_title' => ($user['display_name'] ?: $user['username']) . ' — публичная страница' + ]); + } +} +?> \ No newline at end of file diff --git a/dashboard.php b/dashboard.php deleted file mode 100755 index 841a62b..0000000 --- a/dashboard.php +++ /dev/null @@ -1,191 +0,0 @@ -findByUser($user_id); -$series = $seriesModel->findByUser($user_id); - -// Статистика по книгам -$total_chapters = 0; -$total_words = 0; -foreach ($books as $book) { - $total_chapters += $book['chapter_count']; - $total_words += $book['total_words']; -} - -// Статистика по сериям -$series_stats = [ - 'total_series' => count($series), - 'series_with_books' => 0, - 'total_books_in_series' => 0 -]; - -foreach ($series as $ser) { - $series_books = $seriesModel->getBooksInSeries($ser['id']); - $series_stats['total_books_in_series'] += count($series_books); - if (count($series_books) > 0) { - $series_stats['series_with_books']++; - } -} - -$page_title = "Панель управления"; -include 'views/header.php'; -?> - -

Добро пожаловать, !

- -
- ✏️ Редактировать профиль -
- -
-
-

📚 Мои книги

-

Управляйте вашими книгами и главами

- -
- -
-

📊 Статистика

-
-

Книг:

-

Глав:

-

Всего слов:

- 0): ?> -

Средняя глава: слов

- -
-
- -
-

📖 Мои серии

-

Управляйте сериями книг

- - - 0): ?> -
-

Книг в сериях:

-

Заполненных серий:

-
- -
-
- - -
-

Недавние книги

-
- - - -
- - 3): ?> -
- 📚 Показать все книги () -
- -
- - - -
-

Недавние серии

-
- -
-

- - getBooksInSeries($ser['id']); - $series_words = 0; - $series_chapters = 0; - - foreach ($books_in_series as $book) { - $book_stats = $bookModel->getBookStats($book['id']); - $series_words += $book_stats['total_words'] ?? 0; - $series_chapters += $book_stats['chapter_count'] ?? 0; - } - ?> - -

Книг: | Глав: | Слов:

- - -
- -
- - 3): ?> -
- 📖 Показать все серии () -
- -
- - - -
-

Добро пожаловать в !

-

Начните создавать свои литературные произведения

-
- 📖 Создать первую книгу - 📚 Создать первую серию -
-
- ✏️ Настроить профиль -
-
- - - \ No newline at end of file diff --git a/export_book.php b/export_book.php deleted file mode 100755 index 00fcc5b..0000000 --- a/export_book.php +++ /dev/null @@ -1,869 +0,0 @@ -findByShareToken($share_token); - // Для публичного доступа - только опубликованные главы - $chapters = $bookModel->getPublishedChapters($book['id']); - $is_public = true; -} elseif ($book_id && $user_id) { - $book = $bookModel->findById($book_id); - if (!$book || $book['user_id'] != $user_id) { - $_SESSION['error'] = "Доступ запрещен"; - redirect('books.php'); - } - // Для автора - все главы - $chapters = $chapterModel->findByBook($book_id); - $is_public = false; -} else { - $_SESSION['error'] = "Книга не найдена"; - redirect('books.php'); -} - -if (!$book) { - $_SESSION['error'] = "Книга не найдена"; - redirect('books.php'); -} -// Получаем информацию об авторе -$author_info = "Неизвестный автор"; -if ($book) { - $stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?"); - $stmt->execute([$book['user_id']]); - $author_info = $stmt->fetch(PDO::FETCH_ASSOC); - if ($author_info['display_name'] !=""){ - $author_name = $author_info['display_name']; - }else{ - $author_name = $author_info['username'] ; - } - -} - - -// Функция для преобразования 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); - })); -} - -// Обработка экспорта -switch ($format) { - case 'pdf': - exportPDF($book, $chapters, $is_public, $author_name); - break; - case 'docx': - exportDOCX($book, $chapters, $is_public, $author_name); - break; - case 'html': - exportHTML($book, $chapters, $is_public, $author_name); - break; - case 'txt': - exportTXT($book, $chapters, $is_public, $author_name); - break; - default: - $_SESSION['error'] = "Неверный формат экспорта"; - redirect($share_token ? "view_book.php?share_token=$share_token" : "book_edit.php?id=$book_id"); -} - -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); - - // Устанавливаем метаданные документа - $pdf->SetCreator(APP_NAME); - $pdf->SetAuthor($author_name); - $pdf->SetTitle($book['title']); - $pdf->SetSubject($book['genre'] ?? ''); - - // Устанавливаем margins - $pdf->SetMargins(15, 25, 15); - $pdf->SetHeaderMargin(10); - $pdf->SetFooterMargin(10); - - // Устанавливаем авто разрыв страниц - $pdf->SetAutoPageBreak(TRUE, 15); - - // Добавляем страницу - $pdf->AddPage(); - - // Устанавливаем шрифт с поддержкой кириллицы - $pdf->SetFont('dejavusans', '', 12); - - // Заголовок книги - $pdf->SetFont('dejavusans', 'B', 18); - $pdf->Cell(0, 10, $book['title'], 0, 1, 'C'); - $pdf->Ln(2); - - // Автор - $pdf->SetFont('dejavusans', 'I', 14); - $pdf->Cell(0, 10, $author_name, 0, 1, 'C'); - $pdf->Ln(5); - - // Обложка книги - if (!empty($book['cover_image'])) { - $cover_path = COVERS_PATH . $book['cover_image']; - if (file_exists($cover_path)) { - list($width, $height) = getimagesize($cover_path); - $max_width = 80; - $ratio = $width / $height; - $new_height = $max_width / $ratio; - - $x = (210 - $max_width) / 2; - $pdf->Image($cover_path, $x, $pdf->GetY(), $max_width, $new_height, '', '', 'N', false, 300, '', false, false, 0, false, false, false); - $pdf->Ln($new_height + 5); - } - } - - // Жанр - if (!empty($book['genre'])) { - $pdf->SetFont('dejavusans', 'I', 12); - $pdf->Cell(0, 10, 'Жанр: ' . $book['genre'], 0, 1, 'C'); - $pdf->Ln(5); - } - - // Описание - if (!empty($book['description'])) { - $pdf->SetFont('dejavusans', '', 11); - $pdf->MultiCell(0, 6, $book['description'], 0, 'J'); - $pdf->Ln(10); - } - - // Интерактивное оглавление - $chapterLinks = []; - if (!empty($chapters)) { - $pdf->SetFont('dejavusans', 'B', 14); - $pdf->Cell(0, 10, 'Оглавление', 0, 1, 'C'); - $pdf->Ln(5); - - $toc_page = $pdf->getPage(); - - $pdf->SetFont('dejavusans', '', 11); - foreach ($chapters as $index => $chapter) { - $chapter_number = $index + 1; - $link = $pdf->AddLink(); - $chapterLinks[$chapter['id']] = $link; // Сохраняем ссылку для этой главы - $pdf->Cell(0, 6, "{$chapter_number}. {$chapter['title']}", 0, 1, 'L', false, $link); - } - $pdf->Ln(10); - } - - // Разделитель - $pdf->Line(15, $pdf->GetY(), 195, $pdf->GetY()); - $pdf->Ln(10); - - // Главы с закладками и правильными ссылками - foreach ($chapters as $index => $chapter) { - // Добавляем новую страницу для каждой главы - $pdf->AddPage(); - - // УСТАНАВЛИВАЕМ ЯКОРЬ ДЛЯ ССЫЛКИ - if (isset($chapterLinks[$chapter['id']])) { - $pdf->SetLink($chapterLinks[$chapter['id']]); - } - - // Устанавливаем закладку для этой главы - $pdf->Bookmark($chapter['title'], 0, 0, '', 'B', array(0, 0, 0)); - - // Название главы - $pdf->SetFont('dejavusans', 'B', 14); - $pdf->Cell(0, 8, $chapter['title'], 0, 1); - $pdf->Ln(2); - - // Контент главы - $pdf->SetFont('dejavusans', '', 11); - if ($book['editor_type'] == 'markdown') { - $htmlContent = $Parsedown->text($chapter['content']); - } else { - $htmlContent = $chapter['content']; - } - $pdf->writeHTML($htmlContent, true, false, true, false, ''); - - $pdf->Ln(8); - } - - // Футер с информацией - $pdf->SetY(-25); - $pdf->SetFont('dejavusans', 'I', 8); - $pdf->Cell(0, 6, 'Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), 0, 1, 'C'); - $pdf->Cell(0, 6, 'Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), 0, 1, 'C'); - - // Отправляем файл - $filename = cleanFilename($book['title']) . '.pdf'; - $pdf->Output($filename, 'D'); - exit; -} - -function exportDOCX($book, $chapters, $is_public, $author_name) { - global $Parsedown; - - $phpWord = new PhpWord(); - - // Стили документа - $phpWord->setDefaultFontName('Times New Roman'); - $phpWord->setDefaultFontSize(12); - - // Секция документа - $section = $phpWord->addSection(); - - // Заголовок книги - $section->addText($book['title'], ['bold' => true, 'size' => 16], ['alignment' => 'center']); - $section->addTextBreak(1); - - // Автор - $section->addText($author_name, ['italic' => true, 'size' => 14], ['alignment' => 'center']); - $section->addTextBreak(2); - - // Обложка книги - if (!empty($book['cover_image'])) { - $cover_path = COVERS_PATH . $book['cover_image']; - if (file_exists($cover_path)) { - $section->addImage($cover_path, [ - 'width' => 150, - 'height' => 225, - 'alignment' => 'center' - ]); - $section->addTextBreak(2); - } - } - - // Жанр - if (!empty($book['genre'])) { - $section->addText('Жанр: ' . $book['genre'], ['italic' => true], ['alignment' => 'center']); - $section->addTextBreak(1); - } - - // Описание - if (!empty($book['description'])) { - if ($book['editor_type'] == 'markdown') { - $descriptionParagraphs = markdownToParagraphs($book['description']); - } else { - $descriptionParagraphs = htmlToParagraphs($book['description']); - } - - foreach ($descriptionParagraphs as $paragraph) { - if (!empty(trim($paragraph))) { - $section->addText($paragraph); - } - } - $section->addTextBreak(2); - } - - // Интерактивное оглавление - if (!empty($chapters)) { - $section->addText('Оглавление', ['bold' => true, 'size' => 14], ['alignment' => 'center']); - $section->addTextBreak(1); - - foreach ($chapters as $index => $chapter) { - $chapter_number = $index + 1; - // Создаем гиперссылку на заголовок главы - $section->addLink("chapter_{$chapter['id']}", "{$chapter_number}. {$chapter['title']}", null, null, true); - $section->addTextBreak(1); - } - $section->addTextBreak(2); - } - - // Разделитель - $section->addPageBreak(); - - // Главы с закладками - foreach ($chapters as $index => $chapter) { - // Добавляем закладку для главы - $section->addBookmark("chapter_{$chapter['id']}"); - - // Заголовок главы - $section->addText($chapter['title'], ['bold' => true, 'size' => 14]); - $section->addTextBreak(1); - - // Получаем очищенный текст и разбиваем на абзацы в зависимости от типа редактора - if ($book['editor_type'] == 'markdown') { - $cleanContent = cleanMarkdown($chapter['content']); - $paragraphs = markdownToParagraphs($cleanContent); - } else { - $cleanContent = strip_tags($chapter['content']); - $paragraphs = htmlToParagraphs($chapter['content']); - } - - // Добавляем каждый абзац - foreach ($paragraphs as $paragraph) { - if (!empty(trim($paragraph))) { - $section->addText($paragraph); - $section->addTextBreak(1); - } - } - - // Добавляем разрыв страницы между главами (кроме последней) - if ($index < count($chapters) - 1) { - $section->addPageBreak(); - } - } - - // Футер - $section->addTextBreak(2); - $section->addText('Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), ['italic' => true, 'size' => 9]); - $section->addText('Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), ['italic' => true, 'size' => 9]); - - // Сохраняем и отправляем - $filename = cleanFilename($book['title']) . '.docx'; - header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document'); - header('Content-Disposition: attachment; filename="' . $filename . '"'); - - $objWriter = IOFactory::createWriter($phpWord, 'Word2007'); - $objWriter->save('php://output'); - exit; -} - -// Новая функция для разбивки HTML на абзацы -function htmlToParagraphs($html) { - // Убираем HTML теги и нормализуем пробелы - $text = strip_tags($html); - $text = preg_replace('/\s+/', ' ', $text); - - // Разбиваем на абзацы по точкам и переносам строк - $paragraphs = preg_split('/(?<=[.!?])\s+/', $text); - - // Фильтруем пустые абзацы - $paragraphs = array_filter($paragraphs, function($paragraph) { - return !empty(trim($paragraph)); - }); - - return $paragraphs; -} - -function exportHTML($book, $chapters, $is_public, $author_name) { - global $Parsedown; - - $html = ' - - - - - ' . htmlspecialchars($book['title']) . ' - - - -
' . htmlspecialchars($book['title']) . '
-
' . htmlspecialchars($author_name) . '
'; - - if (!empty($book['genre'])) { - $html .= '
Жанр: ' . htmlspecialchars($book['genre']) . '
'; - } - - // Обложка книги - if (!empty($book['cover_image'])) { - $cover_url = COVERS_URL . $book['cover_image']; - $html .= '
'; - $html .= '' . htmlspecialchars($book['title']) . ''; - $html .= '
'; - } - - if (!empty($book['description'])) { - $html .= '
'; - if ($book['editor_type'] == 'markdown') { - $html .= nl2br(htmlspecialchars($book['description'])); - } else { - $html .= $book['description']; - } - $html .= '
'; - } - - // Интерактивное оглавление - if (!empty($chapters)) { - $html .= '
'; - $html .= '

Оглавление

'; - $html .= ''; - $html .= '
'; - } - - $html .= '
'; - - foreach ($chapters as $index => $chapter) { - $html .= '
'; - $html .= '
' . htmlspecialchars($chapter['title']) . '
'; - - // Обрабатываем контент в зависимости от типа редактора - if ($book['editor_type'] == 'markdown') { - $htmlContent = $Parsedown->text($chapter['content']); - } else { - $htmlContent = $chapter['content']; - } - - $html .= '
' . $htmlContent . '
'; - $html .= '
'; - - if ($index < count($chapters) - 1) { - $html .= '
'; - } - } - - $html .= ' - - '; - - $filename = cleanFilename($book['title']) . '.html'; - header('Content-Type: text/html; charset=utf-8'); - header('Content-Disposition: attachment; filename="' . $filename . '"'); - echo $html; - exit; -} - -function exportTXT($book, $chapters, $is_public, $author_name) { - $content = "=" . str_repeat("=", 80) . "=\n"; - $content .= str_pad($book['title'], 80, " ", STR_PAD_BOTH) . "\n"; - $content .= str_pad($author_name, 80, " ", STR_PAD_BOTH) . "\n"; - $content .= "=" . str_repeat("=", 80) . "=\n\n"; - - if (!empty($book['genre'])) { - $content .= "Жанр: " . $book['genre'] . "\n\n"; - } - - if (!empty($book['description'])) { - $content .= "ОПИСАНИЕ:\n"; - - // Обрабатываем описание в зависимости от типа редактора - if ($book['editor_type'] == 'markdown') { - $descriptionText = cleanMarkdown($book['description']); - } else { - $descriptionText = strip_tags($book['description']); - } - - $content .= wordwrap($descriptionText, 144) . "\n\n"; - } - - // Оглавление - if (!empty($chapters)) { - $content .= "ОГЛАВЛЕНИЕ:\n"; - $content .= str_repeat("-", 60) . "\n"; - foreach ($chapters as $index => $chapter) { - $chapter_number = $index + 1; - $content .= "{$chapter_number}. {$chapter['title']}\n"; - } - $content .= "\n"; - } - - $content .= str_repeat("-", 144) . "\n\n"; - - foreach ($chapters as $index => $chapter) { - $content .= $chapter['title'] . "\n"; - $content .= str_repeat("-", 60) . "\n\n"; - - // Получаем очищенный текст в зависимости от типа редактора - if ($book['editor_type'] == 'markdown') { - $cleanContent = cleanMarkdown($chapter['content']); - $paragraphs = markdownToParagraphs($cleanContent); - } else { - $cleanContent = strip_tags($chapter['content']); - $paragraphs = htmlToPlainTextParagraphs($cleanContent); - } - - foreach ($paragraphs as $paragraph) { - if (!empty(trim($paragraph))) { - $content .= wordwrap($paragraph, 144) . "\n\n"; - } - } - - if ($index < count($chapters) - 1) { - $content .= str_repeat("-", 144) . "\n\n"; - } - } - - $content .= "\n" . str_repeat("=", 144) . "\n"; - $content .= "Экспортировано из " . APP_NAME . " - " . date('d.m.Y H:i') . "\n"; - $content .= "Автор: " . $author_name . " | Всего глав: " . count($chapters) . " | Всего слов: " . array_sum(array_column($chapters, 'word_count')) . "\n"; - $content .= str_repeat("=", 144) . "\n"; - - $filename = cleanFilename($book['title']) . '.txt'; - header('Content-Type: text/plain; charset=utf-8'); - header('Content-Disposition: attachment; filename="' . $filename . '"'); - echo $content; - exit; -} - -// Новая функция для разбивки HTML на абзацы в виде простого текста -function htmlToPlainTextParagraphs($html) { - // Убираем HTML теги - $text = strip_tags($html); - - // Заменяем HTML entities - $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); - - // Нормализуем переносы строк - $text = str_replace(["\r\n", "\r"], "\n", $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 (!empty($currentParagraph)) { - $currentParagraph .= ' ' . $trimmedLine; - } else { - $currentParagraph = $trimmedLine; - } - } - - // Добавляем последний абзац - if (!empty($currentParagraph)) { - $paragraphs[] = $currentParagraph; - } - - return $paragraphs; -} -?> \ No newline at end of file diff --git a/index.php b/index.php index 17c4b3b..b8a4886 100755 --- a/index.php +++ b/index.php @@ -1,9 +1,126 @@ routes[$pattern] = $handler; + } + + public function handle($uri) { + // Убираем базовый URL если есть + $basePath = parse_url(SITE_URL, PHP_URL_PATH) ?? ''; + $uri = str_replace($basePath, '', $uri); + $uri = parse_url($uri, PHP_URL_PATH) ?? '/'; + + foreach ($this->routes as $pattern => $handler) { + if ($this->match($pattern, $uri)) { + return $this->callHandler($handler, $this->params); + } + } + + // 404 + http_response_code(404); + include 'views/errors/404.php'; + exit; + } + + private function match($pattern, $uri) { + $pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $pattern); + $pattern = "#^$pattern$#"; + + if (preg_match($pattern, $uri, $matches)) { + $this->params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); + return true; + } + return false; + } + + private function callHandler($handler, $params) { + if (is_callable($handler)) { + return call_user_func_array($handler, $params); + } + + if (is_string($handler)) { + list($controller, $method) = explode('@', $handler); + $controllerFile = "controllers/{$controller}.php"; + + if (file_exists($controllerFile)) { + require_once $controllerFile; + $controllerInstance = new $controller(); + + if (method_exists($controllerInstance, $method)) { + return call_user_func_array([$controllerInstance, $method], $params); + } + } + } + + throw new Exception("Handler not found"); + } } + +// Инициализация роутера +$router = new Router(); + +// Маршруты +$router->add('/', 'DashboardController@index'); +$router->add('/login', 'AuthController@login'); +$router->add('/logout', 'AuthController@logout'); +$router->add('/register', 'AuthController@register'); + +// Книги +$router->add('/books', 'BookController@index'); +$router->add('/books/create', 'BookController@create'); +$router->add('/books/{id}/edit', 'BookController@edit'); +$router->add('/books/{id}/delete', 'BookController@delete'); +$router->add('/books/{id}/normalize', 'BookController@normalizeContent'); +$router->add('/books/{id}/regenerate-token', 'BookController@regenerateToken'); + +// Главы +$router->add('/books/{book_id}/chapters', 'ChapterController@index'); +$router->add('/books/{book_id}/chapters/create', 'ChapterController@create'); +$router->add('/chapters/{id}/edit', 'ChapterController@edit'); +$router->add('/chapters/{id}/delete', 'ChapterController@delete'); +$router->add('/chapters/preview', 'ChapterController@preview'); + +// Серии +$router->add('/series', 'SeriesController@index'); +$router->add('/series/create', 'SeriesController@create'); +$router->add('/series/{id}/edit', 'SeriesController@edit'); +$router->add('/series/{id}/delete', 'SeriesController@delete'); + + +// Профиль +$router->add('/profile', 'UserController@profile'); +$router->add('/profile/update', 'UserController@updateProfile'); + +// Экспорт с параметром формата +$router->add('/export/{book_id}/{format}', 'ExportController@export'); +$router->add('/export/{book_id}', 'ExportController@export'); // по умолчанию pdf +$router->add('/export/shared/{share_token}/{format}', 'ExportController@exportShared'); +$router->add('/export/shared/{share_token}', 'ExportController@exportShared'); // по умолчанию pdf + +// Публичные страницы +$router->add('/book/{share_token}', 'BookController@viewPublic'); +$router->add('/author/{id}', 'UserController@viewPublic'); +$router->add('/series/{id}/view', 'SeriesController@viewPublic'); + +// Обработка запроса +$requestUri = $_SERVER['REQUEST_URI']; +$router->handle($requestUri); + +// Редирект с корня на dashboard для авторизованных +$router->add('/', function() { + if (is_logged_in()) { + header("Location: " . SITE_URL . "/dashboard"); + } else { + header("Location: " . SITE_URL . "/login"); + } + exit; +}); + + ?> \ No newline at end of file diff --git a/login.php b/login.php deleted file mode 100755 index 8f2847e..0000000 --- a/login.php +++ /dev/null @@ -1,100 +0,0 @@ -findByUsername($username); - - if ($user && $userModel->verifyPassword($password, $user['password_hash'])) { - if (!$user['is_active']) { - $error = 'Ваш аккаунт деактивирован или ожидает активации администратором.'; - } else { - // Успешный вход - $_SESSION['user_id'] = $user['id']; - $_SESSION['username'] = $user['username']; - $_SESSION['display_name'] = $user['display_name'] ?: $user['username']; - $_SESSION['avatar'] = $user['avatar'] ?? null; - // Обновляем время последнего входа - $userModel->updateLastLogin($user['id']); - - $_SESSION['success'] = 'Добро пожаловать, ' . e($user['display_name'] ?: $user['username']) . '!'; - redirect('dashboard.php'); - } - } else { - $error = 'Неверное имя пользователя или пароль'; - } - } - } -} - -$page_title = 'Вход в систему'; -include 'views/header.php'; -?> - -
-

Вход в систему

- - -
- -
- - - -
- - -
- - -
- - -
- - -
- -
- - -
- - -
- -
-

Нет аккаунта? Зарегистрируйтесь здесь

-
-
- - \ No newline at end of file diff --git a/logout.php b/logout.php deleted file mode 100755 index bd5123d..0000000 --- a/logout.php +++ /dev/null @@ -1,20 +0,0 @@ - \ No newline at end of file diff --git a/models/Book.php b/models/Book.php index 31c1be4..c52e6d3 100755 --- a/models/Book.php +++ b/models/Book.php @@ -64,6 +64,10 @@ class Book { $published = isset($data['published']) ? (int)$data['published'] : 0; $editor_type = $data['editor_type'] ?? 'markdown'; + // Преобразуем пустые строки в NULL для integer полей + $series_id = !empty($data['series_id']) ? (int)$data['series_id'] : null; + $sort_order_in_series = !empty($data['sort_order_in_series']) ? (int)$data['sort_order_in_series'] : null; + $stmt = $this->pdo->prepare(" UPDATE books SET title = ?, description = ?, genre = ?, series_id = ?, sort_order_in_series = ?, published = ?, editor_type = ? @@ -73,8 +77,8 @@ class Book { $data['title'], $data['description'] ?? null, $data['genre'] ?? null, - $data['series_id'] ?? null, - $data['sort_order_in_series'] ?? null, + $series_id, // Теперь это либо integer, либо NULL + $sort_order_in_series, // Теперь это либо integer, либо NULL $published, $editor_type, $id, @@ -206,46 +210,73 @@ class Book { } public function convertChaptersContent($book_id, $from_editor, $to_editor) { - try { - $this->pdo->beginTransaction(); + try { + $this->pdo->beginTransaction(); + + // Получаем все главы книги + $chapters = $this->getAllChapters($book_id); + + foreach ($chapters as $chapter) { + $converted_content = $this->convertContent( + $chapter['content'], + $from_editor, + $to_editor + ); - $chapters = $this->getAllChapters($book_id); - - foreach ($chapters as $chapter) { - $converted_content = $this->convertContent( - $chapter['content'], - $from_editor, - $to_editor - ); - - $this->updateChapterContent($chapter['id'], $converted_content); - } - - $this->pdo->commit(); - return true; - } catch (Exception $e) { - $this->pdo->rollBack(); - error_log("Error converting chapters: " . $e->getMessage()); - return false; + // Обновляем контент главы + $this->updateChapterContent($chapter['id'], $converted_content); } + + $this->pdo->commit(); + return true; + } catch (Exception $e) { + $this->pdo->rollBack(); + error_log("Error converting chapters: " . $e->getMessage()); + return false; } +} - private function getAllChapters($book_id) { - $stmt = $this->pdo->prepare("SELECT id, content FROM chapters WHERE book_id = ?"); - $stmt->execute([$book_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); +private function getAllChapters($book_id) { + $stmt = $this->pdo->prepare("SELECT id, content FROM chapters WHERE book_id = ?"); + $stmt->execute([$book_id]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +private function updateChapterContent($chapter_id, $content) { + $word_count = $this->countWords($content); + $stmt = $this->pdo->prepare(" + UPDATE chapters + SET content = ?, word_count = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + "); + return $stmt->execute([$content, $word_count, $chapter_id]); +} + +private function convertContent($content, $from_editor, $to_editor) { + if ($from_editor === $to_editor) { + return $content; } - private function updateChapterContent($chapter_id, $content) { - $word_count = $this->countWords($content); - $stmt = $this->pdo->prepare(" - UPDATE chapters - SET content = ?, word_count = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? - "); - return $stmt->execute([$content, $word_count, $chapter_id]); + require_once __DIR__ . '/../includes/parsedown/ParsedownExtra.php'; + + try { + if ($from_editor === 'markdown' && $to_editor === 'html') { + // Markdown to HTML + $parsedown = new ParsedownExtra(); + return $parsedown->text($content); + } elseif ($from_editor === 'html' && $to_editor === 'markdown') { + // HTML to Markdown (упрощенная версия) + return $this->htmlToMarkdown($content); + } + } catch (Exception $e) { + error_log("Error converting content from {$from_editor} to {$to_editor}: " . $e->getMessage()); + return $content; } + return $content; +} + + private function countWords($text) { $text = strip_tags($text); $text = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $text); @@ -254,26 +285,7 @@ class Book { return count($words); } - private function convertContent($content, $from_editor, $to_editor) { - if ($from_editor === $to_editor) { - return $content; - } - - try { - if ($from_editor === 'markdown' && $to_editor === 'html') { - // Markdown to HTML с улучшенной обработкой абзацев - return $this->markdownToHtmlWithParagraphs($content); - } elseif ($from_editor === 'html' && $to_editor === 'markdown') { - // HTML to Markdown - return $this->htmlToMarkdown($content); - } - } catch (Exception $e) { - error_log("Error converting content from {$from_editor} to {$to_editor}: " . $e->getMessage()); - return $content; - } - - return $content; - } + private function markdownToHtmlWithParagraphs($markdown) { $parsedown = new ParsedownExtra(); diff --git a/register.php b/register.php deleted file mode 100755 index 25a69d9..0000000 --- a/register.php +++ /dev/null @@ -1,180 +0,0 @@ -findByUsername($username)) { - $error = 'Это имя пользователя уже занято'; - } elseif (!empty($email) && $userModel->findByEmail($email)) { - $error = 'Этот email уже используется'; - } else { - // Подготавливаем данные для создания пользователя - $user_data = [ - 'username' => $username, - 'display_name' => $display_name ?: $username, - 'email' => $email, - 'password' => $password - ]; - - // Если пользователя создает администратор - сразу активный - // Если пользователь регистрируется сам - требует активации - if ($is_admin) { - $user_data['is_active'] = 1; - } else { - $user_data['is_active'] = 0; - } - - // Создаем пользователя - $success = $userModel->create($user_data); - - if ($success) { - if ($is_admin) { - $_SESSION['success'] = 'Пользователь успешно создан и активирован'; - redirect('admin/users.php'); - } else { - $_SESSION['success'] = 'Регистрация прошла успешно. Ваш аккаунт ожидает активации администратором.'; - redirect('login.php'); - } - } else { - $error = 'Произошла ошибка при регистрации. Попробуйте еще раз.'; - } - } - } - } -} - -$page_title = $is_admin ? 'Добавление пользователя' : 'Регистрация'; -include 'views/header.php'; -?> - -
-

- - -
- -
- - - -
- -
- - -
- - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - - - - ❌ Отмена - - -
-
- - -
-

Уже есть аккаунт? Войдите здесь

- -

- Примечание: После регистрации ваш аккаунт должен быть активирован администратором. -

- -
- -
- - \ No newline at end of file diff --git a/series.php b/series.php deleted file mode 100644 index 9f90f28..0000000 --- a/series.php +++ /dev/null @@ -1,95 +0,0 @@ -findByUser($user_id); - -// Получаем статистику для каждой серии отдельно -foreach ($series as &$ser) { - $stats = $seriesModel->getSeriesStats($ser['id'], $user_id); - $ser['book_count'] = $stats['book_count'] ?? 0; - $ser['total_words'] = $stats['total_words'] ?? 0; -} -unset($ser); - -$page_title = "Мои серии книг"; -include 'views/header.php'; -?> - -

Мои серии книг

- - -
- - -
- - - -
- - -
- - -
-

Всего серий:

- ➕ Новая серия -
- - -
-

У вас пока нет серий книг

-

Создайте свою первую серию для организации книг!

- 📚 Создать первую серию -
- -
- -
-
-

- -
- - ✏️ - - - 👁️ - -
- - - -
-
-

-
- - -

- - - -
- -
- - - \ No newline at end of file diff --git a/series_delete.php b/series_delete.php deleted file mode 100644 index 372093f..0000000 --- a/series_delete.php +++ /dev/null @@ -1,39 +0,0 @@ -userOwnsSeries($series_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой серии"; - redirect('series.php'); -} - -$series = $seriesModel->findById($series_id); - -if ($seriesModel->delete($series_id, $user_id)) { - $_SESSION['success'] = "Серия «" . e($series['title']) . "» успешно удалена"; -} else { - $_SESSION['error'] = "Ошибка при удалении серии"; -} - -redirect('series.php'); -?> \ No newline at end of file diff --git a/series_edit.php b/series_edit.php deleted file mode 100644 index e84a039..0000000 --- a/series_edit.php +++ /dev/null @@ -1,179 +0,0 @@ -findById($series_id); - if (!$series || !$seriesModel->userOwnsSeries($series_id, $user_id)) { - $_SESSION['error'] = "Серия не найдена или у вас нет доступа"; - redirect('series.php'); - } - $is_edit = true; -} - -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Ошибка безопасности"; - redirect($is_edit ? "series_edit.php?id=$series_id" : 'series_edit.php'); - } - - $title = trim($_POST['title'] ?? ''); - $description = trim($_POST['description'] ?? ''); - - if (empty($title)) { - $_SESSION['error'] = "Название серии обязательно"; - } else { - $data = [ - 'title' => $title, - 'description' => $description, - 'user_id' => $user_id - ]; - - if ($is_edit) { - $success = $seriesModel->update($series_id, $data); - $message = $success ? "Серия успешно обновлена" : "Ошибка при обновлении серии"; - } else { - $success = $seriesModel->create($data); - $message = $success ? "Серия успешно создана" : "Ошибка при создании серии"; - - if ($success) { - $new_series_id = $pdo->lastInsertId(); - redirect("series_edit.php?id=$new_series_id"); - } - } - - if ($success) { - $_SESSION['success'] = $message; - redirect('series.php'); - } else { - $_SESSION['error'] = $message; - } - } -} - -$page_title = $is_edit ? "Редактирование серии" : "Создание новой серии"; -include 'views/header.php'; -?> - -

- - -
- - -
- - -
- - -
- - - - - -
- -
- - - - ❌ Отмена - -
-
- - -
-

Книги в этой серии

- - findBySeries($series_id); - - // Вычисляем общую статистику - $total_chapters = 0; - $total_words = 0; - foreach ($books_in_series as $book) { - $stats = $bookModel->getBookStats($book['id']); - $total_chapters += $stats['chapter_count'] ?? 0; - $total_words += $stats['total_words'] ?? 0; - } - ?> - - -
-

В этой серии пока нет книг.

- 📚 Добавить книги -
- -
- - - - - - - - - - - - - - - - - - - - - -
ПорядокНазвание книгиЖанрСтатусДействия
- - -
- -
- - - - - - Редактировать - -
-
- -
- Статистика серии: - Книг: | - Глав: | - Слов: -
- -
- - - \ No newline at end of file diff --git a/view_book.php b/view_book.php deleted file mode 100755 index 2fb01e0..0000000 --- a/view_book.php +++ /dev/null @@ -1,276 +0,0 @@ -findByShareToken($share_token); -} elseif ($book_id) { - $book = $bookModel->findById($book_id); -} - -if (!$book) { - http_response_code(404); - $page_title = "Книга не найдена"; - include 'views/header.php'; - ?> -
-
-

Книга не найдена

-

Запрошенная книга не существует или была удалена.

- На главную -
-
- getPublishedChapters($book['id']); -$total_words = array_sum(array_column($chapters, 'word_count')); - -// Получаем информацию об авторе -$stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?"); -$stmt->execute([$book['user_id']]); -$author_info = $stmt->fetch(PDO::FETCH_ASSOC); -$author_name = $author_info['display_name'] ?? $author_info['username'] ?? 'Неизвестный автор'; - -$page_title = $book['title']; -include 'views/header.php'; -?> - -
-
-
- -
- <?= e($book['title']) ?> -
- - -

- - prepare("SELECT id, title FROM series WHERE id = ?"); - $series_stmt->execute([$book['series_id']]); - $series = $series_stmt->fetch(); - ?> - -

- 📚 Часть серии: - - - - (Книга ) - - -

- - -

- - -

- Жанр: -

- - - -
-

-
- - -
- Глав: - Слов: - - | - Вернуться к редактированию - -
-
- - - -
-

📖 Оглавление

- -
- $chapter): ?> -
- - . - - -
- -
-
- - -
-

Экспорт книги

- -
- - 📄 PDF - - - 📝 DOCX - - - 🌐 HTML - - - 📄 TXT - -
- -

- Примечание: Экспортируются только опубликованные главы -

-
- - -
-

В этой книге пока нет опубликованных глав

-

Автор еще не опубликовал ни одной главы

-
- -
- $chapter): ?> -
-

- - 🔗 -

-
- - text($chapter['content']) ?> - - - -
- - -
- Обновлено: - ↑ Наверх -
-
- -
- - - -
-
- - - - \ No newline at end of file diff --git a/views/auth/login.php b/views/auth/login.php new file mode 100644 index 0000000..6c16850 --- /dev/null +++ b/views/auth/login.php @@ -0,0 +1,49 @@ + + +
+

Вход в систему

+ + +
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+ +
+

Нет аккаунта? Зарегистрируйтесь здесь

+
+
+ + \ No newline at end of file diff --git a/views/auth/register.php b/views/auth/register.php new file mode 100644 index 0000000..1e3720b --- /dev/null +++ b/views/auth/register.php @@ -0,0 +1,85 @@ + + +
+

Регистрация

+ + +
+ +
+ + + +
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+

Уже есть аккаунт? Войдите здесь

+
+
+ + \ No newline at end of file diff --git a/views/books/create.php b/views/books/create.php new file mode 100644 index 0000000..2751717 --- /dev/null +++ b/views/books/create.php @@ -0,0 +1,66 @@ + +

Создание новой книги

+
+ +
+ + + + + + + + + + +
+ +
+
+
+ + + ❌ Отмена + +
+
+ \ No newline at end of file diff --git a/views/books/edit.php b/views/books/edit.php new file mode 100644 index 0000000..5bb9c58 --- /dev/null +++ b/views/books/edit.php @@ -0,0 +1,259 @@ + +

Редактирование книги

+
+ +
+ + + + + + + + + + + + +
+ + +
+

Текущая обложка:

+ Обложка +
+ +
+
+ + + + Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 5MB. + Рекомендуемый размер: 300×450 пикселей. + + +
+ ❌ +
+ +
+ + +
+ +
+
+
+ + + ❌ Отмена + +
+
+ + +
+
+ + +

+ Если контент глав отображается неправильно после смены редактора, можно нормализовать его структуру. +

+
+
+
+

Публичная ссылка для чтения

+
+ + +
+ + +
+
+

+ Примечание: В публичном просмотре отображаются только главы со статусом "Опубликована" +

+
+
+

Экспорт книги

+

Экспортируйте книгу в различные форматы:

+
+ + 📄 PDF + + + 📝 DOCX + + + 🌐 HTML + + + 📄 TXT + +
+

+ Примечание: Экспортируются все главы книги (включая черновики) +

+
+
+

Главы этой книги

+
+ + 📑 Все главы + + + ✏️ Добавить главу + +
+ +
+ + + + + + + + + + + + + + + + + + + +
НазваниеСтатусСловДействия
+ + + + + + Редактировать + +
+
+ +
+

В этой книге пока нет глав.

+ + ✏️ Добавить первую главу + +
+ +
+
+
+ + +
+
+ + + + + \ No newline at end of file diff --git a/views/books/index.php b/views/books/index.php new file mode 100644 index 0000000..cec2560 --- /dev/null +++ b/views/books/index.php @@ -0,0 +1,66 @@ + + +

Мои книги

+ +
+

Всего книг:

+ ➕ Новая книга +
+ + +
+

У вас пока нет книг

+

Создайте свою первую книгу и начните писать!

+ 📖 Создать первую книгу +
+ +
+ +
+
+

+ +
+ + ✏️ + + + 👁️ + +
+

+ +

+ +
+ + +

+ + + +
+ +
+ + + \ No newline at end of file diff --git a/views/books/view_public.php b/views/books/view_public.php new file mode 100644 index 0000000..131492c --- /dev/null +++ b/views/books/view_public.php @@ -0,0 +1,151 @@ + + +
+
+
+ +
+ <?= e($book['title']) ?> +
+ + +

+ +

+ Автор: +

+ + +

+ +

+ + + +
+ +
+ + +
+ Глав: + Слов: + + 📄 Скачать книгу + +
+
+ + +
+

В этой книге пока нет глав

+

Автор еще не опубликовал содержание книги

+
+ +

Оглавление

+ +
+ $chapter): ?> + + +
+ +
+ + $chapter): ?> +
+

+ Глава : +

+ +
+ + text($chapter['content']) ?> + + + +
+ + +
+ + 📖 Следующая глава + +
+ +
+ + + + +
+
+ + + + \ No newline at end of file diff --git a/views/chapters/create.php b/views/chapters/create.php new file mode 100644 index 0000000..07ddb6b --- /dev/null +++ b/views/chapters/create.php @@ -0,0 +1,134 @@ + + +

Новая глава для:

+ + +
+ +
+ + +
+ + +
+ + + + + + + + +
+
+ + + + + + + + +
+
+ + + +
+ Подсказка: Используйте Markdown для форматирования. + Справка по Markdown +
+ + +
+ + + + Опубликованные главы видны в публичном доступе + +
+
+ +
+ + + + + + ❌ Отмена + +
+
+ + + + + + \ No newline at end of file diff --git a/views/chapters/edit.php b/views/chapters/edit.php new file mode 100644 index 0000000..cb8d848 --- /dev/null +++ b/views/chapters/edit.php @@ -0,0 +1,123 @@ + + +

Редактирование главы:

+ + +
+ +
+ + +
+ + +
+ + + + + + + + + + +
+ Подсказка: Используйте Markdown для форматирования. + Справка по Markdown +
+ + +
+ + + + Опубликованные главы видны в публичном доступе + +
+
+ +
+ + + + + + ❌ Отмена + +
+
+ +
+

Информация о главе

+

Книга:

+

Количество слов:

+

Создана:

+

Обновлена:

+
+ + + + + \ No newline at end of file diff --git a/chapters.php b/views/chapters/index.php old mode 100755 new mode 100644 similarity index 52% rename from chapters.php rename to views/chapters/index.php index ea35053..0a34003 --- a/chapters.php +++ b/views/chapters/index.php @@ -1,120 +1,81 @@ -userOwnsBook($book_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой книге"; - redirect('books.php'); -} - -// Получаем информацию о книге и главах -$book = $bookModel->findById($book_id); -$chapters = $chapterModel->findByBook($book_id); - -$page_title = "Главы книги: " . e($book['title']); -include 'views/header.php'; -?> - -
-

Главы книги:

-
- ➕ Новая глава - ✏️ Редактировать книгу - 👁️ Просмотреть книгу - 📚 Все книги -
-
- - -
- - -
- - - -
- - -
- - - -
-

В этой книге пока нет глав

-

Создайте первую главу для вашей книги

- 📝 Создать первую главу -
- -
- - - - - - - - - - - - - $chapter): ?> - - - - - - - - - - -
Название главыСтатусСловОбновленоДействия
- - -
- -
- - - - - - -
- - ✏️ - -
- - - -
-
-
-
- -
- Статистика: - Всего глав: | - Всего слов: | - Опубликовано: -
- - - \ No newline at end of file + + +
+

Главы книги:

+
+ ➕ Новая глава + ✏️ Редактировать книгу + 👁️ Просмотреть книгу + 📚 Все книги +
+
+ + +
+

В этой книге пока нет глав

+

Создайте первую главу для вашей книги

+ 📝 Создать первую главу +
+ +
+ + + + + + + + + + + + + $chapter): ?> + + + + + + + + + + +
Название главыСтатусСловОбновленоДействия
+ + +
+ +
+ + + + + + +
+ + ✏️ + +
+ + +
+
+
+
+ +
+ Статистика: + Всего глав: | + Всего слов: | + Опубликовано: +
+ + + \ No newline at end of file diff --git a/preview.php b/views/chapters/preview.php old mode 100755 new mode 100644 similarity index 63% rename from preview.php rename to views/chapters/preview.php index 0f0446f..864c553 --- a/preview.php +++ b/views/chapters/preview.php @@ -1,23 +1,6 @@ text($content); -} else { - $html_content = $content; -} - - -$page_title = "Предпросмотр: " . e($title); +// views/chapters/preview.php +include 'views/layouts/header.php'; ?> @@ -25,7 +8,8 @@ $page_title = "Предпросмотр: " . e($title); <?= e($page_title) ?> - + + @@ -102,11 +86,14 @@ $page_title = "Предпросмотр: " . e($title);
- +
-