// ./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']) . '](' . $cover_url . ')
';
$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. Завершение
= htmlspecialchars($error) ?>
= htmlspecialchars($success) ?>
Перед установкой убедитесь, что:
- Сервер MySQL запущен и доступен
- У вас есть данные для подключения к БД (хост, пользователь, пароль)
- Папка
config/ доступна для записи
- Папка
uploads/ доступна для записи
// ./views/layouts/header.php
= e($page_title ?? 'Web Writer') ?>
= e($_SESSION['success']) ?>
= e($_SESSION['error']) ?>
= e($_SESSION['warning']) ?>
= e($_SESSION['info']) ?>
// ./views/layouts/footer.php
// ./views/dashboard/index.php
Панель управления
📚 Книги
= $total_books ?>
Всего книг
📑 Главы
= $total_chapters ?>
Всего глав
📝 Слова
= number_format($total_words) ?>
Всего слов
🌐 Публикации
= $published_books_count ?>
Опубликовано книг
Недавние книги
= e($book['genre']) ?>
= e($book['description']) ?>
У вас пока нет книг.
Создать первую книгу
Мои серии
= e(mb_strimwidth($ser['description'], 0, 100, '...')) ?>
У вас пока нет серий.
Создать первую серию
Быстрые действия
// ./views/auth/login.php
Вход в систему
= e($error) ?>
// ./views/auth/register.php
Регистрация
= e($error) ?>
= e($success) ?>
// ./views/chapters/preview.php
= e($page_title) ?>
= $content ?>
// ./views/chapters/index.php
Главы книги: = e($book['title']) ?>
| № |
Название главы |
Статус |
Слов |
Обновлено |
Действия |
$chapter): ?>
| = $index + 1 ?> |
= e($chapter['title']) ?>
= e(mb_strimwidth($chapter['content'], 0, 100, '...')) ?>
|
= $chapter['status'] == 'published' ? '✅ Опубликована' : '📝 Черновик' ?>
|
= $chapter['word_count'] ?> |
= date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?>
|
|
Статистика:
Всего глав: = count($chapters) ?> |
Всего слов: = array_sum(array_column($chapters, 'word_count')) ?> |
Опубликовано: = count(array_filter($chapters, function($ch) { return $ch['status'] == 'published'; })) ?>
// ./views/chapters/create.php
Новая глава для: = e($book['title']) ?>
= e($error) ?>
// ./views/chapters/edit.php
Редактирование главы: = e($chapter['title']) ?>
= e($error) ?>
Информация о главе
Книга: = e($book['title']) ?>
Количество слов: = $chapter['word_count'] ?>
Создана: = date('d.m.Y H:i', strtotime($chapter['created_at'])) ?>
Обновлена: = date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?>
// ./views/series/index.php
📚
Пока нет серий
Создайте свою первую серию, чтобы организовать книги в циклы и сериалы.
= e($ser['description']) ?>
= $ser['book_count'] ?? 0 ?>
книг
= number_format($ser['total_words'] ?? 0) ?>
слов
0 ? round($ser['total_words'] / $ser['book_count']) : 0;
echo number_format($avg_words);
?>
слов/книга
// ./views/series/create.php
Создание новой серии
= e($error) ?>
Что такое серия?
Серия позволяет объединить несколько книг в одну тематическую коллекцию. Это полезно для:
- Циклов книг с общим сюжетом
- Книг в одном мире или вселенной
- Организации книг по темам или жанрам
Вы сможете добавить книги в серию после её создания.
// ./views/series/edit.php
Редактирование серии: = e($series['title']) ?>
Основная информация
Книги в серии (= count($books_in_series) ?>)
В этой серии пока нет книг. Добавьте книги с помощью формы слева.
// ./views/series/view_public.php
В этой серии пока нет опубликованных книг
Автор еще не опубликовал книги из этой серии
Книги серии
Книга = $book['sort_order_in_series'] ?>
= e($book['title']) ?>
= e($book['genre']) ?>
= nl2br(e($book['description'])) ?>
Читать
getBookStats($book['id'], true);
?>
Глав: = $book_stats['chapter_count'] ?? 0 ?> | Слов: = $book_stats['total_words'] ?? 0 ?>
// ./views/errors/404.php
404 - Страница не найдена
Запрашиваемая страница не существует или была перемещена.
// ./views/admin/add_user.php
// ./views/admin/users.php
Управление пользователями
= e($_SESSION['success']) ?>
= e($_SESSION['error']) ?>
Пользователи не найдены
Зарегистрируйте первого пользователя
📝 Добавить пользователя
| ID |
Имя пользователя |
Отображаемое имя |
Email |
Дата регистрации |
Статус |
Действия |
| = $user['id'] ?> |
= e($user['username']) ?>
(Вы)
|
= e($user['display_name']) ?> |
= e($user['email']) ?> |
= date('d.m.Y H:i', strtotime($user['created_at'])) ?>
Вход: = date('d.m.Y H:i', strtotime($user['last_login'])) ?>
|
= $user['is_active'] ? '✅ Активен' : '❌ Неактивен' ?>
|
Текущий пользователь
|
// ./views/user/profile.php
Мой профиль
= e($message) ?>
Информация об аккаунте
👁️ Посмотреть мою публичную страницу
Дата регистрации: = date('d.m.Y H:i', strtotime($user['created_at'])) ?>
Последний вход: = date('d.m.Y H:i', strtotime($user['last_login'])) ?>
// ./views/user/view_public.php
Публикации автора
У этого автора пока нет опубликованных книг
Следите за обновлениями, скоро здесь появятся новые произведения!
= e($book['title']) ?>
= e($book['genre']) ?>
= nl2br(e($book['description'])) ?>
getBookStats($book['id'], true);
$chapter_count = $book_stats['chapter_count'] ?? 0;
$word_count = $book_stats['total_words'] ?? 0;
?>
Читать книгу
Глав: = $chapter_count ?> | Слов: = $word_count ?>
// ./views/books/index.php
Мои книги (Всего книг: = count($books) ?>)
У вас пока нет книг
Создайте свою первую книгу и начните писать!
📖 Создать первую книгу
📚
📚
= $book['published'] ? '✅' : '📝' ?>
= e($book['genre']) ?>
= e(mb_strimwidth($book['description'], 0, 120, '...')) ?>
= $book['chapter_count'] ?? 0 ?> глав
= number_format($book['total_words'] ?? 0) ?> слов
// ./views/books/create.php
Создание новой книги
= e($_SESSION['error']) ?>
= e($error) ?>
Ошибка загрузки обложки: = e($cover_error) ?>
// ./views/books/edit.php
Ошибка загрузки обложки: = e($_SESSION['cover_error']) ?>
Редактирование книги
Экспорт книги
Экспортируйте книгу в различные форматы:
Примечание: Экспортируются все главы книги (включая черновики)
Главы этой книги
| Название |
Статус |
Слов |
Действия |
| = e($chapter['title']) ?> |
= $chapter['status'] == 'published' ? 'Опубликована' : 'Черновик' ?>
|
= $chapter['word_count'] ?> |
Редактировать
|
// ./views/books/view_public.php
В этой книге пока нет глав
Автор еще не опубликовал содержание книги
Оглавление
$chapter): ?>
Глава = $index + 1 ?>: = e($chapter['title']) ?>
= $chapter['content'] ?>