diff --git a/333.txt b/333.txt deleted file mode 100644 index cffdea3..0000000 --- a/333.txt +++ /dev/null @@ -1,6499 +0,0 @@ -// ./controllers/DashboardController.php -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' => 'Панель управления' - ]); - } -} -?> -// ./controllers/AuthController.php -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' => 'Регистрация' - ]); - } -} -?> -// ./controllers/ChapterController.php -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 = trim($_POST['content']) ?? ''; - $content = $this->cleanChapterContent($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); - - // Получаем главу и книгу - $chapter = $chapterModel->findById($id); - if (!$chapter) { - if (!empty($_POST['autosave']) && $_POST['autosave'] === 'true') { - header('Content-Type: application/json'); - echo json_encode(['success' => false, 'error' => 'Глава не найдена']); - exit; - } - $_SESSION['error'] = "Глава не найдена"; - $this->redirect('/books'); - } - - $book = $bookModel->findById($chapter['book_id']); - - // Проверяем права доступа - if (!$chapterModel->userOwnsChapter($id, $user_id)) { - if (!empty($_POST['autosave']) && $_POST['autosave'] === 'true') { - header('Content-Type: application/json'); - echo json_encode(['success' => false, 'error' => 'Доступ запрещен']); - exit; - } - $_SESSION['error'] = "У вас нет доступа к этой главе"; - $this->redirect('/books'); - } - - // Обработка POST запроса - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $title = trim($_POST['title'] ?? ''); - $content = $this->cleanChapterContent($_POST['content'] ?? ''); - $status = $_POST['status'] ?? 'draft'; - - // Проверяем CSRF - if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { - if (!empty($_POST['autosave']) && $_POST['autosave'] === 'true') { - header('Content-Type: application/json'); - echo json_encode(['success' => false, 'error' => 'Ошибка безопасности']); - exit; - } - $error = "Ошибка безопасности"; - } - - if (empty($title)) { - $error = "Название главы обязательно"; - } - - $data = ['title' => $title, 'content' => $content, 'status' => $status]; - - // Если это автосейв — возвращаем JSON сразу - if (!empty($_POST['autosave']) && $_POST['autosave'] === 'true') { - if (empty($error)) { - $success = $chapterModel->update($id, $data); - header('Content-Type: application/json'); - echo json_encode(['success' => $success, 'error' => $success ? null : 'Ошибка при сохранении']); - } else { - header('Content-Type: application/json'); - echo json_encode(['success' => false, 'error' => $error]); - } - exit; - } - - // Обычное сохранение формы - if (empty($error)) { - 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(); - - $content = $_POST['content'] ?? ''; - $content = $this->cleanChapterContent($content); - $title = $_POST['title'] ?? 'Предпросмотр'; - - $this->render('chapters/preview', [ - 'content' => $content, - 'title' => $title, - 'page_title' => "Предпросмотр: " . e($title) - ]); - } - - // Добавьте эту функцию в начало файла - function cleanChapterContent($content) { - // Удаляем лишние пробелы в начале и конце - $content = trim($content); - - // Удаляем пустые абзацы и параграфы, содержащие только пробелы - $content = preg_replace('/]*>\s*(?:| )?\s*<\/p>/i', '', $content); - $content = preg_replace('/]*>\s*<\/p>/i', '', $content); - - // Удаляем последовательные пустые абзацы - $content = preg_replace('/(<\/p>\s*]*>)+/', '

', $content); - - // Удаляем лишние пробелы в начале и конце каждого параграфа - $content = preg_replace('/(]*>)\s+/', '$1', $content); - $content = preg_replace('/\s+<\/p>/', '

', $content); - - // Удаляем лишние переносы строк - $content = preg_replace('/\n{3,}/', "\n\n", $content); - - return $content; - } - -} -?> -// ./controllers/ExportController.php -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 = $chapterModel->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) { - - - switch ($format) { - case 'pdf': - $this->exportPDF($book, $chapters, $is_public, $author_name); - break; - case 'docx': - $this->exportDOCX($book, $chapters, $is_public, $author_name); - break; - case 'html': - $this->exportHTML($book, $chapters, $is_public, $author_name); - break; - case 'txt': - $this->exportTXT($book, $chapters, $is_public, $author_name); - 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) { - - - $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); - - $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) { - - $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'])) { - - $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); - - // Получаем очищенный текст и разбиваем на абзацы - - $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) { - - $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 .= '
'; - $html .= $book['description']; - $html .= '
'; - } - - // Интерактивное оглавление - if (!empty($chapters)) { - $html .= '
'; - $html .= '

Оглавление

