Инструменты пользователя

Инструменты сайта


tmp_teh_zadanie_2_23.07.26_18:04

Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

Предыдущая версия справа и слеваПредыдущая версия
Следующая версия
Предыдущая версия
tmp_teh_zadanie_2_23.07.26_18:04 [2026/07/23 18:15] – [local.php] VladPolskiytmp_teh_zadanie_2_23.07.26_18:04 [2026/07/23 18:36] (текущий) – [local.php] VladPolskiy
Строка 871: Строка 871:
 </code> </code>
  
-===== local.php ===== +===== auth.php ===== 
-<code php local.php>+<code php auth.php> 
 +<?php 
 +/** 
 + * Слой аутентификации и работы с сессиями (Аналог inc/auth.php в DokuWiki) 
 + */ 
 + 
 +declare(strict_types=1); 
 + 
 +if (!defined('WIKI_ROOT')) { 
 +    die('Прямой доступ к файлу запрещен.'); 
 +
 + 
 +/** 
 + * Инициализация и безопасный старт сессии PHP 
 + */ 
 +function auth_setup(): void { 
 +    ini_set('session.cookie_httponly', '1'); 
 +    ini_set('session.use_only_cookies', '1'); 
 +     
 +    if (session_status() === PHP_SESSION_NONE) { 
 +        session_start(); 
 +    } 
 +
 + 
 +/** 
 + * Проверка учетных данных пользователя по файлу users.auth.php 
 + */ 
 +function auth_login(string $user, string $pass): bool { 
 +    $auth_file = WIKI_ROOT . 'conf/users.auth.php'; 
 +     
 +    if (!file_exists($auth_file)) { 
 +        return false; 
 +    } 
 + 
 +    $handle = fopen($auth_file, 'r'); 
 +    if (!$handle) { 
 +        return false; 
 +    } 
 + 
 +    while (($line = fgets($handle)) !== false) { 
 +        $line = str_replace(["\r", "\n"], '', $line); 
 +        $line = trim($line); 
 +         
 +        if (str_starts_with($line, '<?php') || str_starts_with($line, '#') || empty($line)) { 
 +            continue; 
 +        } 
 + 
 +        $parts = explode(':', $line); 
 +        if (count($parts) < 5) { 
 +            continue;  
 +        } 
 + 
 +        $login  = trim((string)array_shift($parts)); 
 +        $hash   = trim((string)array_shift($parts)); 
 +        $name   = trim((string)array_shift($parts)); 
 +        $email  = trim((string)array_shift($parts)); 
 + 
 +        if ($login === trim($user)) { 
 +            // Штатная, безопасная проверка пароля по хэшу 
 +            if (password_verify(trim($pass), $hash)) { 
 +                fclose($handle); 
 +                 
 +                $_SESSION['auth_status'] = 'logged_in'; 
 +                $_SESSION['auth_login' = $login; 
 +                $_SESSION['auth_name'  = $name; 
 +                $_SESSION['auth_email' = $email; 
 +                return true; 
 +            } 
 +        } 
 +    } 
 + 
 +    fclose($handle); 
 +    return false; 
 +
 + 
 +/** 
 + * Выход пользователя из системы (Удаление сессии) 
 + */ 
 +function auth_logout(): void { 
 +    unset($_SESSION['auth_status']); 
 +    unset($_SESSION['auth_login']); 
 +    unset($_SESSION['auth_name']); 
 +    unset($_SESSION['auth_email']); 
 +    session_destroy(); 
 +
 + 
 +/** 
 + * Проверка, авторизован ли текущий посетитель 
 + */ 
 +function auth_is_logged(): bool { 
 +    return isset($_SESSION['auth_status']) && $_SESSION['auth_status'] === 'logged_in'; 
 +
 + 
 +/** 
 + * Получение имени текущего авторизованного пользователя 
 + */ 
 +function auth_get_name(): string { 
 +    return isset($_SESSION['auth_name']) ? (string)$_SESSION['auth_name'] : 'Гость'; 
 +
 + 
 +/** 
 + * Возвращает массив данных пользователя для совместимости с шаблоном header.php 
 + */ 
 +function auth_get_user(): array { 
 +    return [ 
 +        'login' => isset($_SESSION['auth_login']) ? (string)$_SESSION['auth_login'] : '', 
 +        'name'  => auth_get_name(), 
 +        'email' => isset($_SESSION['auth_email']) ? (string)$_SESSION['auth_email'] : '' 
 +    ]; 
 +}
 </code> </code>
  
-===== local.php ===== +===== users.auth.php ===== 
-<code php local.php>+<code php users.auth.php> 
 +<?php die(); ?> 
 +# MicroWiki Users Auth File 
 +# users.auth.php 
 +# Формат записи: login:password_hash:real_name:email:groups 
 +admin:$2y$10$csfWQtrGXsmJcNQ3aaERWOJ3Am/ljBh/r/gaG8gJM0ajo41FLH0aK:Администратор:admin@wiki.local:admin,user 
 +editor:$2y$10$7R9rMkGzE8hWd2RjK1l8OuOl/Xp4yq3K9vR8fD7bC0mN5eK6wPy1a:Редактор Вася:vasya@wiki.local:user
 </code> </code>
  
 +===== hash.php =====
 +<code php hash.php>
 +<?php
 +echo password_hash('admin123', PASSWORD_DEFAULT);
 +</code>
 +
 +===== storage.php =====
 +<code php storage.php>
 +<?php
 +/**
 + * Класс управления плоскими файлами страниц (Аналог файлового ядра DokuWiki)
 + * storage.php
 + */
 +
 +declare(strict_types=1);
 +
 +if (!defined('WIKI_ROOT')) {
 +    die('Прямой доступ к файлу запрещен.');
 +}
 +
 +class Storage {
 +    private string $baseDir;
 +
 +    /**
 +     * Конструктор инициализирует базовую директорию для хранения текстов
 +     */
 +    public function __construct(string $baseDataPath) {
 +        // Гарантируем правильные слеши в путях для Windows/Linux систем
 +        $this->baseDir = rtrim(str_replace('\\', '/', $baseDataPath), '/') . '/pages/';
 +    }
 +
 +    /**
 +     * Преобразует Wiki ID (например, "отдел:инструкция") в абсолютный путь к .txt файлу
 +     */
 +    public function resolvePath(string $id): string {
 +        // Безопасность: заменяем любые попытки выйти из папки (файлы вида ../)
 +        $id = str_replace(['..', '\\'], '', $id);
 +        
 +        // В DokuWiki двоеточие — это разделитель папок. Меняем ":" на "/"
 +        $relativePath = str_replace(':', '/', $id);
 +        
 +        return $this->baseDir . $relativePath . '.txt';
 +    }
 +
 +    /**
 +     * Проверяет, существует ли физический файл страницы
 +     */
 +    public function exists(string $id): bool {
 +        return file_exists($this->resolvePath($id));
 +    }
 +
 +    /**
 +     * Безопасно читает содержимое вики-страницы
 +     */
 +    public function read(string $id): string {
 +        $filePath = $this->resolvePath($id);
 +        if (file_exists($filePath)) {
 +            return file_get_contents($filePath) ?: '';
 +        }
 +        return '';
 +    }
 +
 +    /**
 +     * Сохраняет текст страницы, автоматически создавая вложенную структуру папок
 +     */
 +    public function save(string $id, string $content): bool {
 +        $filePath = $this->resolvePath($id);
 +        $dirPath  = dirname($filePath);
 +
 +        // Если папки для пространства имен не существуют, рекурсивно создаем их
 +        if (!is_dir($dirPath)) {
 +            if (!mkdir($dirPath, 0755, true) && !is_dir($dirPath)) {
 +                return false; // Не удалось создать папку (проблема прав доступа)
 +            }
 +        }
 +
 +        // Записываем данные в файл атомарно
 +        return file_put_contents($filePath, $content) !== false;
 +    }
 +}
 +</code>
 +
 +===== MediaManager.php =====
 +<code php MediaManager.php>
 +<?php
 +/**
 + * Класс управления медиафайлами и изображениями (Аналог подсистемы Media в DokuWiki)
 + */
 +
 +declare(strict_types=1);
 +
 +if (!defined('WIKI_ROOT')) {
 +    die('Прямой доступ к файлу запрещен.');
 +}
 +
 +class MediaManager {
 +    private string $baseMediaDir;
 +    private array $allowedTypes;
 +
 +    public function __construct(string $baseDataPath) {
 +        $this->baseMediaDir = rtrim(str_replace('\\', '/', $baseDataPath), '/') . '/media/';
 +        // Белый список разрешенных расширений файлов
 +        $this->allowedTypes = [
 +            'jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt'
 +        ];
 +    }
 +
 +    public function resolveDir(string $ns): string {
 +        $ns = str_replace(['..', '\\'], '', $ns);
 +        $relativePath = str_replace(':', '/', $ns);
 +        return rtrim($this->baseMediaDir . $relativePath, '/') . '/';
 +    }
 +
 +    public function getFiles(string $ns): array {
 +        $dir = $this->resolveDir($ns);
 +        if (!is_dir($dir)) {
 +            return [];
 +        }
 +
 +        $result = [];
 +        $files = scandir($dir);
 +        
 +        foreach ($files as $file) {
 +            if ($file === '.' || $file === '..') {
 +                continue;
 +            }
 +            if (is_file($dir . $file)) {
 +                $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
 +                $isImage = in_array($ext, ['jpg', 'jpeg', 'png', 'gif'], true);
 +                
 +                $result[] = [
 +                    'name' => $file,
 +                    'size' => filesize($dir . $file),
 +                    'is_image' => $isImage,
 +                    'ext' => $ext
 +                ];
 +            }
 +        }
 +        return $result;
 +    }
 +
 +    /**
 +     * Безопасная обработка и загрузка файла из глобального массива $_FILES
 +     */
 +    public function upload(string $ns): bool {
 +        if (!isset($_FILES['uploadfile'])) {
 +            return false;
 +        }
 +
 +        $error = $_FILES['uploadfile']['error'];
 +        if ($error !== UPLOAD_ERR_OK) {
 +            return false;
 +        }
 +
 +        $name     = $_FILES['uploadfile']['name'];
 +        $tmp_name = $_FILES['uploadfile']['tmp_name'];
 +
 +        // Очищаем имя файла от опасных символов
 +        $filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $name);
 +        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
 +
 +        // Жесткая проверка: если расширения нет в белом списке — бракуем загрузку
 +        if (!in_array($ext, $this->allowedTypes, true)) {
 +            return false;
 +        }
 +
 +        $targetDir = $this->resolveDir($ns);
 +        if (!is_dir($targetDir)) {
 +            mkdir($targetDir, 0755, true);
 +        }
 +
 +        // Переносим файл из временной папки сервера в каталог data/media/
 +        return move_uploaded_file($tmp_name, $targetDir . $filename);
 +    }
 +
 +    public function delete(string $ns, string $filename): bool {
 +        $filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $filename);
 +        $filePath = $this->resolveDir($ns) . $filename;
 +        
 +        if (file_exists($filePath) && is_file($filePath)) {
 +            return unlink($filePath);
 +        }
 +        return false;
 +    }
 +}
 +</code>
 +
 +===== Parser.php =====
 +<code php Parser.php>
 +<?php
 +/**
 + * Класс трансляции вики-разметки в HTML (Аналог подсистемы парсера DokuWiki)
 + */
 +
 +declare(strict_types=1);
 +
 +if (!defined('WIKI_ROOT')) {
 +    die('Прямой доступ к файлу запрещен.');
 +}
 +
 +class Parser {
 +    public function render(string $text): string {
 +        require_once WIKI_ROOT . 'src/CodeHighlighter.php';
 +        $highlighter = new CodeHighlighter();
 +
 +        $codeBlocks = [];
 +        $blockCount = 0;
 +
 +        // Шаг 1. Вырезаем блоки <code> в сыром виде ДО какого-либо экранирования кавычек
 +        $text = preg_replace_callback('/<code(.*?)>(.*?)<\/code>/s', function(array $matches) use (&$codeBlocks, &$blockCount, $highlighter): string {
 +            $paramLine = isset($matches[1]) ? (string)$matches[1] : '';
 +            $codeBody  = isset($matches[2]) ? (string)$matches[2] : '';
 +
 +            // Обрабатываем блок кода в чистом виде
 +            $renderedBlock = $highlighter->highlight($paramLine, $codeBody);
 +            
 +            // Исправлено: Маркер сделан алфавитно-цифровым, чтобы htmlspecialchars его не портил
 +            $marker = "DokuCodeBlock" . $blockCount . "X";
 +            $codeBlocks[$marker] = $renderedBlock;
 +            $blockCount++;
 +
 +            return $marker;
 +        }, $text);
 +
 +        // Шаг 2. Безопасность: теперь экранируем ОСТАВШИЙСЯ текст страницы от XSS уязвимостей
 +        $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
 +
 +        // Шаг 3. Парсим заголовки DokuWiki
 +        $text = preg_replace('/======\s*(.*?)\s*======/', '<h1>$1</h1>', $text);
 +        $text = preg_replace('/=====\s*(.*?)\s*=====/', '<h2>$1</h2>', $text);
 +        $text = preg_replace('/====\s*(.*?)\s*====/', '<h3>$1</h3>', $text);
 +
 +        // Шаг 4. Стили форматирования текста
 +        $text = preg_replace('/\*\*(.*?)\*\*/', '<strong>$1</strong>', $text);
 +        $text = preg_replace('/\/\/(.*?)\/\//', '<em>$1</em>', $text);
 +        $text = preg_replace('/__(.*?)__/', '<u>$1</u>', $text);
 +
 +        // Шаг 5. Парсим ссылки [[page_id]] или [[page_id|Текст]]
 +        $text = preg_replace_callback('/\[\[(.*?)\]\]/', function(array $matches): string {
 +            $innerContent = isset($matches[1]) ? (string)$matches[1] : '';
 +            if (str_contains($innerContent, '|')) {
 +                $parts = explode('|', $innerContent, 2);
 +                $pageId = trim((string)array_shift($parts));
 +                $label  = trim((string)array_shift($parts));
 +            } else {
 +                $pageId = trim($innerContent);
 +                $label  = $pageId;
 +            }
 +            $pageId = ltrim($pageId, ':');
 +            return '<a href="?id=' . urlencode($pageId) . '">' . htmlspecialchars($label) . '</a>';
 +        }, $text);
 +
 +        // Шаг 6. Парсим медиа-теги изображений {{:картинка.png}}
 +        $text = preg_replace_callback('/\{\{:(.*?)\}\}/', function(array $matches): string {
 +            $innerMedia = isset($matches[1]) ? (string)$matches[1] : '';
 +            if (str_contains($innerMedia, '|')) {
 +                $mediaParts = explode('|', $innerMedia, 2);
 +                $mediaPath = trim((string)array_shift($mediaParts));
 +            } else {
 +                $mediaPath = trim($innerMedia);
 +            }
 +            $realPath = str_replace(':', '/', $mediaPath);
 +            return '<img src="data/media/' . htmlspecialchars($realPath) . '" style="max-width:100%; height:auto; margin: 12px 0; display:block; border: 1px solid #ddd; padding: 4px; border-radius: 4px;" alt="Медиафайл">';
 +        }, $text);
 +
 +        // Шаг 7. Делаем автоматические переносы строк для текста страницы
 +        $text = nl2br($text);
 +
 +        // Шаг 8. Финал: возвращаем сохраненные блоки кода обратно на свои места в обход nl2br
 +        foreach ($codeBlocks as $marker => $renderedHtml) {
 +            $text = str_replace($marker, $renderedHtml, $text);
 +        }
 +
 +        return $text;
 +    }
 +}
 +</code>
 +
 +===== CodeHighlighter.php =====
 +<code php CodeHighlighter.php>
 +<?php
 +/**
 + * Системный компонент: Честная динамическая подсветка синтаксиса PHP/JS (стиль DokuWiki)
 + */
 +
 +declare(strict_types=1);
 +
 +if (!defined('WIKI_ROOT')) {
 +    die('Прямой доступ к файлу запрещен.');
 +}
 +
 +class CodeHighlighter {
 +    public function highlight(string $paramLine, string $code): string {
 +        $code = htmlspecialchars_decode($code, ENT_QUOTES);
 +        $code = trim($code, "\r\n");
 +        $code = trim($code);
 +        
 +        $paramLine = htmlspecialchars_decode($paramLine, ENT_QUOTES);
 +        $paramLine = trim($paramLine);
 +        $parts = explode(' ', $paramLine);
 +        
 +        $firstPart = array_shift($parts);
 +        $lang = $firstPart !== null ? strtolower(trim((string)$firstPart)) : 'text';
 +        
 +        $filename = '';
 +        $highlightLine = 0;
 +        $startLine = 1;
 +
 +        // ЖЕСТКОЕ ИСПРАВЛЕНО: Используем именованную группу ?P<fname> для 100% изоляции строки от массивов
 +        if (preg_match('/"(?P<fname>[^"]+)"/', $paramLine, $fileMatches)) {
 +            $filename = isset($fileMatches['fname']) ? (string)$fileMatches['fname'] : '';
 +            
 +            // Вырезаем кавычки с файлом из параметров
 +            $paramLine = preg_replace('/"[^"]+"/', '', $paramLine);
 +            $parts = explode(' ', $paramLine);
 +        }
 +
 +        foreach ($parts as $part) {
 +            $part = trim((string)$part);
 +            if (empty($part)) continue;
 +            
 +            if (str_contains($part, '=')) {
 +                $kv = explode('=', $part, 2);
 +                $key = isset($kv[0]) ? trim((string)$kv[0]) : '';
 +                $val = isset($kv[1]) ? trim((string)$kv[1]) : '';
 +                
 +                if ($key === 'highlight') $highlightLine = (int)$val;
 +                if ($key === 'start') $startLine = (int)$val;
 +            } elseif (empty($filename) && $part !== $lang) {
 +                $filename = $part;
 +            }
 +        }
 +
 +        $lines = preg_split('/\r\n|\r|\n/', $code);
 +        if ($lines === false) { $lines = [$code]; }
 +        
 +        $firstCodeLineIdx = -1;
 +        foreach ($lines as $idx => $line) {
 +            $t = trim($line);
 +            if (!empty($t) && !str_starts_with($t, '*') && !str_starts_with($t, '/*') && !str_starts_with($t, '*/') && $t !== '/**') {
 +                $firstCodeLineIdx = $idx;
 +                break;
 +            }
 +        }
 +
 +        $htmlLines = '';
 +        $currentLineNum = $startLine;
 +
 +        foreach ($lines as $idx => $line) {
 +            $trimmedLine = trim($line);
 +            $isCommentStar = str_starts_with($trimmedLine, '*') || str_starts_with($trimmedLine, '*/') || $trimmedLine === '/**' || ($trimmedLine === '' && $idx < $firstCodeLineIdx);
 +
 +            $colorLine = $this->colorizeLine($line, $lang, $isCommentStar);
 +            $highlightClass = ($currentLineNum === $highlightLine) ? ' class="code-line-active"' : '';
 +            
 +            $htmlLines .= '<div' . $highlightClass . '>';
 +            $htmlLines .= '<span class="code-ln">' . $currentLineNum . '</span>';
 +            $htmlLines .= '<span class="code-txt">' . ($colorLine === '' ? '&nbsp;' : $colorLine) . '</span>';
 +            $htmlLines .= '</div>';
 +            
 +            $currentLineNum++;
 +        }
 +
 +        $html = '<div class="doku-code-block">';
 +        if (!empty($filename)) {
 +            $html .= '<div class="doku-code-file-label">';
 +            $html .= '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAG9JREFUeNksksENwDAIA02WSboJm6gZun/bZAt6i6pCH8A+YscgInIuOfuS89SIs6vIs8766m6KyvN7S7g7gIio9mZpEXfF3Xg6wF1FfXU36gB3w90g7v6vA+DuZvefOuxwFwN3A7O76wV3A6v7bwwAnwEGADZ0K9776Xm6AAAAAElFTkSuQmCC" style="margin-right:6px; vertical-align:middle; width:12px; height:12px;">';
 +            $html .= htmlspecialchars($filename);
 +            $html .= '</div>';
 +        }
 +        
 +        $html .= '<div class="doku-code-content-wrapper"><pre class="doku-code-content">' . $htmlLines . '</pre></div>';
 +        $html .= '</div>';
 +
 +        return $html;
 +    }
 +
 +    private function colorizeLine(string $line, string $lang, bool $isCommentStar): string {
 +        $line = htmlspecialchars($line, ENT_NOQUOTES, 'UTF-8');
 +
 +        if ($lang === 'php' || $lang === 'javascript' || $lang === 'js') {
 +            if ($isCommentStar) {
 +                return '<span style="color:#408080; font-style:italic;">' . $line . '</span>';
 +            }
 +
 +            $commentPart = '';
 +            if (preg_match('/(\/\/|#)(.*)$/', $line, $matches)) {
 +                $commentPart = isset($matches[0]) ? (string)$matches[0] : '';
 +                $line = str_replace($commentPart, '%%DOKU_COMMENT_PLACEHOLDER%%', $line);
 +            }
 +
 +            $line = preg_replace_callback('/(\'.*?\'|".*?")/', function(array $m): string {
 +                return '<span style="color:#ff0000;">' . (isset($m[0]) ? $m[0] : '') . '</span>';
 +            }, $line);
 +
 +            if ($lang === 'php') {
 +                $line = preg_replace_callback('/(?<!\w)(\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/', function(array $m): string {
 +                    return '<span style="color:#0000cb;">' . (isset($m[0]) ? $m[0] : '') . '</span>';
 +                }, $line);
 +                
 +                $controlKeywords = ['if', 'echo', 'return', 'function'];
 +                foreach ($controlKeywords as $word) {
 +                    $line = preg_replace_callback('/\b' . $word . '\b/', function(array $m) use ($word): string {
 +                        return '<span style="color:#008000;">' . $word . '</span>';
 +                    }, $line);
 +                }
 +
 +                $funcKeywords = ['defined', 'die'];
 +                foreach ($funcKeywords as $word) {
 +                    $line = preg_replace_callback('/\b' . $word . '\b/', function(array $m) use ($word): string {
 +                        return '<span style="color:#0000cb;">' . $word . '</span>';
 +                    }, $line);
 +                }
 +            }
 +
 +            // Обычная замена индексов массивов на безопасный вывод в скобках
 +            $line = preg_replace_callback('/([()\{\}\[\]])/', function(array $m): string {
 +                return '<span style="color:#008000;">' . (isset($m[0]) ? $m[0] : '') . '</span>';
 +            }, $line);
 +
 +            if (!empty($commentPart)) {
 +                $cleanComment = strip_tags($commentPart);
 +                $commentHtml = '<span style="color:#408080; font-style:italic;">' . $cleanComment . '</span>';
 +                $line = str_replace('%%DOKU_COMMENT_PLACEHOLDER%%', $commentHtml, $line);
 +            }
 +        }
 +
 +        return $line;
 +    }
 +}
 +</code>
 +
 +===== toolbar.php =====
 +<file html toolbar.php>
 +<?php
 +/**
 + * Системный компонент редактора: Монолитный Toolbar с горизонтальными пикерами DokuWiki
 + */
 +if (!defined('WIKI_ROOT')) die(); 
 +?>
 +<div class="doku-toolbar-container">
 +    <div class="doku-toolbar-group">
 +        <!-- Текст -->
 +        <button type="button" class="doku-btn-ico" title="Жирный (Ctrl+B)" onclick="insertTags('**', '**', 'жирный текст')"><strong>B</strong></button>
 +        <button type="button" class="doku-btn-ico" title="Курсив (Ctrl+I)" onclick="insertTags('//', '//', 'курсив')"><em>I</em></button>
 +        <button type="button" class="doku-btn-ico" title="Подчеркнутый (Ctrl+U)" onclick="insertTags('__', '__', 'подчеркнутый текст')"><u>U</u></button>
 +        <button type="button" class="doku-btn-ico" title="Моноширинный" onclick="insertTags('\'\'', '\'\'', 'код')"><small>TT</small></button>
 +        <button type="button" class="doku-btn-ico" title="Зачеркнутый" onclick="insertTags('<del>', '</del>', 'зачеркнутый текст')"><del>S</del></button>
 +    </div>
 +
 +    <!-- ВЫПАДАЮЩЕЕ МЕНЮ ЗАГОЛОВКОВ (ГОРИЗОНТАЛЬНЫЙ ПИКЕР) -->
 +    <div class="doku-toolbar-group">
 +        <div class="doku-dropdown">
 +            <button type="button" class="doku-btn-ico" id="dokuBtnH" title="Выбрать заголовок">
 +                <strong>H</strong><span class="doku-arrow"></span>
 +            </button>
 +            <div class="doku-dropdown-content" id="dokuDropH">
 +                <div class="doku-sub-toolbar">
 +                    <!-- Ластик / Очистка стиля -->
 +                    <button type="button" class="doku-sub-btn" title="Очистить стиль" onclick="insertTags('', '', '')">
 +                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
 +                    </button>
 +                    <!-- Кнопки уровней H1-H5 -->
 +                    <button type="button" class="doku-sub-btn" title="Заголовок H1" onclick="insertTags('====== ', ' ======\n', 'Заголовок 1')"><strong>H₁</strong></button>
 +                    <button type="button" class="doku-sub-btn" title="Заголовок H2" onclick="insertTags('===== ', ' =====\n', 'Заголовок 2')"><strong>H₂</strong></button>
 +                    <button type="button" class="doku-sub-btn" title="Заголовок H3" onclick="insertTags('==== ', ' ====\n', 'Заголовок 3')"><strong>H₃</strong></button>
 +                    <button type="button" class="doku-sub-btn" title="Заголовок H4" onclick="insertTags('=== ', ' ===\n', 'Заголовок 4')"><strong>H₄</strong></button>
 +                    <button type="button" class="doku-sub-btn" title="Заголовок H5" onclick="insertTags('== ', ' ==\n', 'Заголовок 5')"><strong>H₅</strong></button>
 +                </div>
 +            </div>
 +        </div>
 +    </div>
 +
 +    <div class="doku-toolbar-group">
 +        <!-- Списки и таблицы -->
 +        <button type="button" class="doku-btn-ico" title="Маркированный список" onclick="insertTags('  * ', '\n', 'Элемент списка')">
 +            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><circle cx="4" cy="6" r="1"/><line x1="9" y1="6" x2="20" y2="6"/><circle cx="4" cy="12" r="1"/><line x1="9" y1="12" x2="20" y2="12"/><circle cx="4" cy="18" r="1"/><line x1="9" y1="18" x2="20" y2="18"/></svg>
 +        </button>
 +        <button type="button" class="doku-btn-ico" title="Нумерованный список" onclick="insertTags('  - ', '\n', 'Элемент списка')">
 +            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4M4 10h2M4 14h2v2H4v2h2"/></svg>
 +        </button>
 +        <button type="button" class="doku-btn-ico" title="Вставить таблицу" onclick="insertTags('^ Заголовок 1 ^ Заголовок 2 ^\n| Элемент 1   | Элемент 2   |\n', '', '')">
 +            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="12" y1="3" x2="12" y2="21"/></svg>
 +        </button>
 +        
 +        <!-- ИСПРАВЛЕНО: Кнопка кода теперь обернута в блок dropdown с корректным ID и стрелочкой -->
 +        <div class="doku-dropdown">
 +            <button type="button" class="doku-btn-ico" id="dokuBtnCode" title="Вставить блок кода">
 +                &lt;&gt;<span class="doku-arrow"></span>
 +            </button>
 +            <div class="doku-dropdown-content" id="dokuDropCode">
 +                <div class="doku-sub-toolbar">
 +                    <!-- Обычный код -->
 +                    <button type="button" class="doku-sub-btn" title="Простой код" onclick="insertTags('<code>\n', '\n</code>', 'текст кода')">
 +                        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#444" stroke-width="2.5"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
 +                    </button>
 +                    <!-- HTML -->
 +                    <button type="button" class="doku-sub-btn" title="Code HTML" onclick="insertTags('<code html>\n', '\n</code>', '<!-- HTML код -->')">
 +                        <div class="file-ico css-html">HTM</div>
 +                    </button>
 +                    <!-- PHP -->
 +                    <button type="button" class="doku-sub-btn" title="Code PHP" onclick="insertTags('<code php>\n', '\n</code>', '// PHP код')">
 +                        <div class="file-ico css-php">PHP</div>
 +                    </button>
 +                    <!-- CSS -->
 +                    <button type="button" class="doku-sub-btn" title="Code CSS" onclick="insertTags('<code css>\n', '\n</code>', '/* CSS стили */')">
 +                        <div class="file-ico css-css">CSS</div>
 +                    </button>
 +                    <!-- JS -->
 +                    <button type="button" class="doku-sub-btn" title="Code JavaScript" onclick="insertTags('<code javascript>\n', '\n</code>', '// JS код')">
 +                        <div class="file-ico css-js">JS</div>
 +                    </button>
 +                    <!-- SQL -->
 +                    <button type="button" class="doku-sub-btn" title="Code SQL" onclick="insertTags('<code sql>\n', '\n</code>', 'SELECT * FROM table;')">
 +                        <div class="file-ico css-sql">SQL</div>
 +                    </button>
 +                    <!-- BASH -->
 +                    <button type="button" class="doku-sub-btn" title="Code Bash" onclick="insertTags('<code bash>\n', '\n</code>', '#!/bin/bash')">
 +                        <div class="file-ico css-bash">BSH</div>
 +                    </button>
 +                </div>
 +            </div>
 +        </div>
 +    </div>
 +
 +    <div class="doku-toolbar-group">
 +        <!-- Ссылки -->
 +        <button type="button" class="doku-btn-ico" title="Внутренняя ссылка" onclick="insertTags('[[:пространство:имя_файла|%sel%', ']]', 'разметка')">
 +            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
 +        </button>
 +        <button type="button" class="doku-btn-ico" title="Вставить изображение" onclick="openMediaModal()">
 +            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
 +        </button>
 +    </div>
 +</div>
 +
 +<!-- Каркас модального окна скрыт ниже, без изменений -->
 +<div id="mediaModal" style="display:none; position:fixed; z-index:9999; left:0; top:0; width:100%; height:100%; background-color:rgba(0,0,0,0.5); align-items:center; justify-content:center;">
 +    <div style="background:#fff; padding:25px; border-radius:6px; width:500px; max-width:90%; box-shadow:0 4px 20px rgba(0,0,0,0.2); position:relative;">
 +        <span onclick="closeMediaModal()" style="position:absolute; right:15px; top:10px; font-size:24px; font-weight:bold; cursor:pointer; color:#aaa;">&times;</span>
 +        <h3 style="margin-top:0; color:#2b5e8f; margin-bottom:20px; font-family: sans-serif;">Загрузка и вставка изображения</h3>
 +        <div id="modalUploadContainer">
 +            <div style="border:2px dashed #ccc; padding:30px; text-align:center; background:#fafafa; border-radius:4px; margin-bottom:15px; cursor:pointer; position:relative;">
 +                <input type="file" id="modalFileInp" name="uploadfile" accept="image/*" style="position:absolute; left:0; top:0; width:100%; height:100%; opacity:0; cursor:pointer;">
 +                <span id="uploadPrompt" style="color:#666; font-size:14px; font-family: sans-serif;">📄 Кликните сюда для выбора картинки</span>
 +            </div>
 +            <div style="text-align:right; font-family: sans-serif;">
 +                <span onclick="closeMediaModal()" style="display:inline-block; padding:8px 15px; background:#eee; border:1px solid #ccc; border-radius:4px; cursor:pointer; margin-right:10px; font-size:14px; color:#333;">Отмена</span>
 +                <span onclick="sendModalFile(event)" style="display:inline-block; padding:8px 20px; background:#2b5e8f; color:#fff; border-radius:4px; cursor:pointer; font-weight:bold; font-size:14px;">Загрузить и вставить</span>
 +            </div>
 +        </div>
 +    </div>
 +</div>
 +</file>
 +
 +===== toolbar.js =====
 +<code js toolbar.js>
 +/**
 + * Функция вставки вики-тегов в поле редактора
 + */
 +function insertTags(openTag, closeTag, defaultText) {
 +    var txtarea = document.getElementById("wikiEditor");
 +    if (!txtarea) return;
 +
 +    var start = txtarea.selectionStart;
 +    var end = txtarea.selectionEnd;
 +    var selText = txtarea.value.substring(start, end);
 +    
 +    if (!selText) { 
 +        selText = defaultText; 
 +    }
 +    
 +    var replacement = '';
 +    var newCursorStart = start;
 +    var newCursorEnd = end;
 +
 +    if (openTag.indexOf('%sel%') !== -1) {
 +        replacement = openTag.replace('%sel%', selText) + closeTag;
 +        txtarea.value = txtarea.value.substring(0, start) + replacement + txtarea.value.substring(end, txtarea.value.length);
 +        newCursorStart = start + 3; 
 +        newCursorEnd = start + 3;
 +    } else {
 +        replacement = openTag + selText + closeTag;
 +        txtarea.value = txtarea.value.substring(0, start) + replacement + txtarea.value.substring(end, txtarea.value.length);
 +        newCursorStart = start;
 +        newCursorEnd = start + replacement.length;
 +    }
 +    
 +    txtarea.focus();
 +    txtarea.selectionStart = newCursorStart;
 +    txtarea.selectionEnd = newCursorEnd;
 +}
 +
 +/**
 + * Инициализация горячих клавиш
 + */
 +document.addEventListener("DOMContentLoaded", function() {
 +    var editor = document.getElementById("wikiEditor");
 +    if (!editor) return;
 +
 +    editor.addEventListener("keydown", function(e) {
 +        var isCtrl = e.ctrlKey || e.metaKey;
 +        if (isCtrl) {
 +            if (e.key.toLowerCase() === 'b') { e.preventDefault(); insertTags('**', '**', 'жирный текст'); }
 +            if (e.key.toLowerCase() === 'i') { e.preventDefault(); insertTags('//', '//', 'курсив'); }
 +            if (e.key.toLowerCase() === 'u') { e.preventDefault(); insertTags('__', '__', 'подчеркнутый текст'); }
 +        }
 +    });
 +});
 +
 +/**
 + * Асинхронный движок предпросмотра текста в стиле DokuWiki
 + */
 +function runLivePreview() {
 +    var editor = document.getElementById("wikiEditor");
 +    var previewBlock = document.getElementById("previewBlock");
 +    var previewContent = document.getElementById("previewContent");
 +    
 +    if (!editor || !previewBlock || !previewContent) return;
 +    
 +    var textValue = editor.value;
 +    
 +    var formData = new FormData();
 +    formData.append("wikitext", textValue);
 +    
 +    var urlParams = new URLSearchParams(window.location.search);
 +    var pageId = urlParams.get('id') || 'start';
 +    
 +    previewContent.innerHTML = "<span style='color:#666; font-style:italic;'>Генерация предпросмотра...</span>";
 +    previewBlock.style.display = "block";
 +    
 +    previewBlock.scrollIntoView({ behavior: 'smooth' });
 +
 +    // Отправляем запрос на бэкенд
 +    fetch('?id=' + encodeURIComponent(pageId) + '&do=api_preview', {
 +        method: 'POST',
 +        body: formData
 +    })
 +    .then(function(response) {
 +        if (!response.ok) {
 +            throw new Error("Сервер вернул статус " + response.status);
 +        }
 +        // ИСПРАВЛЕНО: Ждем чистый текст/HTML, а не JSON!
 +        return response.text(); 
 +    })
 +    .then(function(htmlResult) {
 +        // Мгновенно отрисовываем HTML в блоке превью
 +        previewContent.innerHTML = htmlResult;
 +    })
 +    .catch(function(error) {
 +        console.error('Preview Error:', error);
 +        previewContent.innerHTML = "<span style='color:red;'>Не удалось загрузить предпросмотр: " + error.message + "</span>";
 +    });
 +}
 +
 +// Добавьте этот код в самый конец файла assets/js/toolbar.js:
 +
 +/**
 + * Управление выпадающими горизонтальными пикерами Toolbar
 + */
 +document.addEventListener("DOMContentLoaded", function() {
 +    var btnH = document.getElementById("dokuBtnH");
 +    var btnCode = document.getElementById("dokuBtnCode");
 +    
 +    var dropH = document.getElementById("dokuDropH");
 +    var dropCode = document.getElementById("dokuDropCode");
 +
 +    if (btnH && dropH) {
 +        btnH.addEventListener("click", function(e) {
 +            e.stopPropagation();
 +            if (dropCode) dropCode.classList.remove("doku-dropdown-show");
 +            dropH.classList.toggle("doku-dropdown-show");
 +        });
 +    }
 +
 +    if (btnCode && dropCode) {
 +        btnCode.addEventListener("click", function(e) {
 +            e.stopPropagation();
 +            if (dropH) dropH.classList.remove("doku-dropdown-show");
 +            dropCode.classList.toggle("doku-dropdown-show");
 +        });
 +    }
 +
 +    document.addEventListener("click", function() {
 +        if (dropH) dropH.classList.remove("doku-dropdown-show");
 +        if (dropCode) dropCode.classList.remove("doku-dropdown-show");
 +    });
 +});
 +</code>
 +
 +===== media-modal.js =====
 +<code js media-modal.js>
 +/**
 + * Системный компонент: Управление модальным окном Медиа-менеджера и AJAX загрузкой
 + */
 +
 +function openMediaModal() {
 +    var modal = document.getElementById("mediaModal");
 +    if (modal) {
 +        modal.style.display = "flex";
 +        document.getElementById("uploadPrompt").innerText = "📄 Кликните сюда для выбора картинки";
 +        document.getElementById("modalFileInp").value = ""; 
 +    }
 +}
 +
 +function closeMediaModal() {
 +    var modal = document.getElementById("mediaModal");
 +    if (modal) modal.style.display = "none";
 +}
 +
 +/**
 + * Асинхронная отправка файла на бэкенд без перезагрузки страницы
 + */
 +function sendModalFile(event) {
 +    // Жестко глушим любые системные события браузера, чтобы предотвратить submit формы редактора
 +    if (event) {
 +        event.preventDefault();
 +        event.stopPropagation();
 +    }
 +
 +    var fileInp = document.getElementById("modalFileInp");
 +    if (!fileInp || !fileInp.files || fileInp.files.length === 0) {
 +        alert("Пожалуйста, выберите файл для загрузки.");
 +        return;
 +    }
 +
 +    var urlParams = new URLSearchParams(window.location.search);
 +    var pageId = urlParams.get('id') || 'start';
 +
 +    var targetFile = fileInp.files.item(0);
 +
 +    var formData = new FormData();
 +    formData.append("uploadfile", targetFile);
 +
 +    // Выполняем асинхронный фоновый запрос к бэкенду
 +    fetch('?id=' + encodeURIComponent(pageId) + '&do=api_upload', {
 +        method: 'POST',
 +        body: formData
 +    })
 +    .then(function(response) {
 +        if (!response.ok) {
 +            throw new Error("Ошибка сети: сервер вернул статус " + response.status);
 +        }
 +        return response.json();
 +    })
 +    .then(function(data) {
 +        if (data.success) {
 +            // Вставляем полученный тег в поле редактора
 +            insertTags(data.tag, '', '');
 +            closeMediaModal();
 +        } else {
 +            alert("Ошибка сервера: " + data.error);
 +        }
 +    })
 +    .catch(function(error) {
 +        console.error('AJAX Error:', error);
 +        alert("Критическая ошибка при фоновой отправке файла: " + error.message);
 +    });
 +}
 +
 +document.addEventListener("DOMContentLoaded", function() {
 +    var fileInp = document.getElementById("modalFileInp");
 +    if (fileInp) {
 +        fileInp.addEventListener("change", function() {
 +            if (this.files && this.files.length > 0) {
 +                var currentFile = this.files.item(0);
 +                document.getElementById("uploadPrompt").innerText = "✅ Выбран файл: " + currentFile.name;
 +            }
 +        });
 +    }
 +});
 +</code>
  
  
tmp_teh_zadanie_2_23.07.26_18/04.1784819755.txt.gz · Последнее изменение: VladPolskiy

Если не указано иное, содержимое этой вики предоставляется на условиях следующей лицензии: Public Domain
Public Domain Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki