/**
* REST-эндпойнт правок контента для 1С-Битрикс: Управление сайтом (badaeva1.ru)
*
* Установка: файловый менеджер админки -> создать файл /api/index.php, вставить этот код.
* Проверка без токена (после установки): https://badaeva1.ru/api/index.php?action=ping
*
* Авторизация: заголовок "Authorization: Bearer <TOKEN>" (резерв: параметр ?token=).
* Журнал правок с откатными копиями: /api/_audit/ (файлы .php с защитной шапкой,
* напрямую по HTTP не отдаются и из эндпойнта не читаются).
*
* Чтение (GET):
* ping - проверка доступности (без токена)
* info - версии ядра, php, сайты, модули
* iblocks - список инфоблоков с количеством элементов
* sections&iblock_id=N - разделы инфоблока
* elements&iblock_id=N [&q=&page=&limit=&active=Y|N] - список элементов
* element&id=N - элемент целиком: поля, свойства, эффективные SEO
* file&path=/about/ - содержимое файла (path от корня сайта)
* dir&path=/about/ - список файлов каталога
* audit [&month=YYYY-MM] - журнал правок
*
* Запись (POST, тело - JSON, action можно передавать в query или в теле):
* element.update {id, fields{NAME,DETAIL_TEXT,...}, properties{КОД:значение},
* seo{title,description,keywords},
* picture{field:"DETAIL_PICTURE", base64:"...", name:"img.jpg"}}
* element.create {iblock_id, fields{...}, properties{...}, seo{...}, picture{...}}
* section.update {id, fields{NAME, DESCRIPTION, SORT, ACTIVE, UF_*}}
* file.update {path:"/about/index.php", content:"новое содержимое"}
*
* Каждая запись-мутация перед сохранением кладёт в журнал старое состояние (полные
* поля и свойства элемента либо прежнее содержимое файла) - этого достаточно для отката.
*/
define('REST_TOKEN', 'c880165ae0077cbd59e80e442e17ddef7580521d');
define('REST_AUDIT_DIR', __DIR__.'/_audit');
error_reporting(E_ERROR | E_PARSE);
ini_set('display_errors', '0');
require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php');
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
function r_out($data, $code = 200) {
http_response_code($code);
echo json_encode(array('ok' => true, 'data' => $data), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
die;
}
function r_err($msg, $code = 400) {
http_response_code($code);
echo json_encode(array('ok' => false, 'error' => $msg), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
die;
}
function r_short($s, $max = 500000) {
$s = (string)$s;
return mb_strlen($s) > $max ? mb_substr($s, 0, $max).'...[обрезано]' : $s;
}
function r_audit($action, array $data) {
if (!is_dir(REST_AUDIT_DIR)) @mkdir(REST_AUDIT_DIR, 0755, true);
$file = REST_AUDIT_DIR.'/audit-'.date('Y-m').'.php';
if (!is_file($file)) @file_put_contents($file, "<?php exit('Access denied'); ?>\n");
$line = json_encode(
array('time' => date('c'), 'action' => $action, 'data' => $data),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
@file_put_contents($file, $line."\n", FILE_APPEND | LOCK_EX);
return basename($file);
}
function r_audit_read($month) {
$file = REST_AUDIT_DIR.'/audit-'.$month.'.php';
if (!is_file($file)) return array();
$c = (string)file_get_contents($file);
$nl = strpos($c, "\n");
if ($nl === false) return array();
$out = array();
foreach (explode("\n", substr($c, $nl + 1)) as $line) {
$line = trim($line);
if ($line === '') continue;
$j = json_decode($line, true);
if (is_array($j)) $out[] = $j;
}
return $out;
}
function r_abs_path($rel) {
$root = rtrim($_SERVER['DOCUMENT_ROOT'], '/');
$rel = '/'.ltrim((string)$rel, '/');
if (strpos($rel, '..') !== false) return null;
$abs = $root.$rel;
$real = realpath($abs);
if ($real === false) return $abs; // файла ещё нет - вернём несуществующий путь без ..
if ($real !== $root && strpos($real, $root.'/') !== 0) return null;
return $real;
}
function r_path_allowed($abs) {
$root = rtrim($_SERVER['DOCUMENT_ROOT'], '/');
if (strpos($abs, $root.'/') !== 0) return false;
if ($abs === __FILE__) return false; // сам эндпойнт не отдаём (в нём токен)
if (strpos($abs, REST_AUDIT_DIR) === 0) return false; // журнал не отдаём
$rel = substr($abs, strlen($root));
$deny = array(
'/bitrix/modules', '/bitrix/php_interface', '/bitrix/admin',
'/bitrix/tools', '/bitrix/backup', '/bitrix/managed_cache', '/bitrix/stack_cache',
);
foreach ($deny as $d) if ($rel === $d || strpos($rel, $d.'/') === 0) return false;
$ext = strtolower(pathinfo($abs, PATHINFO_EXTENSION));
return in_array($ext, array('php','html','htm','txt','css','js','json','xml','md','csv','inc'), true);
}
/** Элемент целиком: поля, свойства (по кодам), эффективные SEO-значения. */
function r_el_read($id) {
$rs = CIBlockElement::GetByID((int)$id);
if (!($ob = $rs->GetNextElement())) return null;
$f = $ob->GetFields();
$props = array();
foreach ($ob->GetProperties() as $p) {
$key = $p['CODE'] !== '' ? $p['CODE'] : 'PROP_'.$p['ID'];
$value = $p['MULTIPLE'] === 'Y'
? array_map(function ($v) { return array('value' => $v['VALUE'], 'description' => $v['DESCRIPTION']); }, $p['VALUE'] ?? array())
: $p['VALUE'];
$props[$key] = array(
'id' => $p['ID'], 'name' => $p['NAME'], 'type' => $p['PROPERTY_TYPE'],
'multiple' => $p['MULTIPLE'] === 'Y', 'value' => $value,
);
}
$seo = array();
try {
$ip = new \Bitrix\Iblock\InheritedProperty\ElementValues((int)$f['IBLOCK_ID'], (int)$id);
$v = $ip->collectValues();
$seo = array(
'title' => isset($v['META_TITLE']) ? $v['META_TITLE'] : '',
'description' => isset($v['META_DESCRIPTION']) ? $v['META_DESCRIPTION'] : '',
'keywords' => isset($v['META_KEYWORDS']) ? $v['META_KEYWORDS'] : '',
'page_title' => isset($v['ELEMENT_PAGE_TITLE']) ? $v['ELEMENT_PAGE_TITLE'] : '',
);
} catch (\Throwable $e) { $seo = array('error' => $e->getMessage()); }
return array('fields' => $f, 'properties' => $props, 'seo_effective' => $seo);
}
/** Свои SEO-значения элемента (перекрывают шаблоны инфоблока). */
function r_seo_set($iblockId, $id, $seo) {
$vals = array();
if (array_key_exists('title', $seo)) $vals['ELEMENT_TITLE'] = (string)$seo['title'];
if (array_key_exists('description', $seo)) $vals['ELEMENT_META_DESCRIPTION'] = (string)$seo['description'];
if (array_key_exists('keywords', $seo)) $vals['ELEMENT_META_KEYWORDS'] = (string)$seo['keywords'];
if (!$vals) return;
$tpl = new \Bitrix\Iblock\InheritedProperty\ElementTemplates((int)$iblockId, (int)$id);
$tpl->setValues($vals);
}
/** Картинка из base64 -> массив для CIBlockElement (PREVIEW_PICTURE / DETAIL_PICTURE). */
function r_picture($pic) {
if (empty($pic['base64'])) return null;
$bin = base64_decode($pic['base64'], true);
if ($bin === false) r_err('picture.base64: не декодируется', 400);
$ext = strtolower(pathinfo((string)($pic['name'] ?? 'image.jpg'), PATHINFO_EXTENSION));
if (!in_array($ext, array('jpg','jpeg','png','webp','gif','svg','ico'), true)) $ext = 'jpg';
$tmp = tempnam(sys_get_temp_dir(), 'restimg');
$tmp .= '.'.$ext;
if (file_put_contents($tmp, $bin) === false) r_err('не удалось сохранить временный файл', 500);
return CFile::MakeFileArray($tmp);
}
try {
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443)
|| (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
$method = $_SERVER['REQUEST_METHOD'];
if ($method !== 'GET' && $method !== 'POST') r_err('method not allowed', 405);
$in = array();
if ($method === 'POST') {
$raw = file_get_contents('php://input');
$in = json_decode($raw, true);
if (!is_array($in)) r_err('тело запроса не является корректным JSON', 400);
}
$action = isset($_GET['action']) ? $_GET['action'] : (isset($in['action']) ? $in['action'] : '');
// ping работает без токена: удобно проверить, что файл установлен и исполняется
if ($action === 'ping') {
r_out(array(
'service' => 'badaeva1 rest endpoint',
'bitrix' => defined('SM_VERSION') ? SM_VERSION : '?',
'php' => PHP_VERSION,
'charset' => defined('SITE_CHARSET') ? SITE_CHARSET : '?',
'https' => $isHttps,
'time' => date('c'),
));
}
// --- авторизация ---
$h = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : '';
$given = '';
if (preg_match('/^Bearer\s+(.+)$/i', trim($h), $m)) $given = trim($m[1]);
if ($given === '' && isset($_GET['token'])) $given = trim($_GET['token']);
if (REST_TOKEN === '' || strpos(REST_TOKEN, 'ЗАМЕНИТЕ') === 0) r_err('токен не настроен: замените REST_TOKEN в файле', 500);
if ($given === '' || !hash_equals(REST_TOKEN, $given)) r_err('unauthorized', 401);
if (!$isHttps) r_err('доступ только по HTTPS', 400);
\Bitrix\Main\Loader::includeModule('iblock') || r_err('модуль iblock не установлен', 500);
switch ($action) {
case 'info':
$sites = array();
$by = 'sort'; $ord = 'asc';
$rs = CSite::GetList($by, $ord);
while ($s = $rs->Fetch()) {
$sites[] = array('lid' => $s['LID'], 'name' => $s['NAME'], 'dir' => $s['DIR'], 'active' => $s['ACTIVE']);
}
$mods = array('main');
foreach (\Bitrix\Main\ModuleManager::getInstalledModules() as $m) $mods[] = $m['ID'];
r_out(array(
'bitrix' => defined('SM_VERSION') ? SM_VERSION : '?',
'php' => PHP_VERSION,
'charset' => defined('SITE_CHARSET') ? SITE_CHARSET : '?',
'sites' => $sites,
'modules' => array_values(array_unique($mods)),
));
case 'iblocks':
$out = array();
$rs = CIBlock::GetList(array('SORT' => 'ASC'), array(), true);
while ($b = $rs->Fetch()) {
$out[] = array(
'id' => (int)$b['ID'], 'code' => $b['CODE'], 'name' => $b['NAME'],
'type' => $b['IBLOCK_TYPE_ID'], 'site' => $b['LID'], 'active' => $b['ACTIVE'],
'elements' => isset($b['ELEMENTS_CNT']) ? (int)$b['ELEMENTS_CNT'] : null,
);
}
r_out($out);
case 'sections':
$ib = (int)($_GET['iblock_id'] ?? 0);
if (!$ib) r_err('нужен iblock_id');
$out = array();
$rs = CIBlockSection::GetList(
array('LEFT_MARGIN' => 'ASC'),
array('IBLOCK_ID' => $ib), false,
array('ID', 'NAME', 'CODE', 'SORT', 'ACTIVE', 'DEPTH_LEVEL', 'DESCRIPTION')
);
while ($s = $rs->Fetch()) $out[] = $s;
r_out($out);
case 'elements':
$ib = (int)($_GET['iblock_id'] ?? 0);
if (!$ib) r_err('нужен iblock_id');
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = min(100, max(1, (int)($_GET['limit'] ?? 20)));
$filter = array('IBLOCK_ID' => $ib);
$act = isset($_GET['active']) ? $_GET['active'] : '';
if ($act === 'Y' || $act === 'N') $filter['ACTIVE'] = $act;
$q = trim((string)($_GET['q'] ?? ''));
if ($q !== '') $filter['?NAME'] = '%'.$q.'%';
$out = array();
$rs = CIBlockElement::GetList(
array('SORT' => 'ASC', 'ID' => 'ASC'), $filter, false,
array('iNumPage' => $page, 'nPageSize' => $limit),
array('ID', 'NAME', 'CODE', 'ACTIVE', 'SORT', 'IBLOCK_ID', 'TIMESTAMP_X', 'PREVIEW_TEXT', 'DETAIL_PAGE_URL')
);
while ($ob = $rs->GetNextElement()) {
$f = $ob->GetFields();
$out[] = array(
'id' => (int)$f['ID'], 'name' => $f['NAME'], 'code' => $f['CODE'],
'active' => $f['ACTIVE'], 'sort' => (int)$f['SORT'],
'url' => isset($f['DETAIL_PAGE_URL']) ? $f['DETAIL_PAGE_URL'] : '',
'preview_text' => r_short($f['PREVIEW_TEXT'], 300),
);
}
r_out(array(
'items' => $out,
'total' => (int)$rs->NavRecordCount,
'pages' => (int)$rs->NavPageCount,
'page' => $page,
));
case 'element':
$id = (int)($_GET['id'] ?? 0);
$el = r_el_read($id);
if (!$el) r_err('элемент не найден', 404);
r_out($el);
case 'file':
$abs = r_abs_path($_GET['path'] ?? '');
if (!$abs || !r_path_allowed($abs)) r_err('путь запрещён', 403);
if (!is_file($abs)) r_err('файл не найден', 404);
r_out(array(
'path' => $_GET['path'],
'size' => filesize($abs),
'mtime' => date('c', filemtime($abs)),
'content' => r_short((string)file_get_contents($abs), 1000000),
));
case 'dir':
$abs = r_abs_path($_GET['path'] ?? '');
if (!$abs || !r_path_allowed($abs)) r_err('путь запрещён', 403);
if (!is_dir($abs)) r_err('каталог не найден', 404);
$items = array();
foreach (scandir($abs) as $n) {
if ($n === '.' || $n === '..' || $n[0] === '.') continue;
$p = $abs.'/'.$n;
$items[] = array(
'name' => $n, 'dir' => is_dir($p),
'size' => is_file($p) ? filesize($p) : null,
'mtime' => is_file($p) ? date('c', filemtime($p)) : null,
);
}
r_out(array('path' => $_GET['path'], 'items' => $items));
case 'audit':
$month = preg_match('/^\d{4}-\d{2}$/', (string)($_GET['month'] ?? '')) ? $_GET['month'] : date('Y-m');
r_out(array('month' => $month, 'entries' => r_audit_read($month)));
case 'element.update':
case 'element.create':
$isUpdate = ($action === 'element.update');
$old = null;
$id = 0;
if ($isUpdate) {
$id = (int)($in['id'] ?? 0);
$old = r_el_read($id);
if (!$old) r_err('элемент не найден', 404);
$ib = (int)$old['fields']['IBLOCK_ID'];
} else {
$ib = (int)($in['iblock_id'] ?? 0);
if (!$ib) r_err('нужен iblock_id');
}
$fields = array('IBLOCK_ID' => $ib);
$allowed = array('NAME','CODE','PREVIEW_TEXT','PREVIEW_TEXT_TYPE','DETAIL_TEXT','DETAIL_TEXT_TYPE','ACTIVE','SORT','TAGS');
if (isset($in['fields']) && is_array($in['fields'])) {
foreach ($allowed as $k) if (array_key_exists($k, $in['fields'])) $fields[$k] = $in['fields'][$k];
if (isset($in['fields']['IBLOCK_SECTION'])) {
$fields['IBLOCK_SECTION'] = array_map('intval', (array)$in['fields']['IBLOCK_SECTION']);
} elseif (isset($in['fields']['IBLOCK_SECTION_ID'])) {
$fields['IBLOCK_SECTION'] = array((int)$in['fields']['IBLOCK_SECTION_ID']);
}
}
$pic = r_picture(isset($in['picture']) && is_array($in['picture']) ? $in['picture'] : array());
if ($pic !== null) {
$f = (string)($in['picture']['field'] ?? 'DETAIL_PICTURE');
if (!in_array($f, array('PREVIEW_PICTURE','DETAIL_PICTURE'), true)) r_err('picture.field: допустимы PREVIEW_PICTURE / DETAIL_PICTURE');
$fields[$f] = $pic;
}
$el = new CIBlockElement;
if ($isUpdate) {
$ok = $el->Update($id, $fields);
} else {
$id = (int)$el->Add($fields);
$ok = $id > 0;
}
if (!$ok) {
r_audit($action, array('id' => $id, 'error' => $el->LAST_ERROR));
r_err('ошибка сохранения: '.$el->LAST_ERROR, 500);
}
if (isset($in['properties']) && is_array($in['properties']) && $in['properties']) {
CIBlockElement::SetPropertyValuesEx($id, $ib, $in['properties']);
}
if (isset($in['seo']) && is_array($in['seo'])) r_seo_set($ib, $id, $in['seo']);
BXClearCache(true);
try { \Bitrix\Main\Composite\Helper::clearByTag('all'); } catch (\Throwable $e) { /* композит может быть выключен */ }
$auditIn = $in;
unset($auditIn['picture']); // base64 не логируем
r_audit($action, array(
'id' => $id, 'iblock' => $ib, 'input' => $auditIn,
'old' => $old !== null ? array('fields' => $old['fields'], 'properties' => $old['properties']) : null,
));
r_out(array('id' => $id, 'state_after' => r_el_read($id)));
case 'section.update':
$id = (int)($in['id'] ?? 0);
$rs = CIBlockSection::GetList(array(), array('ID' => $id));
if (!($s = $rs->Fetch())) r_err('раздел не найден', 404);
$allowed = array('NAME','CODE','DESCRIPTION','DESCRIPTION_TYPE','SORT','ACTIVE');
$fields = array();
if (isset($in['fields']) && is_array($in['fields'])) {
foreach ($in['fields'] as $k => $v) {
if (in_array($k, $allowed, true) || strpos($k, 'UF_') === 0) $fields[$k] = $v;
}
}
$sec = new CIBlockSection;
if (!$sec->Update($id, $fields)) r_err('ошибка сохранения: '.$sec->LAST_ERROR, 500);
BXClearCache(true);
r_audit($action, array('id' => $id, 'input' => $in, 'old' => $s));
r_out(array('id' => $id));
case 'file.update':
$rel = (string)($in['path'] ?? '');
$abs = r_abs_path($rel);
if (!$abs || !r_path_allowed($abs)) r_err('путь запрещён', 403);
if (!isset($in['content']) || !is_string($in['content'])) r_err('нужен content (строка)');
$old = is_file($abs) ? (string)file_get_contents($abs) : null;
$saved = false;
try {
global $APPLICATION;
$APPLICATION->SaveFileContent($abs, $in['content']);
$saved = true;
} catch (\Throwable $e) { $saved = false; }
if (!$saved && file_put_contents($abs, $in['content']) === false) r_err('не удалось записать файл', 500);
BXClearCache(true);
r_audit($action, array(
'path' => $rel,
'old' => $old === null ? null : r_short($old, 300000),
'new_size' => strlen($in['content']),
));
r_out(array('path' => $rel, 'size' => filesize($abs), 'replaced_existing' => $old !== null));
default:
r_err('неизвестное действие: "'.$action.'". Доступны: ping, info, iblocks, sections, elements, element, file, dir, audit, element.update, element.create, section.update, file.update');
}
} catch (\Throwable $e) {
r_err('исключение: '.$e->getMessage(), 500);
}
Добро пожаловать в Центр Семейной Стоматологии и Мир красивых улыбок
Наш Центр находится в городе Санкт-Петербурге, в Невском районе, расположенном у реки Невы, на улице Бадаева, дом 1. Это современная стоматология, которая ежедневно обеспечивает комфорт и удобство Пациентам! Мы стремимся, чтобы все наши Пациенты получали от лечения максимальное количество удовольствия!
Центр Семейной Стоматологии – это высококвалифицированные врачи-стоматологи, новейшие технологии, индивидуальный подход к каждому Пациенту, абсолютно безопасные и безболезненные методы лечения, в большинстве случаев совершенно не доставляющие никакого дискомфорта Пациенту.
Мы работаем, совершенствуемся и развиваемся для Вас, чтобы Ваша жизнь стала ярче, а улыбка белоснежней.
Лучшие стоматологи Невского района
В команде Центра Семейной Стоматологии в Невском районе только лучшие специалисты с многолетней практикой и доказанным опытом. Наши врачи не только лечат. Они помогают сохранить Ваши зубы красивыми и здоровыми, находят подход как к взрослым, так и к детям.
О центре
Центр Семейной Стоматологии на Бадаева, дом 1 – это частный медицинский центр для всей семьи.
Наш многолетний опыт работы в области стоматологии и лечении зубов позволяет практически всем посетителям после первого визита остаться нашими постоянными Пациентами. Этому способствует сочетание высокого качества оказываемых услуг, разумных цен и серьезного подхода к вопросам профилактики и гигиены. Наш Центр стоит посетить даже при отсутствии острой потребности в стоматологической помощи в целях профилактического осмотра. Тех, кто прошел у нас лечение зубов, администраторы стоматологии приглашают один раз в полгода на профилактический осмотр зубов (санация полости рта).