'; - $html .= ''; - $html .= '
'; - } - - $html .= '
'; - - foreach ($chapters as $index => $chapter) { - $html .= '
'; - $html .= '
' . htmlspecialchars($chapter['title']) . '
'; - $html .= '
' . $chapter['content']. '
'; - $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"; - - // Обрабатываем описание - $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"; - - // Получаем очищенный текст - $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; - } - - // Функция для разбивки 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; - } -} -?> -// ./controllers/BaseController.php -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 requireAdmin() { - if (!is_logged_in()) { - $this->redirect('/login'); - return; - } - - global $pdo; - $userModel = new User($pdo); - $user = $userModel->findById($_SESSION['user_id']); - - if (!$user || $user['id'] != 1) { // Предполагаем, что администратор имеет ID = 1 - $_SESSION['error'] = "У вас нет прав администратора"; - $this->redirect('/dashboard'); - exit; - } - } - - protected function jsonResponse($data) { - header('Content-Type: application/json'); - echo json_encode($data); - exit; - } -} -?> -// ./controllers/SeriesController.php -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); - $available_books = $bookModel->getBooksNotInSeries($user_id, $id); - - $this->render('series/edit', [ - 'series' => $series, - 'books_in_series' => $books_in_series, - 'available_books' => $available_books, - '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; - } - - $this->render('series/view_public', [ - 'series' => $series, - 'books' => $books, - 'author' => $author, - 'total_words' => $total_words, - 'total_chapters' => $total_chapters, - 'page_title' => $series['title'] . ' — серия книг' - ]); - } - - public function addBook($series_id) { - $this->requireLogin(); - - $user_id = $_SESSION['user_id']; - $seriesModel = new Series($this->pdo); - $bookModel = new Book($this->pdo); - - if (!$seriesModel->userOwnsSeries($series_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой серии"; - $this->redirect('/series'); - } - - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Ошибка безопасности"; - $this->redirect("/series/{$series_id}/edit"); - } - - $book_id = (int)($_POST['book_id'] ?? 0); - $sort_order = (int)($_POST['sort_order'] ?? 0); - - if (!$book_id) { - $_SESSION['error'] = "Выберите книгу"; - $this->redirect("/series/{$series_id}/edit"); - } - - // Проверяем, что книга принадлежит пользователю - if (!$bookModel->userOwnsBook($book_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой книге"; - $this->redirect("/series/{$series_id}/edit"); - } - - // Добавляем книгу в серию - if ($bookModel->updateSeriesInfo($book_id, $series_id, $sort_order)) { - $_SESSION['success'] = "Книга добавлена в серию"; - } else { - $_SESSION['error'] = "Ошибка при добавлении книги в серию"; - } - - $this->redirect("/series/{$series_id}/edit"); - } - } - - public function removeBook($series_id, $book_id) { - $this->requireLogin(); - - if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - $_SESSION['error'] = "Неверный метод запроса"; - $this->redirect("/series/{$series_id}/edit"); - } - - if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Ошибка безопасности"; - $this->redirect("/series/{$series_id}/edit"); - } - - $user_id = $_SESSION['user_id']; - $seriesModel = new Series($this->pdo); - $bookModel = new Book($this->pdo); - - if (!$seriesModel->userOwnsSeries($series_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой серии"; - $this->redirect('/series'); - } - - // Проверяем, что книга принадлежит пользователю - if (!$bookModel->userOwnsBook($book_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой книге"; - $this->redirect("/series/{$series_id}/edit"); - } - - // Удаляем книгу из серии - if ($bookModel->removeFromSeries($book_id)) { - $_SESSION['success'] = "Книга удалена из серии"; - } else { - $_SESSION['error'] = "Ошибка при удалении книги из серии"; - } - - $this->redirect("/series/{$series_id}/edit"); - } - - public function updateBookOrder($series_id) { - $this->requireLogin(); - - if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - $_SESSION['error'] = "Неверный метод запроса"; - $this->redirect("/series/{$series_id}/edit"); - } - - if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Ошибка безопасности"; - $this->redirect("/series/{$series_id}/edit"); - } - - $user_id = $_SESSION['user_id']; - $seriesModel = new Series($this->pdo); - $bookModel = new Book($this->pdo); - - if (!$seriesModel->userOwnsSeries($series_id, $user_id)) { - $_SESSION['error'] = "У вас нет доступа к этой серии"; - $this->redirect('/series'); - } - - $order_data = $_POST['order'] ?? []; - - if (empty($order_data)) { - $_SESSION['error'] = "Нет данных для обновления"; - $this->redirect("/series/{$series_id}/edit"); - } - - // Обновляем порядок книг - if ($bookModel->reorderSeriesBooks($series_id, $order_data)) { - $_SESSION['success'] = "Порядок книг обновлен"; - } else { - $_SESSION['error'] = "Ошибка при обновлении порядка книг"; - } - - $this->redirect("/series/{$series_id}/edit"); - } -} -?> -// ./controllers/UserController.php -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; - } - - - - $this->render('user/view_public', [ - 'user' => $user, - 'books' => $books, - 'total_books' => $total_books, - 'total_words' => $total_words, - 'total_chapters' => $total_chapters, - 'page_title' => ($user['display_name'] ?: $user['username']) . ' — публичная страница' - ]); - } -} -?> -// ./controllers/BookController.php -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']); - $error = ''; - $cover_error = ''; - - 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'], - '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)) { - $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); - if ($cover_result['success']) { - $bookModel->updateCover($new_book_id, $cover_result['filename']); - } else { - $cover_error = $cover_result['error']; - // Сохраняем ошибку в сессии, чтобы показать после редиректа - $_SESSION['cover_error'] = $cover_error; - } - } - - $_SESSION['success'] = "Книга успешно создана" . ($cover_error ? ", но возникла ошибка с обложкой: " . $cover_error : ""); - $this->redirect("/books/{$new_book_id}/edit"); - } else { - $_SESSION['error'] = "Ошибка при создании книги"; - } - - } - - $this->render('books/create', [ - 'series' => $series, - 'error' => $error, - 'cover_error' => $cover_error, - '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']); - - - $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 { - $data = [ - 'title' => $title, - 'description' => trim($_POST['description'] ?? ''), - 'genre' => trim($_POST['genre'] ?? ''), - '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 - ]; - - // Обработка обложки - 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 = "Книга успешно обновлена"; - $_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, - '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 deleteAll() { - $this->requireLogin(); - $user_id = $_SESSION['user_id']; - - if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Ошибка безопасности"; - $this->redirect('/books'); - } - - $bookModel = new Book($this->pdo); - - // Получаем все книги пользователя - $books = $bookModel->findByUser($user_id); - if (empty($books)) { - $_SESSION['info'] = "У вас нет книг для удаления"; - $this->redirect('/books'); - } - - try { - $this->pdo->beginTransaction(); - - $deleted_count = 0; - $deleted_covers = 0; - - foreach ($books as $book) { - // Удаляем обложку если она есть - if (!empty($book['cover_image'])) { - $cover_path = COVERS_PATH . $book['cover_image']; - if (file_exists($cover_path) && unlink($cover_path)) { - $deleted_covers++; - } - } - - // Удаляем главы книги - $stmt = $this->pdo->prepare("DELETE FROM chapters WHERE book_id = ?"); - $stmt->execute([$book['id']]); - - // Удаляем саму книгу - $stmt = $this->pdo->prepare("DELETE FROM books WHERE id = ? AND user_id = ?"); - $stmt->execute([$book['id'], $user_id]); - - $deleted_count++; - } - - $this->pdo->commit(); - - $message = "Все книги успешно удалены ($deleted_count книг"; - if ($deleted_covers > 0) { - $message .= ", удалено $deleted_covers обложек"; - } - $message .= ")"; - - $_SESSION['success'] = $message; - } catch (Exception $e) { - $this->pdo->rollBack(); - error_log("Ошибка при массовом удалении: " . $e->getMessage()); - $_SESSION['error'] = "Произошла ошибка при удалении книг: " . $e->getMessage(); - } - - $this->redirect('/books'); - } - - public function viewPublic($share_token) { - $bookModel = new Book($this->pdo); - $chapterModel = new Chapter($this->pdo); - $book = $bookModel->findByShareToken($share_token); - if (!$book) { - http_response_code(404); - $this->render('errors/404'); - return; - } - $chapters = $chapterModel->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 viewAll($id) { - $bookModel = new Book($this->pdo); - $chapterModel = new Chapter($this->pdo); - $book = $bookModel->findById($id); - if (!$book) { - http_response_code(404); - $this->render('errors/404'); - return; - } - $chapters = $chapterModel->findByBook($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 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"); - } -} -?> -// ./controllers/AdminController.php -requireAdmin(); - } - - - public function users() { - $userModel = new User($this->pdo); - $users = $userModel->findAll(); - - $this->render('admin/users', [ - 'users' => $users, - 'page_title' => 'Управление пользователями' - ]); - } - - public function toggleUserStatus($user_id) { - if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Неверный метод запроса или токен безопасности"; - $this->redirect('/admin/users'); - return; - } - - if ($user_id == $_SESSION['user_id']) { - $_SESSION['error'] = "Нельзя изменить статус собственного аккаунта"; - $this->redirect('/admin/users'); - return; - } - - $userModel = new User($this->pdo); - $user = $userModel->findById($user_id); - - if (!$user) { - $_SESSION['error'] = "Пользователь не найден"; - $this->redirect('/admin/users'); - return; - } - - $newStatus = $user['is_active'] ? 0 : 1; - if ($userModel->updateStatus($user_id, $newStatus)) { - $_SESSION['success'] = "Статус пользователя обновлен"; - } else { - $_SESSION['error'] = "Ошибка при обновлении статуса"; - } - - $this->redirect('/admin/users'); - } - - public function deleteUser($user_id) { - if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !verify_csrf_token($_POST['csrf_token'] ?? '')) { - $_SESSION['error'] = "Неверный метод запроса или токен безопасности"; - $this->redirect('/admin/users'); - return; - } - - if ($user_id == $_SESSION['user_id']) { - $_SESSION['error'] = "Нельзя удалить собственный аккаунт"; - $this->redirect('/admin/users'); - return; - } - - $userModel = new User($this->pdo); - $user = $userModel->findById($user_id); - - if (!$user) { - $_SESSION['error'] = "Пользователь не найден"; - $this->redirect('/admin/users'); - return; - } - - if ($userModel->delete($user_id)) { - $_SESSION['success'] = "Пользователь успешно удален"; - } else { - $_SESSION['error'] = "Ошибка при удалении пользователя"; - } - - $this->redirect('/admin/users'); - } - - public function addUser() { - $error = ''; - $success = ''; - - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { - $error = "Ошибка безопасности"; - } else { - $username = trim($_POST['username'] ?? ''); - $password = $_POST['password'] ?? ''; - $password_confirm = $_POST['password_confirm'] ?? ''; - $email = trim($_POST['email'] ?? ''); - $display_name = trim($_POST['display_name'] ?? ''); - $is_active = isset($_POST['is_active']) ? 1 : 0; - - if (empty($username) || empty($password)) { - $error = 'Имя пользователя и пароль обязательны'; - } elseif ($password !== $password_confirm) { - $error = 'Пароли не совпадают'; - } elseif (strlen($password) < 6) { - $error = 'Пароль должен быть не менее 6 символов'; - } else { - $userModel = new User($this->pdo); - if ($userModel->findByUsername($username)) { - $error = 'Имя пользователя уже занято'; - } elseif (!empty($email) && $userModel->findByEmail($email)) { - $error = 'Email уже используется'; - } else { - $data = [ - 'username' => $username, - 'password' => $password, - 'email' => $email ?: null, - 'display_name' => $display_name ?: $username, - 'is_active' => $is_active - ]; - - if ($userModel->create($data)) { - $success = 'Пользователь успешно создан'; - // Очищаем поля формы - $_POST = []; - } else { - $error = 'Ошибка при создании пользователя'; - } - } - } - } - } - - $this->render('admin/add_user', [ - 'error' => $error, - 'success' => $success, - 'page_title' => 'Добавление пользователя' - ]); - } -} -?> -// ./composer.json -{ - "require": { - "phpoffice/phpword": "^1.0", - "tecnickcom/tcpdf": "^6.6" - } -} - -// ./models/Chapter.php -pdo = $pdo; - } - - public function findById($id) { - $stmt = $this->pdo->prepare(" - SELECT c.*, b.user_id, b.title as book_title - FROM chapters c - JOIN books b ON c.book_id = b.id - WHERE c.id = ? - "); - $stmt->execute([$id]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByBook($book_id) { - $stmt = $this->pdo->prepare(" - SELECT * FROM chapters - WHERE book_id = ? - ORDER BY sort_order, created_at - "); - $stmt->execute([$book_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function create($data) { - $stmt = $this->pdo->prepare("SELECT MAX(sort_order) as max_order FROM chapters WHERE book_id = ?"); - $stmt->execute([$data['book_id']]); - $result = $stmt->fetch(); - $next_order = ($result['max_order'] ?? 0) + 1; - - $word_count = $this->countWords($data['content']); - - $stmt = $this->pdo->prepare(" - INSERT INTO chapters (book_id, title, content, sort_order, word_count, status) - VALUES (?, ?, ?, ?, ?, ?) - "); - return $stmt->execute([ - $data['book_id'], - $data['title'], - $data['content'], - $next_order, - $word_count, - $data['status'] ?? 'draft' - ]); - } - - public function update($id, $data) { - $word_count = $this->countWords($data['content']); - - $stmt = $this->pdo->prepare(" - UPDATE chapters - SET title = ?, content = ?, word_count = ?, status = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? - "); - return $stmt->execute([ - $data['title'], - $data['content'], - $word_count, - $data['status'] ?? 'draft', - $id - ]); - } - - public function delete($id) { - $stmt = $this->pdo->prepare("DELETE FROM chapters WHERE id = ?"); - return $stmt->execute([$id]); - } - - public function updateSortOrder($chapter_id, $new_order) { - $stmt = $this->pdo->prepare("UPDATE chapters SET sort_order = ? WHERE id = ?"); - return $stmt->execute([$new_order, $chapter_id]); - } - - private function countWords($text) { - $text = strip_tags($text); - $text = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $text); - $words = preg_split('/\s+/', $text); - $words = array_filter($words); - return count($words); - } - - public function userOwnsChapter($chapter_id, $user_id) { - $stmt = $this->pdo->prepare(" - SELECT c.id - FROM chapters c - JOIN books b ON c.book_id = b.id - WHERE c.id = ? AND b.user_id = ? - "); - $stmt->execute([$chapter_id, $user_id]); - return $stmt->fetch() !== false; - } - - - public function getPublishedChapters($book_id) { - $stmt = $this->pdo->prepare(" - SELECT * FROM chapters - WHERE book_id = ? AND status = 'published' - ORDER BY sort_order, created_at - "); - $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]); - // } - -} -?> -// ./models/Series.php -pdo = $pdo; - } - - public function findById($id) { - $stmt = $this->pdo->prepare(" - SELECT s.*, - COUNT(b.id) as book_count, - COALESCE(( - SELECT SUM(c.word_count) - FROM chapters c - JOIN books b2 ON c.book_id = b2.id - WHERE b2.series_id = s.id AND b2.published = 1 - ), 0) as total_words - FROM series s - LEFT JOIN books b ON s.id = b.series_id AND b.published = 1 - WHERE s.id = ? - GROUP BY s.id - "); - $stmt->execute([$id]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByUser($user_id, $include_stats = true) { - if ($include_stats) { - $sql = " - SELECT s.*, - COUNT(b.id) as book_count, - COALESCE(( - SELECT SUM(c.word_count) - FROM chapters c - JOIN books b2 ON c.book_id = b2.id - WHERE b2.series_id = s.id AND b2.user_id = ? - ), 0) as total_words - FROM series s - LEFT JOIN books b ON s.id = b.series_id - WHERE s.user_id = ? - GROUP BY s.id - ORDER BY s.created_at DESC - "; - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$user_id, $user_id]); - } else { - $sql = "SELECT * FROM series WHERE user_id = ? ORDER BY created_at DESC"; - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$user_id]); - } - - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function create($data) { - $stmt = $this->pdo->prepare(" - INSERT INTO series (title, description, user_id) - VALUES (?, ?, ?) - "); - return $stmt->execute([ - $data['title'], - $data['description'] ?? null, - $data['user_id'] - ]); - } - - public function update($id, $data) { - $stmt = $this->pdo->prepare(" - UPDATE series - SET title = ?, description = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? AND user_id = ? - "); - return $stmt->execute([ - $data['title'], - $data['description'] ?? null, - $id, - $data['user_id'] - ]); - } - - public function delete($id, $user_id) { - try { - $this->pdo->beginTransaction(); - - $stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE series_id = ? AND user_id = ?"); - $stmt->execute([$id, $user_id]); - - $stmt = $this->pdo->prepare("DELETE FROM series WHERE id = ? AND user_id = ?"); - $result = $stmt->execute([$id, $user_id]); - - $this->pdo->commit(); - return $result; - } catch (Exception $e) { - $this->pdo->rollBack(); - return false; - } - } - - public function userOwnsSeries($series_id, $user_id) { - $stmt = $this->pdo->prepare("SELECT id FROM series WHERE id = ? AND user_id = ?"); - $stmt->execute([$series_id, $user_id]); - return $stmt->fetch() !== false; - } - - public function getBooksInSeries($series_id, $only_published = false) { - $sql = "SELECT * FROM books WHERE series_id = ?"; - if ($only_published) { - $sql .= " AND published = 1"; - } - $sql .= " ORDER BY sort_order_in_series, created_at"; - - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$series_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function getNextSortOrder($series_id) { - $stmt = $this->pdo->prepare("SELECT MAX(sort_order_in_series) as max_order FROM books WHERE series_id = ?"); - $stmt->execute([$series_id]); - $result = $stmt->fetch(); - return ($result['max_order'] ?? 0) + 1; - } - - public function getSeriesStats($series_id, $user_id = null) { - $sql = " - SELECT - COUNT(b.id) as book_count, - COALESCE(SUM(stats.chapter_count), 0) as chapter_count, - COALESCE(SUM(stats.total_words), 0) as total_words - FROM series s - LEFT JOIN books b ON s.id = b.series_id - LEFT JOIN ( - SELECT - book_id, - COUNT(id) as chapter_count, - SUM(word_count) as total_words - FROM chapters - GROUP BY book_id - ) stats ON b.id = stats.book_id - WHERE s.id = ? - "; - - $params = [$series_id]; - - if ($user_id) { - $sql .= " AND s.user_id = ?"; - $params[] = $user_id; - } - - $stmt = $this->pdo->prepare($sql); - $stmt->execute($params); - return $stmt->fetch(PDO::FETCH_ASSOC); - } -} -?> -// ./models/index.php - -// ./models/User.php -pdo = $pdo; - } - - public function findById($id) { - $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?"); - $stmt->execute([$id]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByUsername($username) { - $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?"); - $stmt->execute([$username]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByEmail($email) { - $stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?"); - $stmt->execute([$email]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findAll() { - $stmt = $this->pdo->prepare("SELECT id, username, display_name, email, created_at, last_login, is_active FROM users ORDER BY created_at DESC"); - $stmt->execute(); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function create($data) { - $password_hash = password_hash($data['password'], PASSWORD_DEFAULT); - - $is_active = $data['is_active'] ?? 0; - - $stmt = $this->pdo->prepare(" - INSERT INTO users (username, display_name, email, password_hash, is_active) - VALUES (?, ?, ?, ?, ?) - "); - - return $stmt->execute([ - $data['username'], - $data['display_name'] ?? $data['username'], - $data['email'] ?? null, - $password_hash, - $is_active - ]); - } - - public function update($id, $data) { - $sql = "UPDATE users SET display_name = ?, email = ?"; - $params = [$data['display_name'], $data['email']]; - - if (!empty($data['password'])) { - $sql .= ", password_hash = ?"; - $params[] = password_hash($data['password'], PASSWORD_DEFAULT); - } - - $sql .= " WHERE id = ?"; - $params[] = $id; - - $stmt = $this->pdo->prepare($sql); - return $stmt->execute($params); - } - - public function delete($id) { - $stmt = $this->pdo->prepare("DELETE FROM users WHERE id = ?"); - return $stmt->execute([$id]); - } - - public function updateStatus($id, $is_active) { - $stmt = $this->pdo->prepare("UPDATE users SET is_active = ? WHERE id = ?"); - return $stmt->execute([$is_active, $id]); - } - - public function updateLastLogin($id) { - $stmt = $this->pdo->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?"); - return $stmt->execute([$id]); - } - - public function verifyPassword($password, $hash) { - return password_verify($password, $hash); - } - - public function updateAvatar($id, $filename) { - $stmt = $this->pdo->prepare("UPDATE users SET avatar = ? WHERE id = ?"); - return $stmt->execute([$filename, $id]); - } - - public function updateBio($id, $bio) { - $stmt = $this->pdo->prepare("UPDATE users SET bio = ? WHERE id = ?"); - return $stmt->execute([$bio, $id]); - } - - public function updateProfile($id, $data) { - $sql = "UPDATE users SET display_name = ?, email = ?, bio = ?"; - $params = [ - $data['display_name'] ?? '', - $data['email'] ?? null, - $data['bio'] ?? null - ]; - - if (!empty($data['avatar'])) { - $sql .= ", avatar = ?"; - $params[] = $data['avatar']; - } - - $sql .= " WHERE id = ?"; - $params[] = $id; - - $stmt = $this->pdo->prepare($sql); - return $stmt->execute($params); - } -} -?> -// ./models/Book.php -pdo = $pdo; - } - - public function findById($id) { - $stmt = $this->pdo->prepare("SELECT * FROM books WHERE id = ?"); - $stmt->execute([$id]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByShareToken($share_token) { - $stmt = $this->pdo->prepare("SELECT * FROM books WHERE share_token = ?"); - $stmt->execute([$share_token]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByUser($user_id, $only_published = false) { - $sql = " - SELECT b.*, - COUNT(c.id) as chapter_count, - COALESCE(SUM(c.word_count), 0) as total_words - FROM books b - LEFT JOIN chapters c ON b.id = c.book_id - WHERE b.user_id = ? - "; - if ($only_published) { - $sql .= " AND b.published = 1 "; - } - $sql .= " GROUP BY b.id ORDER BY b.created_at DESC "; - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$user_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function create($data) { - $share_token = bin2hex(random_bytes(16)); - $published = isset($data['published']) ? (int)$data['published'] : 0; - - $stmt = $this->pdo->prepare(" - INSERT INTO books (title, description, genre, user_id, series_id, sort_order_in_series, share_token, published) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - "); - return $stmt->execute([ - $data['title'], - $data['description'] ?? null, - $data['genre'] ?? null, - $data['user_id'], - $data['series_id'] ?? null, - $data['sort_order_in_series'] ?? null, - $share_token, - $published - ]); - } - - public function update($id, $data) { - $published = isset($data['published']) ? (int)$data['published'] : 0; - - // Преобразуем пустые строки в 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 = ? - WHERE id = ? AND user_id = ? - "); - return $stmt->execute([ - $data['title'], - $data['description'] ?? null, - $data['genre'] ?? null, - $series_id, // Теперь это либо integer, либо NULL - $sort_order_in_series, // Теперь это либо integer, либо NULL - $published, - $id, - $data['user_id'] - ]); - } - - - public function delete($id, $user_id) { - try { - $this->pdo->beginTransaction(); - - // Удаляем главы книги - $stmt = $this->pdo->prepare("DELETE FROM chapters WHERE book_id = ?"); - $stmt->execute([$id]); - - // Удаляем саму книгу - $stmt = $this->pdo->prepare("DELETE FROM books WHERE id = ? AND user_id = ?"); - $result = $stmt->execute([$id, $user_id]); - - $this->pdo->commit(); - return $result; - } catch (Exception $e) { - $this->pdo->rollBack(); - return false; - } - } - - public function deleteAllByUser($user_id) { - try { - $this->pdo->beginTransaction(); - - // Получаем ID всех книг пользователя - $stmt = $this->pdo->prepare("SELECT id FROM books WHERE user_id = ?"); - $stmt->execute([$user_id]); - $book_ids = $stmt->fetchAll(PDO::FETCH_COLUMN); - - if (empty($book_ids)) { - $this->pdo->commit(); - return 0; - } - - // Удаляем главы всех книг пользователя (одним запросом) - $placeholders = implode(',', array_fill(0, count($book_ids), '?')); - $stmt = $this->pdo->prepare("DELETE FROM chapters WHERE book_id IN ($placeholders)"); - $stmt->execute($book_ids); - - // Удаляем все книги пользователя (одним запросом) - $stmt = $this->pdo->prepare("DELETE FROM books WHERE user_id = ?"); - $stmt->execute([$user_id]); - - $deleted_count = $stmt->rowCount(); - $this->pdo->commit(); - - return $deleted_count; - } catch (Exception $e) { - $this->pdo->rollBack(); - throw $e; - } - } - - public function userOwnsBook($book_id, $user_id) { - $stmt = $this->pdo->prepare("SELECT id FROM books WHERE id = ? AND user_id = ?"); - $stmt->execute([$book_id, $user_id]); - return $stmt->fetch() !== false; - } - - public function generateNewShareToken($book_id) { - $new_token = bin2hex(random_bytes(16)); - $stmt = $this->pdo->prepare("UPDATE books SET share_token = ? WHERE id = ?"); - $success = $stmt->execute([$new_token, $book_id]); - return $success ? $new_token : false; - } - - - - public function updateCover($book_id, $filename) { - $stmt = $this->pdo->prepare("UPDATE books SET cover_image = ? WHERE id = ?"); - return $stmt->execute([$filename, $book_id]); - } - - public function deleteCover($book_id) { - - $book = $this->findById($book_id); - $old_filename = $book['cover_image']; - - if ($old_filename) { - $file_path = COVERS_PATH . $old_filename; - if (file_exists($file_path)) { - unlink($file_path); - } - } - - $stmt = $this->pdo->prepare("UPDATE books SET cover_image = NULL WHERE id = ?"); - return $stmt->execute([$book_id]); - } - - public function updateSeriesInfo($book_id, $series_id, $sort_order) { - $stmt = $this->pdo->prepare("UPDATE books SET series_id = ?, sort_order_in_series = ? WHERE id = ?"); - return $stmt->execute([$series_id, $sort_order, $book_id]); - } - - public function removeFromSeries($book_id) { - $stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE id = ?"); - return $stmt->execute([$book_id]); - } - - public function findBySeries($series_id) { - $stmt = $this->pdo->prepare(" - SELECT b.* - FROM books b - WHERE b.series_id = ? - ORDER BY b.sort_order_in_series, b.created_at - "); - $stmt->execute([$series_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function getBookStats($book_id, $only_published_chapters = false) { - $sql = " - SELECT - COUNT(c.id) as chapter_count, - COALESCE(SUM(c.word_count), 0) as total_words - FROM books b - LEFT JOIN chapters c ON b.id = c.book_id - WHERE b.id = ? - "; - - if ($only_published_chapters) { - $sql .= " AND c.status = 'published'"; - } - - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$book_id]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - - public function getBooksNotInSeries($user_id, $series_id = null) { - $sql = "SELECT * FROM books - WHERE user_id = ? - AND (series_id IS NULL OR series_id != ? OR series_id = 0)"; - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$user_id, $series_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function reorderSeriesBooks($series_id, $new_order) { - try { - $this->pdo->beginTransaction(); - - foreach ($new_order as $order => $book_id) { - $stmt = $this->pdo->prepare("UPDATE books SET sort_order_in_series = ? WHERE id = ? AND series_id = ?"); - $stmt->execute([$order + 1, $book_id, $series_id]); - } - - $this->pdo->commit(); - return true; - } catch (Exception $e) { - $this->pdo->rollBack(); - error_log("Ошибка при обновлении порядка книг: " . $e->getMessage()); - return false; - } - } - - private function countWords($text) { - $text = strip_tags($text); - $text = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $text); - $words = preg_split('/\s+/', $text); - $words = array_filter($words); - return count($words); - } - -} -?> -// ./index.php - 'text/css', - 'js' => 'application/javascript', - 'png' => 'image/png', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif', - 'webp' => 'image/webp', - 'svg' => 'image/svg+xml', - 'ico' => 'image/x-icon', - 'json' => 'application/json', - 'woff' => 'font/woff', - 'woff2' => 'font/woff2', - 'ttf' => 'font/ttf', - 'eot' => 'application/vnd.ms-fontobject', - ]; - - $extension = strtolower(pathinfo($physicalPath, PATHINFO_EXTENSION)); - if (isset($mimeTypes[$extension])) { - header('Content-Type: ' . $mimeTypes[$extension]); - } - - // Запрещаем кэширование для разработки, в продакшене можно увеличить время - header('Cache-Control: public, max-age=3600'); - - // Отправляем файл - readfile($physicalPath); - exit; - } -} -// Простой роутер -class Router { - private $routes = []; - - public function add($pattern, $handler) { - $this->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, array_values($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], array_values($params)); - } - } - } - throw new Exception("Handler not found"); - } -} - -// Инициализация роутера -$router = new Router(); - -// Маршруты -$router->add('/', 'DashboardController@index'); -$router->add('/dashboard', 'DashboardController@index'); -$router->add('/index.php', 'DashboardController@index'); -$router->add('/login', 'AuthController@login'); -$router->add('/logout', 'AuthController@logout'); -$router->add('/register', 'AuthController@register'); - -// Книги -$router->add('/books', 'BookController@index'); -$router->add('/book/all/{id}', 'BookController@viewAll'); -$router->add('/books/create', 'BookController@create'); -$router->add('/books/{id}/edit', 'BookController@edit'); -$router->add('/books/{id}/delete', 'BookController@delete'); -$router->add('/books/delete-all', 'BookController@deleteAll'); -$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('/series/{id}/add-book', 'SeriesController@addBook'); -$router->add('/series/{id}/remove-book/{book_id}', 'SeriesController@removeBook'); -$router->add('/series/{id}/update-order', 'SeriesController@updateBookOrder'); - -// Профиль -$router->add('/profile', 'UserController@profile'); -$router->add('/profile/update', 'UserController@updateProfile'); - -// Экспорт с параметром формата -//публичный экспорт -$router->add('/export/shared/{share_token}/{format}', 'ExportController@exportShared'); -$router->add('/export/shared/{share_token}', 'ExportController@exportShared'); // по умолчанию pdf -//авторсикй экспорт -$router->add('/export/{book_id}/{format}', 'ExportController@export'); -$router->add('/export/{book_id}', 'ExportController@export'); // по умолчанию pdf - -// Публичные страницы -$router->add('/book/{share_token}', 'BookController@viewPublic'); -$router->add('/author/{id}', 'UserController@viewPublic'); -$router->add('/series/{id}/view', 'SeriesController@viewPublic'); - - -// Администрирование -$router->add('/admin/users', 'AdminController@users'); -$router->add('/admin/add-user', 'AdminController@addUser'); -$router->add('/admin/user/{user_id}/toggle-status', 'AdminController@toggleUserStatus'); -$router->add('/admin/user/{user_id}/delete', 'AdminController@deleteUser'); - - -// Обработка запроса -$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; -}); - - -?> -// ./includes/functions.php - 100) { - $filename = substr($filename, 0, 100); - } - - return $filename; -} - -function handleCoverUpload($file, $book_id) { - global $pdo; - - // Проверяем папку для загрузок - if (!file_exists(COVERS_PATH)) { - mkdir(COVERS_PATH, 0755, true); - } - - $allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; - $max_size = 5 * 1024 * 1024; // 5MB - - // Проверка типа файла - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mime_type = finfo_file($finfo, $file['tmp_name']); - finfo_close($finfo); - - if (!in_array($mime_type, $allowed_types)) { - return ['success' => false, 'error' => 'Разрешены только JPG, PNG, GIF и WebP изображения']; - } - - // Проверка размера - if ($file['size'] > $max_size) { - return ['success' => false, 'error' => 'Размер изображения не должен превышать 5MB']; - } - - // Проверка на ошибки загрузки - if ($file['error'] !== UPLOAD_ERR_OK) { - return ['success' => false, 'error' => 'Ошибка загрузки файла: ' . $file['error']]; - } - - // Генерация уникального имени файла - $extension = pathinfo($file['name'], PATHINFO_EXTENSION); - $filename = 'cover_' . $book_id . '_' . time() . '.' . $extension; - $file_path = COVERS_PATH . $filename; - - // Удаляем старую обложку если есть - $bookModel = new Book($pdo); - $bookModel->deleteCover($book_id); - - // Сохраняем новую обложку - if (move_uploaded_file($file['tmp_name'], $file_path)) { - // Оптимизируем изображение - optimizeImage($file_path); - return ['success' => true, 'filename' => $filename]; - } else { - return ['success' => false, 'error' => 'Не удалось сохранить файл']; - } -} - -function optimizeImage($file_path) { - list($width, $height, $type) = getimagesize($file_path); - - $max_width = 800; - $max_height = 1200; - - if ($width > $max_width || $height > $max_height) { - // Вычисляем новые размеры - $ratio = $width / $height; - if ($max_width / $max_height > $ratio) { - $new_width = $max_height * $ratio; - $new_height = $max_height; - } else { - $new_width = $max_width; - $new_height = $max_width / $ratio; - } - - // Создаем новое изображение - $new_image = imagecreatetruecolor($new_width, $new_height); - - // Загружаем исходное изображение в зависимости от типа - switch ($type) { - case IMAGETYPE_JPEG: - $source = imagecreatefromjpeg($file_path); - break; - case IMAGETYPE_PNG: - $source = imagecreatefrompng($file_path); - // Сохраняем прозрачность для PNG - imagecolortransparent($new_image, imagecolorallocatealpha($new_image, 0, 0, 0, 127)); - imagealphablending($new_image, false); - imagesavealpha($new_image, true); - break; - case IMAGETYPE_GIF: - $source = imagecreatefromgif($file_path); - break; - default: - return; // Не поддерживаемый тип - } - - // Ресайз и сохраняем - imagecopyresampled($new_image, $source, 0, 0, 0, 0, $new_width, $new_height, $width, $height); - - switch ($type) { - case IMAGETYPE_JPEG: - imagejpeg($new_image, $file_path, 85); - break; - case IMAGETYPE_PNG: - imagepng($new_image, $file_path, 8); - break; - case IMAGETYPE_GIF: - imagegif($new_image, $file_path); - break; - } - - // Освобождаем память - imagedestroy($source); - imagedestroy($new_image); - } -} - -function handleAvatarUpload($file, $user_id) { - global $pdo; - - // Проверяем папку для загрузок - if (!file_exists(AVATARS_PATH)) { - mkdir(AVATARS_PATH, 0755, true); - } - - $allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; - $max_size = 2 * 1024 * 1024; // 2MB - - // Проверка типа файла - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mime_type = finfo_file($finfo, $file['tmp_name']); - finfo_close($finfo); - - if (!in_array($mime_type, $allowed_types)) { - return ['success' => false, 'error' => 'Разрешены только JPG, PNG, GIF и WebP изображения']; - } - - // Проверка размера - if ($file['size'] > $max_size) { - return ['success' => false, 'error' => 'Размер изображения не должен превышать 2MB']; - } - - // Проверка на ошибки загрузки - if ($file['error'] !== UPLOAD_ERR_OK) { - return ['success' => false, 'error' => 'Ошибка загрузки файла: ' . $file['error']]; - } - - // Проверка реального типа файла по содержимому - $allowed_signatures = [ - 'image/jpeg' => "\xFF\xD8\xFF", - 'image/png' => "\x89\x50\x4E\x47", - 'image/gif' => "GIF", - 'image/webp' => "RIFF" - ]; - - $file_content = file_get_contents($file['tmp_name']); - $signature = substr($file_content, 0, 4); - - $valid_signature = false; - foreach ($allowed_signatures as $type => $sig) { - if (strpos($signature, $sig) === 0) { - $valid_signature = true; - break; - } - } - - if (!$valid_signature) { - return ['success' => false, 'error' => 'Неверный формат изображения']; - } - - // Генерация уникального имени файла - $extension = pathinfo($file['name'], PATHINFO_EXTENSION); - $filename = 'avatar_' . $user_id . '_' . time() . '.' . $extension; - $file_path = AVATARS_PATH . $filename; - - // Удаляем старый аватар если есть - $userModel = new User($pdo); - $user = $userModel->findById($user_id); - if (!empty($user['avatar'])) { - $old_file_path = AVATARS_PATH . $user['avatar']; - if (file_exists($old_file_path)) { - unlink($old_file_path); - } - } - - // Сохраняем новую аватарку - if (move_uploaded_file($file['tmp_name'], $file_path)) { - // Оптимизируем изображение - optimizeAvatar($file_path); - return ['success' => true, 'filename' => $filename]; - } else { - return ['success' => false, 'error' => 'Не удалось сохранить файл']; - } -} - -function optimizeAvatar($file_path) { - // Оптимизация аватарки - ресайз до 200x200 - list($width, $height, $type) = getimagesize($file_path); - - $max_size = 200; - - if ($width > $max_size || $height > $max_size) { - // Вычисляем новые размеры - $ratio = $width / $height; - if ($ratio > 1) { - $new_width = $max_size; - $new_height = $max_size / $ratio; - } else { - $new_width = $max_size * $ratio; - $new_height = $max_size; - } - - // Создаем новое изображение - $new_image = imagecreatetruecolor($new_width, $new_height); - - // Загружаем исходное изображение в зависимости от типа - switch ($type) { - case IMAGETYPE_JPEG: - $source = imagecreatefromjpeg($file_path); - break; - case IMAGETYPE_PNG: - $source = imagecreatefrompng($file_path); - // Сохраняем прозрачность для PNG - imagecolortransparent($new_image, imagecolorallocatealpha($new_image, 0, 0, 0, 127)); - imagealphablending($new_image, false); - imagesavealpha($new_image, true); - break; - case IMAGETYPE_GIF: - $source = imagecreatefromgif($file_path); - break; - case IMAGETYPE_WEBP: - $source = imagecreatefromwebp($file_path); - break; - default: - return; // Не поддерживаемый тип - } - - // Ресайз и сохраняем - imagecopyresampled($new_image, $source, 0, 0, 0, 0, $new_width, $new_height, $width, $height); - - switch ($type) { - case IMAGETYPE_JPEG: - imagejpeg($new_image, $file_path, 85); - break; - case IMAGETYPE_PNG: - imagepng($new_image, $file_path, 8); - break; - case IMAGETYPE_GIF: - imagegif($new_image, $file_path); - break; - case IMAGETYPE_WEBP: - imagewebp($new_image, $file_path, 85); - break; - } - - // Освобождаем память - imagedestroy($source); - imagedestroy($new_image); - } -} - -function deleteUserAvatar($user_id) { - global $pdo; - - $userModel = new User($pdo); - $user = $userModel->findById($user_id); - - if (!empty($user['avatar'])) { - $file_path = AVATARS_PATH . $user['avatar']; - if (file_exists($file_path)) { - unlink($file_path); - } - - // Обновляем запись в БД - $stmt = $pdo->prepare("UPDATE users SET avatar = NULL WHERE id = ?"); - return $stmt->execute([$user_id]); - } - - return true; -} -?> -// ./includes/index.php - -// ./composer.lock -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "493a3be12648bbe702ed126df05ead04", - "packages": [ - { - "name": "cybermonde/odtphp", - "version": "v1.7", - "source": { - "type": "git", - "url": "https://github.com/cybermonde/odtphp.git", - "reference": "23aba70923ca3c07af15a5600f4072751c1b4a36" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cybermonde/odtphp/zipball/23aba70923ca3c07af15a5600f4072751c1b4a36", - "reference": "23aba70923ca3c07af15a5600f4072751c1b4a36", - "shasum": "" - }, - "require": { - "php": ">=5.2.4" - }, - "type": "library", - "autoload": { - "classmap": [ - "library" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL" - ], - "description": "ODT document generator", - "homepage": "https://github.com/cybermonde/odtphp", - "keywords": [ - "odt", - "php" - ], - "support": { - "issues": "https://github.com/cybermonde/odtphp/issues", - "source": "https://github.com/cybermonde/odtphp/tree/v1.7" - }, - "time": "2015-06-02T07:28:25+00:00" - }, - { - "name": "phpoffice/math", - "version": "0.3.0", - "source": { - "type": "git", - "url": "https://github.com/PHPOffice/Math.git", - "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", - "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xml": "*", - "php": "^7.1|^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^0.12.88 || ^1.0.0", - "phpunit/phpunit": "^7.0 || ^9.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpOffice\\Math\\": "src/Math/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Progi1984", - "homepage": "https://lefevre.dev" - } - ], - "description": "Math - Manipulate Math Formula", - "homepage": "https://phpoffice.github.io/Math/", - "keywords": [ - "MathML", - "officemathml", - "php" - ], - "support": { - "issues": "https://github.com/PHPOffice/Math/issues", - "source": "https://github.com/PHPOffice/Math/tree/0.3.0" - }, - "time": "2025-05-29T08:31:49+00:00" - }, - { - "name": "phpoffice/phpword", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/PHPOffice/PHPWord.git", - "reference": "6d75328229bc93790b37e93741adf70646cea958" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958", - "reference": "6d75328229bc93790b37e93741adf70646cea958", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-gd": "*", - "ext-json": "*", - "ext-xml": "*", - "ext-zip": "*", - "php": "^7.1|^8.0", - "phpoffice/math": "^0.3" - }, - "require-dev": { - "dompdf/dompdf": "^2.0 || ^3.0", - "ext-libxml": "*", - "friendsofphp/php-cs-fixer": "^3.3", - "mpdf/mpdf": "^7.0 || ^8.0", - "phpmd/phpmd": "^2.13", - "phpstan/phpstan": "^0.12.88 || ^1.0.0", - "phpstan/phpstan-phpunit": "^1.0 || ^2.0", - "phpunit/phpunit": ">=7.0", - "symfony/process": "^4.4 || ^5.0", - "tecnickcom/tcpdf": "^6.5" - }, - "suggest": { - "dompdf/dompdf": "Allows writing PDF", - "ext-xmlwriter": "Allows writing OOXML and ODF", - "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpOffice\\PhpWord\\": "src/PhpWord" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-only" - ], - "authors": [ - { - "name": "Mark Baker" - }, - { - "name": "Gabriel Bull", - "email": "me@gabrielbull.com", - "homepage": "http://gabrielbull.com/" - }, - { - "name": "Franck Lefevre", - "homepage": "https://rootslabs.net/blog/" - }, - { - "name": "Ivan Lanin", - "homepage": "http://ivan.lanin.org" - }, - { - "name": "Roman Syroeshko", - "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" - }, - { - "name": "Antoine de Troostembergh" - } - ], - "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", - "homepage": "https://phpoffice.github.io/PHPWord/", - "keywords": [ - "ISO IEC 29500", - "OOXML", - "Office Open XML", - "OpenDocument", - "OpenXML", - "PhpOffice", - "PhpWord", - "Rich Text Format", - "WordprocessingML", - "doc", - "docx", - "html", - "odf", - "odt", - "office", - "pdf", - "php", - "reader", - "rtf", - "template", - "template processor", - "word", - "writer" - ], - "support": { - "issues": "https://github.com/PHPOffice/PHPWord/issues", - "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0" - }, - "time": "2025-06-05T10:32:36+00:00" - }, - { - "name": "tecnickcom/tcpdf", - "version": "6.10.0", - "source": { - "type": "git", - "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "ca5b6de294512145db96bcbc94e61696599c391d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/ca5b6de294512145db96bcbc94e61696599c391d", - "reference": "ca5b6de294512145db96bcbc94e61696599c391d", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "php": ">=7.1.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "config", - "include", - "tcpdf.php", - "tcpdf_barcodes_1d.php", - "tcpdf_barcodes_2d.php", - "include/tcpdf_colors.php", - "include/tcpdf_filters.php", - "include/tcpdf_font_data.php", - "include/tcpdf_fonts.php", - "include/tcpdf_images.php", - "include/tcpdf_static.php", - "include/barcodes/datamatrix.php", - "include/barcodes/pdf417.php", - "include/barcodes/qrcode.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-or-later" - ], - "authors": [ - { - "name": "Nicola Asuni", - "email": "info@tecnick.com", - "role": "lead" - } - ], - "description": "TCPDF is a PHP class for generating PDF documents and barcodes.", - "homepage": "http://www.tcpdf.org/", - "keywords": [ - "PDFD32000-2008", - "TCPDF", - "barcodes", - "datamatrix", - "pdf", - "pdf417", - "qrcode" - ], - "support": { - "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.10.0" - }, - "funding": [ - { - "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", - "type": "custom" - } - ], - "time": "2025-05-27T18:02:28+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "plugin-api-version": "2.9.0" -} - -// ./README.md -# Web Writer - -**Лицензия:** AGPLv3 - -**Web Writer** — веб-приложение для написания, хранения и публикации книг и серий книг. Поддерживает Markdown с расширением для диалогов, автосохранение текста, экспорт и управление пользователями. - ---- - -## 🚀 Возможности - -- **Книги и серии:** создавайте серии и добавляйте книги с главами. -- **Редактор книг:** Markdown, автосохранение текста, интерактивное содержание. -- **Предпросмотр книг:** - - **Автор:** видит все черновики и опубликованные главы. - - **Публичный доступ:** только опубликованные главы по ссылке с `shared_token`. -- **Обложки и аватары:** добавляйте изображения к книгам и профилям. -- **Экспорт:** PDF, DOCX, HTML, TXT. -- **Администрирование пользователей:** - - Управление аккаунтами, активация/деактивация. - - При удалении пользователя удаляются все его книги. -- **Публичные ссылки:** делитесь `shared_token` для просмотра опубликованных глав. - ---- - -## ⚙️ Требования - -- **PHP:** 8.0 и выше -- **MySQL** с InnoDB и внешними ключами -- **PHP расширения:** `mbstring`, `json`, `PDO` -- Веб-сервер с правами на запись в папки `config/` и `uploads/` - -> Все библиотеки уже включены в `vendor/`. Composer не нужен. - ---- - -## 🛠 Установка - -1. Скопируйте файлы на веб-сервер. -2. Проверьте доступность папок `config/` и `uploads/` для записи. -3. Перейдите в браузере на `install.php` и следуйте шагам: - - **Шаг 1: Настройки базы данных** - - Хост БД - - Имя базы данных - - Пользователь и пароль - - **Шаг 2: Создание администратора** - - Имя пользователя - - Пароль - - Email (по желанию) - - Отображаемое имя (по желанию) - -4. После успешной установки файл `config/config.php` будет сгенерирован автоматически. -5. Перейдите на главную страницу приложения (`index.php`) и войдите под админом. -6. **Не забудьте удалить или переместить файл install.php!!!** - ---- - -## 📝 Конфигурация - -Файл `config/config.php` содержит: - -- Подключение к базе данных: `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME` -- Пути к файлам: - - `UPLOAD_PATH` — корневая папка загрузок - - `COVERS_PATH` / `COVERS_URL` — обложки книг - - `AVATARS_PATH` / `AVATARS_URL` — аватары пользователей -- Адрес сайта: `SITE_URL` -- Имя приложения: `APP_NAME` = "Web Writer" - ---- - -## 🛠 Дальнейшее развитие - -- Планирую вынести работу с сущностями (книги, главы, серии, пользователи) в контроллеры. -- Создать единую точку входа для приложения. - ---- - -## ❗ Поддержка - -Все ошибки и предложения шлите в issue - ---- - -## 📜 Лицензия - -Приложение распространяется под лицензией [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html). -// ./config/index.php - -// ./config/config.php -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; - } -}); -?> -// ./install.php -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - // Пытаемся создать базу данных если не существует - $pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); - $pdo->exec("USE `$db_name`"); - - // Сохраняем данные в сессии для следующего шага - session_start(); - $_SESSION['install_data'] = [ - 'db_host' => $db_host, - 'db_name' => $db_name, - 'db_user' => $db_user, - 'db_pass' => $db_pass - ]; - - header('Location: install.php?step=2'); - exit; - - } catch (PDOException $e) { - $error = "Ошибка подключения к базе данных: " . $e->getMessage(); - } - - } elseif ($step === '2') { - // Шаг 2: Создание администратора - session_start(); - if (!isset($_SESSION['install_data'])) { - header('Location: install.php?step=1'); - exit; - } - - $admin_username = $_POST['admin_username'] ?? ''; - $admin_password = $_POST['admin_password'] ?? ''; - $admin_email = $_POST['admin_email'] ?? ''; - $admin_display_name = $_POST['admin_display_name'] ?? $admin_username; - - if (empty($admin_username) || empty($admin_password)) { - $error = 'Имя пользователя и пароль администратора обязательны'; - } else { - try { - $db = $_SESSION['install_data']; - $pdo = new PDO("mysql:host={$db['db_host']};dbname={$db['db_name']}", $db['db_user'], $db['db_pass']); - $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - // Создаем таблицы - $pdo->exec($database_sql); - - // Создаем администратора - $password_hash = password_hash($admin_password, PASSWORD_DEFAULT); - $stmt = $pdo->prepare(" - INSERT INTO users (username, display_name, password_hash, email, is_active, created_at) - VALUES (?, ?, ?, ?, 1, NOW()) - "); - $stmt->execute([$admin_username, $admin_display_name, $password_hash, $admin_email]); - - // Создаем config.php - $config_content = generate_config($db); - if (file_put_contents('config/config.php', $config_content)) { - // Создаем папки для загрузок - if (!file_exists('uploads/covers')) { - mkdir('uploads/covers', 0755, true); - } - if (!file_exists('uploads/avatars')) { - mkdir('uploads/avatars', 0755, true); - } - - $success = 'Установка завершена успешно!'; - session_destroy(); - } else { - $error = 'Не удалось создать файл config.php. Проверьте права доступа к папке config/'; - } - - } catch (PDOException $e) { - $error = "Ошибка при установке: " . $e->getMessage(); - } - } - } -} - -function generate_config($db) { - $site_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]"; - $base_path = str_replace('/install.php', '', $_SERVER['PHP_SELF']); - $site_url .= $base_path; - - return <<setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); -} catch(PDOException \$e) { - error_log("DB Error: " . \$e->getMessage()); - die("Ошибка подключения к базе данных"); -} - - - -// Автозагрузка моделей -spl_autoload_register(function (\$class_name) { - \$model_file = __DIR__ . '/../models/' . \$class_name . '.php'; - if (file_exists(\$model_file)) { - require_once \$model_file; - } -}); -?> -EOT; -} -?> - - - - - - Установка Web Writer - - - - -
-
-

Установка Web Writer

- - -
-
1. База данных
-
2. Администратор
-
3. Завершение
-
- - -
- -
- - - - - - - - - -
-

Настройки базы данных

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

Создание администратора

-

Создайте учетную запись администратора для управления приложением.

- - - - - - - - - -
- Назад - -
-
- - - - -
-

Перед установкой убедитесь, что:

-
    -
  • Сервер MySQL запущен и доступен
  • -
  • У вас есть данные для подключения к БД (хост, пользователь, пароль)
  • -
  • Папка config/ доступна для записи
  • -
  • Папка uploads/ доступна для записи
  • -
-
- -
-
- - -// ./views/layouts/header.php - - - - - - - <?= e($page_title ?? 'Web Writer') ?> - - - - - - - - -
- -
- - -
- - - -
- - -
- - - -
- - -
- - - -
- - -
- -// ./views/layouts/footer.php - -
- - - - - - -// ./views/dashboard/index.php - - -

Панель управления

- -
-
-

📚 Книги

-
- -
- Всего книг -
- -
-

📑 Главы

-
- -
- Всего глав -
- -
-

📝 Слова

-
- -
- Всего слов -
- -
-

🌐 Публикации

-
- -
- Опубликовано книг -
-
- -
-
-

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

- - -
-

- - - -

- -

- - -

- -
- - Глав: | - Слов: | - Статус: - -
-
- - - - - - - - -
- -
-

Мои серии

- - -
-

- - - -

- -

- -
- - Книг: | - Слов: - -
-
- - - - - - - -

Быстрые действия

- -
-
- - -// ./views/auth/login.php - - -
-

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

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

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

-
-
- - -// ./views/auth/register.php - - -
-

Регистрация

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

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

-
-
- - -// ./views/chapters/preview.php - - - - - - <?= e($page_title) ?> - - - - - -
-

-
-
-
- -
- - - -// ./views/chapters/index.php - - - - - -
-

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

-

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

- 📝 Создать первую главу -
- -
- - - - - - - - - - - - - $chapter): ?> - - - - - - - - - - -
Название главыСтатусСловОбновленоДействия
- - -
- -
- - - - - - -
- - ✏️ - -
- - -
-
-
-
- -
- Статистика: - Всего глав: | - Всего слов: | - Опубликовано: -
- - - -// ./views/chapters/create.php - - -

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

- - -
- -
- - -
- - -
- - - - - -
-
- - - -
- - - - Опубликованные главы видны в публичном доступе - -
-
- -
- - - ❌ Отмена -
-
- - - - - - -// ./views/chapters/edit.php - - -

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

- - -
- -
- - -
- - -
- - - - - -
-
- - - - - -
- - - - Опубликованные главы видны в публичном доступе - -
-
- -
- - - - - - ❌ Отмена - -
-
- -
-

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

-

Книга:

-

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

-

Создана:

-

Обновлена:

-
- - - - - -// ./views/series/index.php - - -
-

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

- ➕ Создать серию -
- - - - -
- -
-
-

- -

-
- Создана - - • Обновлена - -
-
- - -
- -
- - -
-
- - книг -
-
- - слов -
-
- - 0 ? round($ser['total_words'] / $ser['book_count']) : 0; - echo number_format($avg_words); - ?> - - слов/книга -
-
- - -
- -
- - - -// ./views/series/create.php - - -

Создание новой серии

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

Что такое серия?

-

Серия позволяет объединить несколько книг в одну тематическую коллекцию. Это полезно для:

-
    -
  • Циклов книг с общим сюжетом
  • -
  • Книг в одном мире или вселенной
  • -
  • Организации книг по темам или жанрам
  • -
-

Вы сможете добавить книги в серию после её создания.

-
- - -// ./views/series/edit.php - - -

Редактирование серии:

-
-

Основная информация

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

Добавить книгу в серию

- -
- - - - -
- -

Все ваши книги уже добавлены в эту серию или у вас нет доступных книг.

- Создать новую книгу - -
-
- -
-
-

Книги в серии ()

- - -
-
- - -
- $book): ?> -
-
-
- - Порядок: -
-
- ✏️ - - - - -
- -
- -
- - - -
- -

В этой серии пока нет книг. Добавьте книги с помощью формы слева.

- -
-
-
- - - - - - - -// ./views/series/view_public.php - - -
-
-
-

-

- Серия книг от - -

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

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

-

Автор еще не опубликовал книги из этой серии

-
- -
-

Книги серии

- - -
- -
- <?= e($book['title']) ?> -
- -
-
- 📚 -
-
- - -
-

- - Книга
- - -

- - -

- - - -

- - -
- - Читать - - - getBookStats($book['id'], true); - ?> - - - Глав: | Слов: - -
-
-
- -
- - -
-

- Серия создана в • - Автор: -

-
-
-
- - - - -// ./views/errors/404.php - - -
-

404 - Страница не найдена

-

- Запрашиваемая страница не существует или была перемещена. -

- -
- - -// ./views/admin/add_user.php - - -
-

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

- - -
- -
- - - -
- -
- - -
- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- - - ❌ Отмена - -
-
-
- - -// ./views/admin/users.php - - -
-

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

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

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

- ➕ Добавить пользователя -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
IDИмя пользователяОтображаемое имяEmailДата регистрацииСтатусДействия
- - -
(Вы) - -
- - -
Вход: - -
- - - - - -
-
- - -
-
- - -
-
- - Текущий пользователь - -
-
- -
- - -// ./views/user/profile.php - - -

