- Изменена логика проверки уведомлений: теперь проверяются ВСЕ уведомления (не только непрочитанные) - Если уведомление прочитано - дубликат НЕ создаётся (ранее создавался) - Добавлена проверка статуса уведомления перед обновлением - Добавлены уведомления для RegionalCourtParser (ранее только для MoscowCourtParser) - Создана документация DUPLICATE_PREVENTION_GUIDE.md с описанием 3 уровней защиты Теперь система полностью защищена от дубликатов: 1. Уровень событий в таблице subject 2. Уровень уведомлений в vtiger_vdnotifierpro (с проверкой статуса) 3. Уровень календаря CRM Для продакшена: НЕ передавать skip_duplicate_check=true (по умолчанию false)
553 lines
24 KiB
PHP
553 lines
24 KiB
PHP
<?php
|
||
require_once 'BaseCourtParser.php';
|
||
|
||
/**
|
||
* Парсер для московских судов (mos-gorsud.ru)
|
||
*/
|
||
class MoscowCourtParser extends BaseCourtParser {
|
||
|
||
public function canHandle($url) {
|
||
// Московские суды имеют домены mos-gorsud.ru и mos-sud.ru
|
||
return preg_match('/mos-(gorsud|sud)\.ru/', $url);
|
||
}
|
||
|
||
public function parse($url, $status) {
|
||
$this->log("Старт парсинга {$this->case_number} для статуса: $status (МОСКОВСКИЙ СУД)");
|
||
$this->log("Парсим данные из ссылки: $url");
|
||
|
||
// Используем Browserless для получения структурированных данных
|
||
$data = $this->loadPageContentViaBrowserless($url);
|
||
|
||
if ($data === false) {
|
||
$this->log("Ошибка: не удалось получить данные через Browserless");
|
||
return null;
|
||
}
|
||
|
||
$this->log("Данные успешно получены через Browserless. Начинаем обработку...");
|
||
|
||
// Извлекаем последнее событие из структурированных данных
|
||
$last_event = $this->extractLastEventFromData($data);
|
||
|
||
if ($last_event === null) {
|
||
$this->log("ВНИМАНИЕ: Не удалось извлечь события из данных Browserless.");
|
||
}
|
||
|
||
return $last_event;
|
||
}
|
||
|
||
/**
|
||
* Извлекает последнее событие из структурированных данных Browserless
|
||
*/
|
||
private function extractLastEventFromData($data) {
|
||
$last_event = null;
|
||
|
||
// Проверяем наличие заседаний (hearings)
|
||
if (isset($data['hearings']) && is_array($data['hearings']) && !empty($data['hearings'])) {
|
||
$this->log("Найдено заседаний: " . count($data['hearings']));
|
||
|
||
// Берем последнее заседание
|
||
$hearing = end($data['hearings']);
|
||
|
||
if (!empty($hearing['datetime'])) {
|
||
// Парсим дату и время
|
||
$datetime = $hearing['datetime'];
|
||
$event_date = '';
|
||
$event_time = '';
|
||
|
||
// Формат: "27.10.2025 09:30" или "27.10.2025"
|
||
if (preg_match('/(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2})/', $datetime, $matches)) {
|
||
$event_date = $matches[1];
|
||
$event_time = $matches[2];
|
||
} elseif (preg_match('/(\d{2}\.\d{2}\.\d{4})/', $datetime, $matches)) {
|
||
$event_date = $matches[1];
|
||
$event_time = '';
|
||
}
|
||
|
||
if (!empty($event_date)) {
|
||
$event_name = $hearing['stage'] ?? 'Судебное заседание';
|
||
$event_result = $hearing['result'] ?? '';
|
||
$location = $hearing['hall'] ?? '';
|
||
$basis = $hearing['basis'] ?? '';
|
||
|
||
$this->log("Найдено заседание: $event_name на $event_date в $event_time");
|
||
|
||
// Форматируем дату для БД
|
||
$formatted_date = date('Y-m-d', strtotime(str_replace('.', '-', $event_date)));
|
||
|
||
$eventData = [
|
||
'event_name' => $event_name,
|
||
'event_date' => $formatted_date,
|
||
'event_time' => $event_time,
|
||
'location' => $location,
|
||
'event_result' => $event_result,
|
||
'event_basis' => $basis,
|
||
'note' => '',
|
||
'publication_date' => $formatted_date,
|
||
];
|
||
|
||
// Сохраняем событие в БД
|
||
$this->saveEvent($eventData);
|
||
$last_event = $eventData;
|
||
|
||
// Создаём уведомление (если указан project_id)
|
||
if ($this->project_id) {
|
||
$notificationId = $this->createCourtEventNotification($this->project_id, $eventData);
|
||
if ($notificationId) {
|
||
$this->log("Создано уведомление ID: $notificationId для события: $event_name");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Если заседаний нет, проверяем историю состояний
|
||
if ($last_event === null && isset($data['history']['states']) && is_array($data['history']['states'])) {
|
||
$this->log("Заседаний нет, проверяем историю состояний: " . count($data['history']['states']));
|
||
|
||
// Берем последнее состояние
|
||
$state = end($data['history']['states']);
|
||
|
||
if (!empty($state['date']) && !empty($state['state'])) {
|
||
$event_date = $state['date'];
|
||
$event_name = $state['state'];
|
||
$basis = $state['basis_doc'] ?? '';
|
||
|
||
$this->log("Найдено состояние: $event_name на $event_date");
|
||
|
||
// Форматируем дату для БД
|
||
$formatted_date = date('Y-m-d', strtotime(str_replace('.', '-', $event_date)));
|
||
|
||
$eventData = [
|
||
'event_name' => $event_name,
|
||
'event_date' => $formatted_date,
|
||
'event_time' => '',
|
||
'location' => '',
|
||
'event_result' => '',
|
||
'event_basis' => $basis,
|
||
'note' => '',
|
||
'publication_date' => $formatted_date,
|
||
];
|
||
|
||
// Сохраняем событие в БД
|
||
$this->saveEvent($eventData);
|
||
$last_event = $eventData;
|
||
|
||
// Создаём уведомление (если указан project_id)
|
||
if ($this->project_id) {
|
||
$notificationId = $this->createCourtEventNotification($this->project_id, $eventData);
|
||
if ($notificationId) {
|
||
$this->log("Создано уведомление ID: $notificationId для состояния: $event_name");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Логируем информацию о деле для отладки
|
||
if (isset($data['case'])) {
|
||
$case = $data['case'];
|
||
$this->log("Информация о деле:");
|
||
$this->log(" UID: " . ($case['uid'] ?? 'не указан'));
|
||
$this->log(" Номер дела: " . ($case['case_number'] ?? 'не указан'));
|
||
$this->log(" Статус: " . ($case['current_status'] ?? 'не указан'));
|
||
$this->log(" Истец: " . ($case['plaintiff'] ?? 'не указан'));
|
||
$this->log(" Ответчик: " . ($case['defendant'] ?? 'не указан'));
|
||
}
|
||
|
||
return $last_event;
|
||
}
|
||
|
||
/**
|
||
* Создаёт уведомление о новом событии суда
|
||
*/
|
||
private function createCourtEventNotification($projectId, $eventData) {
|
||
try {
|
||
// Создаём отдельное соединение с основной БД CRM для уведомлений
|
||
$crmPdo = new PDO('mysql:host=localhost;dbname=ci20465_72new;charset=utf8mb4', 'ci20465_72new', 'EcY979Rn');
|
||
$crmPdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
|
||
// Получаем ответственного по проекту
|
||
$query = "SELECT e.smownerid, p.projectname FROM vtiger_crmentity e
|
||
JOIN vtiger_project p ON p.projectid = e.crmid
|
||
WHERE e.crmid = ? AND e.deleted = 0";
|
||
$stmt = $crmPdo->prepare($query);
|
||
$stmt->execute([$projectId]);
|
||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
||
if (!$result) {
|
||
$this->log("Проект $projectId не найден для уведомления");
|
||
return false;
|
||
}
|
||
|
||
$userId = $result['smownerid'];
|
||
$projectName = $result['projectname'];
|
||
|
||
$this->log("Создаем уведомление для пользователя $userId о событии в проекте $projectName");
|
||
|
||
// Формируем текст уведомления
|
||
$eventName = $eventData['event_name'];
|
||
$eventDate = $eventData['event_date'];
|
||
$eventTime = $eventData['event_time'];
|
||
|
||
$timeStr = !empty($eventTime) ? " в $eventTime" : "";
|
||
$notificationTitle = "Событие суда: $eventName на $eventDate$timeStr";
|
||
|
||
// Формируем ссылку на проект
|
||
$projectLink = "module=Project&view=Detail&record=$projectId";
|
||
|
||
// Проверяем, нет ли уже уведомления для этого события (любого статуса)
|
||
// Используем точное совпадение названия события и даты
|
||
$checkQuery = "SELECT id, status FROM vtiger_vdnotifierpro
|
||
WHERE userid = ? AND crmid = ? AND title = ?
|
||
ORDER BY id DESC LIMIT 1";
|
||
$checkStmt = $crmPdo->prepare($checkQuery);
|
||
$checkStmt->execute([$userId, $projectId, $notificationTitle]);
|
||
$existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
||
|
||
if ($existing) {
|
||
// Если уведомление уже есть - проверяем его статус
|
||
if ($existing['status'] == 5) {
|
||
// Уведомление непрочитанное - обновляем только время
|
||
$updateQuery = "UPDATE vtiger_vdnotifierpro SET modifiedtime = NOW() WHERE id = ?";
|
||
$updateStmt = $crmPdo->prepare($updateQuery);
|
||
$updateStmt->execute([$existing['id']]);
|
||
|
||
$this->log("Обновлено время непрочитанного уведомления ID: {$existing['id']}");
|
||
return $existing['id'];
|
||
} else {
|
||
// Уведомление прочитано - не создаём дубликат
|
||
$this->log("Уведомление ID: {$existing['id']} уже существует (статус: {$existing['status']}), дубликат не создан");
|
||
return $existing['id'];
|
||
}
|
||
} else {
|
||
// Создаем новое уведомление
|
||
$insertQuery = "INSERT INTO vtiger_vdnotifierpro (userid, modulename, crmid, modiuserid, link, title, action, modifiedtime, status) VALUES (?, 'Project', ?, 0, ?, ?, '', NOW(), 5)";
|
||
$insertStmt = $crmPdo->prepare($insertQuery);
|
||
$insertStmt->execute([$userId, $projectId, $projectLink, $notificationTitle]);
|
||
|
||
$notificationId = $crmPdo->lastInsertId();
|
||
$this->log("Создано новое уведомление ID: $notificationId для пользователя $userId");
|
||
|
||
return $notificationId;
|
||
}
|
||
|
||
} catch (Exception $e) {
|
||
$this->log("Ошибка создания уведомления: " . $e->getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Загружает содержимое страницы через cURL
|
||
*/
|
||
private function loadPageContent($url) {
|
||
// Сначала пробуем обычный cURL
|
||
$ch = curl_init();
|
||
curl_setopt($ch, CURLOPT_URL, $url);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||
curl_setopt($ch, CURLOPT_ENCODING, ''); // Автоматически обрабатывает gzip, deflate, br
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||
'Accept-Language: ru-RU,ru;q=0.9,en;q=0.8',
|
||
'Connection: keep-alive',
|
||
'Upgrade-Insecure-Requests: 1',
|
||
'Sec-Fetch-Dest: document',
|
||
'Sec-Fetch-Mode: navigate',
|
||
'Sec-Fetch-Site: none',
|
||
'Cache-Control: max-age=0'
|
||
]);
|
||
|
||
$html = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
// Если cURL успешен, возвращаем результат
|
||
if ($html !== false && $httpCode === 200) {
|
||
$this->log("Страница загружена через cURL (HTTP $httpCode)");
|
||
return $html;
|
||
}
|
||
|
||
$this->log("cURL не удался (HTTP $httpCode), пробуем Browserless...");
|
||
|
||
// Если cURL не сработал, пробуем Browserless
|
||
return $this->loadPageContentViaBrowserless($url);
|
||
}
|
||
|
||
/**
|
||
* Загружает структурированные данные через Browserless function
|
||
*/
|
||
private function loadPageContentViaBrowserless($url) {
|
||
$browserlessUrl = 'http://147.45.146.17:3000/function';
|
||
$browserlessToken = '9ahhnpjkchxtcho9';
|
||
|
||
// JavaScript код функции (тот же, что мы тестировали)
|
||
$jsFunction = '
|
||
export default async function ({ page, context }) {
|
||
const caseUrl =
|
||
context.case_url ||
|
||
"' . addslashes($url) . '";
|
||
|
||
// --- Установка заголовков и поведения браузера ---
|
||
await page.setViewport({ width: 1920, height: 1080 });
|
||
await page.setExtraHTTPHeaders({
|
||
"Referer": "https://mos-sud.ru/",
|
||
"Origin": "https://mos-sud.ru",
|
||
"Accept-Language": "ru,en;q=0.9",
|
||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||
"Upgrade-Insecure-Requests": "1",
|
||
});
|
||
await page.setUserAgent(
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
||
);
|
||
|
||
await page.goto(caseUrl, { waitUntil: "networkidle2", timeout: 60000 });
|
||
|
||
// закрыть баннеры cookies, если есть
|
||
try {
|
||
await page.waitForSelector("#cookie-disclaimer .cd-close-button, .cookie-accept, .cookie__close", { timeout: 3000 });
|
||
const btns = await page.$$("#cookie-disclaimer .cd-close-button, .cookie-accept, .cookie__close");
|
||
if (btns[0]) await btns[0].click();
|
||
} catch (_) {}
|
||
|
||
// ждём карточку
|
||
await page.waitForSelector(
|
||
".detail-cart .row_card, .case-card, .case-details, .content, main .wrapper_innercontent",
|
||
{ timeout: 20000 }
|
||
);
|
||
|
||
// активируем вкладки
|
||
try {
|
||
for (const id of ["#ui-id-1", "#ui-id-2", "#ui-id-3"]) {
|
||
if (await page.$(id)) await page.click(id);
|
||
}
|
||
const tabLinks = await page.$$(`a[href^="#tabs-"], .tabs_wrapper a.ui-tabs-anchor`);
|
||
if (tabLinks.length) for (const a of tabLinks) await a.click();
|
||
await page.waitForTimeout(300);
|
||
} catch (_) {}
|
||
|
||
const data = await page.evaluate(() => {
|
||
const norm = (el) => (el ? el.textContent.replace(/\\s+/g, " ").trim() : "");
|
||
const qsa = (sel) => Array.from(document.querySelectorAll(sel));
|
||
|
||
function collectRows() {
|
||
const rows = [];
|
||
qsa(".detail-cart .row_card").forEach((r) => {
|
||
const left = norm(r.querySelector(".left"));
|
||
const right = norm(r.querySelector(".right"));
|
||
if (left && right) rows.push({ left, right });
|
||
});
|
||
if (!rows.length) {
|
||
qsa("table, .case-card, .case-details").forEach((tbl) => {
|
||
qsa("tr", tbl).forEach((tr) => {
|
||
const tds = tr.querySelectorAll("td, th");
|
||
if (tds.length === 2) {
|
||
const left = norm(tds[0]);
|
||
const right = norm(tds[1]);
|
||
if (left && right) rows.push({ left, right });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
if (!rows.length) {
|
||
qsa(".case-card__row, .kv-row").forEach((row) => {
|
||
const left = norm(row.querySelector(".case-card__key, .kv-key, .left"));
|
||
const right = norm(row.querySelector(".case-card__val, .kv-val, .right"));
|
||
if (left && right) rows.push({ left, right });
|
||
});
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
const rows = collectRows();
|
||
const byLeft = (start) => {
|
||
const row = rows.find((r) =>
|
||
r.left.toLowerCase().startsWith(start.toLowerCase())
|
||
);
|
||
return row ? row.right : null;
|
||
};
|
||
|
||
const uid = byLeft("Уникальный идентификатор дела") || byLeft("UID");
|
||
const numberRaw = byLeft("Номер дела") || byLeft("№ дела") || byLeft("Номер дела ~ материала");
|
||
let case_number = null, material_number = null;
|
||
if (numberRaw) {
|
||
if (numberRaw.includes("∼")) {
|
||
const parts = numberRaw.split("∼").map((s) => s.trim()).filter(Boolean);
|
||
case_number = parts[0] || null;
|
||
material_number = parts[1] || null;
|
||
} else {
|
||
case_number = numberRaw;
|
||
}
|
||
}
|
||
|
||
const intake_date = byLeft("Дата поступления") || byLeft("Поступило");
|
||
const partiesRaw = byLeft("Стороны") || byLeft("Участники") || "";
|
||
let plaintiff = null, defendant = null;
|
||
if (partiesRaw) {
|
||
const m1 = partiesRaw.match(/Истец:\\s*([^<\\n]+)/i);
|
||
const m2 = partiesRaw.match(/Ответчик:\\s*([^<\\n]+)/i);
|
||
plaintiff = m1 ? m1[1].trim() : null;
|
||
defendant = m2 ? m2[1].trim() : null;
|
||
}
|
||
|
||
const judge = byLeft("Судья") || byLeft("Cудья") || byLeft("Председательствующий судья");
|
||
const category = byLeft("Категория дела") || byLeft("Категория");
|
||
|
||
const statusRaw = byLeft("Текущее состояние") || byLeft("Состояние");
|
||
let current_status = null, current_status_date = null;
|
||
if (statusRaw) {
|
||
const m = statusRaw.match(/^(.+?),\\s*([\\d.]{10})$/);
|
||
current_status = m ? m[1].trim() : statusRaw;
|
||
current_status_date = m ? m[2] : null;
|
||
}
|
||
|
||
const first_instance_date =
|
||
byLeft("Дата рассмотрения дела в первой инстанции") || byLeft("Дата рассмотрения (1 инстанция)");
|
||
|
||
const first_inst_decision_raw =
|
||
byLeft("Решение первой инстанции") || byLeft("Решение (1 инстанция)");
|
||
let first_instance_decision = null, first_instance_decision_date = null;
|
||
if (first_inst_decision_raw) {
|
||
const m = first_inst_decision_raw.match(/^(.+?),\\s*([\\d.]{10})$/);
|
||
first_instance_decision = m ? m[1].trim() : first_inst_decision_raw;
|
||
first_instance_decision_date = m ? m[2] : null;
|
||
}
|
||
|
||
// таблицы
|
||
function tableToRows(tbody) {
|
||
return Array.from(tbody.querySelectorAll("tr")).map((tr) => {
|
||
const tds = tr.querySelectorAll("td");
|
||
return Array.from(tds).map((td) => norm(td.querySelector("div") || td));
|
||
});
|
||
}
|
||
|
||
const stTbody = document.querySelector("#tabs-1 #state-history table tbody");
|
||
const stateRows = stTbody ? tableToRows(stTbody) : [];
|
||
const states = stateRows.map((cols) => ({
|
||
date: cols[0] || null,
|
||
state: cols[1] || null,
|
||
basis_doc: cols[2] || null,
|
||
}));
|
||
|
||
const tab1Tbodies = document.querySelectorAll("#tabs-1 table tbody");
|
||
const placeTbody = tab1Tbodies.length > 1 ? tab1Tbodies[1] : null;
|
||
const locationRows = placeTbody ? tableToRows(placeTbody) : [];
|
||
const locations = locationRows.map((cols) => ({
|
||
date: cols[0] || null,
|
||
location: cols[1] || null,
|
||
comment: cols[2] || null,
|
||
}));
|
||
|
||
const sessionsTbody = document.querySelector("#tabs-2 table tbody");
|
||
const hearingsRows = sessionsTbody ? tableToRows(sessionsTbody) : [];
|
||
const hearings = hearingsRows.map((cols) => ({
|
||
datetime: cols[0] || null,
|
||
hall: cols[1] || null,
|
||
stage: cols[2] || null,
|
||
result: cols[3] || null,
|
||
basis: cols[4] || null,
|
||
av_record: cols[5] || null,
|
||
type: cols[6] || null,
|
||
}));
|
||
|
||
const docsTbody = document.querySelector("#tabs-3 table tbody");
|
||
const docsRows = docsTbody ? tableToRows(docsTbody) : [];
|
||
const documents = docsRows.map((cols) => ({
|
||
date: cols[0] || null,
|
||
kind: cols[1] || null,
|
||
text_status: cols[2] || null,
|
||
}));
|
||
|
||
const court =
|
||
(document.querySelector(".court-name")?.textContent || "").trim() ||
|
||
(document.querySelector(\'[class*="court"] [class*="name"]\')?.textContent || "").trim() ||
|
||
(document.querySelector("title")?.textContent.match(/суд[^|]*/i)?.[0] || "").trim() ||
|
||
null;
|
||
|
||
const title = (document.querySelector("h1")?.textContent || "")
|
||
.replace(/\\s+/g, " ")
|
||
.trim();
|
||
|
||
return {
|
||
case: {
|
||
uid,
|
||
case_number,
|
||
material_number,
|
||
intake_date,
|
||
plaintiff,
|
||
defendant,
|
||
judge,
|
||
category,
|
||
current_status,
|
||
current_status_date,
|
||
first_instance_date,
|
||
first_instance_decision,
|
||
first_instance_decision_date,
|
||
},
|
||
history: { states, locations },
|
||
hearings,
|
||
documents,
|
||
meta: { court, title },
|
||
};
|
||
});
|
||
|
||
return { source_url: caseUrl, ...data };
|
||
}';
|
||
|
||
// Подготавливаем данные для отправки
|
||
$postData = [
|
||
'code' => $jsFunction,
|
||
'context' => [
|
||
'case_url' => $url
|
||
]
|
||
];
|
||
|
||
$this->log("Отправляем запрос в Browserless для URL: $url");
|
||
|
||
// Отправляем запрос в Browserless
|
||
$ch = curl_init();
|
||
curl_setopt($ch, CURLOPT_URL, $browserlessUrl);
|
||
curl_setopt($ch, CURLOPT_POST, true);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_TIMEOUT, 120); // Увеличиваем таймаут
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||
'Content-Type: application/json',
|
||
'Authorization: Bearer ' . $browserlessToken
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$error = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
$this->log("Browserless ответ: HTTP $httpCode, длина ответа: " . strlen($response));
|
||
|
||
if ($error) {
|
||
$this->log("cURL ошибка: $error");
|
||
return false;
|
||
}
|
||
|
||
if ($httpCode !== 200) {
|
||
$this->log("Browserless вернул HTTP $httpCode: " . substr($response, 0, 200));
|
||
return false;
|
||
}
|
||
|
||
// Парсим ответ
|
||
$data = json_decode($response, true);
|
||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||
$this->log("Ошибка декодирования JSON: " . json_last_error_msg());
|
||
$this->log("Ответ: " . substr($response, 0, 500));
|
||
return false;
|
||
}
|
||
|
||
$this->log("Данные успешно получены от Browserless");
|
||
return $data;
|
||
}
|
||
}
|
||
?>
|
||
|