Initial commit
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
/**
|
||||
* AegisOne Engineering — Функции управления документацией в Markdown
|
||||
*
|
||||
* Обеспечивает:
|
||||
* - Автоматическое обнаружение .md-файлов в service/docs/
|
||||
* - Разграничение доступа по ролям (owner — всё, engineer — только разрешённое)
|
||||
* - Хранение прав в JSON-файле service/permissions/docs_permissions.json
|
||||
*/
|
||||
|
||||
define('DOCS_DIR', __DIR__ . '/../docs');
|
||||
define('DOCS_PERMISSIONS_FILE', __DIR__ . '/../permissions/docs_permissions.json');
|
||||
|
||||
/**
|
||||
* Сканирует папку docs/ и возвращает массив документов.
|
||||
* Автоматически добавляет новые файлы в файл прав (если их там нет).
|
||||
*
|
||||
* @return array Массив документов: [['slug'=>'...', 'title'=>'...', 'filename'=>'...'], ...]
|
||||
*/
|
||||
function get_docs_list(): array
|
||||
{
|
||||
if (!is_dir(DOCS_DIR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = glob(DOCS_DIR . '/*.md');
|
||||
if ($files === false || empty($files)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$docs = [];
|
||||
$permissions = load_permissions();
|
||||
|
||||
foreach ($files as $path) {
|
||||
$filename = basename($path);
|
||||
$slug = doc_slug($filename);
|
||||
$title = doc_title($slug, $filename);
|
||||
|
||||
// Автоматически добавляем новый документ в права, если его ещё нет
|
||||
if (!isset($permissions[$slug])) {
|
||||
$permissions[$slug] = [
|
||||
'title' => $title,
|
||||
'filename' => $filename,
|
||||
'sort_order' => count($permissions),
|
||||
'engineer' => false,
|
||||
];
|
||||
}
|
||||
|
||||
// Миграция: старый boolean → новый array permissions
|
||||
if (isset($permissions[$slug]['engineer']) && !isset($permissions[$slug]['permissions'])) {
|
||||
$permissions[$slug]['permissions'] = [
|
||||
'engineer' => [
|
||||
'view' => !empty($permissions[$slug]['engineer']),
|
||||
'edit' => false,
|
||||
'cancel' => false,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Если файл на диске отличается от сохранённого — обновляем
|
||||
if ($permissions[$slug]['filename'] !== $filename) {
|
||||
$permissions[$slug]['filename'] = $filename;
|
||||
}
|
||||
|
||||
$docs[] = [
|
||||
'slug' => $slug,
|
||||
'title' => $permissions[$slug]['title'],
|
||||
'filename' => $filename,
|
||||
'sort_order' => $permissions[$slug]['sort_order'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
// Сохраняем, если были добавлены новые
|
||||
save_permissions($permissions);
|
||||
|
||||
// Сортировка по sort_order (ручная), затем по title
|
||||
usort($docs, function($a, $b) {
|
||||
$soA = $a['sort_order'] ?? 0;
|
||||
$soB = $b['sort_order'] ?? 0;
|
||||
if ($soA !== $soB) return $soA - $soB;
|
||||
return strcmp($a['title'], $b['title']);
|
||||
});
|
||||
|
||||
return $docs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает содержимое .md-файла по слагу.
|
||||
* Ищет файл по slug, сканируя директорию (не полагается на сохранённое имя).
|
||||
*
|
||||
* @param string $slug Слаг документа
|
||||
* @return string|null Содержимое файла или null, если не найден
|
||||
*/
|
||||
function get_doc_content(string $slug): ?string
|
||||
{
|
||||
$permissions = load_permissions();
|
||||
if (!isset($permissions[$slug])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Сначала пробуем сохранённое имя файла
|
||||
$savedFilename = $permissions[$slug]['filename'];
|
||||
$path = DOCS_DIR . '/' . $savedFilename;
|
||||
if (file_exists($path)) {
|
||||
$content = file_get_contents($path);
|
||||
return $content !== false ? $content : null;
|
||||
}
|
||||
|
||||
// Если не нашли — ищем по slug среди всех .md файлов
|
||||
$files = glob(DOCS_DIR . '/*.md');
|
||||
if ($files === false) return null;
|
||||
|
||||
foreach ($files as $filePath) {
|
||||
$fn = basename($filePath);
|
||||
$fSlug = doc_slug($fn);
|
||||
if ($fSlug === $slug) {
|
||||
// Обновляем сохранённое имя
|
||||
$permissions[$slug]['filename'] = $fn;
|
||||
save_permissions($permissions);
|
||||
$content = file_get_contents($filePath);
|
||||
return $content !== false ? $content : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует md-заголовки и ссылки в HTML.
|
||||
* Простейший парсер для базовой читаемости.
|
||||
*
|
||||
* @param string $markdown Исходный Markdown
|
||||
* @return string HTML
|
||||
*/
|
||||
function render_markdown_simple(string $markdown): string
|
||||
{
|
||||
$html = htmlspecialchars($markdown, ENT_NOQUOTES, 'UTF-8');
|
||||
|
||||
// Заголовки h1-h3
|
||||
$html = preg_replace('/^### (.+)$/m', '<h3>$1</h3>', $html);
|
||||
$html = preg_replace('/^## (.+)$/m', '<h2>$1</h2>', $html);
|
||||
$html = preg_replace('/^# (.+)$/m', '<h1>$1</h1>', $html);
|
||||
|
||||
// Жирный и курсив
|
||||
$html = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $html);
|
||||
$html = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $html);
|
||||
|
||||
// Списки
|
||||
$html = preg_replace('/^- (.+)$/m', '<li>$1</li>', $html);
|
||||
$html = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $html);
|
||||
|
||||
// Таблицы — группируем строки с | в блоки <table>
|
||||
$lines = explode("\n", $html);
|
||||
$in_table = false;
|
||||
$sep_skipped = false;
|
||||
$table_lines = [];
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = trim($line);
|
||||
if (preg_match('/^\|.+\|$/', $trimmed)) {
|
||||
if (!$in_table) {
|
||||
$table_lines[] = '<table>';
|
||||
$in_table = true;
|
||||
$sep_skipped = false;
|
||||
}
|
||||
if (preg_match('/^\|[-:\s]+\|$/', $trimmed)) {
|
||||
$sep_skipped = true;
|
||||
continue;
|
||||
}
|
||||
$inner = substr($trimmed, 1, -1);
|
||||
$cells = array_map('trim', explode('|', $inner));
|
||||
$tag = $sep_skipped ? 'th' : 'td';
|
||||
$sep_skipped = false;
|
||||
$table_lines[] = "<tr><$tag>" . implode("</$tag><$tag>", $cells) . "</$tag></tr>";
|
||||
} else {
|
||||
if ($in_table) {
|
||||
$table_lines[] = '</table>';
|
||||
$in_table = false;
|
||||
}
|
||||
$sep_skipped = false;
|
||||
$table_lines[] = $line;
|
||||
}
|
||||
}
|
||||
if ($in_table) {
|
||||
$table_lines[] = '</table>';
|
||||
}
|
||||
$html = implode("\n", $table_lines);
|
||||
|
||||
// Ссылки
|
||||
$html = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2" target="_blank">$1</a>', $html);
|
||||
|
||||
// Абзацы
|
||||
$paragraphs = explode("\n\n", $html);
|
||||
$html = '';
|
||||
foreach ($paragraphs as $p) {
|
||||
$p = trim($p);
|
||||
if ($p === '') continue;
|
||||
if (strpos($p, '<h') === 0 || strpos($p, '<ul') === 0 || strpos($p, '<tr') === 0 || strpos($p, '<th') === 0 || strpos($p, '<table') === 0) {
|
||||
$html .= $p . "\n";
|
||||
} elseif (strpos($p, '<li') === 0) {
|
||||
$html .= $p . "\n";
|
||||
} else {
|
||||
$html .= '<p>' . nl2br($p) . "</p>\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает slug из имени файла (архитектура.md → arkhitektura).
|
||||
*/
|
||||
function doc_slug(string $filename): string
|
||||
{
|
||||
$slug = pathinfo($filename, PATHINFO_FILENAME);
|
||||
// Транслитерация кириллицы в латиницу для URL
|
||||
$converted = transliterate($slug);
|
||||
return preg_replace('/[^a-z0-9_-]/', '', strtolower($converted));
|
||||
}
|
||||
|
||||
/**
|
||||
* Транслитерация кириллицы → латиница.
|
||||
*/
|
||||
function transliterate(string $text): string
|
||||
{
|
||||
$map = [
|
||||
'а'=>'a','б'=>'b','в'=>'v','г'=>'g','д'=>'d','е'=>'e','ё'=>'yo','ж'=>'zh',
|
||||
'з'=>'z','и'=>'i','й'=>'y','к'=>'k','л'=>'l','м'=>'m','н'=>'n','о'=>'o',
|
||||
'п'=>'p','р'=>'r','с'=>'s','т'=>'t','у'=>'u','ф'=>'f','х'=>'kh','ц'=>'ts',
|
||||
'ч'=>'ch','ш'=>'sh','щ'=>'shch','ъ'=>'','ы'=>'y','ь'=>'','э'=>'e','ю'=>'yu',
|
||||
'я'=>'ya','А'=>'A','Б'=>'B','В'=>'V','Г'=>'G','Д'=>'D','Е'=>'E','Ё'=>'Yo',
|
||||
'Ж'=>'Zh','З'=>'Z','И'=>'I','Й'=>'Y','К'=>'K','Л'=>'L','М'=>'M','Н'=>'N',
|
||||
'О'=>'O','П'=>'P','Р'=>'R','С'=>'S','Т'=>'T','У'=>'U','Ф'=>'F','Х'=>'Kh',
|
||||
'Ц'=>'Ts','Ч'=>'Ch','Ш'=>'Sh','Щ'=>'Shch','Ъ'=>'','Ы'=>'Y','Ь'=>'','Э'=>'E',
|
||||
'Ю'=>'Yu','Я'=>'Ya',' '=>'-','_'=>'-',
|
||||
];
|
||||
return strtr($text, $map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Формирует человекочитаемый заголовок из слага.
|
||||
* architecture → Архитектура
|
||||
*/
|
||||
function doc_title(string $slug, string $filename = ''): string
|
||||
{
|
||||
// обратная транслитерация для некоторых известных слагов
|
||||
$known = [
|
||||
'owner' => 'Owner',
|
||||
'seo' => 'SEO',
|
||||
'sla' => 'SLA',
|
||||
'kpi-inzhenerov' => 'KPI инженеров',
|
||||
'avtomaticheskaya-sistema-shs' => 'Автоматическая система SHS',
|
||||
'oprosnik-raschet' => 'Опросник-расчёт',
|
||||
'arkhitektura-kompanii' => 'Архитектура компании',
|
||||
'kontent-plan' => 'Контент план',
|
||||
'kp-i-struktura-audita' => 'КП и структура аудита',
|
||||
'utp-i-kp' => 'УТП и КП',
|
||||
'arkhetiktura-brenda' => 'Архетиктура бренда',
|
||||
'marketing' => 'Маркеттинг',
|
||||
'skripty-prodazh' => 'Скрипты продаж',
|
||||
'osnova' => 'Основа',
|
||||
'dokazatelnaya-ekspertiza' => 'Доказательная экспертиза',
|
||||
'voronki-telegram-i-crm' => 'Воронки телеграм и CRM',
|
||||
'voronka' => 'Воронка',
|
||||
'sayt-i-upakovka' => 'Сайт и упаковка',
|
||||
'dashboard-tekhnika' => 'Дашборд техника',
|
||||
'dashboard-inzhenera' => 'Дашборд инженера',
|
||||
'blog' => 'Блог',
|
||||
'tsifrovaya-model-obshchaya' => 'Цифровая модель общая',
|
||||
];
|
||||
|
||||
if (isset($known[$slug])) {
|
||||
return $known[$slug];
|
||||
}
|
||||
|
||||
// Если есть имя файла — используем его без .md
|
||||
if ($filename !== '') {
|
||||
return pathinfo($filename, PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
return strtr($slug, ['-' => ' ', '_' => ' ']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает JSON-файл прав доступа.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function load_permissions(): array
|
||||
{
|
||||
if (!file_exists(DOCS_PERMISSIONS_FILE)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$json = file_get_contents(DOCS_PERMISSIONS_FILE);
|
||||
$data = json_decode($json, true);
|
||||
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет JSON-файл прав доступа.
|
||||
*
|
||||
* @param array $permissions
|
||||
* @return bool
|
||||
*/
|
||||
function save_permissions(array $permissions): bool
|
||||
{
|
||||
$dir = dirname(DOCS_PERMISSIONS_FILE);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
return file_put_contents(
|
||||
DOCS_PERMISSIONS_FILE,
|
||||
json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||
) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, разрешён ли документ для роли engineer.
|
||||
*
|
||||
* @param string $slug Слаг документа
|
||||
* @return bool
|
||||
*/
|
||||
function is_doc_allowed_for_engineer(string $slug): bool
|
||||
{
|
||||
$permissions = load_permissions();
|
||||
if (!isset($permissions[$slug])) return false;
|
||||
$p = $permissions[$slug];
|
||||
// Поддержка старого формата (boolean) и нового (array)
|
||||
if (is_array($p) && isset($p['permissions'])) {
|
||||
return !empty($p['permissions']['engineer']['view']);
|
||||
}
|
||||
return !empty($p['engineer']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет конкретное право инженера для документа.
|
||||
*
|
||||
* @param string $slug Слаг документа
|
||||
* @param string $action Действие: view, edit, cancel
|
||||
* @return bool
|
||||
*/
|
||||
function is_engineer_allowed(string $slug, string $action): bool
|
||||
{
|
||||
$permissions = load_permissions();
|
||||
if (!isset($permissions[$slug])) return false;
|
||||
$p = $permissions[$slug];
|
||||
if (is_array($p) && isset($p['permissions']['engineer'])) {
|
||||
return !empty($p['permissions']['engineer'][$action]);
|
||||
}
|
||||
// Backward compat
|
||||
if ($action === 'view') return !empty($p['engineer']);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает список документов, доступных для указанной роли.
|
||||
*
|
||||
* @param string $role Роль пользователя (owner|engineer|technician)
|
||||
* @return array
|
||||
*/
|
||||
function get_allowed_docs(string $role): array
|
||||
{
|
||||
$all = get_docs_list();
|
||||
|
||||
if ($role === 'owner') {
|
||||
return $all;
|
||||
}
|
||||
|
||||
if ($role === 'engineer') {
|
||||
return array_values(array_filter($all, function($d) {
|
||||
return is_doc_allowed_for_engineer($d['slug']);
|
||||
}));
|
||||
}
|
||||
|
||||
// technician — нет доступа к документам
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user