Мой профиль

- - -
- -
- - -
-
-

Основная информация

-
- - -
- - -
- -
- - -
- -
- - -
- -
- - - - Поддерживается Markdown форматирование - -
- -
- - - ↩️ Назад - -
-
-
- -
-

Аватарка

- -
- - Аватарка - -
- -
- -
- -
- - -
- - - - Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 2MB. - Рекомендуемый размер: 200×200 пикселей. - - - -
- ❌ -
- -
- -
- - - - - -
-
- - -
-

- Примечание: Аватарка отображается на вашей публичной странице автора -

-
- -
-
- - - - -// ./views/user/view_public.php - - -
-
-
- -
- - <?= e($user['display_name'] ?: $user['username']) ?> - -
- -
- -
- -

- - - -
- -
- - - -
-
-
-
Книг
-
-
-
-
Глав
-
-
-
-
Слов
-
-
-
- -

Публикации автора

- - -
-

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

-

Следите за обновлениями, скоро здесь появятся новые произведения!

-
- -
- -
- -
- <?= e($book['title']) ?> -
- -
-
- 📚 -
-
- - -
-

- - -

- - - -

- - - getBookStats($book['id'], true); - $chapter_count = $book_stats['chapter_count'] ?? 0; - $word_count = $book_stats['total_words'] ?? 0; - ?> - -
- - Читать книгу - - - - Глав: | Слов: - -
-
-
- -
- - -
-

