changes
This commit is contained in:
@@ -27,6 +27,8 @@ RUN apt-get update && apt-get install -y \
|
||||
# LibreOffice для конвертации DOC в DOCX
|
||||
libreoffice-common \
|
||||
libreoffice-writer \
|
||||
# Kerberos для IMAP
|
||||
libkrb5-dev \
|
||||
# Установка Node.js (актуальная LTS-версия 18)
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
@@ -34,9 +36,14 @@ RUN apt-get update && apt-get install -y \
|
||||
# Очистка кэша для уменьшения размера образа
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Установка install-php-extensions
|
||||
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/install-php-extensions
|
||||
|
||||
# Установка расширений PHP
|
||||
RUN docker-php-ext-configure gd --with-webp --with-jpeg \
|
||||
&& docker-php-ext-install -j$(nproc) gd pdo_mysql bcmath zip intl
|
||||
&& docker-php-ext-install -j$(nproc) gd pdo_mysql bcmath zip intl \
|
||||
&& install-php-extensions imap
|
||||
|
||||
# Установка Composer
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Actions;
|
||||
|
||||
use App\Containers\Dashboard\Data\EmailAttachmentData;
|
||||
use App\Containers\Dashboard\Exceptions\EmailFetchException;
|
||||
use App\Containers\Dashboard\Tasks\ConnectToImapTask;
|
||||
use App\Containers\Dashboard\Tasks\DownloadAttachmentsTask;
|
||||
use App\Containers\Dashboard\Tasks\FetchUnreadEmailsTask;
|
||||
use App\Containers\Dashboard\Tasks\FilterBySenderTask;
|
||||
use App\Containers\Dashboard\Tasks\MarkEmailAsReadTask;
|
||||
use App\Containers\Dashboard\Actions\ProcessMixedFilesAction;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Webklex\PHPIMAP\Client;
|
||||
use Webklex\PHPIMAP\Folder;
|
||||
|
||||
/**
|
||||
* Оркестрация процесса получения новостей из Email
|
||||
*/
|
||||
class FetchEmailNewsAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConnectToImapTask $connectToImapTask,
|
||||
private readonly FetchUnreadEmailsTask $fetchUnreadEmailsTask,
|
||||
private readonly FilterBySenderTask $filterBySenderTask,
|
||||
private readonly DownloadAttachmentsTask $downloadAttachmentsTask,
|
||||
private readonly MarkEmailAsReadTask $markEmailAsReadTask,
|
||||
private readonly ProcessMixedFilesAction $processMixedFilesAction,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Выполнить получение и обработку новостей из Email
|
||||
*
|
||||
* @return array Результат обработки
|
||||
* @throws EmailFetchException
|
||||
*/
|
||||
public function run(): array
|
||||
{
|
||||
// Проверяем, включена ли функция
|
||||
if (!config('email-news.enabled', true)) {
|
||||
Log::warning('[FetchEmailNewsAction] Функция отключена в конфиге');
|
||||
throw EmailFetchException::featureDisabled();
|
||||
}
|
||||
|
||||
Log::info('[FetchEmailNewsAction] Начало получения новостей из Email');
|
||||
|
||||
$result = [
|
||||
'processed_emails' => 0,
|
||||
'skipped_emails' => 0,
|
||||
'created_posts' => 0,
|
||||
'errors' => [],
|
||||
'posts' => [],
|
||||
];
|
||||
|
||||
try {
|
||||
// Подключаемся к IMAP
|
||||
$client = $this->connectToImapTask->run();
|
||||
|
||||
// Получаем папку
|
||||
$folder = $this->connectToImapTask->getFolder(
|
||||
$client,
|
||||
config('email-news.folder', 'INBOX')
|
||||
);
|
||||
|
||||
// Получаем непрочитанные письма
|
||||
$emails = $this->fetchUnreadEmailsTask->run($folder);
|
||||
|
||||
if (empty($emails)) {
|
||||
Log::info('[FetchEmailNewsAction] Нет непрочитанных писем');
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Фильтруем по отправителю
|
||||
$filteredEmails = $this->filterBySenderTask->run($emails);
|
||||
|
||||
$result['skipped_emails'] = count($emails) - count($filteredEmails);
|
||||
|
||||
// Обрабатываем каждое письмо
|
||||
foreach ($filteredEmails as $email) {
|
||||
$emailResult = $this->processEmail($email, $folder);
|
||||
|
||||
if ($emailResult['success']) {
|
||||
$result['created_posts']++;
|
||||
$result['posts'][] = $emailResult['post'];
|
||||
} else {
|
||||
$result['errors'][] = [
|
||||
'email_subject' => $email['subject'],
|
||||
'error' => $emailResult['error'],
|
||||
];
|
||||
}
|
||||
|
||||
$result['processed_emails']++;
|
||||
}
|
||||
|
||||
Log::info('[FetchEmailNewsAction] Завершено', [
|
||||
'processed' => $result['processed_emails'],
|
||||
'created_posts' => $result['created_posts'],
|
||||
'skipped' => $result['skipped_emails'],
|
||||
'errors_count' => count($result['errors']),
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[FetchEmailNewsAction] Критическая ошибка', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработать одно письмо
|
||||
*
|
||||
* @param array $email Данные письма
|
||||
* @param Folder $folder IMAP папка
|
||||
* @return array Результат обработки
|
||||
*/
|
||||
private function processEmail(array $email, Folder $folder): array
|
||||
{
|
||||
Log::info('[FetchEmailNewsAction:processEmail] Обработка письма', [
|
||||
'subject' => $email['subject'],
|
||||
'from' => $email['from_email'],
|
||||
]);
|
||||
|
||||
try {
|
||||
// Скачиваем вложения
|
||||
$attachments = $this->downloadAttachmentsTask->run($email['message']);
|
||||
|
||||
// Проверяем, есть ли DOC/DOCX файл
|
||||
$hasDocument = collect($attachments)->contains(fn($att) => $att->isDocument());
|
||||
|
||||
if (!$hasDocument) {
|
||||
Log::warning('[FetchEmailNewsAction:processEmail] Нет DOC/DOCX файла во вложениях', [
|
||||
'subject' => $email['subject'],
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Нет DOC/DOCX файла для извлечения текста',
|
||||
];
|
||||
}
|
||||
|
||||
// Конвертируем вложения в UploadedFile
|
||||
$uploadedFiles = $this->convertToUploadedFiles($attachments);
|
||||
|
||||
// Обрабатываем через существующий ProcessMixedFilesAction
|
||||
$postResult = $this->processMixedFilesAction->run($uploadedFiles);
|
||||
|
||||
// Помечаем письмо как прочитанное
|
||||
$this->markEmail($email['message'], $folder);
|
||||
|
||||
Log::info('[FetchEmailNewsAction:processEmail] Письмо успешно обработано', [
|
||||
'subject' => $email['subject'],
|
||||
'post_id' => $postResult['post']->id,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'post' => $postResult['post'],
|
||||
'attachments_count' => count($attachments),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[FetchEmailNewsAction:processEmail] Ошибка обработки письма', [
|
||||
'subject' => $email['subject'],
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертировать EmailAttachmentData в UploadedFile
|
||||
*
|
||||
* @param array<EmailAttachmentData> $attachments
|
||||
* @return \Illuminate\Support\Collection<UploadedFile>
|
||||
*/
|
||||
private function convertToUploadedFiles(array $attachments): \Illuminate\Support\Collection
|
||||
{
|
||||
$uploadedFiles = [];
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
$fullPath = storage_path('app/' . $attachment->path);
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
Log::warning('[FetchEmailNewsAction:convertToUploadedFiles] Файл не найден', [
|
||||
'path' => $attachment->path,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Создаём UploadedFile из сохранённого файла
|
||||
$uploadedFile = new UploadedFile(
|
||||
$fullPath,
|
||||
$attachment->filename,
|
||||
$attachment->mimeType,
|
||||
null,
|
||||
true // test = false (файл валиден)
|
||||
);
|
||||
|
||||
$uploadedFiles[] = $uploadedFile;
|
||||
}
|
||||
|
||||
Log::info('[FetchEmailNewsAction:convertToUploadedFiles] Конвертировано файлов', [
|
||||
'count' => count($uploadedFiles),
|
||||
]);
|
||||
|
||||
return collect($uploadedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Пометить письмо как прочитанное (и возможно переместить)
|
||||
*
|
||||
* @param object $message IMAP сообщение
|
||||
* @param Folder $folder Текущая папка
|
||||
*/
|
||||
private function markEmail(object $message, Folder $folder): void
|
||||
{
|
||||
$moveToFolder = config('email-news.move_to_folder');
|
||||
|
||||
if ($moveToFolder) {
|
||||
$this->markEmailAsReadTask->markAndMove($message, $moveToFolder);
|
||||
} elseif (config('email-news.mark_as_read', true)) {
|
||||
$this->markEmailAsReadTask->run($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Containers\Dashboard\Actions;
|
||||
use App\Containers\Article\Models\Category;
|
||||
use App\Containers\Article\Models\Post;
|
||||
use App\Containers\Dashboard\Tasks\CallAiServiceTask;
|
||||
use App\Containers\Dashboard\Tasks\CompressImageTask;
|
||||
use App\Containers\Dashboard\Tasks\CreatePostFromAiDataTask;
|
||||
use App\Containers\Dashboard\Tasks\ExtractTextFromDocumentTask;
|
||||
use App\Containers\Dashboard\Tasks\FindMainNewsFileTask;
|
||||
@@ -19,6 +20,7 @@ class ProcessMixedFilesAction
|
||||
private readonly ExtractTextFromDocumentTask $extractTextFromDocumentTask,
|
||||
private readonly CallAiServiceTask $callAiServiceTask,
|
||||
private readonly CreatePostFromAiDataTask $createPostFromAiDataTask,
|
||||
private readonly CompressImageTask $compressImageTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -104,18 +106,72 @@ class ProcessMixedFilesAction
|
||||
*/
|
||||
private function saveFiles(Collection $files, UploadedFile $mainFile): array
|
||||
{
|
||||
Log::info('[ProcessMixedFilesAction:saveFiles] Начало сохранения файлов', [
|
||||
'total_files' => $files->count(),
|
||||
]);
|
||||
|
||||
// Сохраняем основной документ
|
||||
$documentPath = $mainFile->store('documents', 'local');
|
||||
|
||||
// Сохраняем остальные файлы как медиа
|
||||
$mediaPaths = $files
|
||||
->filter(fn($file) => $file !== $mainFile)
|
||||
->map(fn($file) => $file->store('media', 'public'))
|
||||
->toArray();
|
||||
// Сжимаем и сохраняем остальные файлы как медиа
|
||||
$mediaPaths = [];
|
||||
$compressionStats = ['total' => 0, 'compressed' => 0, 'saved_bytes' => 0];
|
||||
|
||||
foreach ($files as $file) {
|
||||
// Пропускаем основной файл
|
||||
if ($file === $mainFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$compressionStats['total']++;
|
||||
|
||||
// Если это изображение - сжимаем
|
||||
if ($this->compressImageTask->isImage($file)) {
|
||||
Log::info('[ProcessMixedFilesAction:saveFiles] Обработка изображения', [
|
||||
'file' => $file->getClientOriginalName(),
|
||||
]);
|
||||
|
||||
$result = $this->compressImageTask->run($file);
|
||||
|
||||
if ($result['compressed']) {
|
||||
$compressionStats['compressed']++;
|
||||
$compressionStats['saved_bytes'] += $result['original_size'] - $result['size'];
|
||||
|
||||
Log::info('[ProcessMixedFilesAction:saveFiles] Изображение сжато', [
|
||||
'file' => $file->getClientOriginalName(),
|
||||
'original_size' => $this->formatFileSize($result['original_size']),
|
||||
'compressed_size' => $this->formatFileSize($result['size']),
|
||||
'ratio' => $result['compression_ratio'] . '%',
|
||||
]);
|
||||
}
|
||||
|
||||
// Сохраняем сжатый файл
|
||||
$path = $result['file']->store('media', 'public');
|
||||
|
||||
// Очищаем временный файл
|
||||
if (file_exists($result['file']->getRealPath())) {
|
||||
unlink($result['file']->getRealPath());
|
||||
}
|
||||
|
||||
$mediaPaths[] = $path;
|
||||
} else {
|
||||
// Не изображения сохраняем как есть
|
||||
$path = $file->store('media', 'public');
|
||||
$mediaPaths[] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('[ProcessMixedFilesAction:saveFiles] Статистика сжатия', [
|
||||
'total_images' => $compressionStats['total'],
|
||||
'compressed' => $compressionStats['compressed'],
|
||||
'saved' => $this->formatFileSize($compressionStats['saved_bytes']),
|
||||
'saved_bytes' => $compressionStats['saved_bytes'],
|
||||
]);
|
||||
|
||||
return [
|
||||
'documentPath' => $documentPath,
|
||||
'mediaPaths' => $mediaPaths,
|
||||
'compressionStats' => $compressionStats,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ namespace App\Containers\Dashboard\Actions;
|
||||
use App\Containers\Article\Models\Category;
|
||||
use App\Containers\Article\Models\Post;
|
||||
use App\Containers\Dashboard\Tasks\CallAiServiceTask;
|
||||
use App\Containers\Dashboard\Tasks\CompressImageTask;
|
||||
use App\Containers\Dashboard\Tasks\CreatePostFromAiDataTask;
|
||||
use App\Containers\Dashboard\Tasks\ExtractTextFromDocumentTask;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessUploadedFilesAction
|
||||
{
|
||||
@@ -15,6 +17,7 @@ class ProcessUploadedFilesAction
|
||||
private readonly ExtractTextFromDocumentTask $extractTextFromDocumentTask,
|
||||
private readonly CallAiServiceTask $callAiServiceTask,
|
||||
private readonly CreatePostFromAiDataTask $createPostFromAiDataTask,
|
||||
private readonly CompressImageTask $compressImageTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -64,13 +67,48 @@ class ProcessUploadedFilesAction
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет медиафайлы
|
||||
* Сохраняет медиафайлы со сжатием
|
||||
*/
|
||||
private function saveMediaFiles(array $mediaFiles): array
|
||||
{
|
||||
return collect($mediaFiles)
|
||||
->map(fn($file) => $file->store('media', 'public'))
|
||||
->toArray();
|
||||
$paths = [];
|
||||
$compressionStats = ['total' => 0, 'compressed' => 0, 'saved_bytes' => 0];
|
||||
|
||||
foreach ($mediaFiles as $file) {
|
||||
$compressionStats['total']++;
|
||||
|
||||
// Если это изображение - сжимаем
|
||||
if ($this->compressImageTask->isImage($file)) {
|
||||
$result = $this->compressImageTask->run($file);
|
||||
|
||||
if ($result['compressed']) {
|
||||
$compressionStats['compressed']++;
|
||||
$compressionStats['saved_bytes'] += $result['original_size'] - $result['size'];
|
||||
}
|
||||
|
||||
// Сохраняем сжатый файл
|
||||
$path = $result['file']->store('media', 'public');
|
||||
|
||||
// Очищаем временный файл
|
||||
if (file_exists($result['file']->getRealPath())) {
|
||||
unlink($result['file']->getRealPath());
|
||||
}
|
||||
|
||||
$paths[] = $path;
|
||||
} else {
|
||||
// Не изображения сохраняем как есть
|
||||
$path = $file->store('media', 'public');
|
||||
$paths[] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('[ProcessUploadedFilesAction] Статистика сжатия', [
|
||||
'total_images' => $compressionStats['total'],
|
||||
'compressed' => $compressionStats['compressed'],
|
||||
'saved' => $this->formatFileSize($compressionStats['saved_bytes']),
|
||||
]);
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Commands;
|
||||
|
||||
use App\Containers\Dashboard\Actions\FetchEmailNewsAction;
|
||||
use App\Containers\Dashboard\Exceptions\EmailFetchException;
|
||||
use App\Ship\Abstracts\Commands\ConsoleCommand;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Artisan команда для получения новостей из Email
|
||||
*/
|
||||
class FetchEmailNewsCommand extends ConsoleCommand
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'email:fetch-news
|
||||
{--force : Принудительный запуск, даже если отключено в конфиге}
|
||||
{--log : Выводить подробный лог в консоль}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Получение новостей из Email (IMAP) и создание черновиков';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(FetchEmailNewsAction $fetchEmailNewsAction): int
|
||||
{
|
||||
$this->info('📧 Запуск получения новостей из Email...');
|
||||
|
||||
$force = $this->option('force');
|
||||
$verbose = $this->option('log');
|
||||
|
||||
// Проверяем, включена ли функция
|
||||
if (!config('email-news.enabled', true) && !$force) {
|
||||
$this->error('❌ Функция отключена в конфиге (EMAIL_NEWS_ENABLED=false)');
|
||||
$this->warn('Используйте --force для принудительного запуска');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $fetchEmailNewsAction->run();
|
||||
|
||||
// Вывод результатов
|
||||
$this->newLine();
|
||||
$this->line('📊 Результаты обработки:');
|
||||
$this->table(
|
||||
['Метрика', 'Значение'],
|
||||
[
|
||||
['Обработано писем', $result['processed_emails']],
|
||||
['Пропущено (не редактор)', $result['skipped_emails']],
|
||||
['Создано новостей', $result['created_posts']],
|
||||
['Ошибок', count($result['errors'])],
|
||||
]
|
||||
);
|
||||
|
||||
// Вывод созданных постов
|
||||
if (!empty($result['posts'])) {
|
||||
$this->newLine();
|
||||
$this->info('✅ Созданные новости:');
|
||||
foreach ($result['posts'] as $post) {
|
||||
$status = $post->status instanceof \BackedEnum ? $post->status->value : $post->status;
|
||||
$this->line(" • {$post->title} (ID: {$post->id}, статус: {$status})");
|
||||
}
|
||||
}
|
||||
|
||||
// Вывод ошибок
|
||||
if (!empty($result['errors'])) {
|
||||
$this->newLine();
|
||||
$this->error('❌ Ошибки:');
|
||||
foreach ($result['errors'] as $error) {
|
||||
$this->warn(" • {$error['email_subject']}: {$error['error']}");
|
||||
}
|
||||
}
|
||||
|
||||
// Подробный лог
|
||||
if ($verbose) {
|
||||
$this->newLine();
|
||||
$this->info('📝 Детальный лог доступен в storage/logs/laravel.log');
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info('✅ Завершено!');
|
||||
|
||||
return 0;
|
||||
} catch (EmailFetchException $e) {
|
||||
$this->error('❌ Ошибка Email: ' . $e->getMessage());
|
||||
Log::error('[FetchEmailNewsCommand] EmailFetchException', [
|
||||
'code' => $e->getCode(),
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return 1;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Критическая ошибка: ' . $e->getMessage());
|
||||
$this->warn('Проверьте логи: storage/logs/laravel.log');
|
||||
|
||||
Log::error('[FetchEmailNewsCommand] Critical Error', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Email News Fetching Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Настройки для автоматического получения новостей из Email
|
||||
|
|
||||
*/
|
||||
|
||||
// Включить ли функционал
|
||||
'enabled' => env('EMAIL_NEWS_ENABLED', true),
|
||||
|
||||
// Имя аккаунта из config/imap.php
|
||||
'imap_account' => 'email_news',
|
||||
|
||||
// Email редактора (единственный разрешённый отправитель)
|
||||
'editor_email' => env('EMAIL_NEWS_SENDER_EMAIL'),
|
||||
|
||||
// Whitelist email-адресов (если редакторов несколько)
|
||||
'allowed_senders' => [
|
||||
env('EMAIL_NEWS_SENDER_EMAIL'),
|
||||
// Можно добавить дополнительные email
|
||||
// env('EMAIL_NEWS_SENDER_EMAIL_2'),
|
||||
],
|
||||
|
||||
// IMAP папка для проверки
|
||||
'folder' => env('EMAIL_NEWS_FOLDER', 'INBOX'),
|
||||
|
||||
// Что делать с письмами после обработки
|
||||
'mark_as_read' => env('EMAIL_NEWS_MARK_AS_READ', true),
|
||||
|
||||
// Перемещать ли обработанные письма в другую папку (или null)
|
||||
'move_to_folder' => env('EMAIL_NEWS_MOVE_TO', null),
|
||||
|
||||
// Папка для сохранения вложений
|
||||
'attachments_folder' => 'email_attachments',
|
||||
|
||||
// Максимальный размер вложения (в байтах), 0 = без ограничений
|
||||
'max_attachment_size' => env('EMAIL_NEWS_MAX_ATTACHMENT_SIZE', 41943040), // 40MB
|
||||
|
||||
// Логировать ли отклонённые письма (не от редактора)
|
||||
'log_skipped_emails' => env('EMAIL_NEWS_LOG_SKIPPED', true),
|
||||
];
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Data;
|
||||
|
||||
/**
|
||||
* DTO для представления вложения Email
|
||||
*/
|
||||
class EmailAttachmentData
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $filename,
|
||||
public readonly string $mimeType,
|
||||
public readonly int $size,
|
||||
public readonly string $path,
|
||||
public readonly ?string $contentId = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Создать DTO из файла
|
||||
*/
|
||||
public static function fromFile(string $path, string $originalFilename, string $mimeType, int $size, ?string $contentId = null): self
|
||||
{
|
||||
return new self(
|
||||
filename: $originalFilename,
|
||||
mimeType: $mimeType,
|
||||
size: $size,
|
||||
path: $path,
|
||||
contentId: $contentId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, является ли вложение изображением
|
||||
*/
|
||||
public function isImage(): bool
|
||||
{
|
||||
return str_starts_with($this->mimeType, 'image/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, является ли вложение документом (DOC/DOCX)
|
||||
*/
|
||||
public function isDocument(): bool
|
||||
{
|
||||
$documentMimes = [
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
];
|
||||
|
||||
$documentExtensions = ['doc', 'docx'];
|
||||
|
||||
return in_array($this->mimeType, $documentMimes, true)
|
||||
|| in_array(strtolower(pathinfo($this->filename, PATHINFO_EXTENSION)), $documentExtensions, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить расширение файла
|
||||
*/
|
||||
public function getExtension(): string
|
||||
{
|
||||
return strtolower(pathinfo($this->filename, PATHINFO_EXTENSION));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Исключение для ошибок при получении Email
|
||||
*/
|
||||
class EmailFetchException extends Exception
|
||||
{
|
||||
/**
|
||||
* Ошибка подключения к IMAP
|
||||
*/
|
||||
public static function connectionFailed(string $message = ''): self
|
||||
{
|
||||
return new self(
|
||||
message: 'Не удалось подключиться к IMAP-серверу. ' . ($message ?: ''),
|
||||
code: 1001,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Папка не найдена
|
||||
*/
|
||||
public static function folderNotFound(string $folder): self
|
||||
{
|
||||
return new self(
|
||||
message: "IMAP-папка не найдена: {$folder}",
|
||||
code: 1002,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Нет писем для обработки
|
||||
*/
|
||||
public static function noEmailsFound(): self
|
||||
{
|
||||
return new self(
|
||||
message: 'Не найдено писем для обработки',
|
||||
code: 1003,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ошибка при загрузке вложения
|
||||
*/
|
||||
public static function attachmentDownloadFailed(string $filename, string $message = ''): self
|
||||
{
|
||||
return new self(
|
||||
message: "Не удалось загрузить вложение: {$filename}. " . ($message ?: ''),
|
||||
code: 1004,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ошибка: письмо не от редактора
|
||||
*/
|
||||
public static function senderNotAllowed(string $senderEmail): self
|
||||
{
|
||||
return new self(
|
||||
message: "Письмо получено от неразрешённого отправителя: {$senderEmail}",
|
||||
code: 1005,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ошибка: нет вложений
|
||||
*/
|
||||
public static function noAttachmentsFound(): self
|
||||
{
|
||||
return new self(
|
||||
message: 'В письме не найдено вложений',
|
||||
code: 1006,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ошибка: функционал отключён
|
||||
*/
|
||||
public static function featureDisabled(): self
|
||||
{
|
||||
return new self(
|
||||
message: 'Функция получения новостей из Email отключена в конфиге',
|
||||
code: 1007,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Модель лога получения Email
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $message_id
|
||||
* @property string $from_email
|
||||
* @property string|null $from_name
|
||||
* @property string $subject
|
||||
* @property \Carbon\Carbon $email_date
|
||||
* @property string $status (success, failed, skipped)
|
||||
* @property int|null $post_id
|
||||
* @property string|null $error_message
|
||||
* @property int $attachments_count
|
||||
* @property int $total_size
|
||||
* @property string|null $processed_from
|
||||
* @property array|null $metadata
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
*/
|
||||
class EmailFetchLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = false;
|
||||
|
||||
protected $casts = [
|
||||
'email_date' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
'total_size' => 'integer',
|
||||
'attachments_count' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Связь с постом
|
||||
*/
|
||||
public function post(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Containers\Article\Models\Post::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: успешные
|
||||
*/
|
||||
public function scopeSuccessful($query)
|
||||
{
|
||||
return $query->where('status', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: неудачные
|
||||
*/
|
||||
public function scopeFailed($query)
|
||||
{
|
||||
return $query->where('status', 'failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: пропущенные
|
||||
*/
|
||||
public function scopeSkipped($query)
|
||||
{
|
||||
return $query->where('status', 'skipped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: от конкретного отправителя
|
||||
*/
|
||||
public function scopeFromSender($query, string $email)
|
||||
{
|
||||
return $query->where('from_email', $email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать лог успешной обработки
|
||||
*/
|
||||
public static function logSuccess(
|
||||
string $messageId,
|
||||
string $fromEmail,
|
||||
string $subject,
|
||||
\DateTime $emailDate,
|
||||
int $postId,
|
||||
int $attachmentsCount,
|
||||
int $totalSize,
|
||||
array $metadata = []
|
||||
): self {
|
||||
return static::create([
|
||||
'message_id' => $messageId,
|
||||
'from_email' => $fromEmail,
|
||||
'from_name' => null,
|
||||
'subject' => $subject,
|
||||
'email_date' => $emailDate,
|
||||
'status' => 'success',
|
||||
'post_id' => $postId,
|
||||
'error_message' => null,
|
||||
'attachments_count' => $attachmentsCount,
|
||||
'total_size' => $totalSize,
|
||||
'processed_from' => request()->ip() ?? null,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать лог ошибки
|
||||
*/
|
||||
public static function logFailed(
|
||||
string $messageId,
|
||||
string $fromEmail,
|
||||
string $subject,
|
||||
\DateTime $emailDate,
|
||||
string $errorMessage,
|
||||
int $attachmentsCount = 0,
|
||||
array $metadata = []
|
||||
): self {
|
||||
return static::create([
|
||||
'message_id' => $messageId,
|
||||
'from_email' => $fromEmail,
|
||||
'from_name' => null,
|
||||
'subject' => $subject,
|
||||
'email_date' => $emailDate,
|
||||
'status' => 'failed',
|
||||
'post_id' => null,
|
||||
'error_message' => $errorMessage,
|
||||
'attachments_count' => $attachmentsCount,
|
||||
'total_size' => 0,
|
||||
'processed_from' => request()->ip() ?? null,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать лог пропущенного письма
|
||||
*/
|
||||
public static function logSkipped(
|
||||
string $messageId,
|
||||
string $fromEmail,
|
||||
string $subject,
|
||||
\DateTime $emailDate,
|
||||
string $reason = 'Not from allowed sender',
|
||||
array $metadata = []
|
||||
): self {
|
||||
return static::create([
|
||||
'message_id' => $messageId,
|
||||
'from_email' => $fromEmail,
|
||||
'from_name' => null,
|
||||
'subject' => $subject,
|
||||
'email_date' => $emailDate,
|
||||
'status' => 'skipped',
|
||||
'post_id' => null,
|
||||
'error_message' => $reason,
|
||||
'attachments_count' => 0,
|
||||
'total_size' => 0,
|
||||
'processed_from' => request()->ip() ?? null,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
# 📧 Email News Fetching
|
||||
|
||||
Автоматическое получение новостей из Email и создание черновиков в системе.
|
||||
|
||||
## 📋 Описание
|
||||
|
||||
Функция позволяет автоматически:
|
||||
- Подключаться к IMAP-серверу
|
||||
- Получать непрочитанные письма от редактора
|
||||
- Скачивать вложения (DOC, DOCX, изображения)
|
||||
- Создавать новости через существующий `ProcessMixedFilesAction`
|
||||
- Помечать обработанные письма как прочитанные
|
||||
|
||||
## 🏗 Архитектура (Porto)
|
||||
|
||||
```
|
||||
app/Containers/Dashboard/
|
||||
├── Actions/
|
||||
│ └── FetchEmailNewsAction.php # Оркестрация процесса
|
||||
├── Tasks/
|
||||
│ ├── ConnectToImapTask.php # IMAP подключение
|
||||
│ ├── FetchUnreadEmailsTask.php # Получение писем
|
||||
│ ├── FilterBySenderTask.php # Фильтрация по отправителю
|
||||
│ ├── DownloadAttachmentsTask.php # Загрузка вложений
|
||||
│ └── MarkEmailAsReadTask.php # Пометка как прочитанное
|
||||
├── Data/
|
||||
│ └── EmailAttachmentData.php # DTO для вложений
|
||||
├── Exceptions/
|
||||
│ └── EmailFetchException.php # Исключения
|
||||
├── Commands/
|
||||
│ └── FetchEmailNewsCommand.php # Artisan команда
|
||||
└── Configs/
|
||||
└── email-news.php # Конфигурация
|
||||
```
|
||||
|
||||
## 🔧 Установка
|
||||
|
||||
### 1. Пересобрать PHP-контейнер (требуется расширение IMAP)
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
docker-compose build --no-cache ntspi-php
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Настроить переменные окружения
|
||||
|
||||
Скопируйте `.env.email-news.example` в `.env` и заполните:
|
||||
|
||||
```env
|
||||
EMAIL_NEWS_ENABLED=true
|
||||
EMAIL_NEWS_IMAP_HOST=imap.yandex.ru
|
||||
EMAIL_NEWS_IMAP_PORT=993
|
||||
EMAIL_NEWS_ENCRYPTION=ssl
|
||||
EMAIL_NEWS_IMAP_USER=news@ntspi.ru
|
||||
EMAIL_NEWS_IMAP_PASS=app_password
|
||||
EMAIL_NEWS_SENDER_EMAIL=editor@example.com
|
||||
EMAIL_NEWS_FOLDER=INBOX
|
||||
```
|
||||
|
||||
### 3. Протестировать подключение
|
||||
|
||||
Запустите команду вручную:
|
||||
|
||||
```bash
|
||||
docker exec -it ntspi-php php artisan email:fetch-news --log
|
||||
```
|
||||
|
||||
## 📝 Использование
|
||||
|
||||
### Ручной запуск
|
||||
|
||||
```bash
|
||||
# Обычный запуск
|
||||
docker exec -it ntspi-php php artisan email:fetch-news
|
||||
|
||||
# С подробным логом
|
||||
docker exec -it ntspi-php php artisan email:fetch-news --log
|
||||
|
||||
# Принудительный запуск (если отключено в конфиге)
|
||||
docker exec -it ntspi-php php artisan email:fetch-news --force
|
||||
```
|
||||
|
||||
### Автоматический запуск (Cron)
|
||||
|
||||
Команда автоматически добавлена в расписание Laravel Scheduler:
|
||||
- **Частота:** каждые 5 минут
|
||||
- **Защита от перекрытий:** `withoutOverlapping()`
|
||||
- **Один сервер:** `onOneServer()`
|
||||
|
||||
Scheduler уже настроен в `app/Ship/Kernels/ConsoleKernel.php`.
|
||||
|
||||
Убедитесь, что Cron запущен в контейнере:
|
||||
|
||||
```bash
|
||||
# Проверьте crontab
|
||||
docker exec -it ntspi-php crontab -l
|
||||
|
||||
# Должно быть:
|
||||
# * * * * * php /var/www/artisan schedule:run >> /dev/null 2>&1
|
||||
```
|
||||
|
||||
## 🔐 Безопасность
|
||||
|
||||
### Фильтрация отправителей
|
||||
|
||||
Обрабатываются **только письма от редактора**, указанного в `EMAIL_NEWS_SENDER_EMAIL`.
|
||||
|
||||
Письма от других отправителей:
|
||||
- ❌ Не обрабатываются
|
||||
- ❌ Не скачиваются
|
||||
- ✅ Логируются (если `EMAIL_NEWS_LOG_SKIPPED=true`)
|
||||
- ✅ Остаются в папке (не помечаются прочитанными)
|
||||
|
||||
### Whitelist нескольких отправителей
|
||||
|
||||
В конфиге `app/Containers/Dashboard/Configs/email-news.php` можно указать несколько email:
|
||||
|
||||
```php
|
||||
'allowed_senders' => [
|
||||
env('EMAIL_NEWS_SENDER_EMAIL'),
|
||||
env('EMAIL_NEWS_SENDER_EMAIL_2'),
|
||||
env('EMAIL_NEWS_SENDER_EMAIL_3'),
|
||||
],
|
||||
```
|
||||
|
||||
### App-Specific Password
|
||||
|
||||
**Рекомендуется** использовать специальный пароль приложения вместо основного пароля:
|
||||
|
||||
- **Yandex:** https://passport.yandex.ru/profile/passwords
|
||||
- **Google:** https://myaccount.google.com/apppasswords
|
||||
- **Mail.ru:** https://account.mail.ru/security
|
||||
|
||||
## 📊 Логирование
|
||||
|
||||
Логи доступны в `storage/logs/laravel.log`:
|
||||
|
||||
```log
|
||||
[2024-01-15 10:30:00] local.INFO: [FetchEmailNewsAction] Начало получения новостей из Email
|
||||
[2024-01-15 10:30:01] local.INFO: [ConnectToImapTask] Успешное подключение к IMAP
|
||||
[2024-01-15 10:30:02] local.INFO: [FetchUnreadEmailsTask] Получены письма: count=3
|
||||
[2024-01-15 10:30:02] local.INFO: [FilterBySenderTask] Фильтрация писем: total=3, filtered=2, skipped=1
|
||||
[2024-01-15 10:30:05] local.INFO: [FetchEmailNewsAction:processEmail] Письмо успешно обработано: post_id=123
|
||||
```
|
||||
|
||||
## ⚠️ Обработка ошибок
|
||||
|
||||
### Типичные ошибки
|
||||
|
||||
| Ошибка | Причина | Решение |
|
||||
|--------|---------|---------|
|
||||
| `Не удалось подключиться к IMAP-серверу` | Неправильный хост/порт/пароль | Проверьте `.env` настройки |
|
||||
| `IMAP-папка не найдена` | Папка не существует | Проверьте имя папки в `EMAIL_NEWS_FOLDER` |
|
||||
| `Письмо получено от неразрешённого отправителя` | Отправитель не в whitelist | Добавьте email в `allowed_senders` |
|
||||
| `Нет DOC/DOCX файла для извлечения текста` | Во вложениях нет документа | Редактор должен прикрепить DOC/DOCX |
|
||||
|
||||
### Отладка
|
||||
|
||||
```bash
|
||||
# Запуск с подробным выводом
|
||||
docker exec -it ntspi-php php artisan email:fetch-news --log
|
||||
|
||||
# Просмотр последних логов
|
||||
docker exec -it ntspi-php tail -f storage/logs/laravel.log
|
||||
|
||||
# Проверка расширения IMAP
|
||||
docker exec -it ntspi-php php -m | grep imap
|
||||
```
|
||||
|
||||
## 🔄 Поток данных
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Cron (каждые 5 мин) → php artisan email:fetch-news │
|
||||
│ ↓ │
|
||||
│ FetchEmailNewsCommand │
|
||||
│ ↓ │
|
||||
│ FetchEmailNewsAction │
|
||||
│ ├─ ConnectToImapTask (IMAP-соединение) │
|
||||
│ ├─ FetchUnreadEmailsTask (получение писем) │
|
||||
│ ├─ FilterBySenderTask (проверка отправителя) │
|
||||
│ ├─ DownloadAttachmentsTask (скачивание вложений) │
|
||||
│ └─ ProcessMixedFilesAction (создание поста) ←───┐ │
|
||||
│ ↓ │ │
|
||||
│ MarkEmailAsReadTask (пометка как прочитанное) │ │
|
||||
└───────────────────────────────────────────────────┼────┘
|
||||
│
|
||||
┌───────────────────────────────┘
|
||||
↓
|
||||
Существующий процесс создания новости
|
||||
(AI-распознавание, сжатие изображений, и т.д.)
|
||||
```
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
### Отправка тестового письма
|
||||
|
||||
1. Отправьте письмо с `EMAIL_NEWS_SENDER_EMAIL` на `EMAIL_NEWS_IMAP_USER`
|
||||
2. Прикрепите DOC/DOCX файл (текст новости)
|
||||
3. Прикрепите изображения (опционально)
|
||||
4. Запустите команду:
|
||||
|
||||
```bash
|
||||
docker exec -it ntspi-php php artisan email:fetch-news --log
|
||||
```
|
||||
|
||||
5. Проверьте создание новости в Dashboard
|
||||
|
||||
### Мок-тестирование (для разработчиков)
|
||||
|
||||
```php
|
||||
// В Unit-тестах можно моковать IMAP
|
||||
$this->mock(ClientManager::class, function ($mock) {
|
||||
$mock->shouldReceive('account')
|
||||
->with('email_news')
|
||||
->andReturn($client);
|
||||
});
|
||||
```
|
||||
|
||||
## 📚 Ссылки
|
||||
|
||||
- [Webklex PHP-IMAP Documentation](https://github.com/Webklex/php-imap)
|
||||
- [Laravel Scheduler](https://laravel.com/docs/10.x/scheduling)
|
||||
- [Porto Architecture](https://github.com/AlxDorosenco/Porto)
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Intervention\Image\Facades\Image;
|
||||
|
||||
class CompressImageTask
|
||||
{
|
||||
/**
|
||||
* Максимальное качество сжатия WebP (0-100)
|
||||
*/
|
||||
private const WEBP_QUALITY = 82;
|
||||
|
||||
/**
|
||||
* Максимальное качество для JPEG (0-100)
|
||||
*/
|
||||
private const JPEG_QUALITY = 85;
|
||||
|
||||
/**
|
||||
* Максимальная ширина изображения (px)
|
||||
*/
|
||||
private const MAX_WIDTH = 1920;
|
||||
|
||||
/**
|
||||
* Максимальная высота изображения (px)
|
||||
*/
|
||||
private const MAX_HEIGHT = 1920;
|
||||
|
||||
/**
|
||||
* Порог размера файла для сжатия (в байтах) - 1MB
|
||||
*/
|
||||
private const SIZE_THRESHOLD = 1048576;
|
||||
|
||||
/**
|
||||
* Сжимает изображение и конвертирует в WebP
|
||||
*
|
||||
* @param UploadedFile $file Исходный файл изображения
|
||||
* @return array ['path' => путь к файлу, 'size' => размер в байтах, 'original_size' => исходный размер]
|
||||
*/
|
||||
public function run(UploadedFile $file): array
|
||||
{
|
||||
$extension = strtolower($file->getClientOriginalExtension());
|
||||
|
||||
Log::info('[CompressImageTask] Начало обработки изображения', [
|
||||
'file' => $file->getClientOriginalName(),
|
||||
'extension' => $extension,
|
||||
'size' => $this->formatFileSize($file->getSize()),
|
||||
'size_bytes' => $file->getSize(),
|
||||
]);
|
||||
|
||||
// Если файл уже меньше порога и это WebP - пропускаем сжатие
|
||||
if ($file->getSize() <= self::SIZE_THRESHOLD && $extension === 'webp') {
|
||||
Log::info('[CompressImageTask] Файл уже оптимизирован, пропускаем', [
|
||||
'file' => $file->getClientOriginalName(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'file' => $file,
|
||||
'size' => $file->getSize(),
|
||||
'original_size' => $file->getSize(),
|
||||
'compressed' => false,
|
||||
];
|
||||
}
|
||||
|
||||
// Создаём изображение через Intervention
|
||||
$img = Image::make($file->getRealPath());
|
||||
|
||||
// Получаем исходные размеры
|
||||
$originalWidth = $img->width();
|
||||
$originalHeight = $img->height();
|
||||
|
||||
Log::info('[CompressImageTask] Исходные размеры', [
|
||||
'width' => $originalWidth,
|
||||
'height' => $originalHeight,
|
||||
]);
|
||||
|
||||
// Ресайзим если больше максимальных размеров
|
||||
if ($originalWidth > self::MAX_WIDTH || $originalHeight > self::MAX_HEIGHT) {
|
||||
$img->resize(self::MAX_WIDTH, self::MAX_HEIGHT, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
});
|
||||
|
||||
Log::info('[CompressImageTask] Изображение ресайзнуто', [
|
||||
'new_width' => $img->width(),
|
||||
'new_height' => $img->height(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Конвертируем в WebP и сжимаем
|
||||
$webpData = $img->encode('webp', self::WEBP_QUALITY)->getEncoded();
|
||||
|
||||
// Сохраняем во временный файл
|
||||
$tempFileName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME) . '.webp';
|
||||
$tempPath = sys_get_temp_dir() . '/' . uniqid('img_compress_') . '.webp';
|
||||
file_put_contents($tempPath, $webpData);
|
||||
|
||||
$newSize = filesize($tempPath);
|
||||
|
||||
Log::info('[CompressImageTask] Сжатие завершено', [
|
||||
'original_size' => $this->formatFileSize($file->getSize()),
|
||||
'compressed_size' => $this->formatFileSize($newSize),
|
||||
'compression_ratio' => round((1 - $newSize / $file->getSize()) * 100, 2) . '%',
|
||||
'temp_path' => $tempPath,
|
||||
]);
|
||||
|
||||
// Создаём новый UploadedFile из сжатого
|
||||
$compressedFile = new UploadedFile(
|
||||
$tempPath,
|
||||
$tempFileName,
|
||||
'image/webp',
|
||||
$newSize,
|
||||
true
|
||||
);
|
||||
|
||||
return [
|
||||
'file' => $compressedFile,
|
||||
'size' => $newSize,
|
||||
'original_size' => $file->getSize(),
|
||||
'compressed' => true,
|
||||
'compression_ratio' => round((1 - $newSize / $file->getSize()) * 100, 2),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли файл изображением
|
||||
*/
|
||||
public function isImage(UploadedFile $file): bool
|
||||
{
|
||||
$extension = strtolower($file->getClientOriginalExtension());
|
||||
$imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'tiff', 'svg'];
|
||||
|
||||
return in_array($extension, $imageExtensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует размер файла
|
||||
*/
|
||||
private function formatFileSize(int $bytes): string
|
||||
{
|
||||
$units = ['б', 'КиБ', 'МиБ', 'ГиБ'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks;
|
||||
|
||||
use App\Containers\Dashboard\Exceptions\EmailFetchException;
|
||||
use Webklex\PHPIMAP\Client;
|
||||
use Webklex\PHPIMAP\ClientManager;
|
||||
use Webklex\PHPIMAP\Folder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Подключение к IMAP-серверу
|
||||
*/
|
||||
class ConnectToImapTask
|
||||
{
|
||||
/**
|
||||
* Подключиться к IMAP-серверу
|
||||
*
|
||||
* @param string|null $accountName Имя аккаунта из config/imap.php
|
||||
* @return Client IMAP клиент
|
||||
* @throws EmailFetchException
|
||||
*/
|
||||
public function run(?string $accountName = null): Client
|
||||
{
|
||||
$accountName = $accountName ?? config('email-news.imap_account', 'email_news');
|
||||
|
||||
// Получаем конфиг для webklex/php-imap
|
||||
$imapConfig = config('imap');
|
||||
|
||||
Log::info('[ConnectToImapTask] Попытка подключения к IMAP', [
|
||||
'account' => $accountName,
|
||||
'host' => $imapConfig['accounts'][$accountName]['host'] ?? 'unknown',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Создаём ClientManager с явным конфигом
|
||||
$clientManager = new ClientManager($imapConfig);
|
||||
$client = $clientManager->account($accountName);
|
||||
$client->connect();
|
||||
|
||||
Log::info('[ConnectToImapTask] Успешное подключение к IMAP', [
|
||||
'account' => $accountName,
|
||||
]);
|
||||
|
||||
return $client;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[ConnectToImapTask] Ошибка подключения к IMAP', [
|
||||
'account' => $accountName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw EmailFetchException::connectionFailed($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить папку
|
||||
*
|
||||
* @param Client $client IMAP клиент
|
||||
* @param string $folderName Имя папки
|
||||
* @return Folder
|
||||
* @throws EmailFetchException
|
||||
*/
|
||||
public function getFolder(Client $client, string $folderName): Folder
|
||||
{
|
||||
Log::info('[ConnectToImapTask] Получение папки', [
|
||||
'folder' => $folderName,
|
||||
]);
|
||||
|
||||
try {
|
||||
// Пробуем получить папку напрямую
|
||||
$folder = $client->getFolder($folderName);
|
||||
|
||||
// Если не получилось, ищем в списке папок
|
||||
if (!$folder) {
|
||||
$folders = $client->getFolders();
|
||||
foreach ($folders as $f) {
|
||||
if ($f->name === $folderName || $f->path === $folderName) {
|
||||
$folder = $f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$folder) {
|
||||
throw EmailFetchException::folderNotFound($folderName);
|
||||
}
|
||||
|
||||
Log::info('[ConnectToImapTask] Папка получена успешно', [
|
||||
'folder' => $folderName,
|
||||
'fullName' => $folder->full_name ?? $folder->name ?? $folderName,
|
||||
]);
|
||||
|
||||
return $folder;
|
||||
} catch (EmailFetchException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[ConnectToImapTask] Ошибка получения папки', [
|
||||
'folder' => $folderName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw EmailFetchException::folderNotFound($folderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks;
|
||||
|
||||
use App\Containers\Dashboard\Data\EmailAttachmentData;
|
||||
use App\Containers\Dashboard\Exceptions\EmailFetchException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Webklex\PHPIMAP\Attachment;
|
||||
|
||||
/**
|
||||
* Загрузка и сохранение вложений из письма
|
||||
*/
|
||||
class DownloadAttachmentsTask
|
||||
{
|
||||
/**
|
||||
* Скачать и сохранить вложения
|
||||
*
|
||||
* @param object $message IMAP сообщение
|
||||
* @param string|null $disk Диск для сохранения
|
||||
* @return array<EmailAttachmentData>
|
||||
* @throws EmailFetchException
|
||||
*/
|
||||
public function run(object $message, ?string $disk = null): array
|
||||
{
|
||||
$disk = $disk ?? config('email-news.attachments_folder', 'email_attachments');
|
||||
|
||||
Log::info('[DownloadAttachmentsTask] Начало загрузки вложений', [
|
||||
'message_id' => $message->getMessageId(),
|
||||
'disk' => $disk,
|
||||
]);
|
||||
|
||||
$attachments = $message->getAttachments();
|
||||
|
||||
if (empty($attachments)) {
|
||||
Log::warning('[DownloadAttachmentsTask] Вложения не найдены');
|
||||
throw EmailFetchException::noAttachmentsFound();
|
||||
}
|
||||
|
||||
Log::info('[DownloadAttachmentsTask] Найдено вложений', [
|
||||
'count' => count($attachments),
|
||||
]);
|
||||
|
||||
$savedAttachments = [];
|
||||
$maxSize = config('email-news.max_attachment_size', 41943040); // 40MB по умолчанию
|
||||
|
||||
/** @var Attachment $attachment */
|
||||
foreach ($attachments as $attachment) {
|
||||
try {
|
||||
// Проверяем размер
|
||||
if ($maxSize > 0 && $attachment->getSize() > $maxSize) {
|
||||
Log::warning('[DownloadAttachmentsTask] Вложение превышает максимальный размер', [
|
||||
'filename' => $attachment->getName(),
|
||||
'size' => $attachment->getSize(),
|
||||
'max_size' => $maxSize,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Сохраняем вложение
|
||||
$savedPath = $this->saveAttachment($attachment, $disk);
|
||||
|
||||
if ($savedPath) {
|
||||
$savedAttachments[] = EmailAttachmentData::fromFile(
|
||||
path: $savedPath,
|
||||
originalFilename: $attachment->getName(),
|
||||
mimeType: $attachment->getContentType(),
|
||||
size: $attachment->getSize(),
|
||||
contentId: $attachment->getContentId(),
|
||||
);
|
||||
|
||||
Log::info('[DownloadAttachmentsTask] Вложение сохранено', [
|
||||
'filename' => $attachment->getName(),
|
||||
'path' => $savedPath,
|
||||
'size' => $attachment->getSize(),
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[DownloadAttachmentsTask] Ошибка сохранения вложения', [
|
||||
'filename' => $attachment->getName(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
// Продолжаем обработку остальных вложений
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($savedAttachments)) {
|
||||
Log::error('[DownloadAttachmentsTask] Не удалось сохранить ни одно вложение');
|
||||
throw EmailFetchException::noAttachmentsFound();
|
||||
}
|
||||
|
||||
Log::info('[DownloadAttachmentsTask] Загрузка вложений завершена', [
|
||||
'saved_count' => count($savedAttachments),
|
||||
]);
|
||||
|
||||
return $savedAttachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить вложение на диск
|
||||
*
|
||||
* @param Attachment $attachment Вложение
|
||||
* @param string $disk Диск для сохранения
|
||||
* @return string|null Путь к сохранённому файлу
|
||||
*/
|
||||
private function saveAttachment(Attachment $attachment, string $disk): ?string
|
||||
{
|
||||
$filename = $this->generateUniqueFilename($attachment->getName());
|
||||
$savePath = storage_path('app/' . $disk);
|
||||
|
||||
// Создаём директорию, если не существует
|
||||
if (!is_dir($savePath)) {
|
||||
mkdir($savePath, 0755, true);
|
||||
}
|
||||
|
||||
// Сохраняем вложение (метод save принимает только путь и имя файла)
|
||||
try {
|
||||
$savedPath = $attachment->save($savePath, $filename);
|
||||
|
||||
if ($savedPath) {
|
||||
// Возвращаем относительный путь для сохранения в БД
|
||||
return $disk . '/' . $filename;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[DownloadAttachmentsTask:saveAttachment] Ошибка сохранения', [
|
||||
'filename' => $attachment->getName(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сгенерировать уникальное имя файла
|
||||
*
|
||||
* @param string $originalName Оригинальное имя файла
|
||||
* @return string
|
||||
*/
|
||||
private function generateUniqueFilename(string $originalName): string
|
||||
{
|
||||
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
$basename = pathinfo($originalName, PATHINFO_FILENAME);
|
||||
|
||||
// Очищаем имя от специальных символов
|
||||
$basename = preg_replace('/[^a-zA-Z0-9_\-\p{L}]/u', '_', $basename);
|
||||
|
||||
// Добавляем timestamp для уникальности
|
||||
return $basename . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $extension;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks;
|
||||
|
||||
use Webklex\PHPIMAP\Folder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Получение непрочитанных писем из папки
|
||||
*/
|
||||
class FetchUnreadEmailsTask
|
||||
{
|
||||
/**
|
||||
* Получить непрочитанные письма
|
||||
*
|
||||
* @param Folder $folder IMAP папка
|
||||
* @return array Массив писем
|
||||
*/
|
||||
public function run(Folder $folder): array
|
||||
{
|
||||
Log::info('[FetchUnreadEmailsTask] Получение непрочитанных писем', [
|
||||
'folder' => $folder->full_name ?? $folder->name ?? 'unknown',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Получаем все непрочитанные сообщения
|
||||
$messages = $folder->messages()->unseen()->get();
|
||||
|
||||
$emails = [];
|
||||
foreach ($messages as $message) {
|
||||
$from = $message->getFrom()[0] ?? null;
|
||||
$subject = $message->getSubject();
|
||||
$date = $message->getDate();
|
||||
|
||||
$emails[] = [
|
||||
'message' => $message,
|
||||
'message_id' => $message->getMessageId(),
|
||||
'from_email' => $from?->mail ?? null,
|
||||
'from_name' => $from?->name ?? null,
|
||||
'subject' => $subject,
|
||||
'date' => $date,
|
||||
'has_attachments' => $message->hasAttachments(),
|
||||
];
|
||||
}
|
||||
|
||||
Log::info('[FetchUnreadEmailsTask] Получены письма', [
|
||||
'count' => count($emails),
|
||||
'folder' => $folder->full_name ?? $folder->name ?? 'unknown',
|
||||
]);
|
||||
|
||||
return $emails;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[FetchUnreadEmailsTask] Ошибка получения писем', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Фильтрация писем по отправителю
|
||||
*/
|
||||
class FilterBySenderTask
|
||||
{
|
||||
/**
|
||||
* Отфильтровать письма по разрешённым отправителям
|
||||
*
|
||||
* @param array $emails Массив писем
|
||||
* @param array|null $allowedSenders Whitelist email-адресов
|
||||
* @return array Отфильтрованные письма
|
||||
*/
|
||||
public function run(array $emails, ?array $allowedSenders = null): array
|
||||
{
|
||||
// Если whitelist не передан, берём из конфига
|
||||
$allowedSenders = $allowedSenders ?? config('email-news.allowed_senders', []);
|
||||
|
||||
// Фильтруем пустые значения
|
||||
$allowedSenders = array_filter($allowedSenders, fn($email) => !empty($email));
|
||||
|
||||
// Если список пуст, используем editor_email
|
||||
if (empty($allowedSenders)) {
|
||||
$editorEmail = config('email-news.editor_email');
|
||||
if ($editorEmail) {
|
||||
$allowedSenders = [$editorEmail];
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('[FilterBySenderTask] Фильтрация писем', [
|
||||
'total_emails' => count($emails),
|
||||
'allowed_senders' => $allowedSenders,
|
||||
]);
|
||||
|
||||
if (empty($allowedSenders)) {
|
||||
Log::warning('[FilterBySenderTask] Не указан разрешённый отправитель, пропускаем все письма');
|
||||
return [];
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
$skippedCount = 0;
|
||||
|
||||
foreach ($emails as $email) {
|
||||
$fromEmail = $email['from_email'];
|
||||
|
||||
// Проверяем, есть ли отправитель в whitelist
|
||||
if (!in_array($fromEmail, $allowedSenders, true)) {
|
||||
$skippedCount++;
|
||||
|
||||
// Логируем только если включено логирование
|
||||
if (config('email-news.log_skipped_emails', true)) {
|
||||
Log::debug('[FilterBySenderTask] Пропущено письмо от неразрешённого отправителя', [
|
||||
'from' => $fromEmail,
|
||||
'subject' => $email['subject'],
|
||||
'date' => $email['date'],
|
||||
]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$filtered[] = $email;
|
||||
}
|
||||
|
||||
Log::info('[FilterBySenderTask] Фильтрация завершена', [
|
||||
'total' => count($emails),
|
||||
'filtered' => count($filtered),
|
||||
'skipped' => $skippedCount,
|
||||
]);
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить конкретный email на разрешение
|
||||
*
|
||||
* @param string $email Email для проверки
|
||||
* @return bool
|
||||
*/
|
||||
public function isAllowed(string $email): bool
|
||||
{
|
||||
$allowedSenders = config('email-news.allowed_senders', []);
|
||||
$allowedSenders = array_filter($allowedSenders, fn($e) => !empty($e));
|
||||
|
||||
if (empty($allowedSenders)) {
|
||||
$allowedSenders = [config('email-news.editor_email')];
|
||||
}
|
||||
|
||||
return in_array($email, $allowedSenders, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Пометка письма как прочитанного
|
||||
*/
|
||||
class MarkEmailAsReadTask
|
||||
{
|
||||
/**
|
||||
* Пометить письмо как прочитанное
|
||||
*
|
||||
* @param object $message IMAP сообщение
|
||||
* @return bool
|
||||
*/
|
||||
public function run(object $message): bool
|
||||
{
|
||||
Log::info('[MarkEmailAsReadTask] Пометка письма как прочитанного', [
|
||||
'message_id' => $message->getMessageId(),
|
||||
'subject' => $message->getSubject(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$message->setFlag('Seen');
|
||||
|
||||
Log::info('[MarkEmailAsReadTask] Письмо помечено как прочитанное');
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[MarkEmailAsReadTask] Ошибка пометки письма', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Пометить письмо как прочитанное и переместить в другую папку
|
||||
*
|
||||
* @param object $message IMAP сообщение
|
||||
* @param string $targetFolder Целевая папка
|
||||
* @return bool
|
||||
*/
|
||||
public function markAndMove(object $message, string $targetFolder): bool
|
||||
{
|
||||
Log::info('[MarkEmailAsReadTask] Пометка и перемещение письма', [
|
||||
'message_id' => $message->getMessageId(),
|
||||
'target_folder' => $targetFolder,
|
||||
]);
|
||||
|
||||
try {
|
||||
// Помечаем как прочитанное
|
||||
$message->setFlag('Seen');
|
||||
|
||||
// Перемещаем в другую папку
|
||||
$message->moveToFolder($targetFolder);
|
||||
|
||||
Log::info('[MarkEmailAsReadTask] Письмо обработано и перемещено', [
|
||||
'target_folder' => $targetFolder,
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[MarkEmailAsReadTask] Ошибка обработки письма', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,15 @@ class ProcessMixedFilesController extends Controller
|
||||
|
||||
$result = $this->processMixedFilesAction->run($files);
|
||||
|
||||
// Формируем сообщение со статистикой сжатия
|
||||
$compressionMessage = '';
|
||||
if (isset($result['compressionStats']) && $result['compressionStats']['compressed'] > 0) {
|
||||
$compressionMessage = ' Изображения сжаты: ' . $result['compressionStats']['compressed'] . '/' . $result['compressionStats']['total'] .
|
||||
' (экономия ' . $this->formatFileSize($result['compressionStats']['saved_bytes']) . ')';
|
||||
}
|
||||
|
||||
return back()->with([
|
||||
'success' => 'Файлы успешно загружены! Новость создана: ' . $result['post']->title,
|
||||
'success' => 'Файлы успешно загружены! Новость создана: ' . $result['post']->title . $compressionMessage,
|
||||
'extracted_text' => $result['newsData'],
|
||||
'created_post' => [
|
||||
...$result['post']->toArray(),
|
||||
@@ -45,6 +52,20 @@ class ProcessMixedFilesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует размер файла
|
||||
*/
|
||||
private function formatFileSize(int $bytes): string
|
||||
{
|
||||
$units = ['б', 'КиБ', 'МиБ', 'ГиБ'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Собирает все файлы из request
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Ship\Kernels;
|
||||
|
||||
use AlxDorosenco\PortoForLaravel\Loaders\CommandsLoader;
|
||||
use AlxDorosenco\PortoForLaravel\Loaders\RoutesLoader;
|
||||
use App\Containers\Dashboard\Commands\FetchEmailNewsCommand;
|
||||
use App\Ship\Commands\InitRoles;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as LaravelConsoleKernel;
|
||||
@@ -22,10 +23,17 @@ class ConsoleKernel extends LaravelConsoleKernel
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('sitemap:generate')->dailyAt('03:00');
|
||||
|
||||
// Получение новостей из Email каждые 5 минут
|
||||
$schedule->command(FetchEmailNewsCommand::class)
|
||||
->everyFiveMinutes()
|
||||
->withoutOverlapping()
|
||||
->onOneServer();
|
||||
}
|
||||
|
||||
protected $commands = [
|
||||
InitRoles::class,
|
||||
FetchEmailNewsCommand::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+5
-4
@@ -9,11 +9,11 @@
|
||||
"ext-curl": "*",
|
||||
"ext-zip": "*",
|
||||
"alxdorosenco/porto-for-laravel": "^10.0",
|
||||
"awcodes/filament-tiptap-editor": "3.4.16",
|
||||
"bezhansalleh/filament-shield": "3.2.6",
|
||||
"filament/filament": "v3.2.127",
|
||||
"awcodes/filament-tiptap-editor": "^3.4.16",
|
||||
"bezhansalleh/filament-shield": "^3.2.6",
|
||||
"filament/filament": "^3.3.49",
|
||||
"filament/spatie-laravel-settings-plugin": "^3.2",
|
||||
"filament/spatie-laravel-tags-plugin": "v3.2.113",
|
||||
"filament/spatie-laravel-tags-plugin": "^3.2.113",
|
||||
"fruitcake/laravel-cors": "dev-develop",
|
||||
"guava/filament-icon-picker": "2.2.4",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
@@ -36,6 +36,7 @@
|
||||
"tightenco/ziggy": "^1.0",
|
||||
"tomatophp/filament-icons": "v1.1.4",
|
||||
"vkcom/vk-php-sdk": "^5.131",
|
||||
"webklex/php-imap": "^6.2",
|
||||
"xvladqt/faker-lorem-flickr": "^1.0",
|
||||
"yepsua/filament-range-field": "^0.3.4"
|
||||
},
|
||||
|
||||
Generated
+1160
-1060
File diff suppressed because it is too large
Load Diff
+165
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Set any customizations webklex/php-imap you would like to use.
|
||||
*/
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default date format
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default date format is used to convert any given Carbon::class object
|
||||
| into a valid date string.
|
||||
|
|
||||
*/
|
||||
'date_format' => 'd-M-y',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default account
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default account identifier. It will be used as default for any missing account parameters.
|
||||
| If however the default account is missing a parameter the package default will be used.
|
||||
|
|
||||
*/
|
||||
'default' => 'email_news',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available accounts
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Please list all IMAP accounts which you are planning to use within the
|
||||
| array below.
|
||||
|
|
||||
*/
|
||||
'accounts' => [
|
||||
'email_news' => [
|
||||
'host' => env('EMAIL_NEWS_IMAP_HOST', 'imap.mail.ru'),
|
||||
'port' => env('EMAIL_NEWS_IMAP_PORT', 993),
|
||||
'protocol' => env('EMAIL_NEWS_PROTOCOL', 'imap'),
|
||||
'encryption' => env('EMAIL_NEWS_ENCRYPTION', 'ssl'),
|
||||
'validate_cert' => env('EMAIL_NEWS_VALIDATE_CERT', true),
|
||||
'username' => env('EMAIL_NEWS_IMAP_USER', 'ilya-vavilov@internet.ru'),
|
||||
'password' => env('EMAIL_NEWS_IMAP_PASS', ''),
|
||||
'authentication' => null,
|
||||
'proxy' => [
|
||||
'socket' => null,
|
||||
'request_fulluri' => false,
|
||||
'username' => null,
|
||||
'password' => null,
|
||||
],
|
||||
'timeout' => 30,
|
||||
'extensions' => [],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default account
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This account will be used as default if no account is specified.
|
||||
|
|
||||
*/
|
||||
'default' => [
|
||||
'host' => 'localhost',
|
||||
'port' => 993,
|
||||
'protocol' => 'imap',
|
||||
'encryption' => 'ssl',
|
||||
'validate_cert' => true,
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'authentication' => null,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available IMAP options
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Available php imap config parameters are listed below
|
||||
|
|
||||
*/
|
||||
'options' => [
|
||||
// Append option to the connection string
|
||||
'append' => null,
|
||||
|
||||
// IMAP open options
|
||||
'open' => [
|
||||
'DISABLE_AUTHENTICATOR' => 'GSSAPI',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available flags
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of available flags
|
||||
|
|
||||
*/
|
||||
'flags' => [
|
||||
'recent' => '\Recent',
|
||||
'flagged' => '\Flagged',
|
||||
'answered' => '\Answered',
|
||||
'deleted' => '\Deleted',
|
||||
'seen' => '\Seen',
|
||||
'draft' => '\Draft',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available events
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
'events' => [
|
||||
'message' => [
|
||||
'new' => \Webklex\PHPIMAP\Events\MessageNewEvent::class,
|
||||
'moved' => \Webklex\PHPIMAP\Events\MessageMovedEvent::class,
|
||||
],
|
||||
'folder' => [
|
||||
'new' => \Webklex\PHPIMAP\Events\FolderNewEvent::class,
|
||||
'moved' => \Webklex\PHPIMAP\Events\FolderMovedEvent::class,
|
||||
'deleted' => \Webklex\PHPIMAP\Events\FolderDeletedEvent::class,
|
||||
],
|
||||
'flag' => [
|
||||
'new' => \Webklex\PHPIMAP\Events\FlagNewEvent::class,
|
||||
'deleted' => \Webklex\PHPIMAP\Events\FlagDeletedEvent::class,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available decoding options
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Available php imap config parameters are listed below
|
||||
|
|
||||
*/
|
||||
'decoding' => [
|
||||
'options' => [
|
||||
'UTF7-IMAP' => env('IMAP_DECODING_UTF7_IMAP', true),
|
||||
'attachments' => env('IMAP_DECODING_ATTACHMENTS', true),
|
||||
],
|
||||
'ignore' => [
|
||||
'spelling' => env('IMAP_DECODING_IGNORE_SPELLING', true),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available masking options
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By using the masking option you can define which class should be used
|
||||
|
|
||||
*/
|
||||
'masking' => [
|
||||
'message' => \Webklex\PHPIMAP\Support\Masks\MessageMask::class,
|
||||
'attachment' => \Webklex\PHPIMAP\Support\Masks\AttachmentMask::class,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('email_fetch_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// Информация о письме
|
||||
$table->string('message_id')->index();
|
||||
$table->string('from_email');
|
||||
$table->string('from_name')->nullable();
|
||||
$table->string('subject');
|
||||
$table->timestamp('email_date');
|
||||
|
||||
// Результат обработки
|
||||
$table->enum('status', ['success', 'failed', 'skipped']);
|
||||
$table->foreignId('post_id')->nullable()->constrained('posts')->onDelete('set null');
|
||||
$table->text('error_message')->nullable();
|
||||
|
||||
// Статистика
|
||||
$table->integer('attachments_count')->default(0);
|
||||
$table->bigInteger('total_size')->default(0); // в байтах
|
||||
|
||||
// Мета
|
||||
$table->ipAddress('processed_from')->nullable(); // IP сервера
|
||||
$table->json('metadata')->nullable(); // Дополнительные данные
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Индексы для быстрого поиска
|
||||
$table->index('status');
|
||||
$table->index('created_at');
|
||||
$table->index(['from_email', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('email_fetch_logs');
|
||||
}
|
||||
};
|
||||
Generated
+3
-3
@@ -1774,9 +1774,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001717",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz",
|
||||
"integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==",
|
||||
"version": "1.0.30001780",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
|
||||
"integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};
|
||||
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function n(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let o=s.indexOf(this.lastChecked),r=s.indexOf(t),l=[o,r].sort((i,d)=>i-d),c=[];for(let i=l[0];i<=l[1];i++)s[i].checked=t.checked,c.push(s[i].value);t.checked?this.selectRecords(c):this.deselectRecords(c)}this.lastChecked=t}}}export{n as default};
|
||||
function d(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){this.isLoading=!0;let t=await this.$wire.getGroupedSelectableTableRecordKeys(e);this.areRecordsSelected(this.getRecordsInGroupOnPage(e))?this.deselectRecords(t):this.selectRecords(t),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let l=s.indexOf(this.lastChecked),r=s.indexOf(t),o=[l,r].sort((c,n)=>c-n),i=[];for(let c=o[0];c<=o[1];c++)s[c].checked=t.checked,i.push(s[c].value);t.checked?this.selectRecords(i):this.deselectRecords(i)}this.lastChecked=t}}}export{d as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
body{background:url(../img/background_login.png) no-repeat 50% fixed;background-size:cover;color:#1b1b1b;font-family:Inter,Segoe UI,sans-serif;margin:0;min-height:100vh}.wrapper{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background:hsla(0,0%,100%,.9);border-radius:14px;box-shadow:0 10px 35px rgba(0,0,0,.25);margin:2rem auto;max-width:1100px;padding:2.5rem 3rem;transition:all .3s ease}.wrapper:hover{box-shadow:0 12px 40px rgba(0,0,0,.35)}.header-content{align-items:center;display:flex;padding:10px 0;position:relative}.header-logo{height:auto;width:150px}.header-title{color:#1a1f71;font-size:30px;font-weight:700;text-align:center;width:100%}.vikon-logo{background-image:url(../img/vikon_logo.png);background-position:50%;background-repeat:no-repeat;background-size:contain;display:inline-block;height:50px;text-indent:-9999px;width:150px}@media (max-width:992px){.header-logo{display:none}.header-title{font-size:24px;text-align:center}}.btn-success{border-radius:8px;font-weight:500;letter-spacing:.3px;padding:.65rem 1.2rem;transition:all .2s ease}.btn-success:hover{background-color:#248f55;transform:translateY(-1px)}#modules-container{margin-top:1.5rem}#modules-container>ul{list-style:none;margin-bottom:1.5rem;padding-left:0}#modules-container>ul>li:first-child b{border-left:6px solid coral;color:#1a1f71;display:block;font-size:1.2rem;margin-bottom:.6rem;padding-left:12px}.checkbox{align-items:center;background:hsla(0,0%,100%,.65);border-radius:6px;box-shadow:0 1px 2px rgba(0,0,0,.05);display:flex;gap:10px;margin:4px 0;padding:6px 10px;transition:all .2s ease}.checkbox:hover{background:rgba(230,245,233,.9);transform:translateY(-1px)}.checkbox input[type=checkbox]{accent-color:#2e8048;cursor:pointer;transform:scale(1.1)}.checkbox label{color:#333;cursor:pointer;flex-grow:1;font-size:14px;line-height:1.3}.checkbox label .text-danger{font-size:.85em;margin-right:4px}.badge{border-radius:8px;font-size:.95rem;padding:.45em .75em}.footer{color:#555;font-size:13px;padding:1.5rem 0;text-align:center}#progressbar-container{margin-bottom:.75rem;margin-top:1rem}#progressbar-container .progress{border-radius:6px;height:1.3rem;overflow:hidden}#progressbar-container .progress-bar{font-size:.85rem;font-weight:500;transition:width .3s ease-in-out}.process-container{background:hsla(0,0%,100%,.85);border-radius:6px;font-size:.9rem;line-height:1.4;margin-top:.75rem;max-height:400px;overflow-y:auto;padding:.75rem 1rem}.process-container p{margin:.3rem 0}@keyframes rotate{to{transform:rotate(1turn)}}@keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}to{stroke-dasharray:90,150;stroke-dashoffset:-124}}.static-throbber{animation:rotate 2s linear infinite}.static-throbber .path{stroke-dasharray:1,150;stroke-dashoffset:0;stroke-linecap:round;animation:dash 1.5s ease-in-out infinite}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
'no_use_template_for_routes' => array(
|
||||
'/abitur/profile/index.html' => '/abitur/profile/index.html',
|
||||
'/vsoko/survey/index.html' => '/vsoko/survey/index.html',
|
||||
)
|
||||
);
|
||||
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
$selfDir = dirname(__FILE__);
|
||||
|
||||
ini_set('max_execution_time', '60');
|
||||
ini_set('default_socket_timeout', '60');
|
||||
|
||||
$locationOfVikonModules = '/'; //todo new_core_after del
|
||||
$domenName = 'www.ntspi.ru';
|
||||
$apiDomen = 'https://db-nica.ru/';
|
||||
$vikonDomainResolveBypass = 'db-nica.ru:443:62.76.112.192';
|
||||
$fmDomainResolveBypass = 'file.db-nica.ru:443:62.76.112.192';
|
||||
$apiDomainAuth = 'https://auth.db-nica.ru/';
|
||||
$filemanagerApiDomen = 'https://file.db-nica.ru/';
|
||||
$clientId = '542';
|
||||
$clientSecret = '0mmk80mv8zpk7h5uhjvjsmsskvpkn9nmkywgy83vwg7tpzdw0y7rao0k9pvmn8xxwj5mgw8554nhtq6s';
|
||||
$vuzId = '16775';
|
||||
$vuzName = 'Нижнетагильский государственный социально-педагогический институт (филиал) "Российского государственного профессионально-педагогического университета"';
|
||||
$modulesByPathDeploy = array (
|
||||
2 => 'abitur',
|
||||
1 => 'sveden',
|
||||
6 => 'vsoko',
|
||||
);
|
||||
$allowedFoldersInCoreByModule = array (
|
||||
2 =>
|
||||
array (
|
||||
0 => 'abitur',
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
0 => 'assets',
|
||||
1 => 'files_zaglushka',
|
||||
2 => 'common',
|
||||
3 => 'struct',
|
||||
4 => 'document',
|
||||
5 => 'education',
|
||||
6 => 'managers',
|
||||
7 => 'employees',
|
||||
8 => 'objects',
|
||||
9 => 'paid_edu',
|
||||
10 => 'budget',
|
||||
11 => 'vacant',
|
||||
12 => 'grants',
|
||||
13 => 'inter',
|
||||
14 => 'catering',
|
||||
15 => 'eduStandarts',
|
||||
16 => 'corruption',
|
||||
17 => 'antiterrorism',
|
||||
18 => 'files',
|
||||
19 => 'update',
|
||||
20 => 'index.html',
|
||||
21 => '.vikon',
|
||||
22 => '.htaccess',
|
||||
),
|
||||
6 =>
|
||||
array (
|
||||
0 => 'assets',
|
||||
1 => 'general',
|
||||
2 => 'structure',
|
||||
4 => 'faq',
|
||||
5 => 'procedures',
|
||||
6 => 'results-and-reports',
|
||||
7 => 'plans',
|
||||
8 => 'survey',
|
||||
9 => 'files',
|
||||
10 => '.vikon',
|
||||
11 => 'index.html',
|
||||
12 => '.htaccess',
|
||||
),
|
||||
);
|
||||
|
||||
define('DEBUG_MODE', isset($_GET['debug_mode']) && $_GET['debug_mode'] || isset($_POST['debug_mode']) && $_POST['debug_mode']);
|
||||
|
||||
define('IS_DOMAIN_RESOLVE', isset($_COOKIE['is_resolve_domain']) ? (int) $_COOKIE['is_resolve_domain'] : 0);
|
||||
define('FM_DOMAIN_RESOLVE_BYPASS', $fmDomainResolveBypass);
|
||||
define('VIKON_DOMAIN_RESOLVE_BYPASS', $vikonDomainResolveBypass);
|
||||
|
||||
define('SVEDEN', 1);
|
||||
define('ABITUR', 2);
|
||||
define('VSOKO', 6);
|
||||
|
||||
if (DEBUG_MODE === true) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors','on');
|
||||
} else {
|
||||
error_reporting(0);
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once 'path.php';
|
||||
|
||||
class Filesystem
|
||||
{
|
||||
public static function getExtension($path)
|
||||
{
|
||||
if ($path === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$extension = pathinfo($path, PATHINFO_EXTENSION);
|
||||
return strtolower($extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет безопасный scandir, убирает системные точки
|
||||
*
|
||||
* @param $path
|
||||
* @return array|false - возращает entries внутри сканируемой папки или false, в случае отсутсвия прав (или других причин)
|
||||
*/
|
||||
public static function safeScandir($path)
|
||||
{
|
||||
if (!is_dir($path) || !is_readable($path)) {
|
||||
return false;
|
||||
}
|
||||
$entries = scandir($path);
|
||||
if ($entries === false) {
|
||||
return false;
|
||||
}
|
||||
return array_diff($entries, Path::getSystemDots($path));
|
||||
}
|
||||
|
||||
public static function removeZip($path, $withCheckRights)
|
||||
{
|
||||
if (!Path::isActionAllowedThisPath($path)) {
|
||||
return false;
|
||||
}
|
||||
if (empty($path) || !file_exists($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($withCheckRights) {
|
||||
$checks = array(
|
||||
'is_file' => is_file($path),
|
||||
'is_zip' => self::getExtension($path) === 'zip',
|
||||
'is_writable' => is_writable($path)
|
||||
);
|
||||
|
||||
foreach ($checks as $check) {
|
||||
if (!$check) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unlink($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param bool $withRecursive
|
||||
* @param int|null $moduleId - Важный параметр который дает понимание в рамках какой папки мы работаем
|
||||
* Если null - работаем в рамках vikon_core, переданный moduleId - даст путь к модулю
|
||||
* @return bool
|
||||
*/
|
||||
public static function remove($path, $withRecursive, $moduleId = null)
|
||||
{
|
||||
if (!Path::isActionAllowedThisPath($path, $moduleId)) {
|
||||
return false;
|
||||
}
|
||||
if (!file_exists($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_dir($path)) {
|
||||
return unlink($path);
|
||||
}
|
||||
|
||||
if (!$withRecursive) {
|
||||
return rmdir($path);
|
||||
}
|
||||
|
||||
$success = true;
|
||||
$entries = self::safeScandir($path);
|
||||
foreach ($entries as $entry) {
|
||||
$fullPath = Path::join($path, $entry);
|
||||
if (is_dir($fullPath) && !is_link($fullPath)) {
|
||||
if (!self::remove($fullPath, $withRecursive, $moduleId)) {
|
||||
$success = false;
|
||||
}
|
||||
} else {
|
||||
if (!unlink($fullPath)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
if (!rmdir($path)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Переносит и переименовывает папку и все содержимое рекурсивно
|
||||
* Перед использованием выполни Filesysyem::remove($targetDir)
|
||||
*
|
||||
* В случае с replaceWithRename перемещать мы можем в рамках vikon_core и в рамках модуля
|
||||
*/
|
||||
public static function replaceWithRename($sourceDir, $targetDir, $moduleId = null)
|
||||
{
|
||||
if ($moduleId === null) {
|
||||
if (!Path::isBasePath(Path::getCoreRootPath(), $sourceDir)) {
|
||||
return false;
|
||||
}
|
||||
if (!Path::isBasePath(Path::getCoreRootPath(), $targetDir)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!Path::isBasePath(Path::getModuleRootPath($moduleId), $sourceDir)
|
||||
&& !Path::isBasePath(Path::getCoreRootPath(), $sourceDir)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!Path::isBasePath(Path::getModuleRootPath($moduleId), $targetDir)
|
||||
&& !Path::isBasePath(Path::getCoreRootPath(), $targetDir)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!file_exists($sourceDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file_exists($targetDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parentDir = dirname($targetDir);
|
||||
if (!is_writable($parentDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mkdir($targetDir, 0755, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entries = self::safeScandir($sourceDir);
|
||||
|
||||
if (empty($entries)) {
|
||||
return rmdir($sourceDir);
|
||||
}
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$item = Path::join($sourceDir, $entry);
|
||||
$newPath = Path::join($targetDir, $entry);
|
||||
|
||||
if (is_dir($item)) {
|
||||
if (!self::replaceWithRename($item, $newPath, $moduleId)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!rename($item, $newPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rmdir($sourceDir);
|
||||
}
|
||||
|
||||
public static function safeRenameFile($oldPath, $newPath, $moduleId = null)
|
||||
{
|
||||
if ($moduleId === null) {
|
||||
if (!Path::isBasePath(Path::getCoreRootPath(), $oldPath)) {
|
||||
return false;
|
||||
}
|
||||
if (!Path::isBasePath(Path::getCoreRootPath(), $newPath)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!Path::isBasePath(Path::getModuleRootPath($moduleId), $oldPath)
|
||||
&& !Path::isBasePath(Path::getCoreRootPath(), $oldPath)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!Path::isBasePath(Path::getModuleRootPath($moduleId), $newPath)
|
||||
&& !Path::isBasePath(Path::getCoreRootPath(), $newPath)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!file_exists($oldPath)) {
|
||||
return false;
|
||||
}
|
||||
if (file_exists($newPath)) {
|
||||
return false;
|
||||
}
|
||||
$newDir = dirname($newPath);
|
||||
if (!file_exists($newDir)) {
|
||||
return false;
|
||||
}
|
||||
if (!is_writable($newDir)) {
|
||||
return false;
|
||||
}
|
||||
if (!is_writable($oldPath)) {
|
||||
return false;
|
||||
}
|
||||
$result = rename($oldPath, $newPath);
|
||||
if (!$result) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $restorePath - путь к ядру модуля которое мы будем восстанавливать
|
||||
* @param array $allowedRestoreFolders - разрешенные к восстановлению папки,
|
||||
* т.е папки которые прилетели из архива, будем работать только с ними
|
||||
* @return bool
|
||||
*/
|
||||
public static function restoreUnitCoreAfterFail($restorePath, $allowedRestoreFolders, $moduleId = null)
|
||||
{
|
||||
if (!Path::isActionAllowedThisPath($restorePath, $moduleId)) {
|
||||
return false;
|
||||
}
|
||||
if (!file_exists($restorePath)) {
|
||||
return false;
|
||||
}
|
||||
$restoreEntries = self::safeScandir($restorePath);
|
||||
if ($restoreEntries === false) {
|
||||
return false;
|
||||
}
|
||||
foreach ($restoreEntries as $restoreEntry) {
|
||||
$fullPathRestore = Path::join($restorePath, $restoreEntry);
|
||||
$baseName = substr($restoreEntry, 0, -4);
|
||||
if (
|
||||
file_exists($fullPathRestore)
|
||||
&& substr($restoreEntry, -4) === Path::$n_pstfx
|
||||
&& in_array($baseName, $allowedRestoreFolders)
|
||||
) {
|
||||
self::remove($fullPathRestore, true, $moduleId);
|
||||
}
|
||||
|
||||
if (
|
||||
file_exists($fullPathRestore)
|
||||
&& substr($restoreEntry, -4) === Path::$o_pstfx
|
||||
&& in_array($baseName, $allowedRestoreFolders)
|
||||
) {
|
||||
$newPath = Path::join($restorePath, $baseName);
|
||||
|
||||
if (file_exists($newPath)) {
|
||||
self::remove($newPath, true, $moduleId);
|
||||
}
|
||||
|
||||
self::replaceWithRename($fullPathRestore, $newPath, $moduleId);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $pathClean - Путь к ядру модуля или главному ядру, чтобы почистить его
|
||||
* @param $excludedEntries - папки и файлы которые не нужно чистить в ядре
|
||||
* @param int|null $moduleId - модуль id или false - когда проверяем ядро
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function cleanUnitCore($pathClean, $excludedEntries, $moduleId = null)
|
||||
{
|
||||
if (!Path::isActionAllowedThisPath($pathClean, $moduleId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entries = self::safeScandir($pathClean);
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$item = Path::join($pathClean, $entry);
|
||||
|
||||
if (in_array($entry, $excludedEntries)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir($item) && !is_link($item)) {
|
||||
if (!self::remove($item, true, $moduleId)) {
|
||||
return $item;
|
||||
}
|
||||
} else {
|
||||
if (!unlink($item)) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function safeMkdir($path, $mode, $moduleId = null)
|
||||
{
|
||||
if (!Path::isActionAllowedThisPath($path, $moduleId)) {
|
||||
return false;
|
||||
}
|
||||
if (file_exists($path)) {
|
||||
return true;
|
||||
}
|
||||
$parentDir = dirname($path);
|
||||
if (!is_dir($parentDir)) {
|
||||
return false;
|
||||
}
|
||||
if (!is_writable($parentDir)) {
|
||||
return false;
|
||||
}
|
||||
return mkdir($path, $mode);
|
||||
}
|
||||
|
||||
public static function safeMkfile($filename, $mode, $content = '')
|
||||
{
|
||||
if (file_exists($filename)) {
|
||||
return true;
|
||||
}
|
||||
$directory = dirname($filename);
|
||||
if (!is_writable($directory)) {
|
||||
return false;
|
||||
}
|
||||
if (file_put_contents($filename, $content) === false) {
|
||||
return false;
|
||||
}
|
||||
if (!chmod($filename, $mode)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function ensureValidDirectoryAndFileName($directory, $filename)
|
||||
{
|
||||
if ($directory !== null && (!is_string($directory) || !preg_match("/^[a-z]{3,4}$/", $directory))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_string($filename) || strpos($filename, '/') !== false || strpos($filename, "\\") !== false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
<?php
|
||||
function loadHeaders()
|
||||
{
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
|
||||
header('Content-type: application/json');
|
||||
}
|
||||
|
||||
function sendErrorResponse($code, $message)
|
||||
{
|
||||
$resultBody = array(
|
||||
'success' => false,
|
||||
'forward_code' => $code,
|
||||
'message' => $message,
|
||||
);
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
die();
|
||||
}
|
||||
|
||||
function sendSuccessResponse($message)
|
||||
{
|
||||
$resultBody = array(
|
||||
'success' => true,
|
||||
'forward_code' => 200,
|
||||
'message' => $message,
|
||||
);
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
die();
|
||||
}
|
||||
|
||||
// unpackZip плохо проверяет может ли он распаковать в этой папке
|
||||
// перед использованием сделай Filesystem::remove($pathToUnpack) - чтобы убедиться что проблем не будет
|
||||
function unpackZip($fileName, $pathToUnpack)
|
||||
{
|
||||
$res = array();
|
||||
$res['success'] = false;
|
||||
$res['message'] = '';
|
||||
if($pathToUnpack) {
|
||||
if (extension_loaded('zip')) {
|
||||
$zip = new ZipArchive;
|
||||
if ($isOpenedZip = $zip->open($fileName)) {
|
||||
$zip->extractTo($pathToUnpack);
|
||||
$zip->close();
|
||||
$res['success'] = true;
|
||||
|
||||
} else {
|
||||
$res['message'] = 'Ошибка при распоковке файлов библиотеки ZIP:' . $isOpenedZip;
|
||||
}
|
||||
} else {
|
||||
require_once dirname(__FILE__) . '/../update/zip_helper.php';
|
||||
$archive = new PclZip($fileName);
|
||||
$unZipResult = $archive->extract(PCLZIP_OPT_PATH, $pathToUnpack,
|
||||
PCLZIP_CB_PRE_EXTRACT, 'preExtractCallback', PCLZIP_OPT_REPLACE_NEWER);
|
||||
if ($unZipResult == 0) {
|
||||
$res['message'] = 'Ошибка при распоковке файлов:' . $archive->errorInfo(true);
|
||||
} else {
|
||||
$res['success'] = true;
|
||||
}
|
||||
$archive->privCloseFd();
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
function filterAccessToken($token)
|
||||
{
|
||||
return filter_var($token, FILTER_VALIDATE_REGEXP, array('options' => array('regexp' => '/^[a-z0-9_\-\.]+$/i')));
|
||||
}
|
||||
|
||||
function filterFileName($file)
|
||||
{
|
||||
return filter_var($file, FILTER_VALIDATE_REGEXP,
|
||||
array('options' => array('regexp' => '/^[a-z0-9\+\.\,\(\)\;\#\№\-\_\«\»\!\%\=\$\@\'\&\–]+$/i'))
|
||||
);
|
||||
}
|
||||
|
||||
function filterPath($file)
|
||||
{
|
||||
return filter_var($file, FILTER_VALIDATE_REGEXP,
|
||||
array('options' => array('regexp' => '/^[a-z0-9\.\-\_\–\/]+$/i'))
|
||||
);
|
||||
}
|
||||
|
||||
function filterEntry($entry)
|
||||
{
|
||||
return filter_var($entry, FILTER_VALIDATE_REGEXP,
|
||||
array('options' => array('regexp' => '/^\/(?:[a-zA-Zа-яА-Я0-9_\-\.]+(?:\/)?)+$/'))
|
||||
);
|
||||
}
|
||||
|
||||
function filterInt($number)
|
||||
{
|
||||
return filter_var($number, FILTER_VALIDATE_INT);
|
||||
}
|
||||
|
||||
function filterUrl($url)
|
||||
{
|
||||
return filter_var($url, FILTER_VALIDATE_URL);
|
||||
}
|
||||
|
||||
function filterVersion($version)
|
||||
{
|
||||
return filter_var($version, FILTER_VALIDATE_REGEXP, array('options' => array('regexp' => '/^[0-9\.]+$/')));
|
||||
}
|
||||
|
||||
function filterPartName($part)
|
||||
{
|
||||
return filter_var($part, FILTER_VALIDATE_REGEXP, array('options' => array('regexp' => '/^[a-z_\-]+$/i')));
|
||||
}
|
||||
|
||||
function filterLogin($str)
|
||||
{
|
||||
return filter_var($str, FILTER_VALIDATE_EMAIL);
|
||||
}
|
||||
|
||||
function filterPassword($str)
|
||||
{
|
||||
return filter_var($str, FILTER_SANITIZE_STRING);
|
||||
}
|
||||
|
||||
function setResponseCode($code, $reason = null) {
|
||||
$code = intval($code);
|
||||
|
||||
if (version_compare(phpversion(), '5.4', '>') && is_null($reason)) {
|
||||
http_response_code($code);
|
||||
} else {
|
||||
header(trim("HTTP/1.0 $code $reason"));
|
||||
}
|
||||
}
|
||||
|
||||
function preExtractCallback($preEvent, &$preHeader)
|
||||
{
|
||||
if (!is_dir($preHeader['filename'])) {
|
||||
if (strpos($preHeader['filename'], '.htaccess')) {
|
||||
return 0;
|
||||
}
|
||||
if (file_exists($preHeader['filename'])) {
|
||||
unlink($preHeader['filename']);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
class RemoteResult
|
||||
{
|
||||
public $curlHasError = true;
|
||||
public $curlErrorTxt = '';
|
||||
|
||||
public $code = 0;
|
||||
public $responseBody = '';
|
||||
}
|
||||
|
||||
function remoteRequest($url, $responseAsJson = true, $postFields = false, $headers = array())
|
||||
{
|
||||
$result = new RemoteResult();
|
||||
$ch = curl_init();
|
||||
if (IS_DOMAIN_RESOLVE === 1) {
|
||||
curl_setopt($ch, CURLOPT_RESOLVE, array(
|
||||
VIKON_DOMAIN_RESOLVE_BYPASS,
|
||||
FM_DOMAIN_RESOLVE_BYPASS,
|
||||
));
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_ENCODING, "");
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
|
||||
if (is_array($postFields)) {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
$postVars = "";
|
||||
foreach ($postFields as $key => $value) {
|
||||
$postVars .= $key . '=' . $value . '&';
|
||||
}
|
||||
$postVars = rtrim($postVars, '&');
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postVars);
|
||||
}
|
||||
|
||||
if ($headers !== array()) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
}
|
||||
$output = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch) > 0 || curl_error($ch)) {
|
||||
$result->curlErrorTxt .= ';' . '#:' . curl_errno($ch) . 'Error:' . curl_error($ch);
|
||||
} else{
|
||||
$result->curlHasError = false;
|
||||
|
||||
$curlInfo = curl_getinfo($ch);
|
||||
$result->code = (int) $curlInfo['http_code'];
|
||||
$result->responseBody = ($responseAsJson) ? json_decode($output) : $output;
|
||||
}
|
||||
curl_close($ch);
|
||||
return $result;
|
||||
}
|
||||
|
||||
function tryExtractFmErrorMessage($remoteResultFmWhereBodyIsJson, $prefix = '')
|
||||
{
|
||||
$out = '';
|
||||
if (property_exists($remoteResultFmWhereBodyIsJson->responseBody, 'error')) {
|
||||
$out = $out . $remoteResultFmWhereBodyIsJson->responseBody->error;
|
||||
}
|
||||
|
||||
if (
|
||||
property_exists($remoteResultFmWhereBodyIsJson->responseBody, 'messages')
|
||||
&& is_array($remoteResultFmWhereBodyIsJson->responseBody->messages)
|
||||
) {
|
||||
$message = version_compare(phpversion(), '8.0', '>=')
|
||||
? implode('; ', $remoteResultFmWhereBodyIsJson->responseBody->messages)
|
||||
: implode($remoteResultFmWhereBodyIsJson->responseBody->messages, '; ');
|
||||
$out = $out . $message;
|
||||
}
|
||||
|
||||
if ($out) {
|
||||
$out = $prefix . 'Файловый сервер: ' . $out;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
function moduleDirIsEmptyOrEx($moduleRootPath, $flagFileOfModulePath)
|
||||
{
|
||||
if (is_dir($moduleRootPath)) {
|
||||
if (count(Filesystem::safeScandir($moduleRootPath)) && !file_exists($flagFileOfModulePath)) {
|
||||
throw new Exception(
|
||||
'Ошибка при распаковке ядра модуля. Целевая директория: ' . $moduleRootPath . ' уже содержит данные. ' .
|
||||
'Для запуска обновления целевая директория должна быть пуста. ' .
|
||||
'Удалите или переместите существующие данные и повторите обновление.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
<?php
|
||||
|
||||
class Path
|
||||
{
|
||||
public static $vikonCoreFolder = 'vikon_core';
|
||||
public static $functionalFolder = 'update';
|
||||
public static $srcExecutor = 'src_executor.php';
|
||||
public static $executorFile = 'executor.php';
|
||||
private static $foldersCore = array();
|
||||
|
||||
private static $systemDots = array('.', '..');
|
||||
public static $n_pstfx = '_new';
|
||||
public static $o_pstfx = '_old';
|
||||
|
||||
/**
|
||||
* Инициализация статических путей
|
||||
* @param array $foldersCore
|
||||
*/
|
||||
public static function init($foldersCore)
|
||||
{
|
||||
self::$foldersCore = $foldersCore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $path - Путь относительно которого необходимо вернуть систмные точки (., ..)
|
||||
* @return array
|
||||
*/
|
||||
public static function getSystemDots($path)
|
||||
{
|
||||
$systemDots = array();
|
||||
foreach (self::$systemDots as $systemDot) {
|
||||
$systemDots[] = $systemDot;
|
||||
$systemDots[] = $path.'/'.$systemDot;
|
||||
}
|
||||
return $systemDots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает абсолютный путь к главному ядру
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getCoreRootPath()
|
||||
{
|
||||
return self::join(self::normalize(dirname(dirname(dirname(__FILE__)))), self::$vikonCoreFolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возращает путь к функциональной папке главного ядра.
|
||||
* Функциональная папка - папка в рамках которой проихсодит синхронизация и обновление частей и ядер
|
||||
* vikon_core/update
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getFunctionalPath()
|
||||
{
|
||||
return self::join(self::getCoreRootPath(), self::$functionalFolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает абсолютный путь к папке модуля по его идентификатору
|
||||
*
|
||||
* @param string $moduleId
|
||||
* @return string
|
||||
*/
|
||||
public static function getModuleRootPath($moduleId)
|
||||
{
|
||||
$parentFolderCore = self::normalize(dirname(self::getCoreRootPath()));
|
||||
return self::join($parentFolderCore, self::$foldersCore[$moduleId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает абсолютный путь к папке версий
|
||||
* @return string
|
||||
*/
|
||||
public static function getTmpVersionPath()
|
||||
{
|
||||
return self::join(self::getCoreRootPath(), 'tmp', 'versions');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $moduleId Идентификатор модуля (1, 2 или 6).
|
||||
* @return string|null Возвращает путь к директории модуля или null, если модуль не найден.
|
||||
*/
|
||||
public static function getFsPathByModule($moduleId)
|
||||
{
|
||||
return self::join(self::getModuleRootPath($moduleId), 'files');
|
||||
}
|
||||
|
||||
/**
|
||||
* Возращает путь к исходному коду для синхронизации ядра
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getSrcForExecutorPath()
|
||||
{
|
||||
return self::join(self::getFunctionalPath(), self::$srcExecutor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает, является ли путь базовым для другого пути.
|
||||
*/
|
||||
public static function isBasePath($basePath, $ofPath)
|
||||
{
|
||||
$basePath = self::canonicalize($basePath);
|
||||
$ofPath = self::canonicalize($ofPath);
|
||||
|
||||
$basePath = rtrim($basePath, '/').'/';
|
||||
$ofPath = $ofPath.'/';
|
||||
|
||||
return 0 === strpos($ofPath, $basePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Объединяет несколько частей пути в один канонический путь
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function join()
|
||||
{
|
||||
$paths = func_get_args();
|
||||
|
||||
$finalPath = null;
|
||||
$wasScheme = false;
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if ('' === $path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null === $finalPath) {
|
||||
$finalPath = $path;
|
||||
$wasScheme = (strpos($path, '://') !== false);
|
||||
continue;
|
||||
}
|
||||
|
||||
$lastChar = substr($finalPath, -1);
|
||||
if ('/' !== $lastChar && '\\' !== $lastChar) {
|
||||
$finalPath .= '/';
|
||||
}
|
||||
|
||||
if ($wasScheme) {
|
||||
$finalPath .= $path;
|
||||
} else {
|
||||
$finalPath .= ltrim($path, '/');
|
||||
}
|
||||
|
||||
$wasScheme = false;
|
||||
}
|
||||
|
||||
if ($finalPath === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return self::canonicalize($finalPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует путь в канонический вид (убирает '.', '..', двойные слеши)
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
private static function canonicalize($path)
|
||||
{
|
||||
$path = self::normalize($path);
|
||||
|
||||
$parts = explode('/', $path);
|
||||
$absolutes = array();
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if ('' === $part || '.' === $part) {
|
||||
continue;
|
||||
}
|
||||
if ('..' === $part) {
|
||||
array_pop($absolutes);
|
||||
} else {
|
||||
$absolutes[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
$normalized = implode('/', $absolutes);
|
||||
|
||||
if ('/' === substr($path, 0, 1)) {
|
||||
$normalized = '/' . $normalized;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет возможность дальнейшей работы с заданным путем, дальнейшие действия должны проходить в рамках
|
||||
* базовых путей vikon_core, или путей к модулю ($moduleId)
|
||||
*
|
||||
* @param int|null $moduleId - null - vikon_core, или id модуля
|
||||
*/
|
||||
public static function isActionAllowedThisPath($path, $moduleId = null)
|
||||
{
|
||||
if ($moduleId === null && !Path::isBasePath(Path::getCoreRootPath(), $path)) {
|
||||
return false;
|
||||
}
|
||||
if ($moduleId && !Path::isBasePath(Path::getModuleRootPath($moduleId), $path)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Нормализует слеши в пути
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
public static function normalize($path)
|
||||
{
|
||||
return str_replace('\\', '/', $path);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
$letters = 'ABCDEFGKIJKLMNPQRSTUVWXYZ23456789';
|
||||
$caplen = 4;
|
||||
$width = 120; $height = 20;
|
||||
$font = './../assets/fonts/comic.ttf';
|
||||
$fontsize = 14;
|
||||
|
||||
header('Content-type: image/png');
|
||||
|
||||
$im = imagecreatetruecolor($width, $height);
|
||||
imagesavealpha($im, true);
|
||||
$bg = imagecolorallocatealpha($im, 0, 0, 0, 127);
|
||||
imagefill($im, 0, 0, $bg);
|
||||
|
||||
$captcha = '';
|
||||
|
||||
for ($i = 0; $i < $caplen; $i++) {
|
||||
$captcha .= $letters[ rand(0, strlen($letters)-1) ];
|
||||
$x = ($width - 20) / $caplen * $i + 10;
|
||||
$x = rand($x, $x+4);
|
||||
$y = $height - ( ($height - $fontsize) / 2 );
|
||||
$curcolor = imagecolorallocate( $im, rand(0, 100), rand(0, 100), rand(0, 100) );
|
||||
$angle = rand(-25, 25);
|
||||
imagettftext($im, $fontsize, $angle, $x, $y, $curcolor, $font, $captcha[$i]);
|
||||
}
|
||||
|
||||
session_start();
|
||||
$_SESSION['captcha'] = $captcha;
|
||||
|
||||
imagepng($im);
|
||||
imagedestroy($im);
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
|
||||
$accessToken = filterAccessToken($_POST['access_token']);
|
||||
|
||||
$res = array();
|
||||
try {
|
||||
if (!$accessToken) {
|
||||
throw new RuntimeException('Некорректный access_token');
|
||||
}
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $accessToken);
|
||||
$response = remoteRequest($apiDomainAuth . 'api/profile_applicant/check_access_token', true, false, $headers);
|
||||
if ($response->code == 200) {
|
||||
$res = $response->responseBody;
|
||||
} else {
|
||||
$err = 'Не удалось соединиться с удаленным сервером';
|
||||
sendErrorResponse($response->code, $err);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
setResponseCode($e->getCode());
|
||||
$res['error'] = 'Ошибка при проверке токена. ' . $e->getMessage();
|
||||
}
|
||||
loadHeaders();
|
||||
echo json_encode($res);
|
||||
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
|
||||
$res = array();
|
||||
$res['success'] = true;
|
||||
$res['errorID'] = array();
|
||||
$res['messages'] = array();
|
||||
|
||||
$name = $_POST['name'];
|
||||
$email = $_POST['email'];
|
||||
$subject = $_POST['subject'];
|
||||
$message = $_POST['message'];
|
||||
$consent = filter_var($_POST['consent'], FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
if($consent != true){
|
||||
$res['success'] = false;
|
||||
$res['messages'][] = 'Не получено согласие на обработку персональных данных.';
|
||||
$res['errorID'][] = 'consent';
|
||||
}
|
||||
|
||||
$code = filter_var($_POST['captcha'], FILTER_SANITIZE_STRING);
|
||||
session_start();
|
||||
if (!isset($_SESSION['captcha']) || strtoupper(trim($_SESSION['captcha'])) != strtoupper(trim($code))) {
|
||||
$res['success'] = false;
|
||||
$res['errorID'][] = 'captcha';
|
||||
$res['messages'][] = 'Неверный код с картинки.';
|
||||
}
|
||||
unset($_SESSION['captcha']);
|
||||
|
||||
if ($res['success']) {
|
||||
$res['success'] = false;
|
||||
|
||||
$headers = array('Accept: application/json');
|
||||
$data = remoteRequest($apiDomen . 'oauth2/ClientCredentials', true,
|
||||
array(
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'grant_type' => 'client_credentials',
|
||||
), $headers
|
||||
);
|
||||
|
||||
if ($data->code == 200) {
|
||||
if (isset($data->responseBody)) {
|
||||
$accessToken = $data->responseBody->access_token;
|
||||
|
||||
try {
|
||||
$data = remoteRequest($apiDomen . 'oauth_via_app/feedbackSendMail?access_token='
|
||||
. $accessToken .
|
||||
'&name=' . urlencode($name) .
|
||||
'&email=' . urlencode($email) .
|
||||
'&subject=' . urlencode($subject) .
|
||||
'&message=' . urlencode($message)
|
||||
);
|
||||
|
||||
if ($data->code == 200) {
|
||||
$res['success'] = true;
|
||||
} else {
|
||||
sendErrorResponse($data->code, $data->responseBody->message);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$res['message'] = 'Ошибка. '.$e->getMessage();
|
||||
}
|
||||
|
||||
} else {
|
||||
$res['message'] = 'Ошибка ' . $data->responseBody->message;
|
||||
}
|
||||
} else {
|
||||
$res['message'] = $data->responseBody->message;
|
||||
}
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
|
||||
echo json_encode($res);
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
|
||||
$response = array(
|
||||
'client_id' => $clientId,
|
||||
'vuz_id' => $vuzId,
|
||||
'api_domain_auth' => $apiDomainAuth,
|
||||
'api_domain_vikon' => $apiDomen,
|
||||
'api_domain_filemanager' => $filemanagerApiDomen,
|
||||
'vuz_name' => $vuzName,
|
||||
);
|
||||
loadHeaders();
|
||||
echo json_encode($response);
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
|
||||
$login = filterLogin($_POST['login']);
|
||||
$password = filterPassword($_POST['password']);
|
||||
$code = filterInt($_POST['code']);
|
||||
|
||||
$res = array();
|
||||
try {
|
||||
if (!$login && !$password && !$code) {
|
||||
throw new RuntimeException('Переданы некорректные параметры');
|
||||
}
|
||||
$postFields = array(
|
||||
'grant_type' => 'password',
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'username' => $login,
|
||||
'password' => $password,
|
||||
'code' => $code,
|
||||
'scope' => '',
|
||||
);
|
||||
$response = remoteRequest($apiDomainAuth . 'oauth/token', true, $postFields, array(
|
||||
'App-Language: ' . isset($_POST['lang']) ? $_POST['lang'] : 'ru'
|
||||
));
|
||||
if ($response->code == 200) {
|
||||
setResponseCode($response->code);
|
||||
$res = $response->responseBody;
|
||||
} else {
|
||||
$err = 'Не удалось соединиться с удаленным сервером.';
|
||||
sendErrorResponse($response->code, $err);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
setResponseCode($e->getCode());
|
||||
$res['error'] = 'Ошибка при получении токена. ' . $e->getMessage();
|
||||
}
|
||||
loadHeaders();
|
||||
echo json_encode($res);
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
|
||||
$refreshToken = filterAccessToken($_POST['refresh_token']);
|
||||
|
||||
$res = array();
|
||||
try {
|
||||
if (!$refreshToken) {
|
||||
throw new RuntimeException('Некорректный refresh_token');
|
||||
}
|
||||
$postFields = array(
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'scope' => '',
|
||||
);
|
||||
$response = remoteRequest($apiDomainAuth . 'oauth/token', true, $postFields);
|
||||
if ($response->code == 200) {
|
||||
$res = $response->responseBody;
|
||||
} else {
|
||||
$err = 'Не удалось соединиться с удаленным сервером';
|
||||
sendErrorResponse($response->code, $err);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
setResponseCode($e->getCode());
|
||||
$res['error'] = 'Ошибка при обновлении токена. ' . $e->getMessage();
|
||||
}
|
||||
loadHeaders();
|
||||
echo json_encode($res);
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
|
||||
$res = array();
|
||||
$res['success'] = true;
|
||||
$res['errorID'] = array();
|
||||
$res['messages'] = array();
|
||||
|
||||
$name = $_POST['name'];
|
||||
$email = $_POST['email'];
|
||||
$message = $_POST['message'];
|
||||
$consent = filter_var($_POST['consent'], FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
if($consent != true){
|
||||
$res['success'] = false;
|
||||
$res['messages'][] = 'Не получено согласие на обработку персональных данных.';
|
||||
$res['errorID'][] = 'consent';
|
||||
}
|
||||
|
||||
$code = filter_var($_POST['captcha'], FILTER_SANITIZE_STRING);
|
||||
session_start();
|
||||
if (!isset($_SESSION['captcha']) || strtoupper(trim($_SESSION['captcha'])) != strtoupper(trim($code))) {
|
||||
$res['success'] = false;
|
||||
$res['errorID'][] = 'captcha';
|
||||
$res['messages'][] = 'Неверный код с картинки.';
|
||||
}
|
||||
unset($_SESSION['captcha']);
|
||||
|
||||
if ($res['success']) {
|
||||
$res['success'] = false;
|
||||
|
||||
$headers = array('Accept: application/json');
|
||||
$data = remoteRequest($apiDomen . 'oauth2/ClientCredentials', true,
|
||||
array(
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'grant_type' => 'client_credentials',
|
||||
), $headers
|
||||
);
|
||||
|
||||
if ($data->code == 200) {
|
||||
if (isset($data->responseBody)) {
|
||||
$accessToken = $data->responseBody->access_token;
|
||||
|
||||
try {
|
||||
$data = remoteRequest($apiDomen . 'oauth-via-app/vsoko/feedbackSendMail?access_token='
|
||||
. $accessToken .
|
||||
'&name=' . urlencode($name) .
|
||||
'&email=' . urlencode($email) .
|
||||
'&message=' . urlencode($message)
|
||||
);
|
||||
|
||||
if ($data->code == 200) {
|
||||
$res['success'] = true;
|
||||
} else {
|
||||
sendErrorResponse($data->code, $data->responseBody->message);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$res['message'] = 'Ошибка. '.$e->getMessage();
|
||||
}
|
||||
|
||||
} else {
|
||||
$res['message'] = 'Ошибка ' . $data->responseBody->message;
|
||||
}
|
||||
} else {
|
||||
$res['message'] = $data->responseBody->message;
|
||||
}
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
|
||||
echo json_encode($res);
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
function isCurlExists()
|
||||
{
|
||||
if (in_array('curl', get_loaded_extensions())) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function checkCurl($url, $postFields = false)
|
||||
{
|
||||
$error = '';
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_ENCODING, "");
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
if (is_array($postFields)) {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
$postVars = "";
|
||||
foreach ($postFields as $key => $value) {
|
||||
$postVars .= $key . '=' . $value . '&';
|
||||
}
|
||||
$postVars = rtrim($postVars, '&');
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postVars);
|
||||
}
|
||||
curl_exec($ch);
|
||||
if (curl_errno($ch) > 0 || curl_error($ch)) {
|
||||
$error = 'Curl error code: ' . curl_errno($ch) . '. Error: ' . curl_error($ch);
|
||||
}
|
||||
curl_close($ch);
|
||||
return $error;
|
||||
}
|
||||
|
||||
if (!isCurlExists()) {
|
||||
echo '<p class="alert alert-danger">
|
||||
На Вашем сервере отсутствует расширение PHP для работы с cURL.<br>
|
||||
Вам следует обратиться в службу технической поддержки Вашего хостинг-провайдера с просьбой
|
||||
включить PHP расширение curl.
|
||||
</p>';
|
||||
} else {
|
||||
if (($err = checkCurl($apiDomen)) || ($err = checkCurl($apiDomen, array('post' => 42)))) {
|
||||
echo '<p class="alert alert-danger">
|
||||
В процессе тестирования соединения с сервером обновлений, PHP расширение cURL вернуло
|
||||
следующую ошибку:<br> ' . $err .
|
||||
'</p>';
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
|
||||
if (!isset($_GET['entry']) && filterEntry($_GET['entry'])) {
|
||||
sendErrorResponse(422, 'Некорректный entry');
|
||||
}
|
||||
|
||||
$entry = $_GET['entry'];
|
||||
|
||||
try {
|
||||
$headers = array('Accept: application/json');
|
||||
$result = remoteRequest($apiDomen . 'oauth2/checkEntryPoint?client_id=' . $clientId . '&entry_point=' . $entry, true, false, $headers);
|
||||
if ($result->curlHasError) {
|
||||
sendErrorResponse(500, 'Не удалось соединиться с сервером.' . $result->curlErrorTxt);
|
||||
} else {
|
||||
setResponseCode($result->code);
|
||||
}
|
||||
loadHeaders();
|
||||
echo json_encode(array('success' => $result->responseBody->success));
|
||||
|
||||
} catch (Exception $e) {
|
||||
sendErrorResponse(500, 'Не удалось проверить настройки безопасности.' . $e->getMessage());
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
$updateDir = dirname(__FILE__);
|
||||
$rootDir = dirname($updateDir);
|
||||
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
require_once $updateDir . '/../internal/filesystem.php';
|
||||
|
||||
$errPrefix = 'Для запуска инструмента не удалось подтвердить подлинность вашего токена доступа. ';
|
||||
|
||||
if (!isset($_POST['access_token']) || !filterAccessToken($_POST['access_token'])) {
|
||||
sendErrorResponse(422, $errPrefix . 'Токен доступа отсутствует или некорректен.');
|
||||
}
|
||||
$token = $_POST['access_token'];
|
||||
|
||||
try {
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$res = remoteRequest($apiDomen . 'pull_updates/checkAccessJson', true, false, $headers);
|
||||
if ($res->curlHasError) {
|
||||
sendErrorResponse(500, 'Ошибка CURL при выполнении запроса авторизации. ' . $errPrefix . $res->curlErrorTxt);
|
||||
}
|
||||
if ($res->code !== 200) {
|
||||
$err = $errPrefix . (!empty($res->responseBody->message) ? $res->responseBody->message : 'Неизвестная ошибка.');
|
||||
sendErrorResponse($res->code, $err);
|
||||
}
|
||||
|
||||
Path::init($modulesByPathDeploy);
|
||||
$payload = array(
|
||||
'is_access_allowed' => true,
|
||||
'err' => null
|
||||
);
|
||||
$message = isWritableRecrusive($rootDir);
|
||||
if ($message) {
|
||||
$payload['is_access_allowed'] = false;
|
||||
$payload['err'] = $message;
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode(
|
||||
array(
|
||||
'success' => true,
|
||||
'forward_code' => 200,
|
||||
'payload' => $payload
|
||||
)
|
||||
);
|
||||
die();
|
||||
} catch (Exception $e) {
|
||||
sendErrorResponse(500, $e->getMessage());
|
||||
}
|
||||
|
||||
function isWritableRecrusive($dir)
|
||||
{
|
||||
$message = '';
|
||||
if (is_dir($dir)) {
|
||||
if (is_writable($dir)) {
|
||||
$objects = scandir($dir);
|
||||
foreach ($objects as $object) {
|
||||
if ($object != "." && $object != "..") {
|
||||
$objectPath = Path::join($dir, $object);
|
||||
$message .= isWritableRecrusive($objectPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$message .= 'Отсутствуют права на запись: "' . htmlspecialchars($dir) . '"<br>';
|
||||
}
|
||||
} else {
|
||||
if (!is_writable($dir)) {
|
||||
$message .= 'Отсутствуют права на запись: "' . htmlspecialchars($dir) . '"<br>';
|
||||
}
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
//todo new_core_after del
|
||||
|
||||
require_once dirname(__FILE__) . '/../internal/config.php';
|
||||
require_once dirname(__FILE__) . '/../internal/helper.php';
|
||||
require_once dirname(__FILE__) . '/../internal/filesystem.php';
|
||||
|
||||
if (!isset($_GET['is_access_remove_old_scripts_update'])) {
|
||||
$isAccessRemoveOldScriptsUpdate = 0;
|
||||
}
|
||||
|
||||
if (!in_array((int) $_GET['is_access_remove_old_scripts_update'], array(0, 1))) {
|
||||
$isAccessRemoveOldScriptsUpdate = 0;
|
||||
}
|
||||
|
||||
$isAccessRemoveOldScriptsUpdate = (int) $_GET['is_access_remove_old_scripts_update'];
|
||||
|
||||
Path::init($modulesByPathDeploy);
|
||||
|
||||
$vikonRootPath = Path::getCoreRootPath();
|
||||
|
||||
//remove executor
|
||||
$executorPath = Path::join($vikonRootPath, Path::$executorFile);
|
||||
if (!Filesystem::remove($executorPath, false)) {
|
||||
sendErrorResponse(500, 'Ошибка при очистке ядра. Не удалось удалить исполняемый файл:' . $executorPath);
|
||||
}
|
||||
|
||||
//fail
|
||||
//vikon_core-latest
|
||||
$vikonCoreLatestPath = Path::join($vikonRootPath, 'vikon_core-latest');
|
||||
if (!Filesystem::remove($vikonCoreLatestPath, true)) {
|
||||
sendErrorResponse(500, 'Ошибка при очистке ядра. Не удалось удалить исполняемый файл:' . $vikonCoreLatestPath);
|
||||
}
|
||||
|
||||
//Удаляем старые скрипты в папке sveden/update
|
||||
if ($isAccessRemoveOldScriptsUpdate) {
|
||||
$svedenRoot = Path::getModuleRootPath(SVEDEN);
|
||||
if (!file_exists($svedenRoot)) {
|
||||
sendSuccessResponse('Временные файлы успешно удалены.');
|
||||
}
|
||||
$svedenFuncPath = Path::join($svedenRoot, 'update');
|
||||
if (!file_exists($svedenFuncPath)) {
|
||||
sendSuccessResponse('Временные файлы успешно удалены.');
|
||||
}
|
||||
$entriesFunc = Filesystem::safeScandir($svedenFuncPath);
|
||||
if (!$entriesFunc) {
|
||||
sendSuccessResponse('Временные файлы успешно удалены.');
|
||||
}
|
||||
$excludeEntries = array('index.php');
|
||||
foreach ($entriesFunc as $entry) {
|
||||
if (in_array($entry, $excludeEntries)) {
|
||||
continue;
|
||||
}
|
||||
if (!Filesystem::remove(Path::join($svedenFuncPath, $entry), true, SVEDEN)) {
|
||||
sendErrorResponse(500, 'Ошибка при очистке ядра. Не удалось удалить исполняемый файл:' . Path::join($svedenFuncPath, $entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
sendSuccessResponse('Временные файлы успешно удалены.');
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
|
||||
$res = array();
|
||||
$res['success'] = false;
|
||||
|
||||
if (!isset($_POST['access_token']) || !filterAccessToken($_POST['access_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный access_token');
|
||||
}
|
||||
|
||||
try {
|
||||
$headers = array(
|
||||
'Accept: application/json',
|
||||
'Authorization: Bearer ' . $token = $_POST['access_token'],
|
||||
);
|
||||
|
||||
$response = remoteRequest($apiDomen . 'pull_updates/assist/updateEndedSuccessByNewCoreJson', true, array(), $headers);
|
||||
if (!$response->curlHasError) {
|
||||
if (200 === $response->code) {
|
||||
$resultBody = array(
|
||||
'success' => true,
|
||||
'forward_code' => 200,
|
||||
);
|
||||
} else {
|
||||
$resultBody = array(
|
||||
'success' => false,
|
||||
'forward_code' => $response->code,
|
||||
'message' => !empty($response->responseBody->message )
|
||||
? $response->responseBody->message
|
||||
: 'Неизвестная ошибка'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException( 'CURL ' . $response->curlErrorTxt);
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1 +0,0 @@
|
||||
5.76.10.1
|
||||
@@ -1,239 +0,0 @@
|
||||
<?php
|
||||
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
require_once $updateDir . '/../internal/filesystem.php';
|
||||
|
||||
if (!isset($_GET['access_token']) || !filterAccessToken($_GET['access_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный access_token');
|
||||
}
|
||||
|
||||
if (!isset($_GET['module_id']) || !filterInt($_GET['module_id'])) {
|
||||
sendErrorResponse(422, 'Некорректный module_id');
|
||||
}
|
||||
$moduleId = (int)$_GET['module_id'];
|
||||
$token = $_GET['access_token'];
|
||||
Path::init($modulesByPathDeploy);
|
||||
|
||||
if (!in_array($moduleId, array(SVEDEN, ABITUR, VSOKO))) {
|
||||
sendErrorResponse(422, 'Не верно передан параметр модуля');
|
||||
}
|
||||
|
||||
$moduleFolder = $modulesByPathDeploy[$moduleId];
|
||||
$foldersNeedStay = $allowedFoldersInCoreByModule[$moduleId];
|
||||
|
||||
$moduleRootPath = Path::getModuleRootPath($moduleId);
|
||||
$flagFileOfModulePath = Path::join($moduleRootPath, '.vikon');
|
||||
|
||||
try {
|
||||
if ($moduleId === ABITUR) {
|
||||
$headers = array('Accept-Encoding: zip, gzip', 'Authorization: Bearer ' . $token);
|
||||
$zipModuleCore = remoteRequest(
|
||||
$apiDomen . 'pull_updates/generateEmptyModuleCore/' . $moduleId,
|
||||
false,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
if ($zipModuleCore->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $zipModuleCore->curlErrorTxt);
|
||||
}
|
||||
|
||||
if ($zipModuleCore->code !== 200) {
|
||||
$zipModuleCore->responseBody = json_decode($zipModuleCore->responseBody);
|
||||
$err = 'Не удается скачать файл с обновлениями. ' . (!empty($zipModuleCore->responseBody->message)
|
||||
? $zipModuleCore->responseBody->message
|
||||
: 'Неизвестная ошибка');
|
||||
sendErrorResponse($zipModuleCore->code, $err);
|
||||
}
|
||||
|
||||
$zipModuleCore->responseBody = json_decode($zipModuleCore->responseBody);
|
||||
if ($zipModuleCore->responseBody->success === true) {
|
||||
moduleDirIsEmptyOrEx($moduleRootPath, $flagFileOfModulePath);
|
||||
|
||||
if (!Filesystem::safeMkdir($moduleRootPath, 0755, $moduleId)) {
|
||||
$err = 'Ошибка при распаковке ядра модуля. Не удалось создать папку: ' . $moduleFolder;
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
$filesDirectory = Path::join($moduleRootPath, 'files');
|
||||
if (!Filesystem::safeMkdir($filesDirectory, 0755, $moduleId)) {
|
||||
$err = 'Ошибка при распаковке ядра модуля. Не удалось создать папку: files';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
$msg = 'Ядро модуля "' . $moduleFolder . '" успешно обновлено и синхронизировано.';
|
||||
if (!Filesystem::safeMkfile($flagFileOfModulePath, 0755)) {
|
||||
throw new Exception('Не удалось записать служебную информацию в директорию: ' . $flagFileOfModulePath);
|
||||
}
|
||||
sendSuccessResponse($msg);
|
||||
}
|
||||
sendSuccessResponse('Неизвестная ошибка');
|
||||
}
|
||||
|
||||
$headers = array('Accept-Encoding: zip, gzip', 'Authorization: Bearer ' . $token);
|
||||
$zipModuleCore = remoteRequest(
|
||||
$apiDomen . 'pull_updates/generateEmptyModuleCore/' . $moduleId,
|
||||
false,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
if ($zipModuleCore->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $zipModuleCore->curlErrorTxt);
|
||||
}
|
||||
|
||||
if ($zipModuleCore->code != 200) {
|
||||
$zipModuleCore->responseBody = json_decode($zipModuleCore->responseBody);
|
||||
$err = 'Не удается скачать файл с обновлениями. ' . (!empty($zipModuleCore->responseBody->message)
|
||||
? $zipModuleCore->responseBody->message
|
||||
: 'Неизвестная ошибка');
|
||||
sendErrorResponse($zipModuleCore->code, $err);
|
||||
}
|
||||
|
||||
moduleDirIsEmptyOrEx($moduleRootPath, $flagFileOfModulePath);
|
||||
|
||||
$funcPath = Path::getFunctionalPath();
|
||||
$filenameZip = Path::join($funcPath, $moduleFolder . '_core-latest.zip');
|
||||
|
||||
if (!Filesystem::removeZip($filenameZip, true)) {
|
||||
$err = 'Ошибка при распаковке ядра модуля. Архив уже существует. Недостаточно прав для его удаления';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
$dlHandler = fopen($filenameZip, 'w');
|
||||
if (!fwrite($dlHandler, $zipModuleCore->responseBody)) {
|
||||
$err = 'Ошибка при распаковке ядра модуля. Не удалось записать архив. Проверьте права доступа и свободное место.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
$unpackNewModuleCorePath = Path::join($funcPath, $moduleFolder);
|
||||
if (!Filesystem::remove($unpackNewModuleCorePath, true)) {
|
||||
$err = 'Ошибка при распаковке ядра модуля. Временая директория уже существует и не может быть удалена.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
if (!unpackZip($filenameZip, $funcPath)) {
|
||||
$err = 'Ошибка при распаковке ядра модуля. Проверьте права доступа и свободное место.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
Filesystem::removeZip($filenameZip, false);
|
||||
|
||||
// перед синхронизацией убедимся что папка модуля вообще существует
|
||||
if (!Filesystem::safeMkdir($moduleRootPath, 0755, $moduleId)) {
|
||||
$err = 'Ошибка обновлении ядра модуля. Не удалось создать папку модуля: ' . $moduleFolder;
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
$filesDirectory = Path::join($moduleRootPath, 'files');
|
||||
if (!Filesystem::safeMkdir($filesDirectory, 0755, $moduleId)) {
|
||||
$err = 'Ошибка обновлении ядра модуля. Не удалось создать папку в модуле: files';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
$entriesToSync = Filesystem::safeScandir($unpackNewModuleCorePath);
|
||||
$serviceInfo = array('.vikon');
|
||||
$failedEntryName = null;
|
||||
foreach ($entriesToSync as $entryName) {
|
||||
if (in_array($entryName, $serviceInfo)) {
|
||||
continue;
|
||||
}
|
||||
$currentEntryPath = Path::join($moduleRootPath, $entryName);
|
||||
$newEntryPath = Path::join($unpackNewModuleCorePath, $entryName);
|
||||
$isFile = is_file($newEntryPath);
|
||||
|
||||
if (file_exists($currentEntryPath)) {
|
||||
$newEntryPathWithNewPostfix = Path::join($moduleRootPath, $entryName . Path::$n_pstfx);
|
||||
// try del
|
||||
if (!Filesystem::remove($newEntryPathWithNewPostfix, true, $moduleId)) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
// try replace
|
||||
$renameSuccess = !$isFile
|
||||
? Filesystem::replaceWithRename($newEntryPath, $newEntryPathWithNewPostfix, $moduleId)
|
||||
: Filesystem::safeRenameFile($newEntryPath, $newEntryPathWithNewPostfix, $moduleId);
|
||||
if (!$renameSuccess) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
|
||||
$currentEntryPathWithOldPostfix = Path::join($moduleRootPath, $entryName . Path::$o_pstfx);
|
||||
// try del
|
||||
if (!Filesystem::remove($currentEntryPathWithOldPostfix, true, $moduleId)) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
|
||||
// try replace
|
||||
$renameSuccess = !$isFile
|
||||
? Filesystem::replaceWithRename($currentEntryPath, $currentEntryPathWithOldPostfix, $moduleId)
|
||||
: Filesystem::safeRenameFile($currentEntryPath, $currentEntryPathWithOldPostfix, $moduleId);
|
||||
if (!$renameSuccess) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
|
||||
// try replace
|
||||
$renameSuccess = !$isFile
|
||||
? Filesystem::replaceWithRename($newEntryPathWithNewPostfix, $currentEntryPath, $moduleId)
|
||||
: Filesystem::safeRenameFile($newEntryPathWithNewPostfix, $currentEntryPath, $moduleId);
|
||||
if (!$renameSuccess) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// try replace
|
||||
$renameSuccess = !$isFile
|
||||
? Filesystem::replaceWithRename($newEntryPath, $currentEntryPath, $moduleId)
|
||||
: Filesystem::safeRenameFile($newEntryPath, $currentEntryPath, $moduleId);
|
||||
if (!$renameSuccess) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($failedEntryName !== null) {
|
||||
if (!Filesystem::restoreUnitCoreAfterFail($moduleRootPath, $entriesToSync, $moduleId)) {
|
||||
$err = 'Ошибка при синхронизации ядра модуля. Не удалось восстановить ядро после ошибки.';
|
||||
sendErrorResponse(500, $err);
|
||||
} else {
|
||||
$err = 'Ошибка при синхронизации ядра модуля. Не удалось синхронизировать папку/файл: ' . $failedEntryName;
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
}
|
||||
|
||||
Filesystem::remove($unpackNewModuleCorePath, true);
|
||||
|
||||
$successOrPath = Filesystem::cleanUnitCore($moduleRootPath, $foldersNeedStay, $moduleId);
|
||||
if (is_string($successOrPath)) {
|
||||
$err = 'Ошибка проверки целостности ядра модуля. Не удалось удалить папку: ' . $successOrPath;
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
if (!$successOrPath) {
|
||||
$err = 'Ошибка проверки целостности ядра модуля. Нет доступа к корневой папке ядра.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
//todo new_core_after
|
||||
if ($moduleId === SVEDEN) {
|
||||
$updateDirModuleCore = Path::join($moduleRootPath, 'update');
|
||||
$oldUpdateFile = Path::join($moduleRootPath, 'update', 'index.php');
|
||||
Filesystem::safeMkdir($updateDirModuleCore, 0755, $moduleId);
|
||||
Filesystem::safeMkfile($oldUpdateFile, 0755);
|
||||
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
|
||||
$redirectPath = $locationOfVikonModules . 'vikon_core/update/index.php';
|
||||
$redirectUrl = $protocol . $domenName . $redirectPath;
|
||||
|
||||
$indexContent = "<?php\n"
|
||||
. "header('Location: " . $redirectUrl . "');\n"
|
||||
. "exit;\n";
|
||||
file_put_contents($oldUpdateFile, $indexContent);
|
||||
}
|
||||
|
||||
if (!Filesystem::safeMkfile($flagFileOfModulePath, 0755)) {
|
||||
throw new Exception('Не удалось записать служебную информацию в директорию: ' . $flagFileOfModulePath);
|
||||
}
|
||||
|
||||
} catch (Exception $ex) {
|
||||
sendErrorResponse(500, $ex->getMessage());
|
||||
}
|
||||
sendSuccessResponse('Ядро модуля "' . $moduleFolder . '" успешно обновлено и синхронизировано.');
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
|
||||
$res = array();
|
||||
$res['success'] = false;
|
||||
|
||||
$codeSuccess = isset($_POST['code']) && filterAccessToken($_POST['code']);
|
||||
$urlSuccess = isset($_POST['url']) && filterUrl($_POST['url']);
|
||||
|
||||
if (!$codeSuccess) {
|
||||
sendErrorResponse(422, 'Некорректный авторизационный код');
|
||||
}
|
||||
|
||||
if (!$urlSuccess) {
|
||||
sendErrorResponse(422, 'Некорректный url выполнения запроса');
|
||||
}
|
||||
|
||||
try {
|
||||
$headers = array('Accept: application/json');
|
||||
$data = remoteRequest($apiDomen . 'oauth2/authorize/token', true, array(
|
||||
'code' => $_POST['code'],
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'redirect_uri' => $_POST['url'],
|
||||
'grant_type' => 'authorization_code',
|
||||
), $headers);
|
||||
|
||||
if ($data->curlHasError) {
|
||||
$res['message'] = 'Не удалось соединиться с сервером.' . $data->curlErrorTxt;
|
||||
setResponseCode(500);
|
||||
} else {
|
||||
if (isset($data->responseBody->access_token)) {
|
||||
$res['access_token'] = $data->responseBody->access_token;
|
||||
$res['refresh_token'] = $data->responseBody->refresh_token;
|
||||
$res['success'] = true;
|
||||
} else {
|
||||
$message = '';
|
||||
if (isset($data->responseBody->message)) {
|
||||
$message = $data->responseBody->message;
|
||||
} else {
|
||||
$message = 'Ошибка при получении токена.';
|
||||
}
|
||||
$res['message'] = $message;
|
||||
setResponseCode($data->code);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$res['message'] = 'Ошибка при получении ключа. ' . $e->getMessage();
|
||||
setResponseCode(500);
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
echo json_encode($res);
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
require_once $updateDir . '/../internal/filesystem.php';
|
||||
|
||||
if (!isset($_GET['module_id']) || !filterInt($_GET['module_id'])) {
|
||||
sendErrorResponse(422, 'Отсутствует идентификатор модуля или он указан некорректно');
|
||||
}
|
||||
$moduleId = (int) $_GET['module_id'];
|
||||
if (!in_array($moduleId, array(SVEDEN, ABITUR, VSOKO))) {
|
||||
sendErrorResponse(422, 'Не верно передан идентификатор модуля');
|
||||
}
|
||||
$tmpDir = Path::getTmpVersionPath();
|
||||
$result = null;
|
||||
|
||||
$filePath = Path::join($tmpDir, $moduleId . '.json');
|
||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||
sendErrorResponse(422, 'Не получить файл с версией');
|
||||
}
|
||||
$json = @file_get_contents($filePath);
|
||||
if ($json === false || $json === '') {
|
||||
sendErrorResponse(422, 'Не удалось получить версию');
|
||||
}
|
||||
$data = @json_decode($json, true);
|
||||
|
||||
loadHeaders();
|
||||
echo json_encode(array(
|
||||
'success' => true,
|
||||
'message' => '',
|
||||
'forward_code' => 200,
|
||||
'version' => isset($data['version']) ? htmlspecialchars($data['version']) : null
|
||||
));
|
||||
@@ -1,226 +0,0 @@
|
||||
<?php
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
$currentVersion = file_get_contents($updateDir . '/cur_version.php');
|
||||
|
||||
$showNewCoreInfo = isset($_GET['first_use_new_core']) && $_GET['first_use_new_core'] == '1';
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
<meta http-equiv="Cache-Control" content="no-cache">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<title>Полуавтоматическое обновление VIKON</title>
|
||||
</head>
|
||||
<body class="vikon-wrapper">
|
||||
<input type="hidden" name="domen" value="<?php echo $domenName; ?>">
|
||||
<input type="hidden" name="api_domen" value="<?php echo $apiDomen; ?>">
|
||||
<input type="hidden" name="client_id" value="<?php echo $clientId; ?>">
|
||||
<input type="hidden" name="current_version" value="<?php echo $currentVersion; ?>">
|
||||
|
||||
<div class="wrapper container">
|
||||
|
||||
<div class="main-wrapper">
|
||||
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<div class="header-logo vikon-logo" title="Vikon Logo">Vikon Logo</div>
|
||||
<div class="header-title">Полуавтоматическое обновление VIKON</div>
|
||||
</div>
|
||||
<hr>
|
||||
</header>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="form-group mb-4 d-flex justify-content-between">
|
||||
<div>
|
||||
<a href="/" class="btn btn-success">На главную</a>
|
||||
<a href="#" class="btn btn-success" id="exit" style="display: none;">Выход</a>
|
||||
</div>
|
||||
<a href="https://db-nica.ru/rukovodstvo/selectFaq/43/132" target="_blank" class="btn btn-success help-button">Помощь</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" id="message-container" style="display: none;"></div>
|
||||
|
||||
<div class="row" id="wait-load">
|
||||
<div class="text-center">
|
||||
<svg class="static-throbber" viewBox="0 0 50 50" style="width: 40px; height: 40px; margin-bottom: 10px;">
|
||||
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5" stroke="#007bff" />
|
||||
</svg>
|
||||
идет загрузка страницы
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="no-enter" style="display: none;">
|
||||
<div class="col-sm-12 text-center">
|
||||
<h3>Требуется вход</h3>
|
||||
<div>
|
||||
<button type="button" class="btn btn-success mb-3" id="enter-vikon">
|
||||
Войти через VIKON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($showNewCoreInfo) { ?>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
Переход на новую систему обновления успешно завершен! Для корректной работы системы, пожалуйста, <b>выполните полное обновление всех модулей</b>.
|
||||
</p>
|
||||
<p class="text-danger">Внимание! Директории модулей (/sveden, /abitur) являются точками синхронизации данных системы. Все их содержимое будет автоматически синхронизировано (перезаписано) данными из системы. Пожалуйста, убедитесь, что в этих папках нет личной информации или файлов, не относящихся к данным VIKON, чтобы избежать их потери.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="row" id="yes-enter" style="display: none;">
|
||||
<div class="col-sm-6">
|
||||
<h3>Текущая версия системы: <span class="badge bg-secondary"><?php echo $currentVersion; ?></span></h3>
|
||||
<div class="settings-update" id="settings-update">
|
||||
<div id="modules-container">
|
||||
<!-- Модули будут вставлены здесь через JavaScript -->
|
||||
</div>
|
||||
|
||||
<b>Настройки п/а обновления:</b>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" name="is_resolve_domain" id="is_resolve_domain">
|
||||
<label for="is_resolve_domain">
|
||||
Использовать прямое подключение по IP-адресу к серверам VIKON
|
||||
</label>
|
||||
<span class="fas fa-question-circle"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="Если в процессе работы у ПО не получается связаться с сервером (например, появляется сообщение об ошибке соединения), вы можете включить данную опцию. В этом режиме система будет подключаться напрямую к VIKON через IP-адрес.">
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-none admin-mode-panel" id="admin-mode-panel">
|
||||
<b>Дополнительные опции для администратора:</b>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" name="no_core" id="no_core" value="no_core">
|
||||
<label for="no_core">Не обновлять ядро</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" name="debug_mode" id="debug_mode" value="debug_mode">
|
||||
<label for="debug_mode">Debug Mode</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<h3>Наличие обновления: <span class="badge bg-warning" id="new-version">получение информации...</span></h3>
|
||||
<div>
|
||||
<button type="button" class="btn btn-success" id="start-update">Начать обновление</button>
|
||||
</div>
|
||||
<p class="alert alert-success" id="update-complete" style="display: none;"></p>
|
||||
<p class="alert alert-danger" id="clear_tmp_error_layout" style="display: none;"></p>
|
||||
<div class="row" id="progressbar-container" style="display: none;">
|
||||
<div class="col-12">
|
||||
<div class="progress">
|
||||
<div id="progressbar" class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"
|
||||
style="width: 0;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info process-container" id="process-container" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php require_once $updateDir . '/check_curl.php'; ?>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<p class="copyright">
|
||||
Национальный фонд поддержки инноваций в сфере образования (НФПИ)
|
||||
<br>
|
||||
Copyright © 2013-<?php echo date('Y') ?>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const CURRENT_VERSION = "<?php echo $currentVersion; ?>";
|
||||
const ASSETS_BASE = "./../assets/";
|
||||
|
||||
let PATHNAME = window.location.pathname;
|
||||
PATHNAME = PATHNAME.replace(/\/+$/, '');
|
||||
PATHNAME = PATHNAME.replace(/\/index.php$/, '');
|
||||
|
||||
const getTargetContainer = (tag) => {
|
||||
return document.getElementsByTagName(tag)[0] || document.documentElement;
|
||||
};
|
||||
|
||||
const putStyle = (filename) => {
|
||||
const head = getTargetContainer('head');
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = `${PATHNAME}/${ASSETS_BASE}css/${filename}?v=${CURRENT_VERSION}`;
|
||||
|
||||
head.appendChild(link);
|
||||
};
|
||||
|
||||
const loadScriptsSequentially = (filenames, index = 0) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (index >= filenames.length) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
const filename = filenames[index];
|
||||
const body = getTargetContainer('body');
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = `${PATHNAME}/${ASSETS_BASE}js/${filename}?v=${CURRENT_VERSION}`;
|
||||
|
||||
script.onload = () => {
|
||||
loadScriptsSequentially(filenames, index + 1).then(resolve).catch(reject);
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
loadScriptsSequentially(filenames, index + 1).then(resolve).catch(reject);
|
||||
};
|
||||
|
||||
body.appendChild(script);
|
||||
});
|
||||
};
|
||||
|
||||
const integrate = () => {
|
||||
putStyle('update.css');
|
||||
putStyle('vendor.css');
|
||||
|
||||
loadScriptsSequentially([
|
||||
'vendor.js',
|
||||
'update.js'
|
||||
]).catch((е) => {
|
||||
console.log('Не удалось динамически загрузить js-скрипты для работы полуавтоматического обновления')
|
||||
});
|
||||
};
|
||||
|
||||
integrate();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
$selfDir = dirname(__FILE__);
|
||||
function isWritableR($dir)
|
||||
{
|
||||
$message = '';
|
||||
if (is_dir($dir)) {
|
||||
if (is_writable($dir)) {
|
||||
$objects = scandir($dir);
|
||||
foreach ($objects as $object) {
|
||||
if ($object != "." && $object != "..") {
|
||||
$message .= isWritableR($dir . DIRECTORY_SEPARATOR . $object);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$message .= 'Отсутствуют права на запись: "' . $dir . '"<br>';
|
||||
}
|
||||
} else if (!is_writable($dir)) {
|
||||
$message .= 'Отсутствуют права на запись: "' . $dir . '"<br>';
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
$messageSveden = '';
|
||||
$root = dirname($selfDir);
|
||||
if (file_exists($root)) {
|
||||
$messageSveden = isWritableR($root);
|
||||
}
|
||||
|
||||
if ($messageSveden) {
|
||||
echo '<p class="alert alert-danger">' . $messageSveden . '</p>';
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
require_once $updateDir . '/../internal/filesystem.php';
|
||||
|
||||
if (!isset($_GET['access_token']) || !filterAccessToken($_GET['access_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный access_token');
|
||||
}
|
||||
|
||||
$token = $_GET['access_token'];
|
||||
|
||||
$dlHandler = null;
|
||||
|
||||
try {
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$hasAccess = remoteRequest($apiDomen . 'pull_updates/checkAccessJson', true, false, $headers);
|
||||
if ($hasAccess->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $hasAccess->curlErrorTxt);
|
||||
}
|
||||
|
||||
if ($hasAccess->code !== 200) {
|
||||
$err = 'Не удается скачать файл с обновлениями. ' . (!empty($hasAccess->responseBody->message)
|
||||
? $hasAccess->responseBody->message
|
||||
: 'Неизвестная ошибка');
|
||||
sendErrorResponse($hasAccess->code, $err);
|
||||
}
|
||||
|
||||
if ($hasAccess->responseBody->success) {
|
||||
$vikonRootPath = Path::getCoreRootPath();
|
||||
$executorPath = Path::join($vikonRootPath, Path::$executorFile);
|
||||
|
||||
if (!Filesystem::remove($executorPath, false)) {
|
||||
sendErrorResponse(500, 'Ошибка при генерации ядра. Не удалось удалить устаревший исполняемый скрипт: ' . $executorPath);
|
||||
}
|
||||
|
||||
$srcForExecutorPath = Path::getSrcForExecutorPath();
|
||||
$executorCode = file_get_contents($srcForExecutorPath);
|
||||
if (!$executorCode || !Filesystem::safeMkfile($executorPath, 0755, $executorCode)) {
|
||||
sendErrorResponse(500, 'Ошибка при генерации ядра. Не удалось создать исполняемый скрипт: ' . $executorPath);
|
||||
}
|
||||
sendSuccessResponse('');
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
sendErrorResponse(500, $ex->getMessage());
|
||||
}
|
||||
if ($dlHandler) {
|
||||
fclose($dlHandler);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
$updateDir = dirname(__FILE__);
|
||||
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
require_once $updateDir . '/../internal/filesystem.php';
|
||||
|
||||
if (!isset($_POST['access_token']) || !filterAccessToken($_POST['access_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный access_token');
|
||||
}
|
||||
$token = $_POST['access_token'];
|
||||
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$res = remoteRequest($apiDomen . 'pull_updates/checkAccessJson', true, false, $headers);
|
||||
if ($res->curlHasError) {
|
||||
sendErrorResponse(500, 'CURL ' . $res->curlErrorTxt);
|
||||
}
|
||||
if ($res->code !== 200) {
|
||||
$err = 'Ошибка проверки подлинности токена. '
|
||||
. (!empty($zipCore->responseBody->message) ? $zipCore->responseBody->message : 'Неизвестная ошибка');
|
||||
sendErrorResponse($res->code, $err);
|
||||
}
|
||||
|
||||
$isAccessRemoveOldScriptsUpdate = 0;
|
||||
if (isset($_POST['is_access_remove_old_scripts_update']) && in_array((int) $_POST['is_access_remove_old_scripts_update'], array(0, 1))) {
|
||||
$isAccessRemoveOldScriptsUpdate = (int) $_POST['is_access_remove_old_scripts_update'];
|
||||
}
|
||||
|
||||
Path::init($modulesByPathDeploy);
|
||||
$vikonRootPath = Path::getCoreRootPath();
|
||||
|
||||
//remove executor
|
||||
$executorPath = Path::join($vikonRootPath, Path::$executorFile);
|
||||
if (!Filesystem::remove($executorPath, false)) {
|
||||
sendErrorResponse(500, 'Ошибка при очистке ядра. Не удалось удалить исполняемый файл:' . $executorPath);
|
||||
}
|
||||
|
||||
//fail
|
||||
$vikonCoreLatestPath = Path::join($vikonRootPath, 'vikon_core-latest');
|
||||
if (!Filesystem::remove($vikonCoreLatestPath, true)) {
|
||||
sendErrorResponse(500, 'Ошибка при очистке ядра. Не удалось удалить исполняемый файл:' . $vikonCoreLatestPath);
|
||||
}
|
||||
|
||||
//todo new_core_after
|
||||
//Удаляем старые скрипты в папке sveden/update
|
||||
if ($isAccessRemoveOldScriptsUpdate) {
|
||||
$svedenRoot = Path::getModuleRootPath(SVEDEN);
|
||||
if (!file_exists($svedenRoot)) {
|
||||
sendSuccessResponse('Временные файлы успешно удалены.');
|
||||
}
|
||||
$svedenFuncPath = Path::join($svedenRoot, 'update');
|
||||
if (!file_exists($svedenFuncPath)) {
|
||||
sendSuccessResponse('Временные файлы успешно удалены.');
|
||||
}
|
||||
$entriesFunc = Filesystem::safeScandir($svedenFuncPath);
|
||||
if (!$entriesFunc) {
|
||||
sendSuccessResponse('Временные файлы успешно удалены.');
|
||||
}
|
||||
$excludeEntries = array('index.php');
|
||||
foreach ($entriesFunc as $entry) {
|
||||
if (in_array($entry, $excludeEntries)) {
|
||||
continue;
|
||||
}
|
||||
if (!Filesystem::remove(Path::join($svedenFuncPath, $entry), true, SVEDEN)) {
|
||||
sendErrorResponse(500, 'Ошибка при очистке ядра. Не удалось удалить исполняемый файл:' . Path::join($svedenFuncPath, $entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
sendSuccessResponse('Временные файлы успешно удалены.');
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
|
||||
if (!isset($_POST['refresh_token']) || !filterAccessToken($_POST['refresh_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный code');
|
||||
}
|
||||
|
||||
$res = array();
|
||||
$res['success'] = false;
|
||||
$refreshToken = filterAccessToken($_POST['refresh_token']);
|
||||
|
||||
try {
|
||||
$headers = array('Accept: application/json');
|
||||
$data = remoteRequest($apiDomen . 'oauth2/RefreshToken', true, array(
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'grant_type' => 'refresh_token',
|
||||
), $headers);
|
||||
|
||||
if ($data->curlHasError) {
|
||||
$res['message'] = 'Не удалось соединиться с сервером.' . $data->curlErrorTxt;
|
||||
setResponseCode(500);
|
||||
} else {
|
||||
if (isset($data->responseBody->access_token)) {
|
||||
$res['access_token'] = $data->responseBody->access_token;
|
||||
$res['refresh_token'] = $data->responseBody->refresh_token;
|
||||
$res['success'] = true;
|
||||
} else {
|
||||
$message = '';
|
||||
if (isset($data->responseBody->message)) {
|
||||
$message = $data->responseBody->message;
|
||||
} else {
|
||||
$message = 'Ошибка при получении токена.';
|
||||
}
|
||||
$res['message'] = $message;
|
||||
setResponseCode($data->code);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$res['message'] = 'Ошибка при обновлении ключа. ' . $e->getMessage();
|
||||
setResponseCode(500);
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
echo json_encode($res);
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
|
||||
if (!isset($_POST['access_token']) || !filterAccessToken($_POST['access_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный access_token');
|
||||
}
|
||||
|
||||
if (!isset($_POST['part']) || !filterPartName($_POST['part'])) {
|
||||
sendErrorResponse(422, 'Некорректный part');
|
||||
}
|
||||
|
||||
$token = $_POST['access_token'];
|
||||
$part = $_POST['part'];
|
||||
|
||||
try {
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
|
||||
$post = array('part' => $part, 'is_new_core' => true);
|
||||
$response = remoteRequest($apiDomen . 'pull_updates/generatePartByNewCoreJson', true, $post, $headers);
|
||||
if (!$response->curlHasError) {
|
||||
if (200 === $response->code) {
|
||||
$resultBody = array(
|
||||
'success' => true,
|
||||
'operation_identity' => $response->responseBody->operation_identity,
|
||||
'ttl' => $response->responseBody->ttl,
|
||||
'forward_code' => 200,
|
||||
);
|
||||
} else {
|
||||
$err = !empty($response->responseBody->message) ? $response->responseBody->message : 'Неизвестная ошибка';
|
||||
sendErrorResponse($response->code, $err);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException('CURL ' . $response->curlErrorTxt);
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/filesystem.php';
|
||||
|
||||
$token = filterAccessToken(isset($_POST['access_token']) ? (string) $_POST['access_token'] : '');
|
||||
$moduleId = isset($_POST['moduleId']) ? filterInt($_POST['moduleId']) : null;
|
||||
$dlHandler = null;
|
||||
|
||||
try {
|
||||
Path::init($modulesByPathDeploy);
|
||||
if (!in_array($moduleId, array(SVEDEN, ABITUR, VSOKO))) {
|
||||
throw new RuntimeException('Не верно передан параметр модуля');
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
throw new RuntimeException('Некорректный access_token');
|
||||
}
|
||||
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$response = remoteRequest($filemanagerApiDomen . 'sync/getNewFileInfoByModule?moduleId=' . $moduleId, true, false, $headers);
|
||||
if ($response->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $response->curlErrorTxt);
|
||||
}
|
||||
|
||||
if (
|
||||
$response->code !== 200
|
||||
|| !property_exists($response->responseBody, 'file_name')
|
||||
|| !property_exists($response->responseBody, 'identity')
|
||||
|| !property_exists($response->responseBody, 'dir_name')
|
||||
) {
|
||||
$msg = 'Не удалось получить информацию о файле, котрый требуется загрузить '
|
||||
. tryExtractFmErrorMessage($response, '. ');
|
||||
sendErrorResponse($response->code, $msg);
|
||||
}
|
||||
|
||||
if ($response->responseBody->file_name === null && $response->responseBody->identity === null) {
|
||||
$resultBody = array(
|
||||
'success' => true,
|
||||
'forward_code' => 200,
|
||||
'done' => true,
|
||||
);
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
die();
|
||||
}
|
||||
|
||||
$fileIdentity = $response->responseBody->identity;
|
||||
$filename = $response->responseBody->file_name;
|
||||
$directory = $response->responseBody->dir_name;
|
||||
|
||||
if (!Filesystem::ensureValidDirectoryAndFileName($directory, $filename)) {
|
||||
throw new RuntimeException('Некорректные параметры сохранения: недопустимое имя директории или файла.');
|
||||
}
|
||||
|
||||
$headers = array('Accept-Encoding: zip, gzip', 'Authorization: Bearer ' . $token);
|
||||
$binFile = remoteRequest(
|
||||
$filemanagerApiDomen . 'sync/downloadFileBinary?identity=' . $fileIdentity,
|
||||
false,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
if ($binFile->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $binFile->curlErrorTxt);
|
||||
}
|
||||
|
||||
if ($binFile->code !== 200) {
|
||||
$binFile->responseBody = json_decode($binFile->responseBody);
|
||||
$message = 'Не удается скачать файл ' . $filename . tryExtractFmErrorMessage($binFile, '. ');
|
||||
sendErrorResponse($binFile->code, $message);
|
||||
}
|
||||
|
||||
$fsPathDir = Path::getFsPathByModule($moduleId);
|
||||
if ($directory !== null) {
|
||||
$fsPathDir = Path::join($fsPathDir, $directory);
|
||||
}
|
||||
|
||||
if (!Filesystem::safeMkdir($fsPathDir, 0775, $moduleId)) {
|
||||
throw new RuntimeException('Не удалось создать папку "' . $fsPathDir . '" на вашем сервере');
|
||||
}
|
||||
|
||||
$fsFilePath = Path::join($fsPathDir, $filename);
|
||||
$dlHandler = fopen($fsFilePath, 'w');
|
||||
if ($dlHandler == false || !fwrite($dlHandler, $binFile->responseBody)) {
|
||||
throw new RuntimeException('Не удается записать файл ' . $fsFilePath . ' на диск', 500);
|
||||
}
|
||||
|
||||
$headers = array('Authorization: Bearer ' . $token);
|
||||
$response = remoteRequest(
|
||||
$filemanagerApiDomen . 'sync/markNewFileAsLoaded?identity=' . $fileIdentity . '&moduleId=' . $moduleId,
|
||||
true,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
if ($response->code !== 200) {
|
||||
$message = 'Не удалось пометить файл' . $filename . ' как обновленный'
|
||||
. tryExtractFmErrorMessage($response, '. ');
|
||||
sendErrorResponse($response->code, $message);
|
||||
}
|
||||
|
||||
$resultBody = array(
|
||||
'success' => true,
|
||||
'forward_code' => 200,
|
||||
'message' => 'Файл ' . $filename . ' загружен',
|
||||
'done' => false,
|
||||
);
|
||||
} catch (Exception $ex) {
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
if ($dlHandler) {
|
||||
fclose($dlHandler);
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1,131 +0,0 @@
|
||||
<?php
|
||||
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/filesystem.php';
|
||||
|
||||
$token = filterAccessToken(isset($_POST['access_token']) ? (string) $_POST['access_token'] : '');
|
||||
|
||||
$fileIdentity = isset($_POST['fileIdentity']) && is_scalar($_POST['fileIdentity']) ? $_POST['fileIdentity'] : null;
|
||||
$moduleId = isset($_POST['moduleId']) ? filterInt($_POST['moduleId']) : null;
|
||||
$dlHandler = null;
|
||||
Path::init($modulesByPathDeploy);
|
||||
|
||||
try {
|
||||
if (!in_array($moduleId, array(SVEDEN, ABITUR, VSOKO))) {
|
||||
throw new RuntimeException('Не верно передан параметр модуля');
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
throw new RuntimeException('Некорректный access_token');
|
||||
}
|
||||
|
||||
if (!$fileIdentity) {
|
||||
throw new RuntimeException('Передан невалидный идентификатор файла');
|
||||
}
|
||||
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$response = remoteRequest(
|
||||
$filemanagerApiDomen . 'sync/getFileByIdentityInfo?identity=' . $fileIdentity,
|
||||
true,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
if ($response->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $response->curlErrorTxt);
|
||||
}
|
||||
|
||||
if (
|
||||
$response->code !== 200
|
||||
|| !property_exists($response->responseBody, 'file_name')
|
||||
|| !property_exists($response->responseBody, 'identity')
|
||||
|| !property_exists($response->responseBody, 'dir_name')
|
||||
) {
|
||||
$resultBody = array(
|
||||
'success' => false,
|
||||
'forward_code' => $response->code == 200 ? 500 : $response->code,
|
||||
'message' => (int) $response->code === 404
|
||||
? 'Не удалось получить информацию об одном из синхронизируемых файлов. Файл был удален из системы после запуска процесса синхронизации. Перезапустите синхронизацию файлов.'
|
||||
: 'Не удалось получить информацию о файле, котрый требуется загрузить ' . tryExtractFmErrorMessage($response, '. '),
|
||||
'debug_identity' => $fileIdentity,
|
||||
);
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
die();
|
||||
}
|
||||
|
||||
$identity = $response->responseBody->identity;
|
||||
$filename = $response->responseBody->file_name;
|
||||
$directory = $response->responseBody->dir_name;
|
||||
|
||||
if (!Filesystem::ensureValidDirectoryAndFileName($directory, $filename)) {
|
||||
throw new RuntimeException('Некорректные параметры сохранения: недопустимое имя директории или файла.');
|
||||
}
|
||||
|
||||
$headers = array('Accept-Encoding: zip, gzip', 'Authorization: Bearer ' . $token);
|
||||
$bin = remoteRequest(
|
||||
$filemanagerApiDomen . 'sync/downloadFileBinaryForSync?identity=' . $identity . '&moduleId=' . $moduleId,
|
||||
false,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
if ($bin->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $bin->curlErrorTxt);
|
||||
}
|
||||
|
||||
if ($bin->code !== 200) {
|
||||
$bin->responseBody = json_decode($bin->responseBody);
|
||||
$resultBody = array(
|
||||
'success' => false,
|
||||
'forward_code' => $bin->code,
|
||||
'message' => 'Не удается скачать файл ' . $filename . tryExtractFmErrorMessage($bin, '. '),
|
||||
'debug_identity' => $fileIdentity,
|
||||
);
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
die();
|
||||
}
|
||||
|
||||
$fsPathDir = Path::getFsPathByModule($moduleId);
|
||||
if (!$fsPathDir) {
|
||||
throw new RuntimeException('Неизвестная ошибка');
|
||||
}
|
||||
|
||||
if ($directory !== null) {
|
||||
$fsPathDir = Path::join($fsPathDir, $directory);
|
||||
}
|
||||
|
||||
if (!Filesystem::safeMkdir($fsPathDir, 0775, $moduleId)) {
|
||||
throw new RuntimeException('Не удалось создать папку "' . $fsPathDir . '" на вашем сервере');
|
||||
}
|
||||
|
||||
$filePath = Path::join($fsPathDir, $filename);
|
||||
|
||||
$dlHandler = fopen($filePath, 'w');
|
||||
if ($dlHandler === false) {
|
||||
throw new RuntimeException('Не удается открыть файл для записи: ' . $filePath);
|
||||
}
|
||||
|
||||
if (fwrite($dlHandler, $bin->responseBody) === false) {
|
||||
throw new RuntimeException('Не удается записать файл на диск: ' . $filePath);
|
||||
}
|
||||
|
||||
$resultBody = array(
|
||||
'success' => true,
|
||||
'forward_code' => 200,
|
||||
'message' => 'Файл ' . $filename . ' загружен',
|
||||
);
|
||||
} catch (Exception $ex) {
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
if ($dlHandler) {
|
||||
fclose($dlHandler);
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
|
||||
if (!isset($_POST['access_token']) || !filterAccessToken($_POST['access_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный access_token');
|
||||
}
|
||||
|
||||
if (!isset($_POST['operation_identity']) || !filter_var($_POST['operation_identity'], FILTER_SANITIZE_STRING)) {
|
||||
sendErrorResponse(422, 'Некорректный operation_identity');
|
||||
}
|
||||
|
||||
if (!isset($_POST['part']) || !filterPartName($_POST['part'])) {
|
||||
sendErrorResponse(422, 'Некорректный part');
|
||||
}
|
||||
|
||||
$token = $_POST['access_token'];
|
||||
$operationIdentity = (string) $_POST['operation_identity'];
|
||||
$part = $_POST['part'];
|
||||
|
||||
try {
|
||||
$headers = array(
|
||||
'Accept: application/json',
|
||||
'Authorization: Bearer ' . $token,
|
||||
);
|
||||
|
||||
$response = remoteRequest(
|
||||
$apiDomen . 'pull_updates/checkPartGenerationByNewCoreResultJson?operation_identity=' . $operationIdentity . '&part=' . $part,
|
||||
true,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
if (!$response->curlHasError) {
|
||||
if ($response->code === 200) {
|
||||
$resultBody = array(
|
||||
'success' => true,
|
||||
'forward_code' => 200,
|
||||
);
|
||||
} else {
|
||||
$resultBody = array(
|
||||
'success' => false,
|
||||
'forward_code' => $response->code,
|
||||
'message' => !empty($response->responseBody->message ) ? $response->responseBody->message : 'Неизвестная ошибка'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException('CURL ' . $response->curlErrorTxt);
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1,229 +0,0 @@
|
||||
<?php
|
||||
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
require_once $updateDir . '/../internal/filesystem.php';
|
||||
|
||||
if (!isset($_POST['access_token']) || !filterAccessToken($_POST['access_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный access_token');
|
||||
}
|
||||
|
||||
if (!isset($_POST['operation_identity']) || !filter_var($_POST['operation_identity'], FILTER_SANITIZE_STRING)) {
|
||||
sendErrorResponse(422, 'Некорректный operation_identity');
|
||||
}
|
||||
|
||||
if (!isset($_POST['part']) || !filterPartName($_POST['part'])) {
|
||||
sendErrorResponse(422, 'Некорректный part');
|
||||
}
|
||||
|
||||
$dlHandler = null;
|
||||
$token = $_POST['access_token'];
|
||||
$operationIdentity = (string)$_POST['operation_identity'];
|
||||
$part = $_POST['part'];
|
||||
Path::init($modulesByPathDeploy);
|
||||
try {
|
||||
$headers = array(
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Accept-Encoding: zip, gzip'
|
||||
);
|
||||
|
||||
$response = remoteRequest(
|
||||
$apiDomen . 'pull_updates/downloadPartByNewCoreResult?operation_identity=' . $operationIdentity . '&part=' . $part,
|
||||
false,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
|
||||
if (!$response->curlHasError) {
|
||||
if ($response->code === 200) {
|
||||
$curModuleId = 0;
|
||||
foreach ($allowedFoldersInCoreByModule as $moduleId => $parts) {
|
||||
if (is_array($parts) && in_array($part, $parts)) {
|
||||
$curModuleId = $moduleId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$funcPath = Path::getFunctionalPath();
|
||||
$filenameZip = Path::join($funcPath, $operationIdentity . '.zip');
|
||||
|
||||
if (!Filesystem::removeZip($filenameZip, true)) {
|
||||
$err = 'Ошибка при распаковке раздела: "' . $part . '".'
|
||||
. ' Базовый архив раздела уже существует, недостаточно прав для его удаления.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
$dlHandler = fopen($filenameZip, 'w');
|
||||
if (!fwrite($dlHandler, $response->responseBody)) {
|
||||
$err = 'Ошибка при распаковке раздела: "' . $part . '".'
|
||||
. ' Не удалось записать архив. Проверьте права доступа и свободное место.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
$unpackPath = Path::join($funcPath, $part.'_part-latest');
|
||||
if (!Filesystem::remove($unpackPath, true)) {
|
||||
$err = 'Ошибка при распаковке раздела: "' . $part . '".'
|
||||
. ' Временная директория уже существует и не может быть удалена.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
if (!unpackZip($filenameZip, $unpackPath)) {
|
||||
$err = 'Ошибка при распаковке раздела: ' . $part . '.'
|
||||
. ' Проверьте права доступа и свободное место.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
Filesystem::removeZip($filenameZip, false);
|
||||
|
||||
$errRepeir = 'Ошибка при синхронизации раздела. Не удалось восстановить часть: ' . $part;
|
||||
$errSync = 'Ошибка при синхронизации раздела. Не удалось синхронизировать часть: ' . $part;
|
||||
|
||||
$rootModuleCore = Path::getModuleRootPath($curModuleId);
|
||||
$pathCurFolderPart = Path::join($rootModuleCore, $part);
|
||||
$excludedEntries = array();
|
||||
if ('abitur' !== $part) {
|
||||
$excludedEntries = $allowedFoldersInCoreByModule[$curModuleId];
|
||||
$pathCurFolderPart = Path::join($rootModuleCore, $part);
|
||||
if (file_exists($pathCurFolderPart)) {
|
||||
$pathPartForSync = Path::join($unpackPath, $part);
|
||||
$pathPartForSyncPostfixNew = Path::join($rootModuleCore, $part.Path::$n_pstfx);
|
||||
|
||||
if (!Filesystem::remove($pathPartForSyncPostfixNew, true, $curModuleId)) {
|
||||
if (!Filesystem::restoreUnitCoreAfterFail($rootModuleCore, array($part), $curModuleId)) {
|
||||
sendErrorResponse(500, $errRepeir);
|
||||
}
|
||||
}
|
||||
if (!Filesystem::replaceWithRename($pathPartForSync, $pathPartForSyncPostfixNew, $curModuleId)) {
|
||||
if (!Filesystem::restoreUnitCoreAfterFail($rootModuleCore, array($part), $curModuleId)) {
|
||||
sendErrorResponse(500, $errRepeir);
|
||||
}
|
||||
sendErrorResponse(500, $errSync);
|
||||
}
|
||||
|
||||
$pathCurFolderPartOldPostfix = Path::join($rootModuleCore, $part.Path::$o_pstfx);
|
||||
if (!Filesystem::remove($pathCurFolderPartOldPostfix, true, $curModuleId)) {
|
||||
if (!Filesystem::restoreUnitCoreAfterFail($rootModuleCore, array($part), $curModuleId)) {
|
||||
sendErrorResponse(500, $errRepeir);
|
||||
}
|
||||
sendErrorResponse(500, $errSync);
|
||||
}
|
||||
if (!Filesystem::replaceWithRename($pathCurFolderPart, $pathCurFolderPartOldPostfix, $curModuleId)) {
|
||||
if (!Filesystem::restoreUnitCoreAfterFail($rootModuleCore, array($part), $curModuleId)) {
|
||||
sendErrorResponse(500, $errRepeir);
|
||||
}
|
||||
sendErrorResponse(500, $errSync);
|
||||
}
|
||||
|
||||
if (!Filesystem::replaceWithRename($pathPartForSyncPostfixNew, $pathCurFolderPart, $curModuleId)) {
|
||||
if (!Filesystem::restoreUnitCoreAfterFail($rootModuleCore, array($part), $curModuleId)) {
|
||||
sendErrorResponse(500, $errRepeir);
|
||||
}
|
||||
sendErrorResponse(500, $errSync);
|
||||
}
|
||||
} else {
|
||||
$pathPartForSync = Path::join($unpackPath, $part);
|
||||
if (!Filesystem::replaceWithRename($pathPartForSync, $pathCurFolderPart, $curModuleId)) {
|
||||
if (!Filesystem::restoreUnitCoreAfterFail($rootModuleCore, array($part), $curModuleId)) {
|
||||
sendErrorResponse(500, $errRepeir);
|
||||
}
|
||||
sendErrorResponse(500, $errSync);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$pathCurFolderPart = $rootModuleCore;
|
||||
$entrySyncFail = null;
|
||||
$isRestoreFail = false;
|
||||
$entriesToSync = Filesystem::safeScandir(Path::join($unpackPath, $part));
|
||||
$excludedEntries = array_merge($entriesToSync, array('files', '.htaccess'));//todo подумать как от этого костыля отказаться
|
||||
foreach ($entriesToSync as $entryName) {
|
||||
$currentEntryPath = Path::join($pathCurFolderPart, $entryName);
|
||||
$newEntryPath = Path::join($unpackPath, $part, $entryName);
|
||||
$isFile = is_file($newEntryPath);
|
||||
|
||||
if (file_exists($currentEntryPath)) {
|
||||
$newEntryPathWithNewPostfix = Path::join($pathCurFolderPart, $entryName.Path::$n_pstfx);
|
||||
// try del
|
||||
if (!Filesystem::remove($newEntryPathWithNewPostfix, true, $curModuleId)) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
// try replace
|
||||
$renameSuccess = !$isFile
|
||||
? Filesystem::replaceWithRename($newEntryPath, $newEntryPathWithNewPostfix, $curModuleId)
|
||||
: Filesystem::safeRenameFile($newEntryPath, $newEntryPathWithNewPostfix, $curModuleId);
|
||||
if (!$renameSuccess) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
$currentEntryPathWithOldPostfix = Path::join($pathCurFolderPart, $entryName.Path::$o_pstfx);
|
||||
// try del
|
||||
if (!Filesystem::remove($currentEntryPathWithOldPostfix, true, $curModuleId)) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
|
||||
// try replace
|
||||
$renameSuccess = !$isFile
|
||||
? Filesystem::replaceWithRename($currentEntryPath, $currentEntryPathWithOldPostfix, $curModuleId)
|
||||
: Filesystem::safeRenameFile($currentEntryPath, $currentEntryPathWithOldPostfix, $curModuleId);
|
||||
if (!$renameSuccess) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
|
||||
// try replace
|
||||
$renameSuccess = !$isFile
|
||||
? Filesystem::replaceWithRename($newEntryPathWithNewPostfix, $currentEntryPath, $curModuleId)
|
||||
: Filesystem::safeRenameFile($newEntryPathWithNewPostfix, $currentEntryPath, $curModuleId);
|
||||
if (!$renameSuccess) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// try replace
|
||||
$renameSuccess = !$isFile
|
||||
? Filesystem::replaceWithRename($newEntryPath, $currentEntryPath, $curModuleId)
|
||||
: Filesystem::safeRenameFile($newEntryPath, $currentEntryPath, $curModuleId);
|
||||
if (!$renameSuccess) {
|
||||
$failedEntryName = $entryName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Filesystem::remove($unpackPath, true);
|
||||
|
||||
$successOrPath = Filesystem::cleanUnitCore($rootModuleCore, $excludedEntries, $curModuleId);
|
||||
if (is_string($successOrPath)) {
|
||||
$err = 'Ошибка проверки целостности части. Не удалось удалить папку: ' . $successOrPath;
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
if (!$successOrPath) {
|
||||
$err = 'Ошибка проверки целостности части. Нет доступа к корневой папке части.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
$resultBody = array('success' => true, 'forward_code' => 200, 'message' => 'Раздел ' . $part . ' успешно обновлен.');
|
||||
} else {
|
||||
$response->responseBody = json_decode($response->responseBody);
|
||||
|
||||
$resultBody = array(
|
||||
'success' => false,
|
||||
'forward_code' => $response->code,
|
||||
'message' => !empty($response->responseBody->message ) ? $response->responseBody->message : 'неизвестная ошибка'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException('CURL ' . $response->curlErrorTxt);
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
if ($dlHandler != null) {
|
||||
fclose($dlHandler);
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
//скрипт запускается оносительно vikon_core
|
||||
$vikonDir = dirname(__FILE__);
|
||||
require_once $vikonDir . '/internal/config.php';
|
||||
require_once $vikonDir . '/internal/helper.php';
|
||||
require_once $vikonDir . '/internal/filesystem.php';
|
||||
|
||||
if (!isset($_GET['access_token']) || !filterAccessToken($_GET['access_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный access_token');
|
||||
}
|
||||
|
||||
$token = $_GET['access_token'];
|
||||
|
||||
$dlHandler = null;
|
||||
|
||||
try {
|
||||
$headers = array('Accept-Encoding: zip, gzip', 'Authorization: Bearer ' . $token);
|
||||
$zipCore = remoteRequest($apiDomen . 'pull_updates/generateEmptyCore', false, false, $headers);
|
||||
if ($zipCore->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $zipCore->curlErrorTxt);
|
||||
}
|
||||
|
||||
if ($zipCore->code !== 200) {
|
||||
$zipCore->responseBody = json_decode($zipCore->responseBody);
|
||||
|
||||
$err = 'Не удается скачать файл с обновлениями. ' . (!empty($zipCore->responseBody->message)
|
||||
? $zipCore->responseBody->message
|
||||
: 'Неизвестная ошибка');
|
||||
sendErrorResponse($zipCore->code, $err);
|
||||
}
|
||||
|
||||
$vikonFuncPath = Path::getCoreRootPath();
|
||||
$vikonZipCorePath = Path::join($vikonFuncPath, 'vikon_core-latest.zip');
|
||||
|
||||
if (!Filesystem::removeZip($vikonZipCorePath, true)) {
|
||||
$err = 'Ошибка при распаковке ядра. Архив уже существует. Недостаточно прав для его удаления';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
$dlHandler = fopen($vikonZipCorePath, 'w');
|
||||
if (!fwrite($dlHandler, $zipCore->responseBody)) {
|
||||
$err = 'Ошибка при распаковке ядра модуля. Не удалось записать архив. Проверьте права доступа и свободное место.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
if ($dlHandler) {
|
||||
fclose($dlHandler);
|
||||
}
|
||||
|
||||
$vikonUnpackNewCorePath = Path::join($vikonFuncPath, 'vikon_core-latest');
|
||||
|
||||
if (!Filesystem::remove($vikonUnpackNewCorePath, true)) {
|
||||
$err = 'Ошибка при распаковке ядра. Временная директория уже существует и не может быть удалена.';
|
||||
sendErrorResponse(500, $err);
|
||||
}
|
||||
|
||||
$unpackResult = unpackZip($vikonZipCorePath, $vikonUnpackNewCorePath);
|
||||
if (!$unpackResult['success']) {
|
||||
sendErrorResponse(500, 'Ошибка при распаковке ядра. Проверьте права доступа и свободное место.');
|
||||
}
|
||||
Filesystem::removeZip($vikonZipCorePath, false);
|
||||
|
||||
$vikonRootPath = Path::getCoreRootPath();
|
||||
|
||||
$pathToFoldersNewCore = Path::join($vikonUnpackNewCorePath, Path::$vikonCoreFolder);
|
||||
$nameFoldersForSync = Filesystem::safeScandir($pathToFoldersNewCore);
|
||||
$isRestoreFail = false;
|
||||
$folderSyncFail = null;
|
||||
foreach ($nameFoldersForSync as $nameFolderSync) {
|
||||
$curFolder = Path::join($vikonRootPath, $nameFolderSync);
|
||||
$pathToNewFolder = Path::join($vikonUnpackNewCorePath, Path::$vikonCoreFolder, $nameFolderSync);
|
||||
|
||||
if (file_exists($curFolder)) {
|
||||
$pathToNewFolderPostfix = Path::join($vikonRootPath, $nameFolderSync . Path::$n_pstfx);
|
||||
// try del vikon_core/assets_new
|
||||
if (!Filesystem::remove($pathToNewFolderPostfix, true)) {
|
||||
$folderSyncFail = $nameFolderSync;
|
||||
break;
|
||||
}
|
||||
// try replace vikon_core/vikon_core-latest/vikon_core/assets -> vikon_core/assets_new
|
||||
if (!Filesystem::replaceWithRename($pathToNewFolder, $pathToNewFolderPostfix)) {
|
||||
$folderSyncFail = $nameFolderSync;
|
||||
break;
|
||||
}
|
||||
|
||||
$pathToOldFolderPostfix = Path::join($vikonRootPath, $nameFolderSync . Path::$o_pstfx);
|
||||
// try del vikon_core/assets_old
|
||||
if (!Filesystem::remove($pathToOldFolderPostfix, true)) {
|
||||
$folderSyncFail = $nameFolderSync;
|
||||
break;
|
||||
}
|
||||
// try replace vikon_core/assets -> vikon_core/assets_old
|
||||
if (!Filesystem::replaceWithRename($curFolder, $pathToOldFolderPostfix)) {
|
||||
$folderSyncFail = $nameFolderSync;
|
||||
break;
|
||||
}
|
||||
|
||||
// vikon_core/assets_new -> vikon_core/assets
|
||||
if (!Filesystem::replaceWithRename($pathToNewFolderPostfix, $curFolder)) {
|
||||
$folderSyncFail = $nameFolderSync;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// try replace vikon_core/vikon_core-latest/vikon_core/assets -> vikon_core/assets
|
||||
if (!Filesystem::replaceWithRename($pathToNewFolder, $curFolder)) {
|
||||
$folderSyncFail = $nameFolderSync;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//синхронизированные папки + временная папка с версиями
|
||||
$allFoldersNeedCore = array_merge($nameFoldersForSync, array('tmp'));
|
||||
if ($folderSyncFail !== null) {
|
||||
if (!Filesystem::restoreUnitCoreAfterFail($vikonRootPath, $allFoldersNeedCore)) {
|
||||
sendErrorResponse(500, 'Ошибка при синхронизации ядра. Не удалось восстановить ядро после ошибки.');
|
||||
} else {
|
||||
sendErrorResponse(500, 'Ошибка при синхронизации ядра. Не удалось синхронизировать папку: ' . $folderSyncFail);
|
||||
}
|
||||
}
|
||||
|
||||
$successOrPath = Filesystem::cleanUnitCore($vikonRootPath, $allFoldersNeedCore);
|
||||
if (is_string($successOrPath)) {
|
||||
sendErrorResponse(500, 'Ошибка проверки целостности ядра. Не удалось удалить папку: ' . $successOrPath);
|
||||
}
|
||||
if (!$successOrPath) {
|
||||
sendErrorResponse(500, 'Ошибка проверки целостности ядра. Нет доступа к корневой папке ядра.');
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
sendErrorResponse(500, $ex->getMessage());
|
||||
}
|
||||
|
||||
sendSuccessResponse('Архив с базовыми обновлениями успешно загружен.');
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/filesystem.php';
|
||||
|
||||
$token = filterAccessToken(isset($_POST['access_token']) ? (string) $_POST['access_token'] : '');
|
||||
$moduleId = isset($_POST['moduleId']) ? filterInt($_POST['moduleId']) : null;
|
||||
$dlHandler = null;
|
||||
Path::init($modulesByPathDeploy);
|
||||
try {
|
||||
if (!in_array($moduleId, array(SVEDEN, ABITUR, VSOKO))) {
|
||||
throw new RuntimeException('Не верно передан параметр модуля');
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
throw new RuntimeException('Некорректный access_token');
|
||||
}
|
||||
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$response = remoteRequest($filemanagerApiDomen . 'sync/getUsedDirNamesByModule?moduleId=' . $moduleId, true, false, $headers);
|
||||
|
||||
if ($response->code !== 200) {
|
||||
$msg = 'Не удалось инициировать процедуру синхронизации файлов' . tryExtractFmErrorMessage($response, '. ');
|
||||
sendErrorResponse($response->code, $msg);
|
||||
}
|
||||
|
||||
if (!property_exists($response->responseBody, 'directories') || !is_array($response->responseBody->directories)) {
|
||||
throw new RuntimeException('Невалидный формат ответа при запросе используемых директорий');
|
||||
}
|
||||
|
||||
$directories = $response->responseBody->directories;
|
||||
$fsRootPath = Path::getFsPathByModule($moduleId);
|
||||
if (!$fsRootPath) {
|
||||
throw new RuntimeException('Неизвестная ошибка');
|
||||
}
|
||||
|
||||
$dirObjects = Filesystem::safeScandir($fsRootPath);
|
||||
if (false === $dirObjects) {
|
||||
$msg = 'Не удалось просканировать папку: ' . $fsRootPath;
|
||||
sendErrorResponse(500, $msg);
|
||||
}
|
||||
|
||||
$flippedKnownDirectories = array_flip($directories);
|
||||
foreach ($dirObjects as $objectName) {
|
||||
$objectPath = Path::join($fsRootPath, $objectName);
|
||||
if (
|
||||
!array_key_exists($objectName, $flippedKnownDirectories)
|
||||
&& is_dir($objectPath)
|
||||
) {
|
||||
if (is_link($fsRootPath)) {
|
||||
throw new RuntimeException(
|
||||
'В синхронизируемый директории находится ссылка'
|
||||
. $objectPath
|
||||
. ' , которая не может быть безопасно удалена'
|
||||
);
|
||||
}
|
||||
Filesystem::remove($objectPath, true, $moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
$resultBody = array(
|
||||
'success' => true,
|
||||
'forward_code' => 200,
|
||||
'directories' => $directories,
|
||||
);
|
||||
} catch (Exception $ex) {
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
|
||||
if (!isset($_GET['access_token']) || !filterAccessToken($_GET['access_token'])) {
|
||||
sendErrorResponse(422, 'Некорректный access_token');
|
||||
}
|
||||
|
||||
if (!isset($_GET['operation_identity']) || !filter_var($_GET['operation_identity'], FILTER_SANITIZE_STRING)) {
|
||||
sendErrorResponse(422, 'Некорректный operation_identity');
|
||||
}
|
||||
|
||||
$operationIdentity = (string) $_GET['operation_identity'];
|
||||
$token = $_GET['access_token'];
|
||||
|
||||
try {
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$response = remoteRequest(
|
||||
$apiDomen . 'pull_updates/getStatusPartGenerationByNewCoreJson?operation_identity=' . $operationIdentity,
|
||||
true,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
if (!$response->curlHasError) {
|
||||
if (200 === $response->code) {
|
||||
$resultBody = array(
|
||||
'success' => true,
|
||||
'status' => $response->responseBody->status,
|
||||
'forward_code' => 200,
|
||||
);
|
||||
} else {
|
||||
$resultBody = array(
|
||||
'success' => false,
|
||||
'forward_code' => $response->code,
|
||||
'message' => !empty($response->responseBody->message ) ? $response->responseBody->message : 'неизвестная ошибка'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException('CURL ' . $response->curlErrorTxt);
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
setResponseCode(!empty($response->code) ? $response->code : 500);
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1,109 +0,0 @@
|
||||
<?php
|
||||
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/filesystem.php';
|
||||
|
||||
$token = filterAccessToken(isset($_POST['access_token']) ? (string) $_POST['access_token'] : '');
|
||||
//через jquery нельзя послать пустой массив, если это не json.
|
||||
// json посылать не будем, похоже при некоторых настройках старых серверов есть проблемы с чтением raw-body через php://input($HTTP_RAW_POST_DATA)
|
||||
$isHasSubDirs = isset($_POST['isHasSubDirs']) && is_numeric($_POST['isHasSubDirs'])
|
||||
? (int) $_POST['isHasSubDirs']
|
||||
: null;
|
||||
$knownSubDirs = isset($_POST['knownSubDirs']) && is_array($_POST['knownSubDirs'])
|
||||
? $_POST['knownSubDirs']
|
||||
: array();
|
||||
$moduleId = isset($_POST['moduleId']) ? filterInt($_POST['moduleId']) : null;
|
||||
|
||||
$dlHandler = null;
|
||||
Path::init($modulesByPathDeploy);
|
||||
|
||||
try {
|
||||
if (!in_array($moduleId, array(SVEDEN, ABITUR, VSOKO))) {
|
||||
throw new RuntimeException('Не верно передан параметр модуля');
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
throw new RuntimeException('Некорректный access_token');
|
||||
}
|
||||
|
||||
if (null === $isHasSubDirs || ($isHasSubDirs && !$knownSubDirs) || (!$isHasSubDirs && $knownSubDirs)) {
|
||||
throw new RuntimeException('Не может быть обработано. Неверные параметры запроса.');
|
||||
}
|
||||
|
||||
$fsDirPath = Path::getFsPathByModule($moduleId);
|
||||
if (!$fsDirPath) {
|
||||
throw new RuntimeException('Не удалось определить модуль');
|
||||
}
|
||||
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$response = remoteRequest($filemanagerApiDomen . 'sync/getFileNamesFromRootDirectoryByModule?moduleId=' . $moduleId, true, false, $headers);
|
||||
if ($response->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $response->curlErrorTxt);
|
||||
}
|
||||
|
||||
if (
|
||||
$response->code !== 200
|
||||
|| !property_exists($response->responseBody, 'files')
|
||||
|| !is_array($response->responseBody->files)
|
||||
) {
|
||||
$msg = 'Не удалось получить список файлов с файлового сервера'
|
||||
. tryExtractFmErrorMessage($response, '. ');
|
||||
sendErrorResponse($response->code, $msg);
|
||||
}
|
||||
$filesByNamesFromFm = array();
|
||||
foreach ($response->responseBody->files as $row) {
|
||||
$filesByNamesFromFm[(string) $row->n] = $row->i;
|
||||
}
|
||||
$response = null;
|
||||
|
||||
if (!Filesystem::safeMkdir($fsDirPath, 0775, $moduleId)) {
|
||||
throw new RuntimeException('Не удалось создать папку "' . $fsDirPath . '" на вашем сервере');
|
||||
}
|
||||
$items = Filesystem::safeScandir($fsDirPath);
|
||||
if (false === $items) {
|
||||
$msg = 'Не удалось просканировать существующие файлы';
|
||||
sendErrorResponse(500, $msg);
|
||||
}
|
||||
|
||||
$existingItemsByNames = array();
|
||||
foreach ($items as $fName) {
|
||||
$existingItemsByNames[$fName] = null;
|
||||
}
|
||||
|
||||
$knownSubDirsByNames = array_flip($knownSubDirs);
|
||||
foreach ($existingItemsByNames as $dirItem => $_) {
|
||||
$fsItemPath = Path::join($fsDirPath, $dirItem);
|
||||
if ((array_key_exists($dirItem, $knownSubDirsByNames) && is_dir($fsItemPath))) {
|
||||
unset($existingItemsByNames[$dirItem]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!array_key_exists((string) $dirItem, $filesByNamesFromFm)) {
|
||||
//удаляем только файлы, ненужные папки были удалены в start_sync_files
|
||||
if (is_file($fsItemPath)) {
|
||||
unlink($fsItemPath);
|
||||
}
|
||||
} else {
|
||||
if (!filesize($fsItemPath)) {
|
||||
unset($existingItemsByNames[$dirItem]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filesForSync = array();
|
||||
foreach ($filesByNamesFromFm as $fileName => $identity) {
|
||||
if (!array_key_exists($fileName, $existingItemsByNames)) {
|
||||
$filesForSync[] = $identity;
|
||||
}
|
||||
}
|
||||
|
||||
$resultBody = array('success' => true, 'forward_code' => 200, 'files' => $filesForSync);
|
||||
} catch (Exception $ex) {
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1,100 +0,0 @@
|
||||
<?php
|
||||
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/filesystem.php';
|
||||
|
||||
$dir = isset($_POST['dir']) && is_string($_POST['dir']) ? $_POST['dir'] : '';
|
||||
$token = filterAccessToken(isset($_POST['access_token']) ? (string) $_POST['access_token'] : '');
|
||||
$moduleId = isset($_POST['moduleId']) ? filterInt($_POST['moduleId']) : null;
|
||||
|
||||
$dlHandler = null;
|
||||
Path::init($modulesByPathDeploy);
|
||||
|
||||
try {
|
||||
if (!preg_match("/^[a-z]{3,4}$/", $dir)) {
|
||||
throw new RuntimeException('Невалидное значение для синхронизируемой суб-директории');
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
throw new RuntimeException('Некорректный access_token');
|
||||
}
|
||||
|
||||
if (!in_array($moduleId, array(SVEDEN, ABITUR, VSOKO))) {
|
||||
throw new RuntimeException('Не верно передан параметр модуля');
|
||||
}
|
||||
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$url = $filemanagerApiDomen . 'sync/getFileNamesFromSubDirectoryByModule?dir=' . $dir . '&moduleId=' . $moduleId;
|
||||
$response = remoteRequest($url, true, false, $headers);
|
||||
if ($response->curlHasError) {
|
||||
throw new RuntimeException('CURL ' . $response->curlErrorTxt);
|
||||
}
|
||||
if (
|
||||
$response->code !== 200
|
||||
|| !property_exists($response->responseBody, 'files')
|
||||
|| !is_array($response->responseBody->files)
|
||||
) {
|
||||
$message = 'Не удалось получить список файлов с файлового сервера'
|
||||
. tryExtractFmErrorMessage($response, '. ');
|
||||
sendErrorResponse($response->code, $message);
|
||||
}
|
||||
|
||||
$filesByNamesFromFm = array();
|
||||
foreach ($response->responseBody->files as $row) {
|
||||
$filesByNamesFromFm[(string) $row->n] = $row->i;
|
||||
}
|
||||
$response = null;
|
||||
|
||||
$fsDirPath = Path::getFsPathByModule($moduleId);
|
||||
if (!$fsDirPath) {
|
||||
throw new RuntimeException('Не удалось определить модуль');
|
||||
}
|
||||
|
||||
$dirPath = Path::join($fsDirPath, $dir);
|
||||
if (!Filesystem::safeMkdir($dirPath, 0775, $moduleId)) {
|
||||
throw new RuntimeException('Не удалось создать папку "' . $dirPath . '" на вашем сервере');
|
||||
}
|
||||
|
||||
$items = Filesystem::safeScandir($dirPath);
|
||||
if (false === $items) {
|
||||
$msg = 'Не удалось просканировать существующие файлы';
|
||||
sendErrorResponse(500, $msg);
|
||||
}
|
||||
|
||||
$existingItemsByNames = array();
|
||||
foreach ($items as $fName) {
|
||||
$existingItemsByNames[$fName] = null;
|
||||
}
|
||||
|
||||
foreach ($existingItemsByNames as $subDirItem => $_) {
|
||||
$filePath = Path::join($dirPath, $subDirItem);
|
||||
if (!array_key_exists((string) $subDirItem, $filesByNamesFromFm)) {
|
||||
if (is_dir($filePath)) {
|
||||
Filesystem::remove($filePath, true);
|
||||
} else {
|
||||
unlink($filePath);
|
||||
}
|
||||
} else {
|
||||
if (!filesize($filePath)) {
|
||||
unset($existingItemsByNames[$subDirItem]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filesForSync = array();
|
||||
foreach ($filesByNamesFromFm as $fileName => $identity) {
|
||||
if (!array_key_exists($fileName, $existingItemsByNames)) {
|
||||
$filesForSync[] = $identity;
|
||||
}
|
||||
}
|
||||
|
||||
$resultBody = array('success' => true, 'forward_code' => 200, 'files' => $filesForSync);
|
||||
} catch (Exception $ex) {
|
||||
$resultBody = array('success' => false, 'forward_code' => 500, 'message' => $ex->getMessage());
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
setResponseCode(200);
|
||||
echo json_encode($resultBody);
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
$updateDir = dirname(__FILE__);
|
||||
require_once $updateDir . '/../internal/config.php';
|
||||
require_once $updateDir . '/../internal/helper.php';
|
||||
require_once $updateDir . '/../internal/filesystem.php';
|
||||
|
||||
|
||||
$token = null;
|
||||
if (isset($_GET['access_token'])) { //todo new_core_after del GET
|
||||
$token = $_GET['access_token'];
|
||||
} elseif (isset($_POST['access_token'])) {
|
||||
$token = $_POST['access_token'];
|
||||
}
|
||||
|
||||
if ($token === null || !filterAccessToken($token)) {
|
||||
sendErrorResponse(422, 'Токен доступа отсутствует или некорректен');
|
||||
}
|
||||
|
||||
$moduleId = null;
|
||||
if (isset($_GET['module_id'])) { //todo new_core_after del GET
|
||||
$moduleId = $_GET['module_id'];
|
||||
} elseif (isset($_POST['module_id'])) {
|
||||
$moduleId = $_POST['module_id'];
|
||||
}
|
||||
|
||||
if ($moduleId === null || !filterInt($moduleId)) {
|
||||
sendErrorResponse(422, 'Отсутствует идентификатор модуля или он указан некорректно');
|
||||
}
|
||||
if (!in_array($moduleId, array(SVEDEN, ABITUR, VSOKO))) {
|
||||
sendErrorResponse(422, 'Не верно передан идентификатор модуля');
|
||||
}
|
||||
|
||||
$version = null;
|
||||
if (array_key_exists('version', $_GET)) { //todo new_core_after del GET
|
||||
$version = $_GET['version'];
|
||||
} elseif (array_key_exists('version', $_POST)) {
|
||||
$version = $_POST['version'];
|
||||
}
|
||||
|
||||
if ($version !== null && !filterVersion($version)) {
|
||||
sendErrorResponse(422, 'Передан невалидный параметр версии последнего обновления');
|
||||
}
|
||||
|
||||
$headers = array('Accept: application/json', 'Authorization: Bearer ' . $token);
|
||||
$res = remoteRequest($apiDomen . 'pull_updates/checkAccessJson', true, false, $headers);
|
||||
if ($res->curlHasError) {
|
||||
sendErrorResponse(500, 'CURL ' . $res->curlErrorTxt);
|
||||
}
|
||||
if ($res->code !== 200) {
|
||||
$err = 'Ошибка проверки подлинности токена. '
|
||||
. (!empty($res->responseBody->message) ? $res->responseBody->message : 'Неизвестная ошибка');
|
||||
sendErrorResponse($res->code, $err);
|
||||
}
|
||||
|
||||
try {
|
||||
$tmpPath = Path::join(Path::getCoreRootPath(), 'tmp');
|
||||
if (!Filesystem::safeMkdir($tmpPath, 0755)) {
|
||||
throw new RuntimeException('Не удалось создать директорию "' . $tmpPath . '" на вашем сервере');
|
||||
}
|
||||
|
||||
$moduleVersionsPath = Path::join($tmpPath, 'versions');
|
||||
|
||||
if (!Filesystem::safeMkdir($moduleVersionsPath, 0755)) {
|
||||
throw new RuntimeException('Не удалось создать директорию "' . $moduleVersionsPath . '" на вашем сервере');
|
||||
}
|
||||
$moduleVersionFilePath = Path::join($moduleVersionsPath, $moduleId . '.json');
|
||||
|
||||
if (!Filesystem::remove($moduleVersionFilePath, false)) {
|
||||
throw new RuntimeException('Не удалось удалить файл "' . $moduleVersionFilePath . '" на вашем сервере');
|
||||
}
|
||||
|
||||
$json = json_encode(array('version' => $version));
|
||||
if (!Filesystem::safeMkfile($moduleVersionFilePath, 0755, $json)) {
|
||||
throw new Exception('Не удалось записать служебную информацию в директорию: ' . $moduleVersionFilePath);
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
sendErrorResponse(500, $ex->getMessage());
|
||||
}
|
||||
|
||||
sendSuccessResponse('');
|
||||
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
//todo new_core_after del file
|
||||
|
||||
$selfDir = dirname(__FILE__);
|
||||
require_once $selfDir . '/../internal/helper.php';
|
||||
require_once $selfDir . '/../internal/config.php';
|
||||
|
||||
$res = array();
|
||||
$res['success'] = true;
|
||||
$res['errorID'] = array();
|
||||
$res['messages'] = array();
|
||||
|
||||
$name = $_POST['name'];
|
||||
$email = $_POST['email'];
|
||||
$message = $_POST['message'];
|
||||
$consent = filter_var($_POST['consent'], FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
if($consent != true){
|
||||
$res['success'] = false;
|
||||
$res['messages'][] = 'Не получено согласие на обработку персональных данных.';
|
||||
$res['errorID'][] = 'consent';
|
||||
}
|
||||
|
||||
$code = filter_var($_POST['captcha'], FILTER_SANITIZE_STRING);
|
||||
session_start();
|
||||
if (!isset($_SESSION['captcha']) || strtoupper(trim($_SESSION['captcha'])) != strtoupper(trim($code))) {
|
||||
$res['success'] = false;
|
||||
$res['errorID'][] = 'captcha';
|
||||
$res['messages'][] = 'Неверный код с картинки.';
|
||||
}
|
||||
unset($_SESSION['captcha']);
|
||||
|
||||
if ($res['success']) {
|
||||
$res['success'] = false;
|
||||
|
||||
$headers = array('Accept: application/json');
|
||||
$data = remoteRequest($apiDomen . 'oauth2/ClientCredentials', true,
|
||||
array(
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'grant_type' => 'client_credentials',
|
||||
), $headers
|
||||
);
|
||||
|
||||
if ($data->code == 200) {
|
||||
if (isset($data->responseBody)) {
|
||||
$accessToken = $data->responseBody->access_token;
|
||||
|
||||
try {
|
||||
$data = remoteRequest($apiDomen . 'oauth-via-app/vsoko/feedbackSendMail?access_token='
|
||||
. $accessToken .
|
||||
'&name=' . urlencode($name) .
|
||||
'&email=' . urlencode($email) .
|
||||
'&message=' . urlencode($message)
|
||||
);
|
||||
|
||||
if ($data->code == 200) {
|
||||
$res['success'] = true;
|
||||
} else {
|
||||
sendErrorResponse($data->code, $data->responseBody->message);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$res['message'] = 'Ошибка. '.$e->getMessage();
|
||||
}
|
||||
|
||||
} else {
|
||||
$res['message'] = 'Ошибка ' . $data->responseBody->message;
|
||||
}
|
||||
} else {
|
||||
$res['message'] = $data->responseBody->message;
|
||||
}
|
||||
}
|
||||
|
||||
loadHeaders();
|
||||
|
||||
echo json_encode($res);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 py-8">
|
||||
<div class="max-w-7xl mx-auto px-6">
|
||||
<div class="min-h-screen bg-amber-50/30 py-6 sm:py-8">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<!-- Заголовок страницы -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-2">Панель управления</h1>
|
||||
<p class="text-gray-600">Загрузка и модерация новостей, созданных с помощью AI</p>
|
||||
<div class="mb-6 sm:mb-8">
|
||||
<h1 class="text-2xl sm:text-3xl font-semibold text-stone-800">Панель управления</h1>
|
||||
<p class="mt-1 text-sm text-stone-500">Загрузка и модерация новостей, созданных с помощью AI</p>
|
||||
</div>
|
||||
|
||||
<!-- Уведомления (Flash messages) -->
|
||||
@@ -17,12 +17,12 @@
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-4"
|
||||
>
|
||||
<div v-if="$page.props.flash?.success" class="mb-6 p-4 bg-gradient-to-r from-emerald-50 to-green-50 border-l-4 border-emerald-500 rounded-r-lg shadow-sm">
|
||||
<div v-if="$page.props.flash?.success" class="mb-4 p-4 bg-emerald-50/80 border border-emerald-200 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-emerald-500 mr-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 text-emerald-600 mr-3 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-emerald-800 font-medium">{{ $page.props.flash.success }}</span>
|
||||
<span class="text-sm text-emerald-800 font-medium">{{ $page.props.flash.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -35,12 +35,12 @@
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-4"
|
||||
>
|
||||
<div v-if="$page.props.flash?.error" class="mb-6 p-4 bg-gradient-to-r from-red-50 to-rose-50 border-l-4 border-red-500 rounded-r-lg shadow-sm">
|
||||
<div v-if="$page.props.flash?.error" class="mb-4 p-4 bg-rose-50/80 border border-rose-200 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-red-500 mr-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 text-rose-600 mr-3 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-red-800 font-medium">{{ $page.props.flash.error }}</span>
|
||||
<span class="text-sm text-rose-800 font-medium">{{ $page.props.flash.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -54,53 +54,50 @@
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div v-if="$page.props.flash?.created_post" class="mb-8 bg-gradient-to-br from-indigo-50 via-blue-50 to-purple-50 border border-indigo-200 rounded-2xl p-6 shadow-lg">
|
||||
<div v-if="$page.props.flash?.created_post" class="mb-6 bg-white rounded-lg shadow-sm border border-amber-200 p-5">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-12 h-12 bg-indigo-500 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="w-10 h-10 bg-rose-400/80 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-lg font-bold text-indigo-900 mb-3">Новость успешно создана!</h4>
|
||||
<div class="grid md:grid-cols-2 gap-4 text-sm">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-lg font-medium text-stone-800 mb-3">Новость успешно создана!</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div class="space-y-2">
|
||||
<p class="text-indigo-900"><span class="font-semibold">Заголовок:</span> {{ $page.props.flash.created_post.title }}</p>
|
||||
<p class="text-indigo-900"><span class="font-semibold">Статус:</span>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
|
||||
<p class="text-stone-600"><span class="font-medium">Заголовок:</span> {{ $page.props.flash.created_post.title }}</p>
|
||||
<p class="text-stone-600"><span class="font-medium">Статус:</span>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-amber-100 text-amber-800">
|
||||
{{ $page.props.flash.created_post.status }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="text-indigo-900"><span class="font-semibold">Категория:</span> {{ $page.props.flash.created_post.category?.title || 'Не указана' }}</p>
|
||||
<p class="text-stone-600"><span class="font-medium">Категория:</span> {{ $page.props.flash.created_post.category?.title || 'Не указана' }}</p>
|
||||
</div>
|
||||
<div v-if="$page.props.flash.created_post.preview" class="flex justify-center md:justify-end">
|
||||
<img
|
||||
:src="`/storage/${$page.props.flash.created_post.preview}`"
|
||||
alt="Preview"
|
||||
class="h-32 w-auto rounded-xl shadow-md border-2 border-white object-cover"
|
||||
class="h-28 w-auto rounded-md shadow-sm border border-amber-200 object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-3">
|
||||
<div class="mt-4 flex flex-wrap gap-3">
|
||||
<a
|
||||
:href="`/admin/posts/${$page.props.flash.created_post.id}/edit`"
|
||||
target="_blank" rel="external"
|
||||
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition shadow-md hover:shadow-lg"
|
||||
class="inline-flex items-center px-4 py-2 bg-rose-400/90 text-white text-sm font-medium rounded-md hover:bg-rose-500/90 transition"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Редактировать
|
||||
<svg class="w-3.5 h-3.5 ml-2 text-indigo-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
<button
|
||||
@click.prevent="openPostModal($page.props.flash.created_post)"
|
||||
class="inline-flex items-center px-4 py-2 bg-white text-indigo-600 border border-indigo-200 text-sm font-medium rounded-lg hover:bg-indigo-50 transition shadow-sm hover:shadow-md"
|
||||
class="inline-flex items-center px-4 py-2 bg-white text-rose-500 border border-rose-200 text-sm font-medium rounded-md hover:bg-rose-50 transition"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
@@ -115,33 +112,34 @@
|
||||
</transition>
|
||||
|
||||
<!-- Карточка загрузки файлов -->
|
||||
<div class="bg-white rounded-2xl shadow-xl mb-8 overflow-hidden border border-gray-100">
|
||||
<div class="bg-gradient-to-r from-indigo-600 via-blue-600 to-indigo-700 px-8 py-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6 overflow-hidden border border-amber-200">
|
||||
<div class="bg-gradient-to-r from-amber-50 to-rose-50 px-6 py-5 border-b border-amber-200">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white mb-1">Загрузка файлов</h2>
|
||||
<p class="text-indigo-100 text-sm">
|
||||
<svg class="w-4 h-4 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<h2 class="text-lg font-medium text-stone-800 mb-1">Загрузка файлов</h2>
|
||||
<p class="text-sm text-stone-500 flex items-center flex-wrap gap-2">
|
||||
<svg class="w-4 h-4 inline flex-shrink-0 text-amber-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
DOCX, PDF, XLSX, JPG, PNG, WEBP, ZIP (до 40MB)
|
||||
<span class="whitespace-nowrap text-stone-600">DOCX, PDF, XLSX, JPG, PNG, WEBP, ZIP</span>
|
||||
<span class="hidden sm:inline text-stone-400">(до 40MB)</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="openUploadModal"
|
||||
class="group inline-flex items-center px-6 py-3.5 bg-white text-indigo-600 font-semibold rounded-xl hover:bg-indigo-50 transition-all shadow-lg hover:shadow-xl hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-white"
|
||||
class="w-full sm:w-auto inline-flex items-center justify-center px-5 py-2.5 bg-rose-400 text-white font-medium rounded-md hover:bg-rose-500 transition text-sm shadow-sm"
|
||||
>
|
||||
<svg class="w-5 h-5 mr-2 group-hover:animate-bounce" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
Загрузить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-8 py-4 bg-gray-50 border-t border-gray-100">
|
||||
<p class="text-sm text-gray-600">
|
||||
<span class="font-semibold text-indigo-600">💡 AI-функции:</span>
|
||||
Автоматическое определение текста новости • Распаковка ZIP-архивов (до 40MB) • Извлечение изображений
|
||||
<div class="px-6 py-3 bg-amber-50/50 border-t border-amber-100">
|
||||
<p class="text-sm text-stone-500">
|
||||
<span class="font-medium text-rose-500">AI-функции:</span>
|
||||
<span class="hidden sm:inline">Автоматическое определение текста новости • </span>Распаковка ZIP-архивов (до 40MB) • Извлечение изображений
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,22 +155,22 @@
|
||||
>
|
||||
<div
|
||||
v-if="isUploadModalOpen"
|
||||
class="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
class="fixed inset-0 bg-stone-900/40 backdrop-blur-[2px] z-50 flex items-center justify-center p-4"
|
||||
@click.self="closeUploadModal"
|
||||
>
|
||||
<div class="bg-white rounded-2xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-hidden transform transition-all flex flex-col">
|
||||
<div class="sticky top-0 bg-gradient-to-r from-indigo-600 via-blue-600 to-indigo-700 px-8 py-5 flex items-center justify-between rounded-t-2xl flex-shrink-0">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div class="bg-gradient-to-r from-amber-50 to-rose-50 px-6 py-4 flex items-center justify-between border-b border-amber-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white bg-opacity-20 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="w-10 h-10 bg-rose-100 rounded-md flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-white">Загрузка файлов</h3>
|
||||
<h3 class="text-lg font-medium text-stone-700">Загрузка файлов</h3>
|
||||
</div>
|
||||
<button
|
||||
@click="closeUploadModal"
|
||||
class="text-white hover:text-indigo-100 transition bg-white bg-opacity-10 hover:bg-opacity-20 rounded-lg p-2"
|
||||
class="text-stone-400 hover:text-stone-600 transition bg-white hover:bg-amber-50 rounded-md p-2 border border-transparent hover:border-amber-200"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -181,7 +179,7 @@
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto flex-1">
|
||||
<form @submit.prevent="submitForm" class="p-8 space-y-6">
|
||||
<form @submit.prevent="submitForm" class="p-6 space-y-5">
|
||||
<!-- Зона Drag & Drop -->
|
||||
<div
|
||||
@dragover.prevent="isDragging = true"
|
||||
@@ -189,42 +187,42 @@
|
||||
@drop.prevent="handleDrop"
|
||||
@click="triggerFileInput"
|
||||
:class="[
|
||||
'relative mt-1 flex justify-center px-6 pt-10 pb-12 border-2 border-dashed rounded-2xl cursor-pointer transition-all duration-300',
|
||||
'relative flex justify-center px-6 pt-8 pb-10 border-2 border-dashed rounded-lg cursor-pointer transition-all',
|
||||
isDragging
|
||||
? 'border-indigo-500 bg-indigo-50 scale-[1.02] shadow-inner'
|
||||
: 'border-gray-300 hover:border-indigo-400 hover:bg-gradient-to-br hover:from-gray-50 hover:to-indigo-50'
|
||||
? 'border-rose-400 bg-rose-50'
|
||||
: 'border-amber-200 hover:border-rose-300 hover:bg-amber-50/50'
|
||||
]"
|
||||
>
|
||||
<div class="space-y-4 text-center">
|
||||
<div class="flex justify-center">
|
||||
<div :class="['w-20 h-20 rounded-2xl flex items-center justify-center transition-all duration-300', isDragging ? 'bg-indigo-200 scale-110' : 'bg-indigo-100']">
|
||||
<svg class="w-10 h-10 text-indigo-600" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<div :class="['w-16 h-16 rounded-lg flex items-center justify-center transition-all', isDragging ? 'bg-rose-200' : 'bg-amber-100']">
|
||||
<svg class="w-8 h-8 text-rose-500" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-semibold text-gray-700">
|
||||
<span class="text-indigo-600">Нажмите для выбора</span> или перетащите файлы
|
||||
<p class="text-base font-medium text-stone-700">
|
||||
<span class="text-rose-500">Нажмите для выбора</span> или перетащите файлы
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
<p class="text-sm text-stone-500 mt-1">
|
||||
DOCX, PDF, XLSX, JPG, PNG, WEBP, ZIP
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-center gap-4 text-xs">
|
||||
<span class="inline-flex items-center px-3 py-1.5 bg-indigo-50 text-indigo-700 rounded-full font-medium">
|
||||
<div class="flex flex-wrap justify-center gap-2 text-xs">
|
||||
<span class="inline-flex items-center px-3 py-1 bg-amber-100 text-amber-700 rounded-full font-medium">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Авто-распаковка ZIP
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1.5 bg-green-50 text-green-700 rounded-full font-medium">
|
||||
<span class="inline-flex items-center px-3 py-1 bg-emerald-50 text-emerald-700 rounded-full font-medium">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Извлечение файлов
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1.5 bg-purple-50 text-purple-700 rounded-full font-medium">
|
||||
<span class="inline-flex items-center px-3 py-1 bg-stone-100 text-stone-600 rounded-full font-medium">
|
||||
до 40MB
|
||||
</span>
|
||||
</div>
|
||||
@@ -243,13 +241,13 @@
|
||||
/>
|
||||
|
||||
<!-- Ошибки валидации -->
|
||||
<div v-if="hasFileErrors" class="p-4 bg-red-50 border border-red-200 rounded-xl">
|
||||
<div v-if="hasFileErrors" class="p-4 bg-rose-50/60 border border-rose-200 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 text-rose-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div class="text-sm text-red-700">
|
||||
<p v-if="form.errors.files" class="font-semibold mb-1">{{ form.errors.files }}</p>
|
||||
<div class="text-sm text-rose-700">
|
||||
<p v-if="form.errors.files" class="font-medium mb-1">{{ form.errors.files }}</p>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li v-for="(error, key) in fileSpecificErrors" :key="key">
|
||||
{{ error }}
|
||||
@@ -260,42 +258,42 @@
|
||||
</div>
|
||||
|
||||
<!-- Индикатор распаковки ZIP -->
|
||||
<div v-if="isUnzipping" class="p-4 bg-gradient-to-r from-indigo-50 via-purple-50 to-pink-50 border border-indigo-200 rounded-xl">
|
||||
<div v-if="isUnzipping" class="p-4 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<svg class="animate-spin h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24">
|
||||
<svg class="animate-spin h-5 w-5 text-rose-500" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span class="text-sm font-semibold text-indigo-900">Распаковка архива...</span>
|
||||
<span class="text-sm font-medium text-stone-700">Распаковка архива...</span>
|
||||
</div>
|
||||
<div class="w-full bg-white rounded-full h-3 overflow-hidden shadow-inner">
|
||||
<div class="w-full bg-amber-100 rounded-full h-2.5 overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 rounded-full transition-all duration-300 ease-out"
|
||||
class="h-full bg-gradient-to-r from-rose-400 to-amber-400 rounded-full transition-all duration-300 ease-out"
|
||||
:style="{ width: unzippingProgress + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs text-indigo-700 mt-2 text-center">{{ unzippingProgress }}% завершено</p>
|
||||
<p class="text-xs text-stone-500 mt-2 text-center">{{ unzippingProgress }}% завершено</p>
|
||||
</div>
|
||||
|
||||
<!-- Список выбранных файлов -->
|
||||
<div v-if="form.files.length > 0" class="space-y-6">
|
||||
<h4 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<h4 class="text-sm font-medium text-stone-700 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Выбрано файлов: {{ form.files.length }}
|
||||
</h4>
|
||||
|
||||
<!-- Секция изображений (с drag-and-drop) -->
|
||||
<div v-if="imageFiles.length > 0" class="space-y-2">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div v-if="imageFiles.length > 0" class="space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 bg-rose-100 rounded-md flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h5 class="text-base font-bold text-gray-800">Изображения <span class="text-xs font-normal text-gray-500">(перетаскивайте для изменения порядка)</span></h5>
|
||||
<span class="text-xs font-semibold text-indigo-600 bg-indigo-50 px-2 py-1 rounded-full">{{ imageFiles.length }}</span>
|
||||
<h5 class="text-sm font-medium text-stone-700">Изображения <span class="text-xs font-normal text-stone-500">(перетаскивайте для изменения порядка)</span></h5>
|
||||
<span class="text-xs font-medium text-rose-600 bg-rose-50 px-2 py-1 rounded-full">{{ imageFiles.length }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
||||
<div
|
||||
@@ -307,25 +305,22 @@
|
||||
@dragleave="handleImageDragLeave"
|
||||
@drop="handleImageDrop($event, newIndex)"
|
||||
@dragend="handleImageDragEnd"
|
||||
class="group relative bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-lg hover:border-indigo-300 transition-all duration-200 overflow-hidden cursor-move"
|
||||
class="group relative bg-white border border-amber-200 rounded-md shadow-sm hover:shadow-md hover:border-rose-300 transition-all overflow-hidden cursor-move"
|
||||
>
|
||||
<!-- Миниатюра изображения -->
|
||||
<div class="aspect-square overflow-hidden">
|
||||
<img
|
||||
:src="imagePreviews[item.originalIndex]"
|
||||
alt="Preview"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-200"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Градиентная полоска снизу -->
|
||||
<div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"></div>
|
||||
|
||||
<!-- Информация при наведении -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-40 transition-all duration-200 flex items-center justify-center opacity-0 group-hover:opacity-100">
|
||||
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-200 flex items-center justify-center opacity-0 group-hover:opacity-100">
|
||||
<div class="text-center p-2">
|
||||
<p class="text-white text-xs font-medium truncate max-w-[120px]">{{ item.file.name }}</p>
|
||||
<p class="text-indigo-200 text-[10px] mt-0.5">{{ (item.file.size / 1024 / 1024).toFixed(2) }} MB</p>
|
||||
<p class="text-amber-100 text-[10px] mt-0.5">{{ (item.file.size / 1024 / 1024).toFixed(2) }} MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -333,22 +328,15 @@
|
||||
<button
|
||||
type="button"
|
||||
@click.prevent="removeFile(item.originalIndex)"
|
||||
class="absolute top-2 right-2 p-1.5 bg-red-500 text-white rounded-lg opacity-0 group-hover:opacity-100 hover:bg-red-600 transition-all duration-200 shadow-lg z-10"
|
||||
class="absolute top-2 right-2 p-1.5 bg-rose-500 text-white rounded-md opacity-0 group-hover:opacity-100 hover:bg-rose-600 transition-all shadow-sm z-10"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Индикатор drag-and-drop -->
|
||||
<div class="absolute top-2 left-2 p-1.5 bg-white bg-opacity-90 rounded-lg cursor-move opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-opacity-100 z-10">
|
||||
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Бейдж порядка -->
|
||||
<div class="absolute top-2 right-2 w-6 h-6 bg-white bg-opacity-90 rounded-full flex items-center justify-center text-xs font-bold text-indigo-600 shadow-md">
|
||||
<div class="absolute top-2 left-2 w-6 h-6 bg-white bg-opacity-90 rounded-full flex items-center justify-center text-xs font-medium text-rose-500 shadow-sm">
|
||||
{{ newIndex + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -357,39 +345,37 @@
|
||||
|
||||
<!-- Секция остальных файлов -->
|
||||
<div v-if="otherFiles.length > 0" class="space-y-2">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-8 h-8 bg-gradient-to-br from-slate-500 to-gray-600 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 bg-amber-100 rounded-md flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h5 class="text-base font-bold text-gray-800">Файлы</h5>
|
||||
<span class="text-xs font-semibold text-slate-600 bg-slate-50 px-2 py-1 rounded-full">{{ otherFiles.length }}</span>
|
||||
<h5 class="text-sm font-medium text-stone-700">Файлы</h5>
|
||||
<span class="text-xs font-medium text-amber-700 bg-amber-50 px-2 py-1 rounded-full">{{ otherFiles.length }}</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item) in otherFiles"
|
||||
:key="item.originalIndex"
|
||||
class="group flex items-center gap-3 p-3 bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-md hover:border-slate-300 transition-all"
|
||||
class="group flex items-center gap-3 p-3 bg-white border border-amber-100 rounded-md shadow-sm hover:shadow-md hover:border-amber-300 transition-all"
|
||||
>
|
||||
<!-- Иконка файла -->
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-14 h-14 rounded-lg bg-gradient-to-br from-slate-100 to-gray-100 flex items-center justify-center border border-slate-200"
|
||||
>
|
||||
<svg class="w-7 h-7 text-slate-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z" clip-rule="evenodd" />
|
||||
<div class="w-12 h-12 rounded-md bg-amber-50 flex items-center justify-center border border-amber-200">
|
||||
<svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информация о файле -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-gray-800 font-medium truncate text-sm">{{ item.file.name }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">
|
||||
<p class="text-stone-700 font-medium truncate text-sm">{{ item.file.name }}</p>
|
||||
<p class="text-xs text-stone-500 mt-0.5">
|
||||
{{ (item.file.size / 1024 / 1024).toFixed(2) }} MB
|
||||
</p>
|
||||
<p class="text-xs text-slate-600 font-medium mt-1">
|
||||
<p class="text-xs text-stone-600 font-medium mt-1">
|
||||
{{ getFileTypeIcon(item.file.type) }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -398,9 +384,9 @@
|
||||
<button
|
||||
type="button"
|
||||
@click.prevent="removeFile(item.originalIndex)"
|
||||
class="flex-shrink-0 p-2 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition"
|
||||
class="flex-shrink-0 p-2 text-rose-500 hover:text-rose-700 hover:bg-rose-50 rounded-md transition"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -412,12 +398,12 @@
|
||||
<!-- Прогресс-бар загрузки -->
|
||||
<div v-if="form.progress" class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="font-medium text-gray-700">Загрузка...</span>
|
||||
<span class="text-indigo-600 font-semibold">{{ form.progress.percentage }}%</span>
|
||||
<span class="font-medium text-stone-600">Загрузка...</span>
|
||||
<span class="text-rose-500 font-medium">{{ form.progress.percentage }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-100 rounded-full h-3 overflow-hidden shadow-inner">
|
||||
<div class="w-full bg-amber-100 rounded-full h-2.5 overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-gradient-to-r from-indigo-500 via-blue-500 to-indigo-600 rounded-full transition-all duration-300 ease-out relative overflow-hidden"
|
||||
class="h-full bg-gradient-to-r from-rose-400 to-amber-400 rounded-full transition-all duration-300 ease-out relative overflow-hidden"
|
||||
:style="{ width: form.progress.percentage + '%' }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-white bg-opacity-20 animate-pulse"></div>
|
||||
@@ -429,14 +415,14 @@
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing || form.files.length === 0"
|
||||
class="w-full py-4 px-6 border border-transparent rounded-xl shadow-lg text-base font-semibold text-white bg-gradient-to-r from-indigo-600 via-blue-600 to-indigo-700 hover:from-indigo-700 hover:via-blue-700 hover:to-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:shadow-xl hover:scale-[1.02] disabled:hover:scale-100"
|
||||
class="w-full py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-rose-400 to-amber-400 hover:from-rose-500 hover:to-amber-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-300 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
<span v-if="form.processing" class="flex items-center justify-center gap-2">
|
||||
<svg class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Обработка ({{ form.progress ? form.progress.percentage : 0 }}%)...
|
||||
<span>Обработка ({{ form.progress ? form.progress.percentage : 0 }}%)...</span>
|
||||
</span>
|
||||
<span v-else class="flex items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -460,159 +446,155 @@
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 translate-y-8"
|
||||
>
|
||||
<div v-if="aiPreparedPosts && aiPreparedPosts.length > 0" class="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100">
|
||||
<div v-if="aiPreparedPosts && aiPreparedPosts.length > 0" class="bg-white rounded-lg shadow-sm overflow-hidden border border-amber-200 mb-6">
|
||||
<!-- Заголовок секции -->
|
||||
<div class="bg-gradient-to-r from-violet-600 via-purple-600 to-indigo-600 px-8 py-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 bg-white bg-opacity-20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
|
||||
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="bg-gradient-to-r from-amber-50 to-rose-50 px-6 py-4 border-b border-amber-200">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-rose-100 rounded-md flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-5 h-5 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white flex items-center gap-3">
|
||||
Подготовленные новости (AI)
|
||||
<span class="inline-flex items-center px-3 py-1 bg-white bg-opacity-20 backdrop-blur-sm rounded-full text-sm font-semibold">
|
||||
{{ aiPreparedPosts.length }}
|
||||
</span>
|
||||
</h2>
|
||||
<p class="text-purple-100 text-sm mt-1">
|
||||
<h2 class="text-base font-medium text-stone-700">Подготовленные новости (AI)</h2>
|
||||
<p class="text-stone-500 text-sm mt-0.5">
|
||||
Новости, автоматически сгенерированные из загруженных файлов
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden lg:flex items-center gap-2 px-4 py-2 bg-white bg-opacity-10 backdrop-blur-sm rounded-xl">
|
||||
<svg class="w-5 h-5 text-purple-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="hidden lg:flex items-center gap-2 px-3 py-1.5 bg-rose-50 border border-rose-100 rounded-md">
|
||||
<svg class="w-4 h-4 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-purple-100 text-sm font-medium">Требуют модерации</span>
|
||||
<span class="text-rose-600 text-sm font-medium">Требуют модерации</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Список постов -->
|
||||
<div class="p-6 bg-gradient-to-br from-gray-50 via-white to-purple-50">
|
||||
<div class="grid gap-4">
|
||||
<div class="p-4 bg-amber-50/20">
|
||||
<div class="grid gap-3">
|
||||
<div
|
||||
v-for="(post, index) in aiPreparedPosts"
|
||||
:key="post.id"
|
||||
class="group bg-white rounded-xl p-5 border border-gray-200 hover:border-purple-300 hover:shadow-lg hover:shadow-purple-100 transition-all duration-300 cursor-pointer transform hover:-translate-y-0.5"
|
||||
class="group bg-white rounded-md p-4 border border-amber-100 hover:border-rose-200 hover:shadow-sm transition-all cursor-pointer w-full overflow-hidden"
|
||||
@click="openPostModal(post)"
|
||||
:style="{ animationDelay: `${index * 50}ms` }"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<div class="w-full overflow-hidden">
|
||||
<!-- Заголовок и бейджи -->
|
||||
<div class="flex items-start justify-between gap-3 mb-3">
|
||||
<h3 class="text-lg font-bold text-gray-900 group-hover:text-purple-700 transition-colors line-clamp-2">
|
||||
<div class="flex flex-col gap-2 mb-3">
|
||||
<h3 class="text-sm font-medium text-stone-800 group-hover:text-rose-700 transition-colors line-clamp-2 break-words">
|
||||
{{ post.title }}
|
||||
</h3>
|
||||
<!-- Статус поста -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-if="post.status"
|
||||
class="flex-shrink-0 inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wide"
|
||||
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium uppercase tracking-wide"
|
||||
:class="{
|
||||
'bg-gradient-to-r from-amber-100 to-yellow-100 text-amber-800 border border-amber-200': post.status === 'verification',
|
||||
'bg-gradient-to-r from-red-100 to-rose-100 text-red-800 border border-red-200': post.status === 'rejected',
|
||||
'bg-gradient-to-r from-green-100 to-emerald-100 text-green-800 border border-green-200': post.status === 'published'
|
||||
'bg-amber-50 text-amber-700 border border-amber-200': post.status === 'verification',
|
||||
'bg-rose-50 text-rose-700 border border-rose-200': post.status === 'rejected',
|
||||
'bg-emerald-50 text-emerald-700 border border-emerald-200': post.status === 'published'
|
||||
}"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full mr-2" :class="{
|
||||
<span class="w-1.5 h-1.5 rounded-full mr-2" :class="{
|
||||
'bg-amber-500': post.status === 'verification',
|
||||
'bg-red-500': post.status === 'rejected',
|
||||
'bg-green-500': post.status === 'published'
|
||||
'bg-rose-500': post.status === 'rejected',
|
||||
'bg-emerald-500': post.status === 'published'
|
||||
}"></span>
|
||||
{{ post.status === 'verification' ? 'На рассмотрении' : post.status === 'rejected' ? 'Отклонено' : post.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Мета информация -->
|
||||
<div class="flex flex-wrap items-center gap-2 mb-3">
|
||||
<span class="inline-flex items-center px-2.5 py-1 bg-gray-100 text-gray-600 rounded-md text-xs font-medium">
|
||||
<span class="inline-flex items-center px-2 py-1 bg-stone-100 text-stone-600 rounded-md text-xs font-medium">
|
||||
ID: {{ post.id }}
|
||||
</span>
|
||||
<span v-if="post.category" class="inline-flex items-center px-2.5 py-1 bg-gradient-to-r from-indigo-50 to-blue-50 text-indigo-700 rounded-md text-xs font-medium border border-indigo-100">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<span v-if="post.category" class="inline-flex items-center px-2 py-1 bg-amber-50 text-amber-700 rounded-md text-xs font-medium border border-amber-100">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
{{ post.category.title }}
|
||||
<span class="truncate max-w-[120px] sm:max-w-none">{{ post.category.title }}</span>
|
||||
</span>
|
||||
<span v-if="post.authors" class="inline-flex items-center px-2.5 py-1 bg-gray-100 text-gray-600 rounded-md text-xs font-medium">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<span v-if="post.authors" class="inline-flex items-center px-2 py-1 bg-stone-100 text-stone-600 rounded-md text-xs font-medium">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
{{ Array.isArray(post.authors) ? post.authors.join(', ') : post.authors }}
|
||||
<span class="truncate max-w-[150px] sm:max-w-none">{{ Array.isArray(post.authors) ? post.authors.join(', ') : post.authors }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Превью текста -->
|
||||
<p v-if="post.preview_text" class="text-sm text-gray-600 line-clamp-2 leading-relaxed">
|
||||
<p v-if="post.preview_text" class="text-sm text-stone-600 line-clamp-2 leading-relaxed break-words">
|
||||
{{ post.preview_text }}
|
||||
</p>
|
||||
|
||||
<!-- Даты -->
|
||||
<div class="mt-3 flex items-center gap-4 text-xs text-gray-500">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="mt-3 flex flex-wrap items-center gap-3 text-xs text-stone-500 overflow-hidden">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{{ new Date(post.created_at).toLocaleDateString('ru-RU') }}
|
||||
<span class="truncate">{{ new Date(post.created_at).toLocaleDateString('ru-RU') }}</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{{ new Date(post.updated_at).toLocaleDateString('ru-RU') }}
|
||||
<span class="truncate">{{ new Date(post.updated_at).toLocaleDateString('ru-RU') }}</span>
|
||||
</span>
|
||||
<span v-if="post.reading_time" class="flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<span v-if="post.reading_time" class="flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
{{ post.reading_time }} мин.
|
||||
<span>{{ post.reading_time }} мин.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель действий -->
|
||||
<div class="flex-shrink-0 flex flex-col items-end gap-3">
|
||||
<div class="w-full border-t border-amber-100 pt-3 mt-2">
|
||||
<div class="flex flex-row items-center justify-between gap-2">
|
||||
<!-- Переключатель VK -->
|
||||
<label class="flex items-center gap-2.5 cursor-pointer group/toggle" @click.stop>
|
||||
<label class="flex items-center gap-2 cursor-pointer" @click.stop>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="postPublishSettings[post.id]"
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-purple-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gradient-to-r peer-checked:from-indigo-500 peer-checked:to-purple-600"></div>
|
||||
<div class="w-9 h-5 bg-stone-300 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-rose-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-stone-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-rose-400"></div>
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-gray-600 group-hover/toggle:text-purple-600 transition-colors flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<span class="text-xs font-medium text-stone-600 hover:text-rose-600 transition-colors flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5" :class="postPublishSettings[post.id] ? 'text-rose-500' : 'text-stone-400'" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12c0 5.523 4.477 10 10 10s10-4.477 10-10c0-5.523-4.477-10-10-10zm0 18c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8z"/>
|
||||
</svg>
|
||||
VK
|
||||
<span class="hidden sm:inline">VK</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<button
|
||||
@click.stop="publishPost(post)"
|
||||
:disabled="publishProcessing"
|
||||
class="group/btn inline-flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-green-500 to-emerald-600 text-white text-sm font-semibold rounded-lg hover:from-green-600 hover:to-emerald-700 transition-all shadow-md hover:shadow-lg hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
class="inline-flex items-center justify-center px-3 py-1.5 bg-emerald-500 text-white text-xs font-medium rounded-md hover:bg-emerald-600 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg v-if="!publishProcessing" class="w-4 h-4 mr-2 group-hover/btn:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-if="!publishProcessing" class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<svg v-else class="animate-spin h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24">
|
||||
<svg v-else class="animate-spin h-3.5 w-3.5 mr-1.5" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ publishProcessing ? '...' : 'Опубликовать' }}
|
||||
<span class="hidden sm:inline">{{ publishProcessing ? '...' : 'ОК' }}</span>
|
||||
</button>
|
||||
<span class="text-purple-600 group-hover:text-purple-800 text-xs font-semibold transition-colors flex items-center gap-1 justify-center">
|
||||
Подробнее
|
||||
<svg class="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<span class="text-rose-400 hover:text-rose-600 text-xs font-medium transition-colors flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5 hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</span>
|
||||
@@ -623,6 +605,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Модальное окно просмотра новости -->
|
||||
@@ -636,23 +619,23 @@
|
||||
>
|
||||
<div
|
||||
v-if="selectedPost"
|
||||
class="fixed inset-0 bg-black bg-opacity-70 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
class="fixed inset-0 bg-stone-900/40 backdrop-blur-[2px] z-50 flex items-center justify-center p-4"
|
||||
@click.self="closePostModal"
|
||||
>
|
||||
<div class="bg-white rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col transform transition-all animate-fade-in">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<!-- Заголовок -->
|
||||
<div class="sticky top-0 bg-gradient-to-r from-slate-700 via-gray-700 to-slate-800 px-8 py-5 flex items-center justify-between rounded-t-2xl">
|
||||
<div class="flex items-center gap-4 flex-1 min-w-0">
|
||||
<div class="w-10 h-10 bg-white bg-opacity-10 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="bg-gradient-to-r from-amber-50 to-rose-50 px-6 py-4 flex items-center justify-between border-b border-amber-200">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div class="w-9 h-9 bg-rose-100 rounded-md flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-5 h-5 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-white truncate">{{ selectedPost.title }}</h3>
|
||||
<h3 class="text-base font-medium text-stone-700 truncate">{{ selectedPost.title }}</h3>
|
||||
</div>
|
||||
<button
|
||||
@click="closePostModal"
|
||||
class="ml-4 text-gray-300 hover:text-white transition bg-white bg-opacity-10 hover:bg-opacity-20 rounded-lg p-2"
|
||||
class="ml-4 text-stone-400 hover:text-stone-600 transition bg-white hover:bg-amber-50 rounded-md p-2 border border-transparent hover:border-amber-200"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -661,13 +644,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Контент -->
|
||||
<div class="p-8 overflow-y-auto flex-1">
|
||||
<div class="p-6 overflow-y-auto flex-1">
|
||||
<!-- Мета информация -->
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
<span class="inline-flex items-center px-3 py-1.5 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium">
|
||||
<div class="flex flex-wrap gap-2 mb-5">
|
||||
<span class="inline-flex items-center px-2.5 py-1 bg-stone-100 text-stone-700 rounded-md text-sm font-medium">
|
||||
ID: {{ selectedPost.id }}
|
||||
</span>
|
||||
<span v-if="selectedPost.category" class="inline-flex items-center px-3 py-1.5 bg-gradient-to-r from-indigo-50 to-blue-50 text-indigo-700 rounded-lg text-sm font-medium border border-indigo-100">
|
||||
<span v-if="selectedPost.category" class="inline-flex items-center px-2.5 py-1 bg-amber-50 text-amber-700 rounded-md text-sm font-medium border border-amber-100">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
@@ -675,58 +658,58 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="selectedPost.status"
|
||||
class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-bold uppercase tracking-wide"
|
||||
class="inline-flex items-center px-2.5 py-1 rounded-md text-sm font-medium uppercase tracking-wide"
|
||||
:class="{
|
||||
'bg-gradient-to-r from-amber-100 to-yellow-100 text-amber-800 border border-amber-200': selectedPost.status === 'verification',
|
||||
'bg-gradient-to-r from-red-100 to-rose-100 text-red-800 border border-red-200': selectedPost.status === 'rejected',
|
||||
'bg-gradient-to-r from-green-100 to-emerald-100 text-green-800 border border-green-200': selectedPost.status === 'published'
|
||||
'bg-amber-50 text-amber-700 border border-amber-200': selectedPost.status === 'verification',
|
||||
'bg-rose-50 text-rose-700 border border-rose-200': selectedPost.status === 'rejected',
|
||||
'bg-emerald-50 text-emerald-700 border border-emerald-200': selectedPost.status === 'published'
|
||||
}"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full mr-2" :class="{
|
||||
<span class="w-1.5 h-1.5 rounded-full mr-2" :class="{
|
||||
'bg-amber-500': selectedPost.status === 'verification',
|
||||
'bg-red-500': selectedPost.status === 'rejected',
|
||||
'bg-green-500': selectedPost.status === 'published'
|
||||
'bg-rose-500': selectedPost.status === 'rejected',
|
||||
'bg-emerald-500': selectedPost.status === 'published'
|
||||
}"></span>
|
||||
{{ selectedPost.status === 'verification' ? 'На рассмотрении' : selectedPost.status === 'rejected' ? 'Отклонено' : selectedPost.status }}
|
||||
<span>{{ selectedPost.status === 'verification' ? 'На рассмотрении' : selectedPost.status === 'rejected' ? 'Отклонено' : selectedPost.status }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Главное изображение (Preview) -->
|
||||
<div v-if="selectedPost.preview" class="mb-6">
|
||||
<p class="text-sm font-bold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div v-if="selectedPost.preview" class="mb-5">
|
||||
<p class="text-sm font-medium text-stone-700 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Главное изображение
|
||||
</p>
|
||||
<div class="rounded-xl overflow-hidden shadow-lg border-2 border-gray-100">
|
||||
<div class="rounded-md overflow-hidden shadow-sm border border-amber-200">
|
||||
<img
|
||||
:src="`/storage/${selectedPost.preview}`"
|
||||
alt="Preview"
|
||||
class="w-full h-auto max-h-96 object-cover"
|
||||
class="w-full h-auto max-h-80 object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Галерея изображений -->
|
||||
<div v-if="selectedPost.images && selectedPost.images.length > 0" class="mb-6">
|
||||
<p class="text-sm font-bold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div v-if="selectedPost.images && selectedPost.images.length > 0" class="mb-5">
|
||||
<p class="text-sm font-medium text-stone-700 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Галерея ({{ selectedPost.images.length }})
|
||||
</p>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
||||
<div
|
||||
v-for="(image, index) in selectedPost.images"
|
||||
:key="index"
|
||||
class="group relative rounded-xl overflow-hidden border-2 border-gray-100 shadow-md aspect-square cursor-pointer"
|
||||
@click="viewImage(image)"
|
||||
class="group relative rounded-md overflow-hidden border border-amber-200 shadow-sm aspect-square cursor-pointer"
|
||||
>
|
||||
<img
|
||||
:src="`/storage/${image}`"
|
||||
alt="Gallery image {{ index + 1 }}"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
@click="viewImage(image)"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-all"></div>
|
||||
</div>
|
||||
@@ -734,62 +717,62 @@
|
||||
</div>
|
||||
|
||||
<!-- Авторы -->
|
||||
<div v-if="selectedPost.authors" class="mb-6 p-4 bg-gradient-to-br from-gray-50 to-slate-50 rounded-xl border border-gray-200">
|
||||
<p class="text-sm font-bold text-gray-700 mb-2 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div v-if="selectedPost.authors" class="mb-6 p-4 bg-amber-50 rounded-md border border-amber-200">
|
||||
<p class="text-sm font-medium text-stone-700 mb-2 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
Автор(ы):
|
||||
</p>
|
||||
<p class="text-gray-700">
|
||||
<p class="text-stone-600">
|
||||
{{ Array.isArray(selectedPost.authors) ? selectedPost.authors.join(', ') : selectedPost.authors }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Превью -->
|
||||
<div v-if="selectedPost.preview_text" class="mb-6">
|
||||
<p class="text-sm font-bold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<p class="text-sm font-medium text-stone-700 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
Превью:
|
||||
</p>
|
||||
<p class="text-gray-700 leading-relaxed">{{ selectedPost.preview_text }}</p>
|
||||
<p class="text-stone-600 leading-relaxed">{{ selectedPost.preview_text }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Полный контент -->
|
||||
<div class="mb-6">
|
||||
<p class="text-sm font-bold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<p class="text-sm font-medium text-stone-700 mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Полный текст:
|
||||
</p>
|
||||
<div class="prose prose-sm max-w-none bg-gradient-to-br from-gray-50 to-slate-50 p-5 rounded-xl border border-gray-200">
|
||||
<div v-html="renderContent(selectedPost.content)" class="text-gray-700 leading-relaxed"></div>
|
||||
<div class="prose prose-sm max-w-none bg-amber-50 p-5 rounded-md border border-amber-200">
|
||||
<div v-html="renderContent(selectedPost.content)" class="text-stone-700 leading-relaxed"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительная информация -->
|
||||
<div class="border-t border-gray-200 pt-4">
|
||||
<div class="flex flex-wrap justify-between gap-4 text-xs text-gray-500">
|
||||
<div class="border-t border-amber-200 pt-4">
|
||||
<div class="flex flex-wrap justify-between gap-4 text-xs text-stone-500">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Создано: {{ new Date(selectedPost.created_at).toLocaleString('ru-RU') }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Обновлено: {{ new Date(selectedPost.updated_at).toLocaleString('ru-RU') }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="selectedPost.reading_time" class="flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
{{ selectedPost.reading_time }} мин.
|
||||
@@ -799,20 +782,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<div class="sticky bottom-0 bg-gradient-to-r from-gray-50 via-slate-50 to-gray-50 border-t border-gray-200 px-8 py-5 flex justify-between items-center rounded-b-2xl">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="bg-gradient-to-r from-amber-50 to-rose-50 border-t border-amber-200 px-6 py-4 flex flex-col sm:flex-row justify-between items-stretch sm:items-center gap-3">
|
||||
<div class="flex items-center gap-4 flex-wrap">
|
||||
<!-- Переключатель публикации в VK -->
|
||||
<button
|
||||
type="button"
|
||||
@click.stop="postPublishSettings[selectedPost?.id] = !postPublishSettings[selectedPost?.id]"
|
||||
class="flex items-center gap-3 cursor-pointer focus:outline-none group"
|
||||
class="flex items-center gap-3 cursor-pointer focus:outline-none"
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="w-12 h-7 bg-gray-200 rounded-full transition-colors group-hover:bg-gray-300" :class="postPublishSettings[selectedPost?.id] ? 'bg-gradient-to-r from-indigo-500 to-purple-600 group-hover:from-indigo-600 group-hover:to-purple-700' : ''"></div>
|
||||
<div class="absolute top-1 left-1 w-5 h-5 bg-white rounded-full transition-transform shadow-md" :class="postPublishSettings[selectedPost?.id] ? 'translate-x-5' : 'translate-x-0'"></div>
|
||||
<div class="w-11 h-6 bg-stone-200 rounded-full transition-colors" :class="postPublishSettings[selectedPost?.id] ? 'bg-rose-400' : 'bg-stone-300'"></div>
|
||||
<div class="absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform shadow-sm" :class="postPublishSettings[selectedPost?.id] ? 'translate-x-5' : 'translate-x-0'"></div>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-700 group-hover:text-purple-600 transition-colors flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<span class="text-sm font-medium text-stone-700 hover:text-rose-600 transition-colors flex items-center gap-2">
|
||||
<svg class="w-4 h-4" :class="postPublishSettings[selectedPost?.id] ? 'text-rose-500' : 'text-stone-400'" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12c0 5.523 4.477 10 10 10s10-4.477 10-10c0-5.523-4.477-10-10-10zm0 18c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8z"/>
|
||||
</svg>
|
||||
Опубликовать в VK
|
||||
@@ -824,7 +807,7 @@
|
||||
v-if="!selectedPost.publish_at"
|
||||
@click="publishPost(selectedPost)"
|
||||
:disabled="publishProcessing"
|
||||
class="inline-flex items-center px-5 py-2.5 bg-gradient-to-r from-green-500 to-emerald-600 text-white text-sm font-semibold rounded-lg hover:from-green-600 hover:to-emerald-700 transition-all shadow-md hover:shadow-lg hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
class="inline-flex items-center px-4 py-2 bg-emerald-500 text-white text-sm font-medium rounded-md hover:bg-emerald-600 transition disabled:opacity-50 disabled:cursor-not-allowed flex-1 sm:flex-none justify-center"
|
||||
>
|
||||
<svg v-if="!publishProcessing" class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
@@ -833,13 +816,13 @@
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ publishProcessing ? 'Публикация...' : 'Опубликовать' }}
|
||||
<span>{{ publishProcessing ? 'Публикация...' : 'Опубликовать' }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Кнопка "Редактировать в админке" -->
|
||||
<a
|
||||
:href="`/admin/posts/${selectedPost.id}/edit`"
|
||||
class="inline-flex items-center px-5 py-2.5 bg-gradient-to-r from-indigo-600 to-blue-600 text-white text-sm font-semibold rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all shadow-md hover:shadow-lg hover:scale-105"
|
||||
class="inline-flex items-center px-4 py-2 bg-rose-400 text-white text-sm font-medium rounded-md hover:bg-rose-500 transition flex-1 sm:flex-none justify-center"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
@@ -849,7 +832,7 @@
|
||||
</div>
|
||||
<button
|
||||
@click="closePostModal"
|
||||
class="px-5 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition font-semibold"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-white text-stone-700 rounded-md hover:bg-amber-50 border border-amber-200 transition font-medium text-sm"
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
@@ -977,7 +960,8 @@ const renderContent = (content) => {
|
||||
|
||||
// Просмотр изображения
|
||||
const viewImage = (imagePath) => {
|
||||
window.open(`/storage/${imagePath}`, '_blank');
|
||||
console.log(123)
|
||||
window.open(`/storage/${imagePath}`);
|
||||
};
|
||||
|
||||
// Инициализация формы Inertia
|
||||
|
||||
Reference in New Issue
Block a user