many fixes in design and add shirt story type for creation book
This commit is contained in:
@@ -19,8 +19,12 @@ class AuthController extends BaseController {
|
||||
} else {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
// Проверка Rate Limit
|
||||
if (!checkRateLimit($this->pdo, $ip)) {
|
||||
$error = 'Слишком много попыток входа. Попробуйте позже.';
|
||||
} elseif (empty($username) || empty($password)) {
|
||||
$error = 'Пожалуйста, введите имя пользователя и пароль';
|
||||
} else {
|
||||
$userModel = new User($this->pdo);
|
||||
@@ -29,8 +33,12 @@ class AuthController extends BaseController {
|
||||
if ($user && $userModel->verifyPassword($password, $user['password_hash'])) {
|
||||
if (!$user['is_active']) {
|
||||
$error = 'Ваш аккаунт деактивирован или ожидает активации администратором.';
|
||||
logLoginAttempt($this->pdo, $ip, $username);
|
||||
} else {
|
||||
// Успешный вход
|
||||
// Успешный вход - очищаем попытки
|
||||
clearLoginAttempts($this->pdo, $ip);
|
||||
|
||||
// Обновляем сессию
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
@@ -45,6 +53,7 @@ class AuthController extends BaseController {
|
||||
}
|
||||
} else {
|
||||
$error = 'Неверное имя пользователя или пароль';
|
||||
logLoginAttempt($this->pdo, $ip, $username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,16 @@ class BookController extends BaseController {
|
||||
$this->redirect('/books/create');
|
||||
}
|
||||
|
||||
$is_short_story = isset($_POST['is_short_story']) ? 1 : 0;
|
||||
$content = $is_short_story ? ($_POST['content'] ?? '') : null;
|
||||
|
||||
// Валидация: для рассказа проверяем, что есть хоть какое-то содержание
|
||||
if ($is_short_story && empty(trim(strip_tags($content)))) {
|
||||
$_SESSION['error'] = "Для рассказа необходимо заполнить содержание";
|
||||
$_SESSION['form_data'] = $_POST;
|
||||
$this->redirect('/books/create');
|
||||
}
|
||||
|
||||
$bookModel = new Book($this->pdo);
|
||||
$data = [
|
||||
'title' => $title,
|
||||
@@ -44,12 +54,14 @@ class BookController extends BaseController {
|
||||
'user_id' => $_SESSION['user_id'],
|
||||
'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
|
||||
'published' => isset($_POST['published']) ? 1 : 0,
|
||||
'is_short_story' => $is_short_story,
|
||||
'content' => $content
|
||||
];
|
||||
|
||||
if ($bookModel->create($data)) {
|
||||
$new_book_id = $this->pdo->lastInsertId();
|
||||
|
||||
|
||||
// Обработка загрузки обложки
|
||||
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$cover_result = handleCoverUpload($_FILES['cover_image'], $new_book_id);
|
||||
@@ -61,9 +73,16 @@ class BookController extends BaseController {
|
||||
$_SESSION['cover_error'] = $cover_error;
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['success'] = "Книга успешно создана" . ($cover_error ? ", но возникла ошибка с обложкой: " . $cover_error : "");
|
||||
|
||||
// Если это рассказ с содержанием - редиректим на редактирование
|
||||
// Если рассказ без содержания - тоже на редактирование (чтобы добавить текст)
|
||||
if ($is_short_story) {
|
||||
$_SESSION['success'] = "Рассказ успешно создан! Теперь можно добавить содержание.";
|
||||
$this->redirect("/books/{$new_book_id}/edit");
|
||||
} else {
|
||||
$_SESSION['success'] = "Книга успешно создана" . ($cover_error ? ", но возникла ошибка с обложкой: " . $cover_error : "");
|
||||
$this->redirect("/books/{$new_book_id}/chapters/create");
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при создании книги";
|
||||
}
|
||||
@@ -91,18 +110,73 @@ class BookController extends BaseController {
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$series = $seriesModel->findByUser($_SESSION['user_id']);
|
||||
|
||||
|
||||
$error = '';
|
||||
$cover_error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Обработка запроса автосохранения
|
||||
if (!empty($_POST['autosave']) && $_POST['autosave'] === 'true') {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Подавляем ошибки для автосейва
|
||||
error_reporting(0);
|
||||
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
echo json_encode(['success' => false, 'error' => 'Ошибка безопасности']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$book = $bookModel->findById($id);
|
||||
if (!$book || $book['user_id'] != $_SESSION['user_id']) {
|
||||
echo json_encode(['success' => false, 'error' => 'Книга не найдена']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$is_short_story = isset($_POST['is_short_story']) ? 1 : 0;
|
||||
$content = $is_short_story ? ($_POST['content'] ?? '') : null;
|
||||
|
||||
// Для autosave обновляем только title и user_id из сессии, остальное получаем из базы
|
||||
$title = trim($_POST['title'] ?? $book['title']);
|
||||
$description = trim($_POST['description'] ?? ($book['description'] ?? ''));
|
||||
$genre = trim($_POST['genre'] ?? ($book['genre'] ?? ''));
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$series_id = !empty($_POST['series_id']) ? (int)$_POST['series_id'] : $book['series_id'];
|
||||
$sort_order_in_series = !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : $book['sort_order_in_series'];
|
||||
$published = isset($_POST['published']) ? 1 : $book['published'];
|
||||
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'genre' => $genre,
|
||||
'user_id' => $user_id,
|
||||
'series_id' => $series_id,
|
||||
'sort_order_in_series' => $sort_order_in_series,
|
||||
'published' => $published,
|
||||
'is_short_story' => $is_short_story,
|
||||
'content' => $content
|
||||
];
|
||||
|
||||
$success = $bookModel->update($id, $data);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
if ($success) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Ошибка при сохранении']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$error = "Ошибка безопасности";
|
||||
} else {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
if (empty($title)) {
|
||||
$error = "Название книги обязательно";
|
||||
} else {
|
||||
} else {
|
||||
$is_short_story = isset($_POST['is_short_story']) ? 1 : 0;
|
||||
$content = $is_short_story ? ($_POST['content'] ?? '') : null;
|
||||
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
@@ -110,7 +184,9 @@ class BookController extends BaseController {
|
||||
'user_id' => $_SESSION['user_id'],
|
||||
'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
|
||||
'published' => isset($_POST['published']) ? 1 : 0,
|
||||
'is_short_story' => $is_short_story,
|
||||
'content' => $content
|
||||
];
|
||||
|
||||
// Обработка обложки
|
||||
@@ -252,7 +328,15 @@ class BookController extends BaseController {
|
||||
$this->render('errors/404');
|
||||
return;
|
||||
}
|
||||
$chapters = $chapterModel->getPublishedChapters($book['id']);
|
||||
|
||||
// Если это рассказ - не загружаем главы
|
||||
if ($book['is_short_story']) {
|
||||
$chapters = [];
|
||||
$is_short_story = true;
|
||||
} else {
|
||||
$chapters = $chapterModel->getPublishedChapters($book['id']);
|
||||
$is_short_story = false;
|
||||
}
|
||||
|
||||
// Получаем информацию об авторе
|
||||
$stmt = $this->pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?");
|
||||
@@ -263,6 +347,7 @@ class BookController extends BaseController {
|
||||
'book' => $book,
|
||||
'chapters' => $chapters,
|
||||
'author' => $author,
|
||||
'is_short_story' => $is_short_story,
|
||||
'page_title' => $book['title']
|
||||
]);
|
||||
}
|
||||
@@ -279,7 +364,15 @@ class BookController extends BaseController {
|
||||
$this->render('errors/404');
|
||||
return;
|
||||
}
|
||||
$chapters = $chapterModel->findByBook($book['id']);
|
||||
|
||||
// Если это рассказ - не загружаем главы
|
||||
if ($book['is_short_story']) {
|
||||
$chapters = [];
|
||||
$is_short_story = true;
|
||||
} else {
|
||||
$chapters = $chapterModel->findByBook($book['id']);
|
||||
$is_short_story = false;
|
||||
}
|
||||
|
||||
// Получаем информацию об авторе
|
||||
$stmt = $this->pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?");
|
||||
@@ -290,6 +383,7 @@ class BookController extends BaseController {
|
||||
'book' => $book,
|
||||
'chapters' => $chapters,
|
||||
'author' => $author,
|
||||
'is_short_story' => $is_short_story,
|
||||
'page_title' => $book['title']
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -26,13 +26,19 @@ class ExportController extends BaseController {
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
// Для автора - все главы
|
||||
$chapters = $chapterModel->findByBook($book_id);
|
||||
// Для автора - все главы или контент рассказа
|
||||
if ($book['is_short_story']) {
|
||||
$chapters = [];
|
||||
$is_short_story = true;
|
||||
} else {
|
||||
$chapters = $chapterModel->findByBook($book_id);
|
||||
$is_short_story = false;
|
||||
}
|
||||
|
||||
// Получаем информацию об авторе
|
||||
$author_name = $this->getAuthorName($book['user_id']);
|
||||
|
||||
$this->handleExport($book, $chapters, false, $author_name, $format);
|
||||
|
||||
$this->handleExport($book, $chapters, false, $author_name, $format, $is_short_story);
|
||||
}
|
||||
|
||||
public function exportShared($share_token, $format = 'pdf') {
|
||||
@@ -45,13 +51,19 @@ class ExportController extends BaseController {
|
||||
$this->redirect('/');
|
||||
}
|
||||
|
||||
// Для публичного доступа - только опубликованные главы
|
||||
$chapters = $chapterModel->getPublishedChapters($book['id']);
|
||||
// Для публичного доступа - только опубликованные главы или контент рассказа
|
||||
if ($book['is_short_story']) {
|
||||
$chapters = [];
|
||||
$is_short_story = true;
|
||||
} else {
|
||||
$chapters = $chapterModel->getPublishedChapters($book['id']);
|
||||
$is_short_story = false;
|
||||
}
|
||||
|
||||
// Получаем информацию об авторе
|
||||
$author_name = $this->getAuthorName($book['user_id']);
|
||||
|
||||
$this->handleExport($book, $chapters, true, $author_name, $format);
|
||||
|
||||
$this->handleExport($book, $chapters, true, $author_name, $format, $is_short_story);
|
||||
}
|
||||
|
||||
private function getAuthorName($user_id) {
|
||||
@@ -68,21 +80,20 @@ class ExportController extends BaseController {
|
||||
return "Неизвестный автор";
|
||||
}
|
||||
|
||||
private function handleExport($book, $chapters, $is_public, $author_name, $format) {
|
||||
|
||||
private function handleExport($book, $chapters, $is_public, $author_name, $format, $is_short_story = false) {
|
||||
|
||||
switch ($format) {
|
||||
case 'pdf':
|
||||
$this->exportPDF($book, $chapters, $is_public, $author_name);
|
||||
$this->exportPDF($book, $chapters, $is_public, $author_name, $is_short_story);
|
||||
break;
|
||||
case 'docx':
|
||||
$this->exportDOCX($book, $chapters, $is_public, $author_name);
|
||||
$this->exportDOCX($book, $chapters, $is_public, $author_name, $is_short_story);
|
||||
break;
|
||||
case 'html':
|
||||
$this->exportHTML($book, $chapters, $is_public, $author_name);
|
||||
$this->exportHTML($book, $chapters, $is_public, $author_name, $is_short_story);
|
||||
break;
|
||||
case 'txt':
|
||||
$this->exportTXT($book, $chapters, $is_public, $author_name);
|
||||
$this->exportTXT($book, $chapters, $is_public, $author_name, $is_short_story);
|
||||
break;
|
||||
default:
|
||||
$_SESSION['error'] = "Неверный формат экспорта";
|
||||
@@ -93,8 +104,7 @@ class ExportController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
function exportPDF($book, $chapters, $is_public, $author_name) {
|
||||
|
||||
function exportPDF($book, $chapters, $is_public, $author_name, $is_short_story = false) {
|
||||
|
||||
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
|
||||
|
||||
@@ -157,8 +167,17 @@ class ExportController extends BaseController {
|
||||
$pdf->Ln(10);
|
||||
}
|
||||
|
||||
// Интерактивное оглавление
|
||||
$chapterLinks = [];
|
||||
// Если это рассказ - сразу выводим контент
|
||||
if ($is_short_story && !empty($book['content'])) {
|
||||
$pdf->SetFont('dejavusans', '', 11);
|
||||
$pdf->writeHTML($book['content'], true, false, true, false, '');
|
||||
$pdf->Ln(8);
|
||||
}
|
||||
|
||||
// Интерактивное оглавление (только для обычных книг)
|
||||
if (!$is_short_story) {
|
||||
$chapterLinks = [];
|
||||
if (!empty($chapters)) {
|
||||
if (!empty($chapters)) {
|
||||
$pdf->SetFont('dejavusans', 'B', 14);
|
||||
$pdf->Cell(0, 10, 'Оглавление', 0, 1, 'C');
|
||||
@@ -220,7 +239,7 @@ class ExportController extends BaseController {
|
||||
exit;
|
||||
}
|
||||
|
||||
function exportDOCX($book, $chapters, $is_public, $author_name) {
|
||||
function exportDOCX($book, $chapters, $is_public, $author_name, $is_short_story = false) {
|
||||
|
||||
$phpWord = new PhpWord();
|
||||
|
||||
@@ -271,8 +290,19 @@ class ExportController extends BaseController {
|
||||
$section->addTextBreak(2);
|
||||
}
|
||||
|
||||
// Интерактивное оглавление
|
||||
if (!empty($chapters)) {
|
||||
// Если это рассказ - сразу выводим контент
|
||||
if ($is_short_story && !empty($book['content'])) {
|
||||
$paragraphs = $this->htmlToParagraphs($book['content']);
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
if (!empty(trim($paragraph))) {
|
||||
$section->addText($paragraph);
|
||||
$section->addTextBreak(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Интерактивное оглавление (только для обычных книг)
|
||||
if (!$is_short_story && !empty($chapters)) {
|
||||
$section->addText('Оглавление', ['bold' => true, 'size' => 14], ['alignment' => 'center']);
|
||||
$section->addTextBreak(1);
|
||||
|
||||
@@ -286,10 +316,13 @@ class ExportController extends BaseController {
|
||||
}
|
||||
|
||||
// Разделитель
|
||||
$section->addPageBreak();
|
||||
if (!$is_short_story) {
|
||||
$section->addPageBreak();
|
||||
}
|
||||
|
||||
// Главы с закладками
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
// Главы с закладками (только для обычных книг)
|
||||
if (!$is_short_story) {
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
// Добавляем закладку для главы
|
||||
$section->addBookmark("chapter_{$chapter['id']}");
|
||||
|
||||
@@ -332,7 +365,7 @@ class ExportController extends BaseController {
|
||||
exit;
|
||||
}
|
||||
|
||||
function exportHTML($book, $chapters, $is_public, $author_name) {
|
||||
function exportHTML($book, $chapters, $is_public, $author_name, $is_short_story = false) {
|
||||
|
||||
$html = '<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
@@ -514,8 +547,13 @@ class ExportController extends BaseController {
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
// Интерактивное оглавление
|
||||
if (!empty($chapters)) {
|
||||
// Если это рассказ - выводим контент сразу
|
||||
if ($is_short_story && !empty($book['content'])) {
|
||||
$html .= '<div class="chapter-content">' . $book['content'] . '</div>';
|
||||
}
|
||||
|
||||
// Интерактивное оглавление (только для обычных книг)
|
||||
if (!$is_short_story && !empty($chapters)) {
|
||||
$html .= '<div class="table-of-contents">';
|
||||
$html .= '<h3>Оглавление</h3>';
|
||||
$html .= '<ul>';
|
||||
@@ -554,7 +592,7 @@ class ExportController extends BaseController {
|
||||
exit;
|
||||
}
|
||||
|
||||
function exportTXT($book, $chapters, $is_public, $author_name) {
|
||||
function exportTXT($book, $chapters, $is_public, $author_name, $is_short_story = false) {
|
||||
$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";
|
||||
@@ -578,20 +616,39 @@ class ExportController extends BaseController {
|
||||
$content .= "\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) {
|
||||
// Если это рассказ - выводим контент сразу
|
||||
if ($is_short_story && !empty($book['content'])) {
|
||||
$content .= "СОДЕРЖАНИЕ РАССКАЗА:\n";
|
||||
$content .= str_repeat("-", 60) . "\n\n";
|
||||
|
||||
$plainText = $this->htmlToPlainText($book['content']);
|
||||
$paragraphs = explode("\n", $plainText);
|
||||
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
$trimmed = trim($paragraph);
|
||||
if (!empty($trimmed)) {
|
||||
$wrapped = wordwrap($trimmed, 144);
|
||||
$content .= $wrapped . "\n\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Оглавление (только для обычных книг)
|
||||
if (!$is_short_story && !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";
|
||||
|
||||
// Главы (только для обычных книг)
|
||||
if (!$is_short_story) {
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
$content .= $chapter['title'] . "\n";
|
||||
$content .= str_repeat("-", 60) . "\n\n";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user