- Страница автора создана в • - -

-
-
-
- - - - -// ./views/books/index.php - - -

Мои книги (Всего книг: )

- - - - - - - -
- - - -
- - - - - - - - -// ./views/books/create.php - -

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

- -
- - -
- - - -
- -
- - -
- Ошибка загрузки обложки: -
- -
- -
- - - - - - - - - -
- - - - Разрешены форматы: JPG, PNG, GIF, WebP. Максимальный размер: 5MB. - -
-
- -
-
-
- - - ❌ Отмена - -
-
- -// ./views/books/edit.php - - -
- Ошибка загрузки обложки: - -
- -

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

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

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

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

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

-
- - -
- - -
-
-

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

-
-
-

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

-

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

- -

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

-
-
-

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

- - -
- - - - - - - - - - - - - - - - - - - -
НазваниеСтатусСловДействия
- - - - - - Редактировать - -
-
- -
-

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

- - ✏️ Добавить первую главу - -
- -
-
-
- - -
-
- - - - - -// ./views/books/view_public.php - - -
-
-
- -
- <?= e($book['title']) ?> -
- - -

- -

- Автор: -

- - -

- -

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

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

-

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

-
- -

Оглавление

-
- $chapter): ?> - -
- - Глава : - -
- -
- -
- - $chapter): ?> -
-

- Глава : -

- -
- -
-
- - - -
-

- Книга создана в • - -

-
-
-
- - - - diff --git a/config/config.php b/config/config.php deleted file mode 100755 index 35e767f..0000000 --- a/config/config.php +++ /dev/null @@ -1,51 +0,0 @@ -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