mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-10-23 13:45:06 +00:00
release: AVE.cms 3.3 build 0.42
This commit is contained in:
@@ -62,6 +62,7 @@ AddDefaultCharset UTF-8
|
||||
RewriteRule ^setup/?$ setup/index.php [L,QSA]
|
||||
|
||||
# Internal implementation and runtime data must not be addressed directly.
|
||||
RewriteRule ^system/App/Frontend/Auth/assets/(?:phone-mask|auth)\.js$ - [L]
|
||||
RewriteRule ^(?:configs|storage|system|tmp)(?:/|$) - [R=404,L]
|
||||
# Module packages expose only static files from their declared asset roots.
|
||||
# Manifests, migrations, views, LESS sources and executable code stay private.
|
||||
|
||||
+75
-1
@@ -11,7 +11,81 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
Новые изменения будут добавлены после build 0.41.
|
||||
- Исправлен единый результат оформления заказа: первый запрос и его повтор
|
||||
получают одинаковый сохранённый снимок для оплаты и чека.
|
||||
|
||||
- В ИИ-помощнике исправлен стандартный профиль описания товара: разрешены
|
||||
абзацы `<p>`, при этом запрет `h1`, стилей и атрибутов сохранён.
|
||||
|
||||
- Упрощена вкладка расхождений прайса: компактный выбор файлов, видимые
|
||||
фильтры по разделу, виду сверки, полю и сопоставлению, сортировка,
|
||||
таблица товаров и штатная номерная пагинация. Сверка отдельного товара
|
||||
открывается в правой панели; фильтры сохраняются при повторном сравнении.
|
||||
|
||||
- Исправлена переоценка матрасов с условной ценой при покупке с кроватью:
|
||||
обычная цена больше не заменяется комплектной. Добавлен отдельный режим
|
||||
восстановления только цен таких матрасов без изменения остальных товаров
|
||||
и правил акций; предпросмотры старой версии отклоняются.
|
||||
|
||||
- В импорте прайса добавлена вкладка «Расхождения»: сравнение нативных
|
||||
характеристик, описаний, комплектации и отметок с текущими товарами,
|
||||
необязательная сверка с предыдущим XLSX, поиск, фильтры и CSV. Изменения
|
||||
цен отделены от изменений данных; сверка не изменяет товары. Исправлено
|
||||
распознавание несъёмных и неповоротных подножек.
|
||||
|
||||
- В модуле импорта прайса появился режим «Только цены» по умолчанию: гарантия,
|
||||
РУ, акции, новинки и характеристики не меняются при переоценке. Гарантия на
|
||||
обивку больше не распознаётся как гарантия на весь товар; изменённый файл
|
||||
или режим обновления требует нового предпросмотра.
|
||||
|
||||
- YooKassa формирует смешанный чек корректно: товары передаются без НДС,
|
||||
а платная доставка — как услуга с НДС 22%. Локальный SDK принимает актуальный
|
||||
код ставки `11`, действующий с 1 января 2026 года.
|
||||
|
||||
## 3.3 build 0.42 — 13 октября 2026
|
||||
|
||||
### Ядро и панель
|
||||
|
||||
- Исправлены проверки готовности и базовая конфигурация сайта: страница 404
|
||||
берётся из настройки, навигация сохраняет явно пустые шаблоны, описание
|
||||
запроса расширяется с TINYTEXT до TEXT. Главная в sitemap использует
|
||||
собственные настройки и дату документа, учитывает запреты индексации;
|
||||
искусственная текущая дата в индексе sitemap убрана.
|
||||
- Core-обновления сохраняют проектные `robots.txt` и корневой `.htaccess`,
|
||||
включая применение старых подписанных патчей и откат. В стандартном Apache
|
||||
конфиге разрешены только публичные `phone-mask.js` и `auth.js` внутри
|
||||
закрытого `/system/`, остальные служебные файлы остаются недоступными.
|
||||
|
||||
- Убрана зависимость от PHP CLI при проверке сниппетов и заданий документов,
|
||||
а также при пересборке фильтров каталога. Нативные и старые фильтры
|
||||
пересобираются штатными сервисами с откатом при ошибке, без изменения цен
|
||||
и исходных характеристик. Консоль и сборщик используют общий поиск PHP CLI
|
||||
с поддержкой Windows/OpenServer, проверкой SAPI и без запуска Apache/FPM;
|
||||
поиск команд больше не требует `sh`.
|
||||
|
||||
- Проверка PHP в блоках больше не требует CLI и временных файлов: синтаксис
|
||||
разбирается встроенным парсером без выполнения кода. Исправлен отказ
|
||||
сохранения корректных блоков при PHP, работающем модулем Apache.
|
||||
|
||||
- Доработан richtext-редактор: вставка из Word с очисткой стилей и сохранением
|
||||
структуры, полноэкранный режим, предпросмотр, таблицы, очистка форматирования
|
||||
и локальное восстановление текста. Исправлены переключение заголовков
|
||||
и создание новых документов с незаполненными полями.
|
||||
|
||||
- Добавлены массовая очистка корзины медиа, очистка inline-стилей только
|
||||
в выбранных описаниях и заметные очереди внимания менеджеров.
|
||||
|
||||
### Каталог модулей
|
||||
|
||||
- Обновлены «Готовность сайта», «Контактные формы» и «Источники трафика».
|
||||
У трафика добавлены отчёт по страницам, CSV и график за 14 дней;
|
||||
уведомления форм учитывают все формы, не только первую.
|
||||
|
||||
### Выпуск
|
||||
|
||||
- Пересборка сохраняет опубликованные ZIP неизменённых модулей.
|
||||
При отсутствии исходного core 0.20 строится прямой патч от предыдущего
|
||||
оригинального core-архива, старые переходы остаются в каталоге.
|
||||
|
||||
## 3.3 build 0.41 — 7 сентября 2026
|
||||
|
||||
|
||||
+118
-74
@@ -2,29 +2,29 @@
|
||||
"format": "ave-core-package-v2",
|
||||
"product": "AVE.cms",
|
||||
"version": "3.3",
|
||||
"build": "0.41",
|
||||
"build": "0.42",
|
||||
"package": "core",
|
||||
"built_at": "2026-09-07T04:07:54+00:00",
|
||||
"built_at": "2026-10-13T15:01:23+00:00",
|
||||
"installed_modules": 0,
|
||||
"bundled_optional_modules": [],
|
||||
"optional_after_install": [
|
||||
"setup/"
|
||||
],
|
||||
"core_fingerprint": "7896669cffd40910cf19bca24b6784de5653e80ca4feee3c182c309971f9dcb8",
|
||||
"core_fingerprint": "9717e3250c604abd868d4149bce909b3df72fcc5e72b840dd1fe6aa238d527ab",
|
||||
"files": {
|
||||
".gitignore": "20d4fb177d47da6944a7a1c2dbf5918fd62766881aa52b00901f9054b1db4b49",
|
||||
".htaccess": "71c06b7bc41b09c347a562ca72768acaa0ee1ef8388c749fc67956c793d8a826",
|
||||
"CHANGELOG.md": "ae00742770c10c9ed32f028b7df22df1ce55627635569b38de6f2043e3bcd093",
|
||||
"README.md": "19b820f1b092780039aae011e133dc6a1e5f1cf213b435e0ff1962b592b9095b",
|
||||
".htaccess": "8f891b9bf25d536848f169a539d0a42baf5e9cd9599321542598874476ca3ad3",
|
||||
"CHANGELOG.md": "4595b4f99447da7ff5915807bc2aa6320edcf42594d195a2f0203cbfb2f224a9",
|
||||
"README.md": "0ef45b0e6da8dc25c4d64bbaaa2163ee5078302888e4515b75e60f827902665a",
|
||||
"adminx/.htaccess": "d03ea2d2972af8b7545a2d57c6253f303d15adeb30b775e161d887ca5ff73378",
|
||||
"adminx/Support/AdminLocale.php": "a7fe84d86d65c9f021106172b1789c8bc6221c36b2fe39cf883ca9a505e3e8ad",
|
||||
"adminx/Support/BulkActionExecutor.php": "82859d10eec39a53091ed46604d6819e7111b7d879e5f0f1595c26f8a7ff1a60",
|
||||
"adminx/Support/CodeEditor.php": "51532980b1990e595b796d884c1a8bcc46f77f88344386e5153b4bef4cae973c",
|
||||
"adminx/Support/GlobalSearchRegistry.php": "7f0cb38a76ecb4c71af2891176406b1d46004c7b778e44cd2fce8367407e9da1",
|
||||
"adminx/Support/InterfaceSettings.php": "752bd345a6595604b3ef87bd93bd0259a76f144ed887c7ff443a4fe53651494d",
|
||||
"adminx/Support/ModuleExtensions.php": "e7925a287351ef338a3733551e36646cb914c3ce069bed5f3a0c24aec7596b9a",
|
||||
"adminx/Support/ModuleExtensions.php": "ade2a624386485d051016758de17dd0420642fa1f2d3b529e0a17e10a976c2b7",
|
||||
"adminx/Support/Notifications.php": "ab08805d571cd3d8a4b2a09fdc6ce96fb4c7b5ec75691322cf8a4e1a142112a3",
|
||||
"adminx/Support/PhpCode.php": "a9b53565414c9784919ca06ce6d3926f939ddbb42b097f62bb8111b8d235dd71",
|
||||
"adminx/Support/PhpCode.php": "d5b37cc684e6780919bb6e11188fddf17f1b27f9382ecd41da24bfbccd04c3db",
|
||||
"adminx/Support/ReAuth.php": "f8c2146fb4b8f03b7d0cd2f3576e3f39fe2749823c5d6f1dca494268dcaba303",
|
||||
"adminx/Support/Roles.php": "6be335dd643317f77bd7c2db92f25895e601fe72818befd62f6aeb3ecac9fce7",
|
||||
"adminx/Support/RouteGuard.php": "e9e20c097c0db354851546585be72bfc71a5d14652871f8bd1160e845d4dab1b",
|
||||
@@ -39,7 +39,7 @@
|
||||
"adminx/Support/Twig/AdminTextNode.php": "e51d8d3ee49411f42c8a874cd283f60c1fa0af6506b34568a119ebe2d18d7268",
|
||||
"adminx/Support/ViewOverrideEditor.php": "9704c974f36b6f9e204b6c609582d61b0a0f65aa182a3f1381fe7a7d1c0b15e7",
|
||||
"adminx/Support/WorkItemContext.php": "d4b87b78ff1f2bdd89d77bc0483ebb46e5c11616635211ead9a43b34568e3a59",
|
||||
"adminx/assets/css/adminkit.css": "5cbcb6bf1e506d35afa6ffccbbc56385c8feb31a5eec52af5054c315335b71a2",
|
||||
"adminx/assets/css/adminkit.css": "073a02fb05b0b1d2f73d968d2f3b72dcedaee869d0b2a94c0548a164fc9a03b7",
|
||||
"adminx/assets/fonts/NunitoSans-Bold.woff2": "2d48580b7e1be60fa94b8092ebe86471fe0adfe5ca75242a7af2042ac9da825f",
|
||||
"adminx/assets/fonts/NunitoSans-ExtraBold.woff2": "66612ee9d1cdc341c9eec8a43104bbf515e7cc52821558bfc7dfcf33c8cb0cc1",
|
||||
"adminx/assets/fonts/NunitoSans-Regular.woff2": "878adc25278872933f004783f0ebbe092cebb3f9161aa30cffb2b9a8a07db040",
|
||||
@@ -51,12 +51,16 @@
|
||||
"adminx/assets/js/bulk-actions.js": "d7ee80678e6703aff2418a4679ba8e5027e7220cb089848bcced6d9f100b3aae",
|
||||
"adminx/assets/js/command-palette.js": "f62f0bad5ddf2b6e813e7eda094d73df63dce09a6e581025971e8f10430e8d9f",
|
||||
"adminx/assets/js/editor-codemirror.js": "90a04c219e84f5391a40b0d1fc807ecfe4ca582a198bc202bdc5aa6dccec51fa",
|
||||
"adminx/assets/js/editor-tiptap.js": "d5c7596bb17de87cb061c18daf629f322d03e2c81be19dab78897e93691d52de",
|
||||
"adminx/assets/js/editor-tiptap.js": "c3e79ed4fb4635b2bbdaebfa8ec8680c3fe1b64f3dee48354669d3baa5a29b07",
|
||||
"adminx/assets/js/media-picker.js": "74481279a7d352c7c72893b4043e02454ff3cc28b6582c5463dfa712b17e23a2",
|
||||
"adminx/assets/js/saved-views.js": "30c02121634f6f035dcff6a730e6fe300a1892fc9321971f142dcff600802f30",
|
||||
"adminx/assets/src/_rich-extensions.js": "bf6cdd7537852f271aa79f7f56a733d1bbb18b7340c3a81bfdb101cd56fbdaa9",
|
||||
"adminx/assets/src/_rich-recovery.js": "b9cae994ab21debae2d1194e1f48018aa1067401617aaac15800a58264a06bbb",
|
||||
"adminx/assets/src/_rich-tools.js": "01674cafdd98ed8b718fd0719d2b5e5eb681da7dd9dc2d18b57ccb35948d5e5f",
|
||||
"adminx/assets/src/_word-paste.js": "06671edab949ba84c8599cd25f4fc21923db3d2a1e98a02e6ed22d0b8610c816",
|
||||
"adminx/assets/src/bulk-actions.js": "29bcc568366ce4a86a71b12a0f8be050bd71a9eabb0f65af6d9ca57281db9201",
|
||||
"adminx/assets/src/command-palette.js": "ff86b0dc0f1ad59d551af772643dbe094cdfb2367d402eca49fd1a4c5b1bb9db",
|
||||
"adminx/assets/src/editor-tiptap.js": "1cb23f5262b50aa8ac0fbf19d98b77ce063af966f47d9b2ab6b0e5e3fadf28ae",
|
||||
"adminx/assets/src/editor-tiptap.js": "19a42915a34a5efcce867da59eeb442c15599713d1c1c4b15d85a64a21c4489f",
|
||||
"adminx/assets/vendor/codemirror/addon/dialog/dialog.css": "5df690d771f12118348562dd2a90060433ae2edeccd0908abcaa4480b1e3e468",
|
||||
"adminx/assets/vendor/codemirror/addon/dialog/dialog.js": "1eea0e500dce10d8594d8d681a91d1b71a4209355c9c29885e86e26b2d76685e",
|
||||
"adminx/assets/vendor/codemirror/addon/edit/closetag.js": "700b8aa7c46e040287e32ffd1edb86a42d09edc5b2a9594241590b154c0232bd",
|
||||
@@ -142,13 +146,15 @@
|
||||
"adminx/assets/vendor/tabler/fonts/tabler-icons.woff2": "cce741fcb6c526dfea3c0d48ff486279ac723617cd5b4de2bd5d726e94cef477",
|
||||
"adminx/assets/vendor/tabler/tabler-icons.min.css": "676509f34c06d34b75e4227e4c36105e5d220b458f609fffec247f20a8d72a8d",
|
||||
"adminx/core-permissions.php": "084fbff2f7625f0e1b6a6a15d00fce536212adbdf40f6a7fa2eef34cab54394b",
|
||||
"adminx/index.php": "267b4eb5ee65f64e1388acaf6e64ce27608b4b405744ff4fed6eda5f656bc7fb",
|
||||
"adminx/language/en/client.xml": "7a6a03448ca15df594f38e3ccb63f11cabc9c3c5ce301022074a14490e3dbee5",
|
||||
"adminx/index.php": "4d1796147aa9d0cd32604e85517e1c5937755fb0a979f4a346c4088d68e0a2ec",
|
||||
"adminx/language/en/attention.xml": "93824acb0f746a10620a09cb44fbe7aec4e029cdc049afadf1a9c81633d575d8",
|
||||
"adminx/language/en/client.xml": "3b7e8ded4253fd3e28199564563f8f61506e16a3d15373cb2989b4b2e4ab907a",
|
||||
"adminx/language/en/common.xml": "e5f9972786f1d2a8e05f86913b07c361b82811f99cf51bee6d38e9ed275fc995",
|
||||
"adminx/language/en/interface.xml": "80ccb298e1ae8ea31c533832072b99c1eb1369871f6d1b357635aa59622d9d52",
|
||||
"adminx/language/en/runtime.xml": "68b00572d69484240e1716007ce64c2febe66cae4e021b09c428e758de990ae2",
|
||||
"adminx/language/en/section-help.xml": "f82ba8d3b78d06d26b92afb81e036b241c8dd3ef9e4ded6158a0be10491e90b9",
|
||||
"adminx/language/ru/client.xml": "606dac2a0fec6fdb5fbd4ca8201d1fef5f6607556a27d249688646c3dc595fe4",
|
||||
"adminx/language/ru/attention.xml": "fc1bf362f801ddfa068ba853abf216fec5c35e457ec2170667999aa18e73d387",
|
||||
"adminx/language/ru/client.xml": "3be6c0998f4ac1d624c7d8d5861fb7468affa6634f90b35096d4597d6d4de836",
|
||||
"adminx/language/ru/common.xml": "d27d863bf82591dbe857317d97a888a23980c87529e2b4c1d4125a6c0ba20966",
|
||||
"adminx/language/ru/interface.xml": "03fa6351745d2e47c2b3407da65c8b7b048b8e862409dc9bb934e939b9439c75",
|
||||
"adminx/language/ru/runtime.xml": "3c411a18695af7a63dd0685a8351ac3a51695aea7d1fa2550fb24cc9e3e82be6",
|
||||
@@ -164,13 +170,13 @@
|
||||
"adminx/modules/Blocks/Controller.php": "5665327885171e07f87823b473f97e22f416ab896471a25ff7a2dff498a30d22",
|
||||
"adminx/modules/Blocks/Model.php": "69dcdd7488f308349741bff0911066c5f1ceace23216ea408fbc79da4a73bc14",
|
||||
"adminx/modules/Blocks/Revisions.php": "9c820024215c0760ba93fbed902b9cbbfdbe402fc9ac514e9908d06fcb794817",
|
||||
"adminx/modules/Blocks/Syntax.php": "9b2eb8d40171633cdbef6cf21545958f801f4983d62176e9256bec8e4763a624",
|
||||
"adminx/modules/Blocks/Syntax.php": "5e1d9a4f4eece94db48734ed6a3866729adf57100c4aa345cd70ac1a393741b0",
|
||||
"adminx/modules/Blocks/assets/blocks.css": "13ec7cd2b5a9c2abda0ff2d1419fed50dc1e122edf8ec278e11cb12d75b8ff38",
|
||||
"adminx/modules/Blocks/assets/blocks.js": "22d512d111a1f0dcae0a649bc1d49439c733151e87382f55dd5d5d17863fa61c",
|
||||
"adminx/modules/Blocks/language/en/interface.xml": "6b358b561f81febe1a3284045ff20132737e624a04b2481b51681e258f748e22",
|
||||
"adminx/modules/Blocks/language/en/runtime.xml": "cd76ddcfb5751496dc4cc160dc2c6f38ae5699a3a5c0041b5fb214fa434f04e1",
|
||||
"adminx/modules/Blocks/language/en/runtime.xml": "021a5840f5d41b315663c1961c9df93c377ecb1dc9ea2bbac0586bd31991ea57",
|
||||
"adminx/modules/Blocks/language/ru/interface.xml": "6048c75a95fb474287cf4903fe1b82f2608741b355134be885eadf511da5b656",
|
||||
"adminx/modules/Blocks/language/ru/runtime.xml": "594a41e03eb86238ad18a45dc5c0f71b15e8f8eecc2a912e3f9fb5439bbedc4c",
|
||||
"adminx/modules/Blocks/language/ru/runtime.xml": "918f9471b440c7b3eaee676ee0d59e93fd4e5c34928828f02d29d09234fd4245",
|
||||
"adminx/modules/Blocks/migrations/001_create_sysblocks_tables.sql": "492d9c428f5baa305c420821cef852ced24cca6fb661d12ac57d79453103a335",
|
||||
"adminx/modules/Blocks/migrations/002_create_sysblock_revisions.sql": "7a5d78fe304edcb48f9bf8891c99f60c1c295ead95444bc59883a8945b074f35",
|
||||
"adminx/modules/Blocks/migrations/003_native_public_document_context.sql": "3bc0481b3f85c799436a7f70262b8bd3b5cb87f1b3c4c87ac30393c03cce78d2",
|
||||
@@ -183,11 +189,11 @@
|
||||
"adminx/modules/Blocks/migrations/010_disable_retired_catalog_endpoints.sql": "c80e95a32c596ed073ca1e15ae027f2e3f813d67867c31926ea5060e3ff5dace",
|
||||
"adminx/modules/Blocks/migrations/011_sysblock_editor_mode.php": "e8c4046116dc2241b1eed3cb677515497fc07c25425628e4d349688df698743d",
|
||||
"adminx/modules/Blocks/migrations/012_merge_visual_blocks.php": "5c9cd6efd66a027134621859da1059b2c403bacb732885d6b405699cbce8fc14",
|
||||
"adminx/modules/Blocks/module.php": "53eb434566d820814f5b25382200b75ed02b0067c066c8f8cb62bee13529b964",
|
||||
"adminx/modules/Blocks/module.php": "d82b66828bbd549eb043d177b6f7673552942eb23220ca156d751d0074b251a1",
|
||||
"adminx/modules/Blocks/view/index.twig": "39f9225b6a95c65549d0c861afb7db7879ca338c251290653dded6ab7cb715cc",
|
||||
"adminx/modules/Catalog/Controller.php": "f9bf3bf72e15cf0b886157a9b421c4f6a2364b35a70464e7285259a151d51bbe",
|
||||
"adminx/modules/Catalog/EditorContextLinks.php": "0319a6f237aa3aa627aba1f44a3efe4c6de007b26a9e75ba0df6b3b2ccc5478e",
|
||||
"adminx/modules/Catalog/Model.php": "937cec97bcb67aad4ce4da40e9da2b1ad691de6969ae5b12bbec2118b997653d",
|
||||
"adminx/modules/Catalog/Model.php": "cf86b67f956ae1c693e25133d2959a3bed0fdc6e254bde580ff6418f72641f44",
|
||||
"adminx/modules/Catalog/assets/catalog-drawer.css": "27af3ab3f1b3cfe6425467528350489a870388b34427a79b0b2c4e94af9f1cd6",
|
||||
"adminx/modules/Catalog/assets/catalog.css": "49e7a33fc444f8330806e1cc5cbb0d523b673c64faafd25dbf0fe33067643efd",
|
||||
"adminx/modules/Catalog/assets/catalog.js": "fe5b3f7df7c8c70348badc433dab5b91268a69bf081a2bc87e52dd253923fad4",
|
||||
@@ -206,7 +212,7 @@
|
||||
"adminx/modules/Catalog/language/ru/runtime.xml": "fe59945f4ceb2bf2d85699623369e7e5889613b8cecd8551a390c78023531d14",
|
||||
"adminx/modules/Catalog/migrations/005_add_catalog_purpose.sql": "006ff237e25042a4fa9b9f74d5079741eeee1aa52db0cd996ea4c8b817e6700f",
|
||||
"adminx/modules/Catalog/migrations/006_add_item_sources.php": "97a023140d1380ef2ef6a2ab1dd1a86b0901689fa2f1ccdf0fc76608dd18e546",
|
||||
"adminx/modules/Catalog/module.php": "f393b86b20c962f5262840381a397533e34e4c7f5b8bb786ae9aaa6de70f5d05",
|
||||
"adminx/modules/Catalog/module.php": "a6e75ae55c4be87df1c752b55b31a9efa1553ec082305cefa536e52c0967655e",
|
||||
"adminx/modules/Catalog/view/_context-links.twig": "fa079e6dbc48c87a901dcb9272b00728b0697884c6607ba93c36fc56bef2ea5f",
|
||||
"adminx/modules/Catalog/view/_product-role-preview.twig": "9c3b5ce52dc23fe5af3acf682ee1470a07bef1bd8cee8cca946fb36745a3f026",
|
||||
"adminx/modules/Catalog/view/_product-roles.twig": "905b2ed079e74e481e0ae7aa9782cc2d339dda75109be097b3994aef85a9571a",
|
||||
@@ -215,7 +221,7 @@
|
||||
"adminx/modules/Catalog/view/index.twig": "754c7c4dc81176e8c99b3d9e8c1fda2f62f03df341c3df1f7393f72d43c6f8e2",
|
||||
"adminx/modules/Console/Controller.php": "af0309a50d7f4366279be8ea12959cf7718a3c68b895a65ec5846554fd6a4857",
|
||||
"adminx/modules/Console/Model.php": "53aad480b8d4e5d68c4e9345d219ea5cbff6d04eee7ce4aa15deab6f00dac1cc",
|
||||
"adminx/modules/Console/Runner.php": "6c8c216003d8228b091683acaae99791f612a93d4150542c2ff52c67dd029a33",
|
||||
"adminx/modules/Console/Runner.php": "b6685e349cee269037b7fc8f366da4324c6bca6d37dab2dcb7f46a2726211e63",
|
||||
"adminx/modules/Console/assets/console.css": "e2645bf876bfeafc15e6f4e5ad9889adb773aea35ddc42ff151d8ed1f8866eef",
|
||||
"adminx/modules/Console/assets/console.js": "f0f2cec892c4b5cdeb11730bf42e9cf68ff779974338086cdf92618ebcacdb10",
|
||||
"adminx/modules/Console/language/en/client.xml": "02bef78a30b83340eb8968297639b6283dc642236e853bef413e5b7d2ef70315",
|
||||
@@ -225,7 +231,7 @@
|
||||
"adminx/modules/Console/language/ru/interface.xml": "55f68a6bb3fea3fbaf953482b36593a1f3e585d3a99b6e2da30d6c097915ed08",
|
||||
"adminx/modules/Console/language/ru/runtime.xml": "441ebbfbd7f0e21cef5d298ec5fe314d3236cddd57e05f05fe60eb1ac3d1b8b0",
|
||||
"adminx/modules/Console/migrations/001_create_console_snippets.sql": "70272e04d744eca3d48416d9ffc83d78ad00a02d2a6f68bc388618e350c427e5",
|
||||
"adminx/modules/Console/module.php": "c6c883cc51ee1a27460f71eca88edd44726f2be493934b84adeee1b7d8ccf8dc",
|
||||
"adminx/modules/Console/module.php": "f9f3ea405583e293be5c96c33c11a0bc70da996e822dbdc7c803d656d762a144",
|
||||
"adminx/modules/Console/view/index.twig": "54f877d5feea19bc4f4b618eea7cf7523a2266bb2e3909f3fb723edd16d4a326",
|
||||
"adminx/modules/Customers/Controller.php": "0d9b8dfaf794c378ef8da5ddf2084e461b54ce0ed7eb490bda62c30574f05763",
|
||||
"adminx/modules/Customers/CustomerCenter.php": "af34954e6330bc9036c04fb56b439b143f591e95b83124d95a034d87d6870bdf",
|
||||
@@ -252,8 +258,11 @@
|
||||
"adminx/modules/Customers/view/customer-center-detail.twig": "2239602955041c4eebe704c70e835fd45272f721abd28bf254904901bd77a4eb",
|
||||
"adminx/modules/Customers/view/customer-center.twig": "223e657861275fbfe482ce624a6216a91e8df893cd1c5fe58a2999606468addf",
|
||||
"adminx/modules/Customers/view/index.twig": "20868f4edf7f9ca7770a733d4593eb56ffb225199940212d60aa709f2c760070",
|
||||
"adminx/modules/Dashboard/Controller.php": "7e6ad0610df7581bb010cee574d8d093c4737af439bd2b0763142f50ddee1c54",
|
||||
"adminx/modules/Dashboard/Attention.php": "300eb3b32a92ce14e7f3260cdf82824949bfe4dec799f5dca63ea6e0293877ec",
|
||||
"adminx/modules/Dashboard/Controller.php": "3f061b93f22ed95b2382e4ae749edab812c675dc979946a27011e91ad4bd28aa",
|
||||
"adminx/modules/Dashboard/Widgets.php": "56fc7ba7c1f9e30d7eae8c61524b6441b1c706a2cd56b1db883bb8bff7186eed",
|
||||
"adminx/modules/Dashboard/assets/attention.css": "da72506b21621dfbfa16b1acd46c2afd562a2b3b29c7be51999849fe74dc2066",
|
||||
"adminx/modules/Dashboard/assets/attention.js": "f6625ed4585f00e698dc92e04c9dc5ba989a2feb5f1b590fc5e0d732c9654e6d",
|
||||
"adminx/modules/Dashboard/assets/dashboard.css": "8e28b56cac2e55d9b57304ea6e265fd94a52400674c082fbdd78f1eed1872424",
|
||||
"adminx/modules/Dashboard/assets/dashboard.js": "ac683db96673fb4f70b8cb2d23ab7eda2251f96cc2852223f5ee68bd11a911e7",
|
||||
"adminx/modules/Dashboard/language/en/common.xml": "10445e46f7445366125f761c2d29567637d6d2c9ba1e9766136280ff28c3d465",
|
||||
@@ -262,8 +271,8 @@
|
||||
"adminx/modules/Dashboard/language/ru/common.xml": "877574f94ff436cbd64a81fd1b8448efb75244b2fbdc258e9a57f4eaf47a6078",
|
||||
"adminx/modules/Dashboard/language/ru/interface.xml": "b1a030382924d5a7adb4e0f9db56da95723b055791da48f18ff75bf535179cbd",
|
||||
"adminx/modules/Dashboard/language/ru/runtime.xml": "700418374efc27723967f14c0db8a7314586c3e4de819ddda1a734dfaa4a4966",
|
||||
"adminx/modules/Dashboard/module.php": "3c6939a1b5d07948a45f18287b5f95685a542db68cd18ac7685735e3c2572845",
|
||||
"adminx/modules/Dashboard/view/index.twig": "00ef6338aa822c33acbd74d3cf38f4db35e2999a7d9e053115fe529fcaccf8d0",
|
||||
"adminx/modules/Dashboard/module.php": "b46cd918395bc2cbef1cf6979f95e7b63c9f33ad9d45a4340b1cc727faf1db3f",
|
||||
"adminx/modules/Dashboard/view/index.twig": "92ae9d59eee15ab437b2884d1c5cf7f5ee5734d8f6e4625ca8bcb17feb900af6",
|
||||
"adminx/modules/Dashboard/view/widgets/recent-documents.twig": "d03f36f047574f8ce57fdcfd79b26d9b20ca01dba69ce7c71a3e9a277f0105d0",
|
||||
"adminx/modules/Dashboard/view/widgets/shortcuts.twig": "af165425ea7889cdc3edbd72c7f0dd9dc5c4c06486bc4ab37816112cfb696718",
|
||||
"adminx/modules/Dashboard/view/widgets/system-kpis.twig": "52dab540a189443b76811ac4b457a566cf90134366749aa9c7d82e2d23ffdaa3",
|
||||
@@ -296,22 +305,22 @@
|
||||
"adminx/modules/Directories/migrations/001_create_directories.sql": "d1f84fccf75892fc1dcfe6b770112101e233d96d9f4866a08d1977646a690530",
|
||||
"adminx/modules/Directories/module.php": "f6b7f0bfb96d1c2c7aa52c51e60abaa09cdba7a4309d8363394a3a02a8df96a0",
|
||||
"adminx/modules/Directories/view/index.twig": "0bde23bc578b9ffad3c47534c304c0016b661a36588d6d3c9ffb92e2daa388a0",
|
||||
"adminx/modules/Documents/BulkEditor.php": "24a2c33a50e5025e4f8a15426bf8d4f6ac21e6574ef636f30fb5c6fad0249133",
|
||||
"adminx/modules/Documents/Controller.php": "283aec04707c7b7fdd01e2ba69c1961b925f259655e9a455889bdd5dfc66f4fe",
|
||||
"adminx/modules/Documents/BulkEditor.php": "64c010ee7ce1b79d9c1af90d0b06d06a252a25764ddc1b61472f6b8bfcbee399",
|
||||
"adminx/modules/Documents/Controller.php": "aebb52d4dfe9ae449c620e6f26f76ed64af6260c8d65d122fe40cc63b65ee50e",
|
||||
"adminx/modules/Documents/DocumentHookRunner.php": "eb915de91391858987386925da37d943318fea1f46893de1b3fcc0957ce43e07",
|
||||
"adminx/modules/Documents/DocumentMediaReplacement.php": "e97d960c852162e655a4389cc5f477f0178597da50e733b00bdf5bcab076c65d",
|
||||
"adminx/modules/Documents/EditConflict.php": "ebc3ebb92c3fc3ccf3909d2ff517014fc888ee20576a520b4979f68bde632b97",
|
||||
"adminx/modules/Documents/Model.php": "ff03bcfe54618d1b83a6780c57cb86816ea25879351f1beb4e847cbb1ae4c9a5",
|
||||
"adminx/modules/Documents/Model.php": "5f1bc233f99e55a91f48a78c66a05e1df002f05d190cb6ceb15f00380e2c4a22",
|
||||
"adminx/modules/Documents/Revisions.php": "f17f0f7ebb4fc3ccc44695e4a1ca7093dfa3f4fe3f9c895e17fa8631567769fe",
|
||||
"adminx/modules/Documents/assets/documents.css": "88c0911854e90ed0d71b005ec95560520ba8db2f44c8e3a457c074a3b5aa323d",
|
||||
"adminx/modules/Documents/assets/documents.js": "9d0abe758f84ed6fe4f262c335628ffc07678a012b579aacb5787a250cac8065",
|
||||
"adminx/modules/Documents/assets/documents.css": "378ce4712ceeb9e9cc899d81c9fc13848b7b288ad68fbc1eb31e9a5bdc5dbad8",
|
||||
"adminx/modules/Documents/assets/documents.js": "9d7bddf7832f3187a4b04c494c01fea983854089d91cb779453f7ef102c5e483",
|
||||
"adminx/modules/Documents/assets/redirects.js": "95ca395c6b4fd3814da37450c27da351e75f509e26421c975dd8e0ee5f657efd",
|
||||
"adminx/modules/Documents/language/en/bulk-editor.xml": "b7af92bd151c7899a56cc461f4a09edaa842213cb6e6b4b4363af31aee228e50",
|
||||
"adminx/modules/Documents/language/en/bulk-editor.xml": "ddbb6c41d98ea5513c3359c4d8b818b00a15294100c68285e38c8c3954a04391",
|
||||
"adminx/modules/Documents/language/en/client.xml": "a139276798beac506ad56539ca0ead06e7cd7dc69149c99e04d6cac12d72bde6",
|
||||
"adminx/modules/Documents/language/en/interface.xml": "87b858717419a48ed12e1181183b8de0285483df4c9de37403730f8a7c034b52",
|
||||
"adminx/modules/Documents/language/en/runtime.xml": "7009fa27d67ab168fecd9d9bf182216dfb0397d9c006eb6563ee3c464daa5b5e",
|
||||
"adminx/modules/Documents/language/en/zz-interface.xml": "839ea37d585ff10d7b819a4fcd4bda876c94d51019489cc8d35af15fbc905f2b",
|
||||
"adminx/modules/Documents/language/ru/bulk-editor.xml": "29500cbda63189ed9a0afdb57b20f998241594b254d0225a776a8be32d108834",
|
||||
"adminx/modules/Documents/language/ru/bulk-editor.xml": "72cba2efceb35b0d22eb7bdcefee08c173fd6facc784abfc1a47273a41fa442a",
|
||||
"adminx/modules/Documents/language/ru/client.xml": "e265a5a3c8f6bf973dbde8f3f8970ab9cc88557cd71df6449f3b4a93dbbaffec",
|
||||
"adminx/modules/Documents/language/ru/interface.xml": "f8d60edba8ac5b6d0eb6deaa3b9b89733cd0b8d3eb951c105a9874ea6769c305",
|
||||
"adminx/modules/Documents/language/ru/runtime.xml": "6c3e9a2dbf862bef5a76ff0bb600084fcd9d5d1a7f0cbfd02377242b74fcd759",
|
||||
@@ -333,11 +342,12 @@
|
||||
"adminx/modules/Documents/migrations/015_document_creation_presets.sql": "a123f8867ab6f6346002d08317acac37062c2792913ad14102c81812d45571bd",
|
||||
"adminx/modules/Documents/migrations/016_reconcile_document_content_columns.php": "06671bb26bf29479dce24888fb3712c0d20e2aac5b3d70196604ce4097c875fc",
|
||||
"adminx/modules/Documents/migrations/017_document_public_visibility.sql": "2a8298ade4af686a4aba41bcda0aaff5504c40f3140bf3ae710e8be727f38e18",
|
||||
"adminx/modules/Documents/module.php": "81a2c35cb77fc142972a40d33e3fba3f712a6af886e3b4538246f1058f8ec38b",
|
||||
"adminx/modules/Documents/view/_product_panels.twig": "d0e681a94f6e48c061dbc61cc47c384bd52c4eb66d648e36784b68d0c8ada603",
|
||||
"adminx/modules/Documents/module.php": "d3876535fd0e7c08f124a20a1f00693955730e3908af98efdda1f0268cef0fd7",
|
||||
"adminx/modules/Documents/view/_product_package_fields.twig": "04e592a9b54d8c1488ef3980574e812fc714fbd4814b8573a68513f34b6d001b",
|
||||
"adminx/modules/Documents/view/_product_panels.twig": "3f047cbebbd81326290d47751454cd3e8e718c1246120338fbd399ec81b41ae8",
|
||||
"adminx/modules/Documents/view/api.twig": "a035a2d566298587ce8530c98915e80e4e7f9b0f41a60e0e55d4547b996a6c40",
|
||||
"adminx/modules/Documents/view/bulk-editor.twig": "270d6a2e52aa6aa8efed73b4b5c311e8e8f7d8e0cf82ef1e510e7e6fb4d3f029",
|
||||
"adminx/modules/Documents/view/edit.twig": "cb4b983e868912bd3d072e3cbc6b57f21616e7d35cabacec7dca8b9e40ce09ef",
|
||||
"adminx/modules/Documents/view/bulk-editor.twig": "17aa7b3383f4943f2304136a823f318d9bb02f44c1a6a749c141971f2e6c8296",
|
||||
"adminx/modules/Documents/view/edit.twig": "41967969a80e1bc5e404886074b24b18470dbdad44fd0e735ba9cc79da17312e",
|
||||
"adminx/modules/Documents/view/index.twig": "ccf2bd0d384091f8a98ead4e93a8ad389a515176d17e5b5ee661e7e3ea0908de",
|
||||
"adminx/modules/Documents/view/redirects.twig": "4a4903edc81e4770220852d9be9f02a983ddde6f829a4360ea01d3b68456582b",
|
||||
"adminx/modules/Documents/view/views.twig": "821f19085ba3997b6ef8f005e838f6fd449f0b32bce0d2804271311856cce8f0",
|
||||
@@ -387,28 +397,30 @@
|
||||
"adminx/modules/Media/ImagePresets.php": "8874cbf436a1a33f655c9a395b5f16e143c04c7df1d957c43e3d4ce3ae0645d5",
|
||||
"adminx/modules/Media/MediaAudit.php": "2deeacd899b4410cc620b40b6e448c388f54eb553b5a57a23475ac0b5cae02f3",
|
||||
"adminx/modules/Media/MediaSearchIndex.php": "7625627ecc00b74ea75aa1818dec55c468515c966dc00333fe1f2706eef154cc",
|
||||
"adminx/modules/Media/MediaTrash.php": "3251e6cccd092d0c04c0cac70838c070210443c87dd0967c84a4e323bb15c5fb",
|
||||
"adminx/modules/Media/MediaTrash.php": "308e94caf5fc8213860d56c235444421937a5d6a5a25cda55d9a490d0165d7e4",
|
||||
"adminx/modules/Media/MediaUsageIndex.php": "d39d232bc56352c9cc2335a2e3bd2a7b7f415709c70ece4b38f1a462c2969415",
|
||||
"adminx/modules/Media/Model.php": "1ac78ca828c6f492857e731a715d54939525dd29545bf81fb4065fcc02f0bc50",
|
||||
"adminx/modules/Media/assets/media.css": "e7db9f602bc1a46644ccf172f977ebe0798c572c73d33f64584c9569d531611f",
|
||||
"adminx/modules/Media/assets/media.js": "2fdd0c2541150e5c2bbb4ac3ef54f1f40edc029d48383297d432af0431607111",
|
||||
"adminx/modules/Media/assets/media.css": "aaaad15f150c7b1388a6a3c5eb1f124d01d6d58517ad9d6cf0c92c54f9e817af",
|
||||
"adminx/modules/Media/assets/media.js": "43687587d315612c6ae4e2910688b724b717cbc01ea191345fe94ad6289bc717",
|
||||
"adminx/modules/Media/language/en/analyzer.xml": "5c6e8bec29fe73d0403cf041c7580800ecc2677d94f2148ffaf44c04d337c6d4",
|
||||
"adminx/modules/Media/language/en/archive_processing.xml": "567d6e3571a36d9d75324b1b59cf7e966ea6eb00602eaf5cc57876cbc5536c21",
|
||||
"adminx/modules/Media/language/en/client.xml": "06cecdb6cacdd0871334a3da0a3c7be27d7f835e2a048d37ea31ae4f37469270",
|
||||
"adminx/modules/Media/language/en/improvements.xml": "5873d8eb9c43db0a584161ae3f086dfd101a8df25bbd4d518a2fd283f4f8cb3d",
|
||||
"adminx/modules/Media/language/en/interface.xml": "2eab0a0d6acf094c43c690e4c09683378a26dfaec7e258c23f3f7bfb1fc9e9a7",
|
||||
"adminx/modules/Media/language/en/runtime.xml": "8768433d736f0e8b3508b1a68de1281b2f841c2a9decbd8195603a31bff3a92f",
|
||||
"adminx/modules/Media/language/ru/analyzer.xml": "c0102f4d337a3279b412e56e508a1d8d4b1196bdcebecd46bc3e95e56ba520ae",
|
||||
"adminx/modules/Media/language/ru/archive_processing.xml": "1ce7412716cfceccfba36f346f972d0c3f7a936da13b5acd9bd5bccd5baf9497",
|
||||
"adminx/modules/Media/language/ru/client.xml": "272d1eddafc77c79533be4fa89d5daa2d560c1860bc2bab4a1fbf6d609849768",
|
||||
"adminx/modules/Media/language/ru/improvements.xml": "d3b92183909c2159debe29d4594544129f3981d253dded9f20704e0394b158a5",
|
||||
"adminx/modules/Media/language/ru/interface.xml": "743b1b2e5f498ce05225aca69f866abe2f24765a260baa9d81c1947954029fa9",
|
||||
"adminx/modules/Media/language/ru/runtime.xml": "538db360abf2d61e32d6476846173ea4f5115703021362314a9100df3854d6f7",
|
||||
"adminx/modules/Media/migrations/001_image_presets.sql": "303dec5f331135ab3fda9d264b7a7b674b987cdefa0a7904c544a5d63554f15a",
|
||||
"adminx/modules/Media/migrations/002_media_search_index.sql": "e84168429acdd900d494c91e398a8974ae97023d8bee3385321c2947dd627378",
|
||||
"adminx/modules/Media/module.php": "45634e3fc5bd65c1bf545d1aa79912692a38297d390288d956af0f646dbc8c1c",
|
||||
"adminx/modules/Media/module.php": "8e602a4b6e18d759683ec05122e7ecac41ec1d317d993f7246d42c3f3ef75230",
|
||||
"adminx/modules/Media/view/audit.twig": "06936cd87c80b5c7f6ecec8729f227c2ab3fd492e61b11ae7040e6423939b518",
|
||||
"adminx/modules/Media/view/file.twig": "b317dacbc61411831a9029e30fb1ff3457fa1d34164df3b891b6a9a2fdbe32ce",
|
||||
"adminx/modules/Media/view/index.twig": "c3a4c42477df3a10fcc8ee558bb03fe25782a58f6f47734cb078686798a2d06a",
|
||||
"adminx/modules/Media/view/trash.twig": "991f5db2deac791b9d378d2c06bc85d69d4afe3960202c57929e335ce4fd8a90",
|
||||
"adminx/modules/Media/view/trash.twig": "1ec8e74a26ac229d0f0a17e77ac400bf0d5fa933848a7bee8e45ed38ee81721e",
|
||||
"adminx/modules/Modules/Controller.php": "de3c800b32a05ab41dac31d4964242fb0635aef6ef32f69a332509159b8e755d",
|
||||
"adminx/modules/Modules/ModuleArchiveInstaller.php": "22f015a6695de9f4fea12a580adfb76837e59ee4e9de92c153782c5a40a97560",
|
||||
"adminx/modules/Modules/ModuleRepository.php": "29ced1e9d5c36dab4f0018c10fab1b80282b8d225805bf13f075e148016c45f9",
|
||||
@@ -425,7 +437,7 @@
|
||||
"adminx/modules/Modules/module.php": "710e4441a369d89c29e99a68f94b591dadb2b3df1d951ca6dcd16385b428941c",
|
||||
"adminx/modules/Modules/view/index.twig": "2ca402c0886f611129e884129f6e0b0022e14a8c385d5d4ad55abe49ee39f5c2",
|
||||
"adminx/modules/Navigation/Controller.php": "968da27af968f5f6e29b0e015f4c5df438c17735daf646a9d6343c055acfd48c",
|
||||
"adminx/modules/Navigation/Model.php": "58d08733e246f92abc02d9ccfa74b88627970588fd2223688a59d03a4d6c4b87",
|
||||
"adminx/modules/Navigation/Model.php": "1f181b9e57ffb34c65967678da115e0edac194558ce11e020e61b2b12673d69f",
|
||||
"adminx/modules/Navigation/Revisions.php": "165a2e2d8ad218c1437fffbed9297b7be25d0ae6567f0d72aa78da872cefa1b8",
|
||||
"adminx/modules/Navigation/assets/navigation.css": "e7ad029b253b19546ed2c0eb2a3527dbb3b03a56db13c6f3089c3da9c12933ca",
|
||||
"adminx/modules/Navigation/assets/navigation.js": "72d68867419fab1b3e7e822a42a7fbcd866ccdbef8fa3051a245e23f23f5ec30",
|
||||
@@ -437,7 +449,7 @@
|
||||
"adminx/modules/Navigation/language/ru/runtime.xml": "45d994c39fa7fa092525ed4a61d6775008f3993f4cdcdd615d61e5f15a749cff",
|
||||
"adminx/modules/Navigation/migrations/001_create_navigation_tables.sql": "43c937b8d7d7f5b70569c591db58ee8592386b988e8cf148fae723b1f68b3012",
|
||||
"adminx/modules/Navigation/migrations/002_navigation_revisions.sql": "cf24c9e9cdb95c2ba4ccea63ec4375d62f29370d5ef909a82a0c990624b8ad02",
|
||||
"adminx/modules/Navigation/module.php": "5eb07314b6c156893288f1616e248ac316ae3a3b561c996d63e772e4ea4e8b6f",
|
||||
"adminx/modules/Navigation/module.php": "bbee9c86df3b2f00abb4f172ed4cc80bef406709066d1c95f1c6ed735387ecfa",
|
||||
"adminx/modules/Navigation/view/index.twig": "b7dd1784238fc53456b1676a7bfd4d14c2d1efdd7f15cee34c2c4613bd0e7590",
|
||||
"adminx/modules/PublicSite/Controller.php": "8c00240eca08149df4572732d1e8b03eeca5910d40f03c2742021035f16aa621",
|
||||
"adminx/modules/PublicSite/Model.php": "912fcd71f2ba7215fc9a411882c1eb5e0953b6ebaedfbb92f01b7d3de477a015",
|
||||
@@ -485,7 +497,8 @@
|
||||
"adminx/modules/Requests/migrations/012_condition_value_sources.php": "c874ea024343e4d698cda7a138e13b23776a58f51706c2a16102ded1234b2aa2",
|
||||
"adminx/modules/Requests/migrations/013_sort_rules.php": "4c58c56d9766c16517bc2ceee1d6313b1728e564a332fe9a5cdf69a04d7afe70",
|
||||
"adminx/modules/Requests/migrations/014_request_revisions.sql": "1149785b788aeca0e6e074dbb0251f92e3813b4083170b7731348a7f8edf6bf5",
|
||||
"adminx/modules/Requests/module.php": "d18ef457c854b5d223c731aeac386a806c14bc6fa0941d9f79911f2b15334060",
|
||||
"adminx/modules/Requests/migrations/015_request_description_capacity.php": "c09090d46dffb50aa6020f9bd8b31656d64b04e053769068ba85539584d78764",
|
||||
"adminx/modules/Requests/module.php": "e9c546b1ada48e01a518faa3bdbc001ad3d8d86a5cba893f04bc2544026e6e4e",
|
||||
"adminx/modules/Requests/view/edit.twig": "b4ffac2dcbdc0895e00befe395c29e3c9d71bfe47852998e89f6ba8fe4e0d0a6",
|
||||
"adminx/modules/Requests/view/index.twig": "164490c16e041b928e1ecf604915656399690055d1f7de4bc88caf5f5abd485d",
|
||||
"adminx/modules/Requests/view/native-audit.twig": "42120fd41aa4afa39560fea3f2625a4e78a61cb4f9ccc0c0388ca319d4f39c06",
|
||||
@@ -500,7 +513,7 @@
|
||||
"adminx/modules/Rubrics/FieldEditors/ListFieldEditor.php": "bc1c7ead2ed6666f7861418ed2222166a44870edc5efb401226a5b9d80e549a2",
|
||||
"adminx/modules/Rubrics/FieldEditors/MediaFieldEditor.php": "e63ba6ea4c569ac5237f057ee722e5a936a2235d670eb29fdcf171f61a95e3ad",
|
||||
"adminx/modules/Rubrics/FieldEditors/RelationFieldEditor.php": "9504549a3b81893ec8a114a286a960450bbd302cb2f8f9af97660c8332f91925",
|
||||
"adminx/modules/Rubrics/FieldEditors/SimpleFieldEditor.php": "e961e7ab2bf3a16d6d9131513f11d2d4b9520ecee5572ae7e1b7ae8475d1cacb",
|
||||
"adminx/modules/Rubrics/FieldEditors/SimpleFieldEditor.php": "a40e6d4e0cc8c3b9b937328c12f3d4ec82feea217780dff3ef5c4df9469bf299",
|
||||
"adminx/modules/Rubrics/FieldTypes.php": "c6cc64b0e1b1fb29b65275d337e5747b9f9f68ca46171e7e7f07129cd27b8409",
|
||||
"adminx/modules/Rubrics/Model.php": "85aac47a064c1d2d95cc3e620e106c03989ebe9cf237cb4516253c338e2f49a9",
|
||||
"adminx/modules/Rubrics/RubricFieldPresets.php": "80109bd9f066f077ad19e48d7c7599e4dbf7b1d0c06c54db9fc921992bca2e77",
|
||||
@@ -533,7 +546,7 @@
|
||||
"adminx/modules/Rubrics/migrations/015_merge_directory_permissions.php": "648b3e232e9426f84d69238295515342e42695e46288b901950cde2e8979b0e6",
|
||||
"adminx/modules/Rubrics/migrations/016_rubric_trash.sql": "09385e8d14d5d4d977db59c60035338404019783cc44c6c784cc2172faab08f0",
|
||||
"adminx/modules/Rubrics/migrations/017_rubric_public_visibility.sql": "2a6ef9686235b453807701c24d1cd8dc81ddfdbb80824a7ff10a70ca3f949827",
|
||||
"adminx/modules/Rubrics/module.php": "a7ebe05a45eb4270f057f2d59f717b2f06df0350a0e86041f2319c70895b9a5b",
|
||||
"adminx/modules/Rubrics/module.php": "361f4dc64b887dd12efb7901c4967b54b7c0aff13e52583199e5191298209e01",
|
||||
"adminx/modules/Rubrics/view/_content_tabs.twig": "e4c0a9b899b5085aeaec2e1793b8eeac4a7619959fffc7ce39151af1250e0d9f",
|
||||
"adminx/modules/Rubrics/view/field-sets.twig": "911dd7cc9546411e1b29724466328c7d2db8db630b2b52d33e8443a066b16810",
|
||||
"adminx/modules/Rubrics/view/index.twig": "d25a68bbbde3bf2da618c465cc2ce6cdaa70b557388db6633c719e0d6deff163",
|
||||
@@ -617,7 +630,7 @@
|
||||
"adminx/modules/Updates/language/ru/client.xml": "7dc664bbf759fe501c96a145dd6a052f2dd5541890a8aeb1d533fee378cde0f3",
|
||||
"adminx/modules/Updates/language/ru/interface.xml": "1d8a827ac996bf7da2d15f75b52d2205f0f36105cd73b50cebbbf1e99ea98bc7",
|
||||
"adminx/modules/Updates/language/ru/runtime.xml": "45d622ea8c6f772d5f53690bd7c08beb55efcbd51c7308f0336c995885b68258",
|
||||
"adminx/modules/Updates/module.php": "d3507184cdf713263cbeb20c6ac8761c941ac021853dbb40f27a8a851e65cd3a",
|
||||
"adminx/modules/Updates/module.php": "6c377451763584d00f93eaa53505e85152da9549d8573242fbfcfa13a8d07c0e",
|
||||
"adminx/modules/Updates/view/index.twig": "8d975051ca96fd99902714bd60b6a85131d6304578ea65a3dbf732523b638ece",
|
||||
"adminx/modules/Users/Controller.php": "dd8e11ec359064b37437057f0e1295dd1f20239186d5726119ee0764bce2fd10",
|
||||
"adminx/modules/Users/Model.php": "afd334340bbbeed9ac638421bc490262a21173007734740d2724d4ddc8cace63",
|
||||
@@ -634,14 +647,15 @@
|
||||
"adminx/modules/Users/migrations/002_harden_public_remember_tokens.php": "85c92acd3ed962dfc4bf85b78e07de87569b1f0aacb2c616287c95bb39fbee2a",
|
||||
"adminx/modules/Users/migrations/003_system_user_security.php": "6e2cd00d63dd7c050c0a578fde2031764d52a697a70b711ddddad1e32dbad917",
|
||||
"adminx/modules/Users/migrations/004_system_session_expiry.php": "00a1012683681fc5fbdbf3cc6258001ab2c6377f445f7b616fd1c513d2b30bcc",
|
||||
"adminx/modules/Users/module.php": "c952fe3182dc176870aa155f5848089c21f4026146bab53efac0902d2de7730d",
|
||||
"adminx/modules/Users/migrations/005_public_session_capacity.sql": "d7fed81d1f68319b8f47d645414e746dc9fda58b575539afa12371400e81014e",
|
||||
"adminx/modules/Users/module.php": "f04a577e15b42ed2469e0402d553c2b316fea1fb6af1ad742377b8e4bded60da",
|
||||
"adminx/modules/Users/view/list.twig": "845e0b0c8a04891096a2352b1a683fbd4614333017beea413434793173749efa",
|
||||
"adminx/view/404.twig": "86f93ca1cb44b0c225a03a33704b903f8a96e1697a05d40b0b71dedb8f665769",
|
||||
"adminx/view/_pagination.twig": "9d5e761594ae97d7cb7b518592028adfd2041b5d99f736b24a318b2766e350dd",
|
||||
"adminx/view/_saved_views.twig": "51217212a3b0ef7437de4dd55a07fdf55aaa3138155b00f65fd85af686412da1",
|
||||
"adminx/view/auth.twig": "e44eb6337248470418be7015886e6e5d7bf5b0021fc917f4cc0dcd98c6492d85",
|
||||
"adminx/view/error.twig": "1187e6d0c325e0bbf6a7bd9836a5ef6d96cb8f72a0599050d3da7120683a6202",
|
||||
"adminx/view/main.twig": "6845837fe7c9adab56c611dcbdd713f910d65cbee0c7b62eff7ed7bcf571d6f5",
|
||||
"adminx/view/main.twig": "b1ea3ab2356171f45e5174f44e271f3b07ab898f866400e9eab12f0eb341163f",
|
||||
"configs/db.config.example.php": "e42bdb47aa7d1bc9cd85cb36e05acf940cafd951d6220664c485d1b971f18809",
|
||||
"configs/index.php": "48e6b249e3d83530187c095ede0198af498c59f384bd458db74f9e0c2306eba9",
|
||||
"configs/public.config.php": "882ea6a03ee8df191be8ea68ba78d39ae600d918a0880ebdff158fc38f534aa6",
|
||||
@@ -663,16 +677,17 @@
|
||||
"help/api/errors.md": "630270fb28d5d385e368f3b0cc1c5cc2acb3c56607bb8c9af62c2e62a6d09993",
|
||||
"help/content/README.md": "273451d12b6360df56cc58323bfb8fd7521b4e3daf8f7ad93c874fe6877af048",
|
||||
"help/content/blocks.md": "1d55be2a9f1a48df426e5856a54307d77b352d0037d68f08ed7e16a322d9ef8c",
|
||||
"help/content/bulk-editor.md": "821107322b125c9344bd04346037fbd19f8ccd0ad6da822fe78af3299d01f746",
|
||||
"help/content/bulk-editor.md": "5960ecea40856bf94c4c7ff2cc62f5a69b80e84ef25bb0df5d60773d3ec18a8d",
|
||||
"help/content/catalog.md": "12434610186c9d797270ee0bd802abe79b67e976a88a94dbb10f2ee1fb4dbdbf",
|
||||
"help/content/content-model-tools.md": "baaf738aec454b1855ea110b8f5e1ef3ceaaad51cb920d4551cd088daeab7e76",
|
||||
"help/content/content-studio.md": "91ef485e614a9a71a07d4e6c143377f7052ddf3b00d3f2f0d0a32258bdb14dbe",
|
||||
"help/content/directories.md": "ff6c5ea9bd7fe1fb9d0aa1875d03fc3bc00b2bd2b0ec83d6496cd55168db294c",
|
||||
"help/content/documents.md": "273851452c0fb8fddd47019fea600da187025dcca271bdd91d59acae15d6b4c5",
|
||||
"help/content/documents.md": "2b8a8ca4e323a869091bc61af821f7b58f8d5e276f3e7dee9f6eddb721610560",
|
||||
"help/content/navigation.md": "50425db83ae70b8be72c69ecc6c54f77a15f029a244185021de1cb56efbb344f",
|
||||
"help/content/presentations.md": "6e349bb4d1d1577617e6bf3d987b86ca6b255f64a47cd1ddb0e38c5242b788cb",
|
||||
"help/content/public-site.md": "9b7cd51d6d36d21daa73b47cb243b84eeb79f0ff48e9eb61c5951e82c24f7c73",
|
||||
"help/content/requests.md": "c008b1026520c85400fb9240cb117fcaf384531c06e859c1e36ccbb3a79a8590",
|
||||
"help/content/rich-editor.md": "7816fce22a9187bd689a7a5e1fd8e9ffd66c7c321836e371d8fe010982804465",
|
||||
"help/content/rubrics.md": "2c9842ce5647fc85663b02cef14f141c673694f0a20856c9262b6ccbea436c3c",
|
||||
"help/content/templates.md": "c6fae2ef91c741479d1f34fe66d00e83f2d2ec1aba1a4099773a04a6e1b6552c",
|
||||
"help/content/themes.md": "f522107e2ee27590a40d726f26538a3101925d0d0173fa1e58422089b5418388",
|
||||
@@ -794,7 +809,7 @@
|
||||
"help/en/modules/benchmark.md": "e7b9d233abd386db41413f45c79639a3bebfc37241bdbab461028e87a5615f87",
|
||||
"help/en/modules/cache-warmup.md": "d92183948e03dd4f9a18308d36df7d5ba57e7e87f268b5f3f1de38ed99c518dc",
|
||||
"help/en/modules/comments.md": "55f45f748f32fbd36233c43574e212f3fa3fdd1ce8eb819f9d1cafb53215ed97",
|
||||
"help/en/modules/commerce.md": "e0a156c6ba634fe4c5beeae5eca0e0d592adddedbdab7e444da647609dbe01f7",
|
||||
"help/en/modules/commerce.md": "48c45b60ce849f88cb965e455b5784ea4829b316a4a1f34e4c92fed14558492d",
|
||||
"help/en/modules/commerceml.md": "610f6b2f9abbeeac604da0408192ae52dae7d6f3fb09e0d321e14d1cc190b808",
|
||||
"help/en/modules/contacts.md": "81c818e9cbcd1d61f34d9199219ea160a04bf88af110b32c3527c65fba65d39e",
|
||||
"help/en/modules/content-packages.md": "4b89df6d05fa75e484e38e404d53fa7ecc6b61aa6930fbf74cf9b6611bfbf532",
|
||||
@@ -815,6 +830,7 @@
|
||||
"help/en/modules/legacy-migration.md": "f9dabcd0ca6d69999ef989457a98b6a52ad29b42fc9958079e4b3f203b1bc15e",
|
||||
"help/en/modules/localization.md": "f84bd43534cb3147f5675908deed2503271e9a96ef1cacca8b3fab17d8917f6c",
|
||||
"help/en/modules/mcp.md": "98afdfe8128282672e6915159e39f6b1337b295022d9091c392c5a76d41ae239",
|
||||
"help/en/modules/metrica_ecommerce.md": "5bb1b3966bacbf27349c191ea1c9c49c4f9e1dbfa85e80b30c64058d925fdef6",
|
||||
"help/en/modules/migrations.md": "5a2d09f1252ad7c4670d06db1a1dd029593856fabd219005bfc0d9891bd6c2f2",
|
||||
"help/en/modules/module-php.md": "d61b3a47097623c1d969b716d78d08939f446980648b0718c1dcef45b7af8f36",
|
||||
"help/en/modules/not-found.md": "77ab48c7428a8f053938a780986d9c2d435be5c7aebc1d70093574776436c691",
|
||||
@@ -893,7 +909,7 @@
|
||||
"help/modules/benchmark.md": "6c31f16fc3f083f29c65398c38b78609a3d94da168d30a93825af8ee9650eb0d",
|
||||
"help/modules/cache-warmup.md": "56c05ec252b44a1a0d1c4b09efa90cf9d450b0b138787903c898f56b377b8749",
|
||||
"help/modules/comments.md": "04bcdea5aa7740e3749b51dcc9fc2954e98b22298d3e4fcdcc9865d94e1d0376",
|
||||
"help/modules/commerce.md": "d398d0dfc1edca5010d81ae660d412b4b1ed8d4df448fc78f237ad0ee2cc731a",
|
||||
"help/modules/commerce.md": "eeecc0171c134a995d22d3348bb237247e77b536367c27366f2e465f20c30d4f",
|
||||
"help/modules/commerceml.md": "60a0a5c541b560b5a6a4ed8213d9145caace68c15be534af13f5f817eb5251ba",
|
||||
"help/modules/contacts.md": "cb4284709f7b469164e3ef4c817893cdcaf4eba1b460164fb36f75a798bc457d",
|
||||
"help/modules/content-packages.md": "8dde0ebc897b7891a6afadb6491f4797bef26622918addab67f0f4ebfd719678",
|
||||
@@ -912,8 +928,10 @@
|
||||
"help/modules/help-viewer.md": "175db88e150ee02dadd8b841ea74262490b8211c67bc42bf05cf1dbafba1a25f",
|
||||
"help/modules/interactions.md": "6c62a4abb3422609265b1898cb448dd6bd840edeec98f30fb0062c6d18808e18",
|
||||
"help/modules/legacy-migration.md": "773acd5026409522bdd3a0d3fbbfcd915ca54849d64c97dbc0cbfc2a79fce36e",
|
||||
"help/modules/live_chat.md": "97cd5169676008546757509147f61f88ca64ae0e7c4a76a7d73be9ed9ff72769",
|
||||
"help/modules/localization.md": "f6a58cba016a55c7a9a88c594ae25fc79828411dae13a70aa84a54860821ae43",
|
||||
"help/modules/mcp.md": "a7b63a32773a1517de3c5bc328771bb3e2b9ff402f1004ededac2f94add25b02",
|
||||
"help/modules/metrica_ecommerce.md": "3a9b1c9605d7a5f4a18a9a1d3b774bcff488309028679a5d547aff3c42e66ce9",
|
||||
"help/modules/migrations.md": "e144ba009df225508d5f73e9ce71e17d4a379f9ddff49e640c2c1234e389588f",
|
||||
"help/modules/module-php.md": "3594f19c10381b4d450b6ce5b9b1d83b2f413067d67f7cca334d3a14cdbf2a0e",
|
||||
"help/modules/not-found.md": "50e45e048e07e27190b8583928c8fe20cd77c5e3401adfc47b9b2effa1ccd799",
|
||||
@@ -937,7 +955,7 @@
|
||||
"help/modules/search.md": "a39ffbd45e7d5786cd16d2678f5386c55c3081063177f6525b12849da1c251d3",
|
||||
"help/modules/seo-audit.md": "b0fb78eda0e83c0711b7717fcc730e7bc0cb86c6d692f21aa749cc4975f0ee27",
|
||||
"help/modules/site-readiness.md": "2c7f845ece693ef14f23ea2b3b226f38ebfed305fc67bf842fae971f0e12255a",
|
||||
"help/modules/sliders.md": "f3022a36b5c483f49d63b86b4651989e98636018fc8a7449ca45b2875277a3fe",
|
||||
"help/modules/sliders.md": "13c9d0f212bfa880fa965128b4b072a81f14eb4dba2e11c3889f0b6a57c1764f",
|
||||
"help/modules/smsc-auth.md": "361b46fbc4812513f59e8ba15d095607851f835d7765ac396c3efd79ecde10e3",
|
||||
"help/modules/system-health.md": "671d44d89f0d3ada4fcaa4ef3d8a7662a6c77dea6c0cd567d8f3e923d29e12a5",
|
||||
"help/modules/traffic-analytics.md": "12f966acc6ffdf73e2d40682c6db203f8dc3918ba5461a78607c1d783ed54d02",
|
||||
@@ -952,12 +970,12 @@
|
||||
"setup/PackageExtractor.php": "57cb5473905992333936d7984221e5188cd72756d6e48206a42d96e8ab59442d",
|
||||
"setup/assets/installer.css": "79e43dc7123f37206429e8535d92c5069c53231402c67b63ea9481bbea60ab42",
|
||||
"setup/assets/installer.js": "892a982b872390fea21455562353408da906114b973ccbfc3a3cce06465ab1e8",
|
||||
"setup/core-migrations.php": "c747a8a9289f27385310cd8189e7b618b7b78b0ddcc22c1d283425a5ee4a962b",
|
||||
"setup/index.php": "cdbb99ab89035afb665d4a288c87633dc0749a75cf64a0bbf00e5ec8470fef36",
|
||||
"setup/language/en.php": "963dd284ee244e6d006dede8522c1118dc0909974dcd24d477fd06b7e1965fa1",
|
||||
"setup/core-migrations.php": "b1647f6f7d4dbe0e8dc69584b8f1373763b7626ccc108223c1a4cc39f0966303",
|
||||
"setup/index.php": "daa384199bb69dc9aed3fd718c19d6d9bb100bb73e284651bcd16a48841b2036",
|
||||
"setup/language/en.php": "a735bc01632ee20c900e619eebe7954a7b8851d05d4a78d70660851aaacdf34a",
|
||||
"setup/language/ru.php": "8522b13c4f147804538eaa857585296db811c736e1361e6b403daecea05f34f8",
|
||||
"setup/recovery.php": "359ae9b79508a382bdde175824b18a5bdb0fd69ddb7776c7d18ac9eb826d127a",
|
||||
"setup/schema.sql": "4942144465f8854cce3c3e6f06bc9d2ebd11ad1239d4a086feda318a0ae70448",
|
||||
"setup/schema.sql": "d5197503945c0bb9f2fc2a4a77bf667d032ca103c21215edd3d5d676246840d5",
|
||||
"storage/.htaccess": "ca761a92f2b6eb47feffcf200a8fd846e7fdd8abe065008d29beff0319ca3496",
|
||||
"system/App/Common/AdminAssets.php": "dcd58f19fda48263b2912a10dfdc24e3f12967683649c4f614a8d54c62d51b86",
|
||||
"system/App/Common/AdminLocation.php": "d2729b0d627ddbdeb7f0c4b110664ed949f373946f80929c51688391c64352de",
|
||||
@@ -967,14 +985,15 @@
|
||||
"system/App/Common/Auth/IdentityLinker.php": "26c85deda5918b413f2ee4fc48ef00d5268aa6ef1081095b801b2428d61979f7",
|
||||
"system/App/Common/Auth/PublicUserNames.php": "0345d1f452e0a9accaa9b98eea71f27a428692a5b4f2ad225a10ad8bcf777684",
|
||||
"system/App/Common/Auth/PublicUserProvider.php": "5d3690dcbc0e9cace7e09350d283cba541f15cf8a346c9bedcbd9f6cec58d65f",
|
||||
"system/App/Common/Cache.php": "bb322246cbfa12cbcdd67c16a24c55715ad67b31d356b4f18091c0aa1d00eb9a",
|
||||
"system/App/Common/Cache.php": "887a9c4f1b7a2065707c22e3e42ff8c01f882d2c00e462438cd01caa308a5e76",
|
||||
"system/App/Common/CacheKey.php": "47400cd90d1880d061995bfea585b56210e18a870607d9b1cf7f0eeaccf15032",
|
||||
"system/App/Common/CliRuntime.php": "2f33eb99cc61917e8e1112c83e8ff24cb2463a8ea7024ba1d907b3a097166613",
|
||||
"system/App/Common/CompiledModuleRegistry.php": "9ab055fa759f4041944bb94bfc2e28361986a09b742210981928bd6ee15f758e",
|
||||
"system/App/Common/Controller.php": "b80ca1f6f1f2cbff6effdcbdb9e9606b5aa9877922ab442cf7b1ae1798aa9f00",
|
||||
"system/App/Common/CoreUpdate/DatabaseBackup.php": "6f32b82fc8ec06b5fee851dbd1c455210f2e41bf1f5290317f358d152cfc7669",
|
||||
"system/App/Common/CoreUpdate/PatchPackage.php": "2e54a4c7e10c56f2cc77afecb97f75d69742696982d0aaf554c35f4d7469ed52",
|
||||
"system/App/Common/CoreUpdate/ReleaseState.php": "8917d093677b05b3dc0b4ee97e8b26d18f9a98af78a3f87123fd7779d024af41",
|
||||
"system/App/Common/CoreUpdate/UpdateJob.php": "60edfe53acfaaf55898488d889a0abf62d226fdbfe17740059eec5cb2e6b3173",
|
||||
"system/App/Common/CoreUpdate/ReleaseState.php": "2c9e5955b3d4e57a9429cc15c566b95606265ba1440c7da488c6ff0f894ba671",
|
||||
"system/App/Common/CoreUpdate/UpdateJob.php": "ae50e802ab0f55e043b0a389e15e73a8e874d7776f60ad523a0fb58cf97c1500",
|
||||
"system/App/Common/CoreUpdate/UpdateRepository.php": "b8fbc3e0852b0d3d8be7b65a4513d12487dc3a42fd67c6f14330c7d5d85edc09",
|
||||
"system/App/Common/CsvLogWriter.php": "9deaa3e2bd85eec300c65e89e22549f48aa9e2850ad50b2ee8826f8d4fa53aa1",
|
||||
"system/App/Common/DatabaseConfiguration.php": "3eec09e9a0d9302761ca6b9ebfcb47b01aaded1bb7ed5834b1d55cac806918d3",
|
||||
@@ -1019,7 +1038,7 @@
|
||||
"system/App/Common/ModuleTagRegistry.php": "9e68b64303825a25ef4f71f31a7adedee9d2752e4d1d06162605da1dc510a6a5",
|
||||
"system/App/Common/Navigation.php": "1a1b0a3552d941dc0fc59c334ab705e86a73ee96ee42871dc958012bec1d81f6",
|
||||
"system/App/Common/NotFoundLog.php": "f5630e839aee069c1122452da42f42ecae5d2082fe582dda7be6760059f3eb71",
|
||||
"system/App/Common/OutboundHttpClient.php": "dc03f3093e70438ba92cd8d15ae690141186b6cf7916a863bd6f7de4c15be0bd",
|
||||
"system/App/Common/OutboundHttpClient.php": "56f6c08ecef0dddaaf662c84d05afb4f1e2ad27d72b7c51749eab07359bbe757",
|
||||
"system/App/Common/PackageModuleRuntime.php": "5ad492b9053bcea4d3bd4398b11fb2d976272e2036d379bdc24c19dd5fe7486f",
|
||||
"system/App/Common/Permission.php": "8e2241bca829de0104741dc2518860cd15489866dc310437e07aeab2bf0f0355",
|
||||
"system/App/Common/Proxy.php": "707564208ec8c24ac2ffb5bf41cba34f1b39059ce6bda31a69aaf4b116d864e2",
|
||||
@@ -1117,6 +1136,7 @@
|
||||
"system/App/Content/Fields/PublicFieldRuntime.php": "9abd8e5dd43c56b27dcbf87d258cd9764ae1d67e95a397a63a6c6516f7a417f8",
|
||||
"system/App/Content/Fields/PublicFieldTemplateContext.php": "785d6757710dee59d8d30756917235bb4f49eb5bd743c01d2365c05d74bf0a5a",
|
||||
"system/App/Content/Fields/PublicFieldTemplateResolver.php": "72c0750c50129cbabc0a97147fe2f2f4b2b3811cd77f4de197dc3753db46903a",
|
||||
"system/App/Content/Fields/RichEditorSettings.php": "d583e8e5b52c278c06637315ea493dd9e97212a8303bcd69ae20684d6a848a7b",
|
||||
"system/App/Content/Fields/SerialFieldType.php": "61685c9a19c0fe9aede73f16f7bdd730dc3562d6223e7b3d4eae5e3e303fce1f",
|
||||
"system/App/Content/Fields/Types/AddressValue.php": "ae244e819453247a385f604b7ee4df3735bb6474346686bc29a0cc32c35133ec",
|
||||
"system/App/Content/Fields/Types/Analoque.php": "d4169e7474f700831a5b572163a75185e410447c2f6c56050226fdbcc5a61e19",
|
||||
@@ -1128,7 +1148,7 @@
|
||||
"system/App/Content/Fields/Types/ColorValue.php": "8a68a4fdf628dc30922357bb0918235f373e76034d137afa270004edcd947740",
|
||||
"system/App/Content/Fields/Types/Computed.php": "82ff8ff37fdb9802a6626edb7abfe97e0867901157b23d05fb75a84710aa851c",
|
||||
"system/App/Content/Fields/Types/ContactValue.php": "b6f8533a8de393b4c760ea69423b8bd1c1fa0717bc5a608cca7710f58c751f11",
|
||||
"system/App/Content/Fields/Types/Content.php": "e7e41bca3dc574e5735446593ba875d59ec1baf4769cb130ecdcecefda65709d",
|
||||
"system/App/Content/Fields/Types/Content.php": "012dd0df95b21fccab274874ba127e3121512084015a699c6dd2c409223d182e",
|
||||
"system/App/Content/Fields/Types/Date.php": "4952e8ff31abdc8d133e6f812e075847a49790c9fdd1865357cbb4080b0bb8f0",
|
||||
"system/App/Content/Fields/Types/DateTimeValue.php": "592761636f12f9674d928d9ee7c21307b2c8402de6e9a2c2b537e5cfb51fe07d",
|
||||
"system/App/Content/Fields/Types/DimensionsValue.php": "36a323ab98c2c011dee03eb30ee9ebd55f0450aa74645b0031f7202bcff77a7a",
|
||||
@@ -1146,7 +1166,7 @@
|
||||
"system/App/Content/Fields/Types/ImageSingle.php": "54c1253fcdf6c3726b2f6d4881d0bc7844fe3279e50f1d796d2bea14ed93dba1",
|
||||
"system/App/Content/Fields/Types/Link.php": "6c3b7031d8a1b78371c041b142a90a373217afce12e637d29cc0a6b4ed126d7b",
|
||||
"system/App/Content/Fields/Types/MultiCheckbox.php": "040dcf7824d15a5bb9cd032ce2de171a91c8f4379e3482a85a59d6c7b34fc2dd",
|
||||
"system/App/Content/Fields/Types/MultiLine.php": "9cdba0fb14c1053a2c88c667f436eed2da243836d49ca6c3240612f32bdfab52",
|
||||
"system/App/Content/Fields/Types/MultiLine.php": "eed21babbc30a6a253bc6d9cbeedd59d4439fd6361b52ead753db43f17aa9006",
|
||||
"system/App/Content/Fields/Types/MultiLineSimple.php": "0286b13ad1a2e83b5fdc8b11f05bcb99b80b38a018650c25e58141785fda0fda",
|
||||
"system/App/Content/Fields/Types/MultiLineSlim.php": "4c39e5810a9e2df82a7e03af7eab36b07c20c863fc31d3aabc2e172103b41d84",
|
||||
"system/App/Content/Fields/Types/MultiLinks.php": "a4d1ae15f0c23934a5185fe7407605079873f7a018c6ddfae0998f2d1c3312f6",
|
||||
@@ -1159,7 +1179,7 @@
|
||||
"system/App/Content/Fields/Types/PackagesValue.php": "b83d3ac394c42204ab89d4edcc5bba85e95c8850d07839f53273e958db346c11",
|
||||
"system/App/Content/Fields/Types/PeriodValue.php": "afe067af7705b027652e2210ae784977a4fa00344667b672c3314d463d238197",
|
||||
"system/App/Content/Fields/Types/RangeValue.php": "396b6dcc4f4b4a1875c033c31dbf7b1e4ccd120a79bd86cbde813d7ec57bfa40",
|
||||
"system/App/Content/Fields/Types/RichText.php": "90db4b29a796916533260de916443d6e4a76bc4329622fca7481f0597de8468a",
|
||||
"system/App/Content/Fields/Types/RichText.php": "757e235fd99958e436d72c37b5badcf07cd085bcb92c991694923e7c6ab8a2e6",
|
||||
"system/App/Content/Fields/Types/SingleLine.php": "5d41fa26af3bb6dc0ceac95534ea228b3355df4ad2856277f62034232e31e41b",
|
||||
"system/App/Content/Fields/Types/SingleLineNumeric.php": "a7cf2fec71fe4c0efa6a6d97dbee69510627068d1a5192661d3051ff660448ea",
|
||||
"system/App/Content/Fields/Types/SingleLineNumericThree.php": "38f5c7a093933e879d6930323ace96ab8b3acdd482ef8231af9cb0a80df48153",
|
||||
@@ -1234,7 +1254,7 @@
|
||||
"system/App/Frontend/Auth/assets/auth.css": "4b9ecaf9050577c8c9d412f436a22e3f6aa89bc596fc0b1395dea06a6a48e33f",
|
||||
"system/App/Frontend/Auth/assets/auth.js": "840c7322fc21f9a8dd61af8cd16e0ceed36d6beaeac45d948bedbcb87b6012c4",
|
||||
"system/App/Frontend/Auth/assets/phone-mask.js": "b9001aeeeda7f2c290bcc24b3453ef828e43c54ec0db9c891b6ffc1a555d2878",
|
||||
"system/App/Frontend/Auth/view/login.twig": "6d0435f45cdc75ca3c1454bd1c6f0286d6227ac4dd943715927906623e01efd8",
|
||||
"system/App/Frontend/Auth/view/login.twig": "5205bc84c3cc1fdc8f95ee32437c977c881e74fc655f36deceb06809e59446cc",
|
||||
"system/App/Frontend/Auth/view/message.twig": "4e9a660af7d0deae9424a07550816c401902e75279414b11657e0eddf573deea",
|
||||
"system/App/Frontend/Auth/view/oauth.twig": "7ccb2c7ae36a378586334887c32fda96909cb6fa235601f1f697d4c8049af648",
|
||||
"system/App/Frontend/Auth/view/oauth_connections.twig": "e2d845ce9d906ca12d3f642e92ede1aa415203f7dc2ac472006f40e80eb8bf27",
|
||||
@@ -1243,7 +1263,7 @@
|
||||
"system/App/Frontend/Auth/view/password.twig": "099d7c1e48649faba57eb50df9bd9a091c79dad12e9c1fee9b38c3f2e025ed80",
|
||||
"system/App/Frontend/Auth/view/phone.twig": "b0803ff6b00d4f63ab056a2129d2cf3a2a870bdada106e7d706ba5427cd9a6a8",
|
||||
"system/App/Frontend/Auth/view/profile.twig": "a2523111d171324a2a116ab4200ade94299af08de45639a0a5d36c9df05eb5dd",
|
||||
"system/App/Frontend/Auth/view/register.twig": "513dd690a902a51752b7239b113ff6b612d754c238718e6ccc1f5698dfa42c52",
|
||||
"system/App/Frontend/Auth/view/register.twig": "c8ccff65a32ac068e6e04cbe50f532fe17bf95f4a19f960268db9fe5e5086d27",
|
||||
"system/App/Frontend/Auth/view/remember.twig": "9872a6f4812aeb7d8f04870156e0427e9a73be4277c59b1480dfb81d9dc0dc5d",
|
||||
"system/App/Frontend/Auth/view/reset.twig": "a8d13ea135ca16cc20af3d1940cd8ed5cb13143043992e2ec73b5c7d2c60c8cf",
|
||||
"system/App/Frontend/BlockRenderer.php": "1d23d7ff85ba1052f896e204d1885f99ec2c0f6382e01d5d1903d69fe661305b",
|
||||
@@ -1252,7 +1272,7 @@
|
||||
"system/App/Frontend/Captcha/Controller.php": "eb398d3bdc711a0543dd293b00f250e05bda477fa4577e76b33fafc23f7d2eb2",
|
||||
"system/App/Frontend/Captcha/Feature.php": "8e31714dc3101a3da8bac23989cba32ee9776a9f1fa5ec888a50cc65c8494972",
|
||||
"system/App/Frontend/ConfiguredPaginationRenderer.php": "df4d28a386e4180fd0dbddbc30f08ca8c6345e2f3b0d196482fa9d2a5d4f4d32",
|
||||
"system/App/Frontend/ContentCollectionTags.php": "337085d73f0ad609b5a37a4fa72537459f4ae2e5f503e39a8750be95127d89b5",
|
||||
"system/App/Frontend/ContentCollectionTags.php": "36978ecfded9bfcf37e7fc9f44a976b5e440a653679c96e8162bdc60c07abe7b",
|
||||
"system/App/Frontend/Debug/PublicDebugCollector.php": "6755ad710e9d045b2bfa74745354f717c101236b5d64ad5530fd779468969a00",
|
||||
"system/App/Frontend/Debug/PublicDebugToolbar.php": "7a92630b59483e63d623fa769e13c246e6bd657c6d027a5d14ab8f95986da0de",
|
||||
"system/App/Frontend/Debug/PublicLayoutInspector.php": "8a7f5ea4cff9834ad13cac7e0d2380d878978e406e71860e1ad9ca183e427dab",
|
||||
@@ -1328,9 +1348,9 @@
|
||||
"system/App/Frontend/RubricPermissionResolver.php": "1223d6c2839952c98475bf8d802e4c1c9f058be3b2cadabe03fe7bb8cf3973af",
|
||||
"system/App/Frontend/RubricTemplateRenderer.php": "87ebe5c718f25e0bad5ae1e178d2c5cdaa86e0744f97183ef6d0b1bbeb2dc617",
|
||||
"system/App/Frontend/SiteAccess.php": "a1cc5a86ab9f88c22214b9cb642556d7e6ffe73b00084de988ddd730805410fc",
|
||||
"system/App/Frontend/Sitemap/Controller.php": "7a1504cb00897bba10ba0d6d297c7338f8c9eda8782577ef1ef8f8316a35846a",
|
||||
"system/App/Frontend/Sitemap/Controller.php": "314efe61465fcdb881a45028e741685f156d7940e4ea58b587feec1dfc536a8d",
|
||||
"system/App/Frontend/Sitemap/Feature.php": "7ff7afcc57c8533acdd1050a40a0c4ca65815668f5e8c72d4b02bdd6c92ae8fd",
|
||||
"system/App/Frontend/Sitemap/Repository.php": "2dff32e665493d6cd7e6946952541ad414d460d8c1b0d873161b134e24ed1ffe",
|
||||
"system/App/Frontend/Sitemap/Repository.php": "4219a96faee7aec1aae5f8ca6ae054b7348e7c1e1d096991dc4eec699235ef49",
|
||||
"system/App/Frontend/StoredTemplateApi.php": "042e21a93a6c914ac6cf53465b5292833a8240645870ac1bcf29e6fed0a2288f",
|
||||
"system/App/Frontend/ThemeAssets.php": "5870bace8823561820b64752e5dba16e434449e12de317ad4085087b31232396",
|
||||
"system/App/Frontend/ThemePageShellRenderer.php": "86638d6559c6b57f540ff2705ea3ba7607001ee46c94e9398ece474a34b05f52",
|
||||
@@ -1347,13 +1367,14 @@
|
||||
"system/App/Helpers/Dir.php": "0791ed320ab894e2c768d16768c227e9c68bbc627723eae289f587551037fb66",
|
||||
"system/App/Helpers/File.php": "4a43dacb8964e023a4361f2a3057da33a2b044628a4af3c7bd7e36171c024291",
|
||||
"system/App/Helpers/Hooks.php": "266e65cd317245297652756f2cbfb0ae4af44f7f1249d9009d1c955594994803",
|
||||
"system/App/Helpers/Html.php": "4b4b6387bf40ef4f91fef02ef115ee4b7230694eed93633ec5caa6afb195e013",
|
||||
"system/App/Helpers/Html.php": "e75a342898e8dc2598a15e855e80fbbe1a30ac0715d3fe67f427650c441ac1a0",
|
||||
"system/App/Helpers/Ip.php": "e982ea7edd23850100b31bd4a82fac97bd9dc4615e13a3e559da266d8299b6ba",
|
||||
"system/App/Helpers/Json.php": "05765a9e559bdbc78f35c0a97bf139a475fe83430f3100ae2cef26979b9cde3f",
|
||||
"system/App/Helpers/Locales.php": "f03ab5f4abfdb45eda598cd977e80759f3b7e677b8e43074e6bb51c0c965b508",
|
||||
"system/App/Helpers/MarketParser.php": "b0381686f962d0066f8fe3bb563cd85448769fdf949c3a3ff5def34ead10a92e",
|
||||
"system/App/Helpers/Number.php": "96f05c954df90dad805741b89df90169b857b1802e493c8c62ddbe7dd364e860",
|
||||
"system/App/Helpers/Phone.php": "0171df8f6df4df7f4a3fdf168aadcd23461c00499d1da4a9ecbd24d8e611112f",
|
||||
"system/App/Helpers/PhpSyntax.php": "6c94f8e5aa16a174d554f499e3cd4f83f79df2df315a64c7e4bbf3bcf32674c0",
|
||||
"system/App/Helpers/Request.php": "29cc2cf20e1e4acd6e2f23c3de9b9606aec3f44d0b3577613b4fde376f7ae858",
|
||||
"system/App/Helpers/Response.php": "beea1596703526b7a2424459e392487e8bc8a9ce216f8d70bd2b09564709e0b9",
|
||||
"system/App/Helpers/RussianStemmer.php": "60a896c8254290ae554273132c103c1c209a14944f52cfd631e7a3ae13145529",
|
||||
@@ -1371,7 +1392,7 @@
|
||||
"system/language/ru/lang.xml": "7d46cc9822f064a1af4d6528ccfe2dd4a9cc318912e327fd807befd2ca3dfd11",
|
||||
"system/preload.php": "66bfb1aaf36ba85945b729b09a37e96ead0be1cc2749ebd9abd4b9da4f25ba0e",
|
||||
"system/public.php": "3f72b5bf44bc0ba5ba6fb6b3adc2e2f9d5e9902d9cdfa3d7b748f4aebd179b87",
|
||||
"system/release.php": "96de420b08cdefac694c48399015fdbccc9bc81c5992d1052254934c8e0d2f2e",
|
||||
"system/release.php": "2b574ac66c0b98ee791f231d29b371e2936c202c11bd144912c85bad60b2321d",
|
||||
"system/vendor/Captcha/fonts/palatino_linotype_bold.png": "38b6a1d4a4bd1d0c056205119c5e24176913ac0d898371f53a0339952a319365",
|
||||
"system/vendor/Captcha/fonts/perpetua_bold.png": "55dbbe97dae128dea47b5e8d640414b3d813119c854c688037a20a581ec8fdc4",
|
||||
"system/vendor/Captcha/fonts/times_bold.png": "d3d32ce1f418e3c3c3b1afe3b6acabadb36207bc12368f5e86420cf1a529ff8f",
|
||||
@@ -1957,12 +1978,13 @@
|
||||
"templates/public/theme.json": "4fb596d58dbda7f58c26c4f3640c56d6f6981f402ffbd368229adddc23683690",
|
||||
"tmp/.htaccess": "ca761a92f2b6eb47feffcf200a8fd846e7fdd8abe065008d29beff0319ca3496",
|
||||
"uploads/.htaccess": "90cd571b4c9f09a63732254fbcdca92cc7da8560a6e5b4d9ec7e58ac40e528bf",
|
||||
"uploads/images/noimage.png": "bdb69cbc3e5df374b80ec1ee03c81c610554b4eca9fa6df7d5b55fd387d7afea",
|
||||
"uploads/index.php": "e91434fe4986c22a2bba7ceda8e59d1aa2c9f943353035c83939cfd4f423fdd8",
|
||||
"robots.txt": "d8119db904b34c2aab74cc2b6a09d13119f7c19add8b51ffbf4717eba65e8218"
|
||||
},
|
||||
"file_owners": {
|
||||
".gitignore": "core",
|
||||
".htaccess": "core",
|
||||
".htaccess": "project",
|
||||
"CHANGELOG.md": "core",
|
||||
"README.md": "core",
|
||||
"adminx/.htaccess": "core",
|
||||
@@ -2003,6 +2025,10 @@
|
||||
"adminx/assets/js/editor-tiptap.js": "core",
|
||||
"adminx/assets/js/media-picker.js": "core",
|
||||
"adminx/assets/js/saved-views.js": "core",
|
||||
"adminx/assets/src/_rich-extensions.js": "core",
|
||||
"adminx/assets/src/_rich-recovery.js": "core",
|
||||
"adminx/assets/src/_rich-tools.js": "core",
|
||||
"adminx/assets/src/_word-paste.js": "core",
|
||||
"adminx/assets/src/bulk-actions.js": "core",
|
||||
"adminx/assets/src/command-palette.js": "core",
|
||||
"adminx/assets/src/editor-tiptap.js": "core",
|
||||
@@ -2092,11 +2118,13 @@
|
||||
"adminx/assets/vendor/tabler/tabler-icons.min.css": "core",
|
||||
"adminx/core-permissions.php": "core",
|
||||
"adminx/index.php": "core",
|
||||
"adminx/language/en/attention.xml": "core",
|
||||
"adminx/language/en/client.xml": "core",
|
||||
"adminx/language/en/common.xml": "core",
|
||||
"adminx/language/en/interface.xml": "core",
|
||||
"adminx/language/en/runtime.xml": "core",
|
||||
"adminx/language/en/section-help.xml": "core",
|
||||
"adminx/language/ru/attention.xml": "core",
|
||||
"adminx/language/ru/client.xml": "core",
|
||||
"adminx/language/ru/common.xml": "core",
|
||||
"adminx/language/ru/interface.xml": "core",
|
||||
@@ -2201,8 +2229,11 @@
|
||||
"adminx/modules/Customers/view/customer-center-detail.twig": "core",
|
||||
"adminx/modules/Customers/view/customer-center.twig": "core",
|
||||
"adminx/modules/Customers/view/index.twig": "core",
|
||||
"adminx/modules/Dashboard/Attention.php": "core",
|
||||
"adminx/modules/Dashboard/Controller.php": "core",
|
||||
"adminx/modules/Dashboard/Widgets.php": "core",
|
||||
"adminx/modules/Dashboard/assets/attention.css": "core",
|
||||
"adminx/modules/Dashboard/assets/attention.js": "core",
|
||||
"adminx/modules/Dashboard/assets/dashboard.css": "core",
|
||||
"adminx/modules/Dashboard/assets/dashboard.js": "core",
|
||||
"adminx/modules/Dashboard/language/en/common.xml": "core",
|
||||
@@ -2283,6 +2314,7 @@
|
||||
"adminx/modules/Documents/migrations/016_reconcile_document_content_columns.php": "core",
|
||||
"adminx/modules/Documents/migrations/017_document_public_visibility.sql": "core",
|
||||
"adminx/modules/Documents/module.php": "core",
|
||||
"adminx/modules/Documents/view/_product_package_fields.twig": "core",
|
||||
"adminx/modules/Documents/view/_product_panels.twig": "core",
|
||||
"adminx/modules/Documents/view/api.twig": "core",
|
||||
"adminx/modules/Documents/view/bulk-editor.twig": "core",
|
||||
@@ -2342,11 +2374,13 @@
|
||||
"adminx/modules/Media/assets/media.css": "core",
|
||||
"adminx/modules/Media/assets/media.js": "core",
|
||||
"adminx/modules/Media/language/en/analyzer.xml": "core",
|
||||
"adminx/modules/Media/language/en/archive_processing.xml": "core",
|
||||
"adminx/modules/Media/language/en/client.xml": "core",
|
||||
"adminx/modules/Media/language/en/improvements.xml": "core",
|
||||
"adminx/modules/Media/language/en/interface.xml": "core",
|
||||
"adminx/modules/Media/language/en/runtime.xml": "core",
|
||||
"adminx/modules/Media/language/ru/analyzer.xml": "core",
|
||||
"adminx/modules/Media/language/ru/archive_processing.xml": "core",
|
||||
"adminx/modules/Media/language/ru/client.xml": "core",
|
||||
"adminx/modules/Media/language/ru/improvements.xml": "core",
|
||||
"adminx/modules/Media/language/ru/interface.xml": "core",
|
||||
@@ -2434,6 +2468,7 @@
|
||||
"adminx/modules/Requests/migrations/012_condition_value_sources.php": "core",
|
||||
"adminx/modules/Requests/migrations/013_sort_rules.php": "core",
|
||||
"adminx/modules/Requests/migrations/014_request_revisions.sql": "core",
|
||||
"adminx/modules/Requests/migrations/015_request_description_capacity.php": "core",
|
||||
"adminx/modules/Requests/module.php": "core",
|
||||
"adminx/modules/Requests/view/edit.twig": "core",
|
||||
"adminx/modules/Requests/view/index.twig": "core",
|
||||
@@ -2583,6 +2618,7 @@
|
||||
"adminx/modules/Users/migrations/002_harden_public_remember_tokens.php": "core",
|
||||
"adminx/modules/Users/migrations/003_system_user_security.php": "core",
|
||||
"adminx/modules/Users/migrations/004_system_session_expiry.php": "core",
|
||||
"adminx/modules/Users/migrations/005_public_session_capacity.sql": "core",
|
||||
"adminx/modules/Users/module.php": "core",
|
||||
"adminx/modules/Users/view/list.twig": "core",
|
||||
"adminx/view/404.twig": "core",
|
||||
@@ -2622,6 +2658,7 @@
|
||||
"help/content/presentations.md": "core",
|
||||
"help/content/public-site.md": "core",
|
||||
"help/content/requests.md": "core",
|
||||
"help/content/rich-editor.md": "core",
|
||||
"help/content/rubrics.md": "core",
|
||||
"help/content/templates.md": "core",
|
||||
"help/content/themes.md": "core",
|
||||
@@ -2764,6 +2801,7 @@
|
||||
"help/en/modules/legacy-migration.md": "core",
|
||||
"help/en/modules/localization.md": "core",
|
||||
"help/en/modules/mcp.md": "core",
|
||||
"help/en/modules/metrica_ecommerce.md": "core",
|
||||
"help/en/modules/migrations.md": "core",
|
||||
"help/en/modules/module-php.md": "core",
|
||||
"help/en/modules/not-found.md": "core",
|
||||
@@ -2861,8 +2899,10 @@
|
||||
"help/modules/help-viewer.md": "core",
|
||||
"help/modules/interactions.md": "core",
|
||||
"help/modules/legacy-migration.md": "core",
|
||||
"help/modules/live_chat.md": "core",
|
||||
"help/modules/localization.md": "core",
|
||||
"help/modules/mcp.md": "core",
|
||||
"help/modules/metrica_ecommerce.md": "core",
|
||||
"help/modules/migrations.md": "core",
|
||||
"help/modules/module-php.md": "core",
|
||||
"help/modules/not-found.md": "core",
|
||||
@@ -2918,6 +2958,7 @@
|
||||
"system/App/Common/Auth/PublicUserProvider.php": "core",
|
||||
"system/App/Common/Cache.php": "core",
|
||||
"system/App/Common/CacheKey.php": "core",
|
||||
"system/App/Common/CliRuntime.php": "core",
|
||||
"system/App/Common/CompiledModuleRegistry.php": "core",
|
||||
"system/App/Common/Controller.php": "core",
|
||||
"system/App/Common/CoreUpdate/DatabaseBackup.php": "core",
|
||||
@@ -3066,6 +3107,7 @@
|
||||
"system/App/Content/Fields/PublicFieldRuntime.php": "core",
|
||||
"system/App/Content/Fields/PublicFieldTemplateContext.php": "core",
|
||||
"system/App/Content/Fields/PublicFieldTemplateResolver.php": "core",
|
||||
"system/App/Content/Fields/RichEditorSettings.php": "core",
|
||||
"system/App/Content/Fields/SerialFieldType.php": "core",
|
||||
"system/App/Content/Fields/Types/AddressValue.php": "core",
|
||||
"system/App/Content/Fields/Types/Analoque.php": "core",
|
||||
@@ -3303,6 +3345,7 @@
|
||||
"system/App/Helpers/MarketParser.php": "core",
|
||||
"system/App/Helpers/Number.php": "core",
|
||||
"system/App/Helpers/Phone.php": "core",
|
||||
"system/App/Helpers/PhpSyntax.php": "core",
|
||||
"system/App/Helpers/Request.php": "core",
|
||||
"system/App/Helpers/Response.php": "core",
|
||||
"system/App/Helpers/RussianStemmer.php": "core",
|
||||
@@ -3906,7 +3949,8 @@
|
||||
"templates/public/theme.json": "core",
|
||||
"tmp/.htaccess": "core",
|
||||
"uploads/.htaccess": "core",
|
||||
"uploads/images/noimage.png": "core",
|
||||
"uploads/index.php": "core",
|
||||
"robots.txt": "core"
|
||||
"robots.txt": "project"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AVE.cms
|
||||
|
||||
**AVE.cms 3.3 build 0.37** — модульная система управления сайтами на PHP.
|
||||
**AVE.cms 3.3 build 0.42** — модульная система управления сайтами на PHP.
|
||||
Она подходит для проектов, в которых структуру содержимого нужно собирать из
|
||||
собственных рубрик, полей, документов, запросов, блоков и шаблонов, не
|
||||
привязываясь к заранее заданному типу сайта.
|
||||
|
||||
@@ -62,6 +62,12 @@
|
||||
return self::contributions('header');
|
||||
}
|
||||
|
||||
/** Small, permission-scoped work queues; providers must not mutate their records. */
|
||||
public static function attentionQueues()
|
||||
{
|
||||
return self::contributions('attention');
|
||||
}
|
||||
|
||||
public static function dashboardWidgets(array $visibleCodes = null)
|
||||
{
|
||||
return self::contributions('dashboard', $visibleCodes);
|
||||
|
||||
+11
-137
@@ -16,158 +16,32 @@
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Common\CliRuntime;
|
||||
use App\Common\PublicConfiguration;
|
||||
use App\Helpers\PhpSyntax;
|
||||
|
||||
class PhpCode
|
||||
{
|
||||
const LINT_TIMEOUT = 3;
|
||||
|
||||
public static function source($code)
|
||||
{
|
||||
$code = trim((string) $code);
|
||||
if (preg_match('/^<\?(?:php|=)?/i', $code)) {
|
||||
return $code;
|
||||
}
|
||||
|
||||
if (preg_match('/^<\?(?:php|=)?/i', $code)) { return $code; }
|
||||
return "<?php\n" . $code . "\n";
|
||||
}
|
||||
|
||||
public static function lint($code)
|
||||
{
|
||||
$directory = BASEPATH . '/tmp/console';
|
||||
if (!is_dir($directory) && !@mkdir($directory, 0775, true)) {
|
||||
return array('ok' => false, 'message' => 'Не удалось создать временный файл.', 'output' => '', 'line' => 0);
|
||||
}
|
||||
|
||||
$file = $directory . '/lint-' . bin2hex(random_bytes(8)) . '.php';
|
||||
if (@file_put_contents($file, self::source($code)) === false) {
|
||||
@unlink($file);
|
||||
return array('ok' => false, 'message' => 'Не удалось записать временный файл.', 'output' => '', 'line' => 0);
|
||||
}
|
||||
|
||||
if (!function_exists('proc_open')) {
|
||||
@unlink($file);
|
||||
return array('ok' => false, 'message' => 'Проверка PHP недоступна на сервере.', 'output' => 'Функция proc_open отключена хостингом.', 'line' => 0);
|
||||
}
|
||||
|
||||
$php = self::resolvePhpBinary();
|
||||
if ($php === '') {
|
||||
@unlink($file);
|
||||
return array('ok' => false, 'message' => 'PHP CLI не найден.', 'output' => 'Укажите путь к PHP CLI в ADMINX_PHP_BINARY.', 'line' => 0);
|
||||
}
|
||||
|
||||
$result = self::runProcess(
|
||||
escapeshellarg($php) . ' -l ' . escapeshellarg($file),
|
||||
self::LINT_TIMEOUT
|
||||
);
|
||||
@unlink($file);
|
||||
if ($result['timed_out']) {
|
||||
return array('ok' => false, 'message' => 'Проверка PHP превысила лимит времени.', 'output' => 'Проверьте ADMINX_PHP_BINARY: требуется консольный PHP, а не php-fpm.', 'line' => 0);
|
||||
}
|
||||
|
||||
$text = trim($result['output']);
|
||||
$text = str_replace($file, 'код', $text);
|
||||
$line = 0;
|
||||
if (preg_match('/ on line ([0-9]+)/i', $text, $match)) {
|
||||
$line = (int) $match[1];
|
||||
}
|
||||
|
||||
return array(
|
||||
'ok' => $result['exit_code'] === 0 && $text !== '',
|
||||
'message' => $result['exit_code'] === 0 ? 'Синтаксис PHP без ошибок.' : 'В PHP-коде есть синтаксическая ошибка.',
|
||||
'output' => $text,
|
||||
'line' => $line,
|
||||
);
|
||||
$code = trim((string) $code);
|
||||
$offset = preg_match('/^<\?(?:php|=)?/i', $code) ? 0 : -1;
|
||||
return PhpSyntax::check(self::source($code), $offset);
|
||||
}
|
||||
|
||||
protected static function resolvePhpBinary()
|
||||
public static function cliBinary()
|
||||
{
|
||||
$candidates = array();
|
||||
$configured = trim((string) getenv('ADMINX_PHP_BINARY'));
|
||||
if ($configured !== '') { $candidates[] = $configured; }
|
||||
$config = PublicConfiguration::value('admin_console', array());
|
||||
if (is_array($config) && !empty($config['php_binary'])) {
|
||||
$candidates[] = trim((string) $config['php_binary']);
|
||||
}
|
||||
|
||||
if (defined('PHP_BINDIR') && PHP_BINDIR !== '') {
|
||||
$candidates[] = rtrim(PHP_BINDIR, '/\\') . DIRECTORY_SEPARATOR . 'php';
|
||||
}
|
||||
|
||||
if (defined('PHP_BINARY') && PHP_BINARY !== '') {
|
||||
$candidates[] = dirname(PHP_BINARY) . DIRECTORY_SEPARATOR . 'php';
|
||||
$candidates[] = PHP_BINARY;
|
||||
}
|
||||
|
||||
$candidates[] = 'php';
|
||||
|
||||
foreach (array_unique($candidates) as $candidate) {
|
||||
$result = self::runProcess(
|
||||
escapeshellarg((string) $candidate) . ' -r ' . escapeshellarg('echo PHP_VERSION_ID;'),
|
||||
1
|
||||
);
|
||||
if (!$result['timed_out'] && $result['exit_code'] === 0 && preg_match('/^\d{5,6}$/', trim($result['output']))) {
|
||||
return (string) $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected static function runProcess($command, $timeout)
|
||||
{
|
||||
$pipes = array();
|
||||
try {
|
||||
$process = @proc_open($command, array(
|
||||
0 => array('pipe', 'r'),
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
), $pipes);
|
||||
} catch (\Throwable $e) {
|
||||
return array('exit_code' => 1, 'timed_out' => false, 'output' => $e->getMessage());
|
||||
}
|
||||
|
||||
if (!is_resource($process)) {
|
||||
return array('exit_code' => 1, 'timed_out' => false, 'output' => 'Не удалось запустить PHP CLI.');
|
||||
}
|
||||
|
||||
fclose($pipes[0]);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
$started = microtime(true);
|
||||
$output = '';
|
||||
$error = '';
|
||||
$timedOut = false;
|
||||
$knownExitCode = null;
|
||||
do {
|
||||
$output .= (string) stream_get_contents($pipes[1]);
|
||||
$error .= (string) stream_get_contents($pipes[2]);
|
||||
$status = proc_get_status($process);
|
||||
if (!$status['running']) {
|
||||
$knownExitCode = isset($status['exitcode']) ? (int) $status['exitcode'] : null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (microtime(true) - $started >= max(1, (int) $timeout)) {
|
||||
$timedOut = true;
|
||||
proc_terminate($process, 9);
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(20000);
|
||||
} while (true);
|
||||
|
||||
$output .= (string) stream_get_contents($pipes[1]);
|
||||
$error .= (string) stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
$closedExitCode = proc_close($process);
|
||||
$exitCode = $knownExitCode !== null && $knownExitCode >= 0 ? $knownExitCode : $closedExitCode;
|
||||
|
||||
return array(
|
||||
'exit_code' => $exitCode,
|
||||
'timed_out' => $timedOut,
|
||||
'output' => $output . ($error !== '' ? ($output !== '' ? "\n" : '') . $error : ''),
|
||||
);
|
||||
$preferred = array(trim((string) getenv('ADMINX_PHP_BINARY')));
|
||||
if (is_array($config) && !empty($config['php_binary'])) { $preferred[] = trim((string) $config['php_binary']); }
|
||||
try { return CliRuntime::php($preferred); }
|
||||
catch (\RuntimeException $e) { throw new \RuntimeException($e->getMessage() . ' Для панели также доступны ADMINX_PHP_BINARY и admin_console.php_binary.'); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4563,6 +4563,212 @@ ul.sortable {
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
body.rich-editor-fullscreen-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
.rich-editor-status {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--border-default);
|
||||
background: var(--background-muted);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.rich-table-tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 0 0 auto;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
background: var(--background-surface);
|
||||
}
|
||||
.rich-table-tools[hidden],
|
||||
.rich-recovery[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.rich-search-match {
|
||||
background: #fff1ad;
|
||||
color: #242a33;
|
||||
}
|
||||
.rich-search-match.is-current {
|
||||
background: #ffcc66;
|
||||
outline: 1px solid #926600;
|
||||
}
|
||||
.rich-tools-modal {
|
||||
width: 640px;
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
.rich-tools-modal .modal-header {
|
||||
gap: 12px;
|
||||
}
|
||||
.rich-tools-modal .modal-header h3 {
|
||||
flex: 1;
|
||||
}
|
||||
.rich-tools-modal .modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.rich-tools-modal .modal-footer {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.rich-tools-modal .modal-footer .btn {
|
||||
white-space: normal;
|
||||
}
|
||||
.rich-tools-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.rich-symbols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(48px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.rich-symbols .btn {
|
||||
height: 48px;
|
||||
font-size: 22px;
|
||||
}
|
||||
.rich-snippets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.rich-snippets .btn {
|
||||
justify-content: flex-start;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
}
|
||||
.rich-review-row,
|
||||
.rich-block-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
.rich-review-row:last-child,
|
||||
.rich-block-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.rich-review-row > span,
|
||||
.rich-block-row > span {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.rich-review-row b,
|
||||
.rich-review-row small,
|
||||
.rich-block-row b,
|
||||
.rich-block-row small {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.rich-review-row small,
|
||||
.rich-block-row small {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.rich-block-row {
|
||||
cursor: grab;
|
||||
}
|
||||
.rich-block-row small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rich-tools-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 24px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.rich-recovery {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
.rich-recovery > span {
|
||||
flex: 1;
|
||||
}
|
||||
.rich-recovery-compare {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
.rich-recovery-compare textarea {
|
||||
min-height: 140px;
|
||||
}
|
||||
.rich-editor-prose .content-callout {
|
||||
padding: 14px 18px;
|
||||
border-left: 3px solid var(--color-primary);
|
||||
background: var(--background-muted);
|
||||
}
|
||||
.rich-editor-prose .content-callout--warning {
|
||||
border-left-color: #b38a20;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.rich-tools-modal {
|
||||
max-width: 100%;
|
||||
}
|
||||
.rich-snippets {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.rich-editor-fullscreen-parent {
|
||||
transform: none !important;
|
||||
filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
perspective: none !important;
|
||||
contain: none !important;
|
||||
will-change: auto !important;
|
||||
overflow: visible !important;
|
||||
position: relative !important;
|
||||
z-index: 196 !important;
|
||||
}
|
||||
.rich-editor.rich-editor-fullscreen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 197;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.rich-editor.rich-editor-fullscreen .rich-editor-toolbar {
|
||||
flex: 0 0 auto;
|
||||
max-height: 45%;
|
||||
overflow: auto;
|
||||
}
|
||||
.rich-editor.rich-editor-fullscreen .rich-editor-surface,
|
||||
.rich-editor.rich-editor-fullscreen .rich-editor-inline-source {
|
||||
flex: 1 1 0;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.rich-editor.rich-editor-fullscreen .rich-editor-content,
|
||||
.rich-editor.rich-editor-fullscreen .rich-editor-prose {
|
||||
min-height: 100% !important;
|
||||
}
|
||||
.rich-editor.rich-editor-fullscreen .rich-editor-inline-source .CodeMirror {
|
||||
height: 100% !important;
|
||||
min-height: 0;
|
||||
}
|
||||
.rich-editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,73 @@
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state';
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||
import { closeHistory } from '@tiptap/pm/history';
|
||||
|
||||
export const searchKey = new PluginKey('richSearch');
|
||||
|
||||
export function findMatches(doc, query, sensitive) {
|
||||
if (!query) { return []; }
|
||||
const matches = [];
|
||||
const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), sensitive ? 'g' : 'gi');
|
||||
doc.descendants((node, pos) => {
|
||||
if (!node.isTextblock) { return; }
|
||||
// Leaf nodes occupy one ProseMirror position; never match across them.
|
||||
const text = node.textBetween(0, node.content.size, '', '\ufffc');
|
||||
let found;
|
||||
while ((found = pattern.exec(text))) {
|
||||
if (!found[0].includes('\ufffc')) { matches.push({ from: pos + 1 + found.index, to: pos + 1 + found.index + found[0].length }); }
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return matches;
|
||||
}
|
||||
|
||||
export const RichSearch = Extension.create({
|
||||
name: 'richSearch',
|
||||
addProseMirrorPlugins() {
|
||||
return [new Plugin({
|
||||
key: searchKey,
|
||||
state: {
|
||||
init: () => ({ query: '', sensitive: false, active: 0, matches: [] }),
|
||||
apply(tr, previous) {
|
||||
const state = { ...previous, ...(tr.getMeta(searchKey) || {}) };
|
||||
if (tr.docChanged || tr.getMeta(searchKey)) { state.matches = findMatches(tr.doc, state.query, state.sensitive); }
|
||||
state.active = Math.max(0, Math.min(state.active, state.matches.length - 1));
|
||||
return state;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
const search = searchKey.getState(state);
|
||||
return DecorationSet.create(state.doc, search.matches.map((match, index) => Decoration.inline(match.from, match.to, { class: index === search.active ? 'rich-search-match is-current' : 'rich-search-match' })));
|
||||
}
|
||||
}
|
||||
})];
|
||||
}
|
||||
});
|
||||
|
||||
export const ContentFragments = Extension.create({
|
||||
name: 'contentFragments',
|
||||
addGlobalAttributes() {
|
||||
return [{ types: ['blockquote'], attributes: { class: {
|
||||
default: null,
|
||||
parseHTML: element => {
|
||||
const name = element.getAttribute('class') || '';
|
||||
return /\bcontent-callout(?:--(?:warning|info))?\b/.test(name) ? name : null;
|
||||
}
|
||||
} } }];
|
||||
}
|
||||
});
|
||||
|
||||
export function moveBlock(editor, fromIndex, toIndex) {
|
||||
const blocks = [];
|
||||
editor.state.doc.forEach((node, pos) => blocks.push({ node, pos }));
|
||||
if (!blocks[fromIndex] || toIndex < 0 || toIndex > blocks.length || fromIndex === toIndex || fromIndex + 1 === toIndex) { return false; }
|
||||
const source = blocks[fromIndex];
|
||||
let target = toIndex === blocks.length ? editor.state.doc.content.size : blocks[toIndex].pos;
|
||||
if (target > source.pos) { target -= source.node.nodeSize; }
|
||||
const tr = closeHistory(editor.state.tr).delete(source.pos, source.pos + source.node.nodeSize).insert(target, source.node);
|
||||
tr.setSelection(TextSelection.near(tr.doc.resolve(target))).scrollIntoView();
|
||||
editor.view.dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { toolDialog } from './_rich-tools';
|
||||
|
||||
export function installRecovery(api, root, textarea, editor) {
|
||||
const form = textarea.form;
|
||||
// Documents own a full-form draft already, including rich editors and source mode.
|
||||
if (form && form.querySelector('[data-document-draft-recovery]')) { return; }
|
||||
const actor = (form && form.dataset.actorId) || document.querySelector('[data-attention-user]')?.dataset.attentionUser;
|
||||
if (!actor || actor === '0' || !textarea.name) { return; }
|
||||
const key = 'ave.rich-draft:' + actor + ':' + location.pathname + location.search + ':' + (form?.id || form?.getAttribute('action') || '') + ':' + textarea.name;
|
||||
const baseline = textarea.value;
|
||||
let candidate;
|
||||
let timer;
|
||||
let disposed = false;
|
||||
const remove = () => { try { localStorage.removeItem(key); } catch (e) {} };
|
||||
try {
|
||||
candidate = JSON.parse(localStorage.getItem(key) || 'null');
|
||||
if (candidate && (typeof candidate.html !== 'string' || Date.now() - candidate.saved > 7 * 86400000 || candidate.html === baseline)) { remove(); candidate = null; }
|
||||
} catch (e) { candidate = null; }
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'rich-recovery';
|
||||
banner.hidden = !candidate;
|
||||
banner.innerHTML = '<span>Найден несохранённый текст</span><button type="button" class="btn btn-secondary btn-sm" data-recover>Посмотреть</button><button type="button" class="btn btn-ghost btn-icon btn-sm" aria-label="Удалить локальную копию" data-discard><i class="ti ti-trash"></i></button>';
|
||||
root.querySelector('.rich-editor-toolbar').after(banner);
|
||||
banner.querySelector('[data-discard]').addEventListener('click', () => { candidate = null; banner.hidden = true; remove(); });
|
||||
banner.querySelector('[data-recover]').addEventListener('click', () => {
|
||||
if (!candidate) { return; }
|
||||
const dialog = toolDialog('Восстановить текст', '<div class="rich-recovery-compare"><label class="field"><span class="field-label">Сейчас на сервере</span><textarea class="textarea" readonly data-current></textarea></label><label class="field"><span class="field-label">Локальная копия</span><textarea class="textarea" readonly data-local></textarea></label></div>', editor);
|
||||
dialog.body.querySelector('[data-current]').value = baseline;
|
||||
dialog.body.querySelector('[data-local]').value = candidate.html;
|
||||
const restore = document.createElement('button');
|
||||
restore.type = 'button'; restore.className = 'btn btn-primary'; restore.textContent = 'Восстановить в редактор';
|
||||
restore.addEventListener('click', () => { api.replaceHtml(textarea, candidate.html); candidate = null; banner.hidden = true; dialog.close(); });
|
||||
dialog.footer.appendChild(restore);
|
||||
});
|
||||
const save = () => {
|
||||
clearTimeout(timer);
|
||||
if (disposed || candidate || editor.isDestroyed) { return; }
|
||||
const html = api.syncTextarea(textarea, editor);
|
||||
if (html === baseline) { remove(); return; }
|
||||
if (html.length > 1000000) { return; }
|
||||
try { localStorage.setItem(key, JSON.stringify({ html, saved: Date.now() })); } catch (e) {}
|
||||
};
|
||||
const schedule = () => { clearTimeout(timer); timer = setTimeout(save, 700); };
|
||||
textarea.addEventListener('input', schedule);
|
||||
window.addEventListener('pagehide', save);
|
||||
const acknowledged = () => { clearTimeout(timer); remove(); };
|
||||
if (form) { form.addEventListener('rich:saved', acknowledged); }
|
||||
editor.on('destroy', () => {
|
||||
disposed = true; clearTimeout(timer);
|
||||
textarea.removeEventListener('input', schedule);
|
||||
window.removeEventListener('pagehide', save);
|
||||
if (form) { form.removeEventListener('rich:saved', acknowledged); }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import { searchKey, moveBlock } from './_rich-extensions';
|
||||
import { closeHistory } from '@tiptap/pm/history';
|
||||
|
||||
const escape = value => String(value == null ? '' : value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
export const tr = value => window.Adminx.tr ? window.Adminx.tr(value) : value;
|
||||
const button = (action, title, icon, text = '') => `<button type="button" class="btn btn-ghost btn-icon btn-sm" data-tool="${action}" aria-label="${escape(tr(title))}" data-tooltip="${escape(tr(title))}"><i class="ti ti-${icon}"></i>${escape(text)}</button>`;
|
||||
|
||||
export function readOptions(textarea) {
|
||||
let options = {};
|
||||
try { options = JSON.parse(textarea.getAttribute('data-rich-options') || '{}'); } catch (e) {}
|
||||
const profile = options.profile || (textarea.getAttribute('data-rich-preset') === 'slim' ? 'compact' : 'full');
|
||||
let headings = options.headings || 'profile';
|
||||
if (headings === 'profile') { headings = ['product', 'news'].includes(profile) ? 'no_h1' : 'all'; }
|
||||
return { ...options, profile, headings, product: !!textarea.closest('[data-catalog-mode="1"]'), snippets: options.snippets || 'all' };
|
||||
}
|
||||
|
||||
// One reusable dialog for the editor tools; content is trusted markup, values are escaped.
|
||||
export function toolDialog(title, content, editor, onClose) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'overlay rich-editor-tools-overlay show';
|
||||
overlay.innerHTML = `<div class="modal rich-tools-modal" role="dialog" aria-modal="true" aria-label="${escape(tr(title))}"><div class="modal-header"><h3>${escape(tr(title))}</h3><button class="modal-close" type="button" data-tools-close aria-label="${escape(tr('Закрыть'))}"><i class="ti ti-x"></i></button></div><div class="modal-body">${content}</div><div class="modal-footer"><button class="btn btn-secondary" type="button" data-tools-close>${escape(tr('Закрыть'))}</button></div></div>`;
|
||||
document.body.appendChild(overlay);
|
||||
let closed = false;
|
||||
const previous = document.activeElement;
|
||||
const close = () => {
|
||||
if (closed) { return; }
|
||||
closed = true;
|
||||
if (editor) { editor.off('destroy', close); }
|
||||
document.removeEventListener('keydown', key, true);
|
||||
overlay.remove();
|
||||
if (onClose) { onClose(); }
|
||||
if (previous && previous.isConnected) { previous.focus({ preventScroll: true }); }
|
||||
};
|
||||
const key = event => {
|
||||
if (event.key === 'Escape') { event.preventDefault(); event.stopImmediatePropagation(); close(); }
|
||||
if (event.key === 'Tab') {
|
||||
const nodes = Array.from(overlay.querySelectorAll('button:not(:disabled),input,select,textarea,a[href]')).filter(node => !node.hidden && node.getClientRects().length);
|
||||
if (!nodes.length) { return; }
|
||||
const index = nodes.indexOf(document.activeElement);
|
||||
event.preventDefault();
|
||||
nodes[(index + (event.shiftKey ? nodes.length - 1 : 1)) % nodes.length].focus();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', key, true);
|
||||
if (editor) { editor.on('destroy', close); }
|
||||
overlay.addEventListener('click', event => { if (event.target === overlay || event.target.closest('[data-tools-close]')) { close(); } });
|
||||
const first = overlay.querySelector('.modal-body input, .modal-body select, .modal-body button, .modal-close');
|
||||
if (first) { first.focus(); }
|
||||
return { overlay, close, body: overlay.querySelector('.modal-body'), footer: overlay.querySelector('.modal-footer') };
|
||||
}
|
||||
|
||||
export function openSearch(editor) {
|
||||
const dialog = toolDialog('Найти и заменить', `<label class="field"><span class="field-label">${tr('Найти')}</span><input class="input" data-find autocomplete="off"></label><label class="field"><span class="field-label">${tr('Заменить на')}</span><input class="input" data-replace autocomplete="off"></label><label class="checkbox"><input type="checkbox" data-case> ${tr('Учитывать регистр')}</label><div class="rich-tools-actions">${button('prev', 'Предыдущее', 'arrow-up')}${button('next', 'Следующее', 'arrow-down')}<output data-find-count aria-live="polite"></output></div><div class="rich-tools-actions"><button type="button" class="btn btn-secondary" data-replace-one>${tr('Заменить')}</button><button type="button" class="btn btn-secondary" data-replace-all>${tr('Заменить все')}</button></div>`, editor, () => {
|
||||
editor.off('transaction', updateCount);
|
||||
if (!editor.isDestroyed) { editor.view.dispatch(editor.state.tr.setMeta(searchKey, { query: '' })); }
|
||||
});
|
||||
const query = dialog.body.querySelector('[data-find]');
|
||||
const replacement = dialog.body.querySelector('[data-replace]');
|
||||
const sensitive = dialog.body.querySelector('[data-case]');
|
||||
function updateCount() {
|
||||
const state = searchKey.getState(editor.state);
|
||||
dialog.body.querySelector('[data-find-count]').textContent = state.matches.length ? `${state.active + 1} / ${state.matches.length}` : tr('Нет совпадений');
|
||||
dialog.body.querySelectorAll('[data-replace-one],[data-replace-all],[data-tool]').forEach(node => { node.disabled = !state.matches.length; });
|
||||
}
|
||||
function search() { editor.view.dispatch(editor.state.tr.setMeta(searchKey, { query: query.value, sensitive: sensitive.checked, active: 0 })); }
|
||||
function next(step) {
|
||||
const state = searchKey.getState(editor.state);
|
||||
if (!state.matches.length) { return; }
|
||||
const active = (state.active + step + state.matches.length) % state.matches.length;
|
||||
editor.view.dispatch(editor.state.tr.setMeta(searchKey, { active }));
|
||||
const node = editor.view.dom.querySelector('.rich-search-match.is-current');
|
||||
if (node) { node.scrollIntoView({ block: 'nearest' }); }
|
||||
}
|
||||
query.addEventListener('input', search);
|
||||
sensitive.addEventListener('change', search);
|
||||
query.addEventListener('keydown', event => { if (event.key === 'Enter') { event.preventDefault(); next(event.shiftKey ? -1 : 1); } });
|
||||
dialog.body.addEventListener('click', event => {
|
||||
const control = event.target.closest('button');
|
||||
if (!control || control.disabled) { return; }
|
||||
if (control.dataset.tool) { next(control.dataset.tool === 'prev' ? -1 : 1); return; }
|
||||
const state = searchKey.getState(editor.state);
|
||||
const matches = control.hasAttribute('data-replace-all') ? state.matches : [state.matches[state.active]];
|
||||
const transaction = closeHistory(editor.state.tr);
|
||||
matches.slice().reverse().forEach(match => {
|
||||
// Insert plain text with the first character's marks, never parse replacement as HTML.
|
||||
if (replacement.value) { transaction.replaceWith(match.from, match.to, editor.schema.text(replacement.value, transaction.doc.nodeAt(match.from)?.marks || [])); }
|
||||
else { transaction.delete(match.from, match.to); }
|
||||
});
|
||||
editor.view.dispatch(transaction);
|
||||
editor.view.dispatch(closeHistory(editor.state.tr));
|
||||
});
|
||||
editor.on('transaction', updateCount);
|
||||
const selected = editor.state.doc.textBetween(editor.state.selection.from, editor.state.selection.to, ' ');
|
||||
query.value = selected;
|
||||
search();
|
||||
}
|
||||
|
||||
export function openSymbols(editor) {
|
||||
const selection = { from: editor.state.selection.from, to: editor.state.selection.to };
|
||||
const symbols = [['\u00a0', 'Неразрывный пробел'], ['°', 'Градус'], ['×', 'Умножение'], ['±', 'Плюс-минус'], ['®', 'Зарегистрированный знак'], ['©', 'Авторское право'], ['™', 'Товарный знак'], ['≤', 'Меньше или равно'], ['≥', 'Больше или равно'], ['µ', 'Микро'], ['²', 'Квадрат'], ['³', 'Куб'], ['«', 'Открывающая кавычка'], ['»', 'Закрывающая кавычка'], ['–', 'Короткое тире'], ['—', 'Длинное тире'], ['…', 'Многоточие']];
|
||||
const dialog = toolDialog('Специальные символы', `<div class="rich-symbols">${symbols.map(([symbol, name], index) => `<button type="button" class="btn btn-secondary" data-symbol="${index}" aria-label="${escape(tr(name))}" data-tooltip="${escape(tr(name))}">${index ? escape(symbol) : '␣'}</button>`).join('')}</div>`, editor);
|
||||
dialog.body.addEventListener('click', event => {
|
||||
const item = event.target.closest('[data-symbol]');
|
||||
if (!item) { return; }
|
||||
editor.chain().focus().insertContentAt(selection, { type: 'text', text: symbols[Number(item.dataset.symbol)][0] }).run();
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
|
||||
const snippets = [
|
||||
{ id: 'warning', title: 'Внимание', group: 'info', html: '<blockquote class="content-callout content-callout--warning"><p><strong>Внимание</strong></p><p>Текст предупреждения.</p></blockquote>' },
|
||||
{ id: 'important', title: 'Важно', group: 'info', html: '<blockquote class="content-callout content-callout--info"><p><strong>Важно</strong></p><p>Дополнительная информация.</p></blockquote>' },
|
||||
{ id: 'equipment', title: 'Комплектация', group: 'product', html: '<h2>Комплектация</h2><ul><li><p>Наименование позиции</p></li></ul>' },
|
||||
{ id: 'specs', title: 'Таблица характеристик', group: 'product', html: '<h2>Характеристики</h2><table><tr><th><p>Параметр</p></th><th><p>Значение</p></th></tr><tr><td><p>Наименование</p></td><td><p>Значение</p></td></tr></table>' }
|
||||
];
|
||||
|
||||
export function openSnippets(editor, options) {
|
||||
const selection = { from: editor.state.selection.from, to: editor.state.selection.to };
|
||||
const available = options.snippets === 'none' ? [] : snippets.filter(item => options.snippets === 'all' || item.group === options.snippets).filter(item => !options.hide_table || item.id !== 'specs');
|
||||
const dialog = toolDialog('Готовые фрагменты', `<div class="rich-snippets">${available.map(item => `<button type="button" class="btn btn-secondary" data-snippet="${item.id}"><i class="ti ti-${item.id === 'specs' ? 'table' : 'template'}"></i>${escape(tr(item.title))}</button>`).join('')}</div>`, editor);
|
||||
dialog.body.addEventListener('click', event => {
|
||||
const control = event.target.closest('[data-snippet]');
|
||||
const item = control && available.find(candidate => candidate.id === control.dataset.snippet);
|
||||
if (!item) { return; }
|
||||
editor.chain().focus().insertContentAt(selection, item.html).run();
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
|
||||
export function openBlocks(editor) {
|
||||
const dialog = toolDialog('Порядок блоков', '<div class="rich-block-list" data-block-list></div>', editor, () => editor.off('update', render));
|
||||
const list = dialog.body.firstElementChild;
|
||||
let dragged = null;
|
||||
function render() {
|
||||
const rows = [];
|
||||
editor.state.doc.forEach((node, pos, index) => {
|
||||
const names = { paragraph: 'Абзац', heading: 'Заголовок', bulletList: 'Список', orderedList: 'Нумерованный список', blockquote: 'Цитата', image: 'Изображение', table: 'Таблица', horizontalRule: 'Разделитель', codeBlock: 'Код' };
|
||||
const title = node.type.name === 'heading' ? `H${node.attrs.level}` : tr(names[node.type.name] || node.type.name);
|
||||
rows.push(`<div class="rich-block-row" draggable="true" data-block-index="${index}"><i class="ti ti-grip-vertical" aria-hidden="true"></i><span><b>${escape(title)}</b><small>${escape((node.textContent || node.attrs.alt || node.attrs.src || '').slice(0, 120))}</small></span>${button('up', 'Переместить вверх', 'arrow-up')}${button('down', 'Переместить вниз', 'arrow-down')}</div>`);
|
||||
});
|
||||
list.innerHTML = rows.join('');
|
||||
if (list.firstElementChild) { list.firstElementChild.querySelector('[data-tool=up]').disabled = true; list.lastElementChild.querySelector('[data-tool=down]').disabled = true; }
|
||||
}
|
||||
list.addEventListener('dragstart', event => {
|
||||
const row = event.target.closest('[data-block-index]');
|
||||
if (!row) { return; }
|
||||
dragged = Number(row.dataset.blockIndex);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', String(dragged));
|
||||
});
|
||||
list.addEventListener('dragover', event => { if (dragged !== null) { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; } });
|
||||
list.addEventListener('drop', event => {
|
||||
if (dragged === null) { return; }
|
||||
event.preventDefault();
|
||||
const row = event.target.closest('[data-block-index]');
|
||||
let target = editor.state.doc.childCount;
|
||||
if (row) { target = Number(row.dataset.blockIndex) + (event.clientY > row.getBoundingClientRect().top + row.offsetHeight / 2 ? 1 : 0); }
|
||||
moveBlock(editor, dragged, target);
|
||||
dragged = null;
|
||||
});
|
||||
list.addEventListener('dragend', () => { dragged = null; });
|
||||
list.addEventListener('click', event => {
|
||||
const control = event.target.closest('[data-tool]');
|
||||
if (!control || control.disabled) { return; }
|
||||
const index = Number(control.closest('[data-block-index]').dataset.blockIndex);
|
||||
moveBlock(editor, index, control.dataset.tool === 'up' ? index - 1 : index + 2);
|
||||
});
|
||||
editor.on('update', render);
|
||||
render();
|
||||
}
|
||||
|
||||
export function inspectHtml(html, options) {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const issues = [];
|
||||
doc.body.querySelectorAll('*').forEach(node => {
|
||||
const tag = node.tagName;
|
||||
const sample = node.textContent.trim().slice(0, 90);
|
||||
if (tag === 'H1' && (options.product || options.headings !== 'all')) { issues.push({ node, kind: 'heading', title: 'H1 в тексте', sample }); }
|
||||
else if (/^H[1-6]$/.test(tag) && !node.textContent.trim() && !node.querySelector('img')) { issues.push({ node, kind: 'empty', title: 'Пустой заголовок', sample }); }
|
||||
if (tag === 'A' && (!(node.getAttribute('href') || '').trim() || (!node.textContent.trim() && !node.querySelector('img')))) { issues.push({ node, kind: 'link', title: 'Пустая ссылка', sample }); }
|
||||
if (node.hasAttribute('style')) { issues.push({ node, kind: 'style', title: 'Встроенное оформление', sample: `${sample} (${node.getAttribute('style')})`.slice(0, 140) }); }
|
||||
if (tag === 'P' && !node.textContent.trim() && !node.querySelector('img,table') && node.previousElementSibling && node.previousElementSibling.tagName === 'P' && !node.previousElementSibling.textContent.trim()) { issues.push({ node, kind: 'empty', title: 'Лишний пустой абзац', sample }); }
|
||||
});
|
||||
return { doc, issues };
|
||||
}
|
||||
|
||||
export function openReview(editor, textarea, options) {
|
||||
const api = window.Adminx.RichEditor;
|
||||
const dialog = toolDialog('Проверка оформления', '<div data-review-list></div>', editor);
|
||||
function render() {
|
||||
const result = inspectHtml(api.syncTextarea(textarea, editor), options);
|
||||
dialog.body.firstElementChild.innerHTML = result.issues.length ? result.issues.map((issue, index) => `<div class="rich-review-row"><span><b>${escape(tr(issue.title))}</b><small>${escape(issue.sample)}</small></span><button type="button" class="btn btn-secondary btn-sm" data-fix="${index}">${tr('Исправить')}</button></div>`).join('') : `<div class="rich-tools-empty"><i class="ti ti-circle-check"></i>${tr('Замечаний нет')}</div>`;
|
||||
dialog.body.querySelectorAll('[data-fix]').forEach(control => control.addEventListener('click', () => {
|
||||
const issue = result.issues[Number(control.dataset.fix)];
|
||||
if (issue.kind === 'style') { issue.node.removeAttribute('style'); }
|
||||
if (issue.kind === 'empty') { issue.node.remove(); }
|
||||
if (issue.kind === 'link') { issue.node.replaceWith(...Array.from(issue.node.childNodes)); }
|
||||
if (issue.kind === 'heading') {
|
||||
const heading = result.doc.createElement('h2');
|
||||
Array.from(issue.node.attributes).forEach(attr => heading.setAttribute(attr.name, attr.value));
|
||||
while (issue.node.firstChild) { heading.appendChild(issue.node.firstChild); }
|
||||
issue.node.replaceWith(heading);
|
||||
}
|
||||
api.replaceHtml(textarea, result.doc.body.innerHTML);
|
||||
render();
|
||||
}));
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
export function reviewBeforeSave(form, done) {
|
||||
const fields = Array.from(form.querySelectorAll('textarea[data-rich-editor]')).filter(node => node._adminxTiptap && !node._adminxTiptap.isDestroyed);
|
||||
const issues = fields.map(field => ({ field, result: inspectHtml(window.Adminx.RichEditor.syncTextarea(field, field._adminxTiptap), readOptions(field)) })).filter(item => item.result.issues.length);
|
||||
if (!issues.length) { return false; }
|
||||
const dialog = toolDialog('Проверьте оформление', `<div class="rich-review-summary">${issues.map((item, index) => `<div class="rich-review-row"><span><b>${escape(item.field.closest('.ax-document-field')?.querySelector('.field-label')?.textContent || item.field.name || tr('Текст'))}</b><small>${escape(item.result.issues.map(issue => tr(issue.title)).filter((v, i, a) => a.indexOf(v) === i).join(', '))}</small></span><button type="button" class="btn btn-secondary btn-sm" data-check-field="${index}">${tr('Проверить')}</button></div>`).join('')}</div>`, null);
|
||||
const save = document.createElement('button');
|
||||
save.type = 'button'; save.className = 'btn btn-primary'; save.textContent = tr('Сохранить без исправлений');
|
||||
save.addEventListener('click', () => { dialog.close(); done(); });
|
||||
dialog.footer.appendChild(save);
|
||||
dialog.body.addEventListener('click', event => {
|
||||
const control = event.target.closest('[data-check-field]');
|
||||
if (!control) { return; }
|
||||
const field = issues[Number(control.dataset.checkField)].field;
|
||||
dialog.close();
|
||||
openReview(field._adminxTiptap, field, readOptions(field));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export function installTools(api, root, textarea, editor, options) {
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'rich-editor-status';
|
||||
root.appendChild(footer);
|
||||
const tablebar = document.createElement('div');
|
||||
tablebar.className = 'rich-table-tools';
|
||||
tablebar.hidden = true;
|
||||
const commands = [
|
||||
['addRowBefore', 'Строка выше', 'row-insert-top'], ['addRowAfter', 'Строка ниже', 'row-insert-bottom'], ['deleteRow', 'Удалить строку', 'row-remove'],
|
||||
['addColumnBefore', 'Столбец слева', 'column-insert-left'], ['addColumnAfter', 'Столбец справа', 'column-insert-right'], ['deleteColumn', 'Удалить столбец', 'column-remove'],
|
||||
['mergeCells', 'Объединить ячейки', 'table-alias'], ['splitCell', 'Разделить ячейку', 'table-options'], ['toggleHeaderRow', 'Строка заголовков', 'table-row'], ['toggleHeaderColumn', 'Столбец заголовков', 'table-column'], ['deleteTable', 'Удалить таблицу', 'table-minus']
|
||||
];
|
||||
tablebar.innerHTML = commands.map(([name, title, icon]) => button(name, title, icon)).join('');
|
||||
root.querySelector('.rich-editor-toolbar').after(tablebar);
|
||||
tablebar.addEventListener('mousedown', event => event.preventDefault());
|
||||
tablebar.addEventListener('click', event => { const control = event.target.closest('[data-tool]'); if (control && !control.disabled) { editor.chain().focus()[control.dataset.tool]().run(); } });
|
||||
let lastDoc;
|
||||
let total = '';
|
||||
const stats = value => `${tr('Слов')}: ${(value.trim().match(/\S+/gu) || []).length} · ${tr('Символов')}: ${Array.from(value).length}`;
|
||||
function update() {
|
||||
const source = root.classList.contains('rich-editor-source-open');
|
||||
tablebar.hidden = source || !editor.isActive('table') || options.hide_table;
|
||||
if (!tablebar.hidden) { commands.forEach(([name]) => { tablebar.querySelector(`[data-tool="${name}"]`).disabled = !editor.can()[name](); }); }
|
||||
const selected = editor.state.doc.textBetween(editor.state.selection.from, editor.state.selection.to, ' ');
|
||||
if (source && root._richSourceEditor) {
|
||||
total = stats(new DOMParser().parseFromString(root._richSourceEditor.getValue(), 'text/html').body.textContent);
|
||||
lastDoc = null;
|
||||
} else if (lastDoc !== editor.state.doc) {
|
||||
lastDoc = editor.state.doc;
|
||||
total = stats(lastDoc.textBetween(0, lastDoc.content.size, ' '));
|
||||
}
|
||||
footer.textContent = total + (!source && selected ? ' | ' + tr('Выделено') + ': ' + stats(selected) : '');
|
||||
}
|
||||
editor.on('transaction', update);
|
||||
root.addEventListener('rich:source-change', update);
|
||||
root.addEventListener('rich:mode-change', update);
|
||||
update();
|
||||
editor.on('destroy', () => { editor.off('transaction', update); root.removeEventListener('rich:source-change', update); root.removeEventListener('rich:mode-change', update); });
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Clipboard HTML is parsed in an inert document, never inserted into the live DOM.
|
||||
export function isWordHtml(html) {
|
||||
return /\bmso-|class=["']?Mso|urn:schemas-microsoft-com:office|Microsoft Word/i.test(html);
|
||||
}
|
||||
|
||||
export function cleanWordHtml(html) {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
doc.querySelectorAll('script,style,meta,link,iframe,object,embed,svg,math,form,input,button').forEach(node => node.remove());
|
||||
const comments = doc.createTreeWalker(doc.body, NodeFilter.SHOW_COMMENT);
|
||||
const remove = [];
|
||||
while (comments.nextNode()) { remove.push(comments.currentNode); }
|
||||
remove.forEach(node => node.remove());
|
||||
|
||||
doc.querySelectorAll('p').forEach(node => {
|
||||
const level = (node.className || '').match(/\bMsoHeading([1-6])\b/i) || (node.getAttribute('style') || '').match(/mso-outline-level:\s*([1-6])\b/i);
|
||||
if (!level) { return; }
|
||||
const heading = doc.createElement('h' + level[1]);
|
||||
while (node.firstChild) { heading.appendChild(node.firstChild); }
|
||||
node.replaceWith(heading);
|
||||
});
|
||||
|
||||
// Desktop Word exports list items as paragraphs with mso-list and marker spans.
|
||||
const lists = new Map();
|
||||
doc.querySelectorAll('p').forEach(node => {
|
||||
const style = node.getAttribute('style') || '';
|
||||
const match = style.match(/mso-list:\s*(l\d+)\s+level(\d+)\s+(lfo\d+)/i);
|
||||
if (!match) { return; }
|
||||
const marker = Array.from(node.querySelectorAll('span')).find(span => /mso-list:\s*ignore/i.test(span.getAttribute('style') || ''));
|
||||
const label = marker ? marker.textContent.trim() : '';
|
||||
const ordered = /^(?:\d+|[a-z]+)[.)]/i.test(label);
|
||||
lists.set(node, { key: match[1] + match[3], level: Number(match[2]), type: ordered ? 'ol' : 'ul', start: /^\d+/.test(label) ? parseInt(label, 10) : 1 });
|
||||
if (marker) { marker.remove(); }
|
||||
});
|
||||
const parents = new Set(Array.from(lists.keys()).map(node => node.parentNode));
|
||||
parents.forEach(parent => {
|
||||
let stack = [];
|
||||
let key = '';
|
||||
Array.from(parent.childNodes).forEach(node => {
|
||||
if (node.nodeType === 3 && !node.textContent.trim()) { return; }
|
||||
const item = lists.get(node);
|
||||
if (!item) { stack = []; key = ''; return; }
|
||||
if (key !== item.key) { stack = []; key = item.key; }
|
||||
while (stack.length && stack[stack.length - 1].level > item.level) { stack.pop(); }
|
||||
if (stack.length && stack[stack.length - 1].level === item.level && stack[stack.length - 1].type !== item.type) { stack.pop(); }
|
||||
if (!stack.length || stack[stack.length - 1].level < item.level) {
|
||||
const list = doc.createElement(item.type);
|
||||
if (item.type === 'ol' && item.start > 1) { list.setAttribute('start', item.start); }
|
||||
if (stack.length) { stack[stack.length - 1].last.appendChild(list); }
|
||||
else { parent.insertBefore(list, node); }
|
||||
stack.push({ level: item.level, type: item.type, list: list, last: null });
|
||||
}
|
||||
const li = doc.createElement('li');
|
||||
const entry = stack[stack.length - 1];
|
||||
entry.list.appendChild(li);
|
||||
li.appendChild(node);
|
||||
entry.last = li;
|
||||
});
|
||||
});
|
||||
|
||||
const allowed = new Set('p div br h1 h2 h3 h4 h5 h6 strong b em i u s strike del blockquote pre code ul ol li a table thead tbody tfoot tr td th hr'.split(' '));
|
||||
Array.from(doc.body.querySelectorAll('*')).reverse().forEach(node => {
|
||||
if (!node.parentNode) { return; }
|
||||
const tag = node.tagName.toLowerCase();
|
||||
// Convert meaningful inline formatting before stripping presentation attributes.
|
||||
const marks = [];
|
||||
const weight = node.style.fontWeight;
|
||||
if (weight === 'bold' || Number(weight) >= 600) { marks.push('strong'); }
|
||||
if (node.style.fontStyle === 'italic') { marks.push('em'); }
|
||||
const decoration = node.style.textDecoration;
|
||||
if (/underline/.test(decoration)) { marks.push('u'); }
|
||||
if (/line-through/.test(decoration)) { marks.push('s'); }
|
||||
marks.forEach(mark => {
|
||||
const wrapper = doc.createElement(mark);
|
||||
while (node.firstChild) { wrapper.appendChild(node.firstChild); }
|
||||
node.appendChild(wrapper);
|
||||
});
|
||||
Array.from(node.attributes).forEach(attr => {
|
||||
const name = attr.name.toLowerCase();
|
||||
const value = attr.value.trim();
|
||||
const safeLink = tag === 'a' && name === 'href' && /^(https?:\/\/|mailto:|tel:|\/|#)/i.test(value) && !/[\u0000-\u0020]/.test(value);
|
||||
const span = /^(td|th)$/.test(tag) && /^(colspan|rowspan)$/.test(name) && /^[1-9]\d{0,2}$/.test(value);
|
||||
const start = tag === 'ol' && name === 'start' && /^[1-9]\d{0,5}$/.test(value);
|
||||
if (!safeLink && !span && !start) { node.removeAttribute(attr.name); }
|
||||
});
|
||||
if (!allowed.has(tag)) {
|
||||
node.replaceWith(...Array.from(node.childNodes));
|
||||
}
|
||||
});
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
@@ -7,6 +7,10 @@ import TableRow from '@tiptap/extension-table-row';
|
||||
import TableCell from '@tiptap/extension-table-cell';
|
||||
import TableHeader from '@tiptap/extension-table-header';
|
||||
import TextAlign from '@tiptap/extension-text-align';
|
||||
import { isWordHtml, cleanWordHtml } from './_word-paste';
|
||||
import { RichSearch, ContentFragments } from './_rich-extensions';
|
||||
import { tr, readOptions, openSearch, openSymbols, openSnippets, openBlocks, openReview, reviewBeforeSave, installTools } from './_rich-tools';
|
||||
import { installRecovery } from './_rich-recovery';
|
||||
|
||||
var EnhancedImage = Image.extend({
|
||||
addAttributes: function () {
|
||||
@@ -90,6 +94,8 @@ var EnhancedImage = Image.extend({
|
||||
|
||||
extensions: function () {
|
||||
return [
|
||||
RichSearch,
|
||||
ContentFragments,
|
||||
StarterKit.configure({ link: false }),
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
@@ -107,6 +113,7 @@ var EnhancedImage = Image.extend({
|
||||
},
|
||||
|
||||
create: function (textarea) {
|
||||
var options = readOptions(textarea);
|
||||
var preset = textarea.getAttribute('data-rich-preset') || 'full';
|
||||
var root = el('div', { class: 'rich-editor rich-editor-' + preset });
|
||||
var toolbar = el('div', { class: 'rich-editor-toolbar', role: 'toolbar' });
|
||||
@@ -131,6 +138,9 @@ var EnhancedImage = Image.extend({
|
||||
extensions: this.extensions(),
|
||||
content: textarea.value || '',
|
||||
editorProps: {
|
||||
transformPastedHTML: function (html) {
|
||||
return isWordHtml(html) ? cleanWordHtml(html) : html;
|
||||
},
|
||||
attributes: {
|
||||
class: 'rich-editor-prose',
|
||||
spellcheck: textarea.getAttribute('spellcheck') || 'true'
|
||||
@@ -155,15 +165,18 @@ var EnhancedImage = Image.extend({
|
||||
}
|
||||
});
|
||||
|
||||
this.buildToolbar(toolbar, editor);
|
||||
this.buildToolbar(toolbar, editor, options, textarea);
|
||||
textarea.setAttribute('data-rich-editor-ready', '1');
|
||||
textarea._adminxTiptap = editor;
|
||||
this.instances.push(editor);
|
||||
this.bindForm(textarea, editor);
|
||||
this.syncToolbar(root, editor);
|
||||
installTools(this, root, textarea, editor, options);
|
||||
installRecovery(this, root, textarea, editor);
|
||||
},
|
||||
|
||||
button: function (name, tooltip, iconName, run) {
|
||||
tooltip = tr(tooltip);
|
||||
var btn = el('button', {
|
||||
class: 'btn btn-ghost btn-icon btn-sm rich-editor-btn',
|
||||
type: 'button',
|
||||
@@ -175,7 +188,7 @@ var EnhancedImage = Image.extend({
|
||||
return btn;
|
||||
},
|
||||
|
||||
buildToolbar: function (toolbar, editor) {
|
||||
buildToolbar: function (toolbar, editor, options, textarea) {
|
||||
var group;
|
||||
var self = this;
|
||||
function addGroup() {
|
||||
@@ -183,6 +196,11 @@ var EnhancedImage = Image.extend({
|
||||
toolbar.appendChild(group);
|
||||
}
|
||||
function add(name, tooltip, iconName, run) {
|
||||
if (options.hide_image && name === 'image') { return; }
|
||||
if (options.hide_table && name === 'table') { return; }
|
||||
if (options.hide_source && name === 'source') { return; }
|
||||
if (options.hide_align && name.indexOf('align') === 0) { return; }
|
||||
if (options.profile === 'compact' && ['image', 'table', 'code', 'alignCenter', 'alignRight', 'showBlocks'].indexOf(name) !== -1) { return; }
|
||||
group.appendChild(self.button(name, tooltip, iconName, run));
|
||||
}
|
||||
function addBlockSelect() {
|
||||
@@ -201,16 +219,20 @@ var EnhancedImage = Image.extend({
|
||||
['heading-5', 'Заголовок H5'],
|
||||
['heading-6', 'Заголовок H6']
|
||||
].forEach(function (item) {
|
||||
if (item[0].indexOf('heading-') === 0 && (options.headings === 'none' || (options.headings === 'no_h1' && item[0] === 'heading-1'))) { return; }
|
||||
var option = el('option', { value: item[0] });
|
||||
option.textContent = item[1];
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener('change', function () {
|
||||
if (select.value === 'paragraph') {
|
||||
// Focusing in Safari/iOS synchronously refreshes the toolbar value.
|
||||
var block = select.value;
|
||||
var level = parseInt(block.replace('heading-', ''), 10);
|
||||
if (block === 'paragraph') {
|
||||
editor.chain().focus().setParagraph().run();
|
||||
return;
|
||||
}
|
||||
editor.chain().focus().setHeading({ level: parseInt(select.value.replace('heading-', ''), 10) }).run();
|
||||
editor.chain().focus().setHeading({ level: level }).run();
|
||||
});
|
||||
group.appendChild(select);
|
||||
}
|
||||
@@ -218,6 +240,7 @@ var EnhancedImage = Image.extend({
|
||||
addGroup();
|
||||
add('undo', 'Отменить', 'arrow-back-up', function () { editor.chain().focus().undo().run(); });
|
||||
add('redo', 'Повторить', 'arrow-forward-up', function () { editor.chain().focus().redo().run(); });
|
||||
add('pasteWord', 'Вставить из Word', 'clipboard-text', function () { self.openWordPaste(editor); });
|
||||
|
||||
addGroup();
|
||||
add('bold', 'Жирный', 'bold', function () { editor.chain().focus().toggleBold().run(); });
|
||||
@@ -238,6 +261,14 @@ var EnhancedImage = Image.extend({
|
||||
|
||||
addGroup();
|
||||
add('link', 'Ссылка', 'link', function () { self.openLink(editor); });
|
||||
add('documentLink', 'Ссылка на документ или товар', 'file-search', function () {
|
||||
var selection = { from: editor.state.selection.from, to: editor.state.selection.to };
|
||||
self.openDocumentLinkPicker(function (item) {
|
||||
var href = item.url || '/' + String(item.alias || '').replace(/^\/+/, '');
|
||||
if (selection.from !== selection.to) { editor.chain().focus().setTextSelection(selection).setLink({ href: href }).run(); }
|
||||
else { editor.chain().focus().insertContentAt(selection, { type: 'text', text: item.title || href, marks: [{ type: 'link', attrs: { href: href } }] }).run(); }
|
||||
});
|
||||
});
|
||||
add('image', 'Изображение', 'photo-plus', function () {
|
||||
self.openMediaPicker(editor);
|
||||
});
|
||||
@@ -252,10 +283,140 @@ var EnhancedImage = Image.extend({
|
||||
});
|
||||
|
||||
addGroup();
|
||||
add('fullscreen', 'Развернуть редактор', 'arrows-maximize', function () { self.toggleFullscreen(editor); });
|
||||
if (window.CodeMirror) {
|
||||
add('source', 'Исходник', 'code-dots', function () { self.openSource(editor); });
|
||||
}
|
||||
add('clear', 'Очистить формат', 'eraser', function () { editor.chain().focus().unsetAllMarks().clearNodes().run(); });
|
||||
addGroup();
|
||||
add('find', 'Найти и заменить', 'replace', function () { openSearch(editor); });
|
||||
add('symbols', 'Специальные символы', 'omega', function () { openSymbols(editor); });
|
||||
if (options.snippets !== 'none') { add('snippets', 'Готовые фрагменты', 'template', function () { openSnippets(editor, options); }); }
|
||||
if (!options.hide_blocks) { add('blocks', 'Порядок блоков', 'list-details', function () { openBlocks(editor); }); }
|
||||
add('review', 'Проверка оформления', 'text-spellcheck', function () { openReview(editor, textarea, options); });
|
||||
toolbar.querySelectorAll('.rich-editor-group').forEach(function (node) { if (!node.children.length) { node.remove(); } });
|
||||
},
|
||||
|
||||
reviewBeforeSave: reviewBeforeSave,
|
||||
|
||||
replaceHtml: function (textarea, html) {
|
||||
var editor = textarea._adminxTiptap;
|
||||
if (!editor || editor.isDestroyed) { return; }
|
||||
var root = textarea.closest('.rich-editor');
|
||||
editor.commands.setContent(html || '<p></p>');
|
||||
if (root && root._richSourceEditor) {
|
||||
root._richSourceSyncing = true;
|
||||
root._richSourceEditor.setValue(html);
|
||||
root._richSourceSyncing = false;
|
||||
}
|
||||
// Preserve source attributes Tiptap does not model until the next visual edit.
|
||||
textarea.value = html;
|
||||
textarea.removeAttribute('data-rich-editor-dirty');
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
},
|
||||
|
||||
toggleFullscreen: function (editor) {
|
||||
if (this.fullscreen && this.fullscreen.editor === editor) { this.fullscreen.close(); return; }
|
||||
if (this.fullscreen) { this.fullscreen.close(); }
|
||||
var self = this;
|
||||
var root = editor.view.dom.closest('.rich-editor');
|
||||
var ancestors = [];
|
||||
var scrollX = window.scrollX;
|
||||
var scrollY = window.scrollY;
|
||||
var parent = root.parentElement;
|
||||
// Keep the editor inside its form; transformed drawers must not clip fixed positioning.
|
||||
while (parent && parent !== document.body) {
|
||||
parent.classList.add('rich-editor-fullscreen-parent');
|
||||
ancestors.push(parent);
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
root.classList.add('rich-editor-fullscreen');
|
||||
document.body.classList.add('rich-editor-fullscreen-open');
|
||||
function refresh() {
|
||||
if (root._richSourceEditor) { root._richSourceEditor.refresh(); }
|
||||
}
|
||||
function close() {
|
||||
if (!self.fullscreen || self.fullscreen.editor !== editor) { return; }
|
||||
root.classList.remove('rich-editor-fullscreen');
|
||||
ancestors.forEach(function (node) { node.classList.remove('rich-editor-fullscreen-parent'); });
|
||||
document.body.classList.remove('rich-editor-fullscreen-open');
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
window.removeEventListener('resize', refresh);
|
||||
observer.disconnect();
|
||||
editor.off('destroy', close);
|
||||
self.fullscreen = null;
|
||||
if (!editor.isDestroyed) { self.syncToolbar(root, editor); refresh(); }
|
||||
window.scrollTo(scrollX, scrollY);
|
||||
}
|
||||
function onKey(event) {
|
||||
if (event.key !== 'Escape') { return; }
|
||||
var childDialog = Array.from(document.querySelectorAll('.overlay.show, .rich-editor-tools-overlay, .rich-editor-word-overlay, .rich-editor-link-overlay, .rich-editor-image-overlay, .rich-editor-document-picker-overlay, .media-picker-overlay.show')).some(function (node) { return !node.contains(root); });
|
||||
if (childDialog) { return; }
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
close();
|
||||
}
|
||||
var observer = new MutationObserver(function () { if (!root.isConnected || editor.isDestroyed) { close(); } });
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
this.fullscreen = { editor: editor, close: close };
|
||||
editor.on('destroy', close);
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
window.addEventListener('resize', refresh);
|
||||
this.syncToolbar(root, editor);
|
||||
requestAnimationFrame(refresh);
|
||||
},
|
||||
|
||||
openWordPaste: function (editor) {
|
||||
var self = this;
|
||||
var selection = { from: editor.state.selection.from, to: editor.state.selection.to };
|
||||
var overlay = el('div', { class: 'overlay rich-editor-word-overlay' });
|
||||
var modal = el('div', { class: 'modal rich-editor-prompt', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'richEditorWordTitle' });
|
||||
modal.innerHTML = '<div class="modal-header"><span class="dialog-icon info"><i class="ti ti-clipboard-text"></i></span>' +
|
||||
'<div style="flex:1"><h3 id="richEditorWordTitle">Вставить из Word</h3></div>' +
|
||||
'<button class="modal-close" type="button" data-word-cancel aria-label="Закрыть"><i class="ti ti-x"></i></button></div>' +
|
||||
'<div class="modal-body"><textarea class="input" rows="8" style="width:100%;resize:vertical;min-height:180px" aria-label="Текст из Word" placeholder="Вставьте скопированный текст" data-word-input></textarea></div>' +
|
||||
'<div class="modal-footer"><button class="btn btn-ghost" type="button" data-word-cancel>Отмена</button><button class="btn btn-primary" type="button" data-word-apply disabled><i class="ti ti-check"></i>Вставить</button></div>';
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
var input = overlay.querySelector('[data-word-input]');
|
||||
var apply = overlay.querySelector('[data-word-apply]');
|
||||
var html = null;
|
||||
var closed = false;
|
||||
function close() {
|
||||
if (closed) { return; }
|
||||
closed = true;
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
overlay.remove();
|
||||
if (!editor.isDestroyed) { editor.commands.focus(); }
|
||||
}
|
||||
function onKey(event) {
|
||||
if (event.key === 'Escape') { event.preventDefault(); event.stopImmediatePropagation(); close(); }
|
||||
if (event.key === 'Tab') {
|
||||
var controls = Array.from(modal.querySelectorAll('button:not(:disabled),textarea'));
|
||||
var index = controls.indexOf(document.activeElement);
|
||||
event.preventDefault();
|
||||
controls[(index + (event.shiftKey ? controls.length - 1 : 1)) % controls.length].focus();
|
||||
}
|
||||
}
|
||||
input.addEventListener('paste', function (event) {
|
||||
var data = event.clipboardData;
|
||||
if (!data) { return; }
|
||||
event.preventDefault();
|
||||
var source = data.getData('text/html');
|
||||
html = source ? cleanWordHtml(source) : null;
|
||||
input.value = data.getData('text/plain') || (html ? new DOMParser().parseFromString(html, 'text/html').body.textContent : '');
|
||||
apply.disabled = !input.value.trim();
|
||||
});
|
||||
input.addEventListener('input', function () { html = null; apply.disabled = !input.value.trim(); });
|
||||
overlay.addEventListener('click', function (event) {
|
||||
if (event.target === overlay || event.target.closest('[data-word-cancel]')) { close(); return; }
|
||||
if (!event.target.closest('[data-word-apply]') || apply.disabled || editor.isDestroyed) { return; }
|
||||
var content = html !== null ? html : input.value.split(/\r?\n/).map(function (line) { return '<p>' + self.escape(line) + '</p>'; }).join('');
|
||||
editor.chain().focus().insertContentAt(selection, content).run();
|
||||
close();
|
||||
});
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
requestAnimationFrame(function () { if (!closed) { overlay.classList.add('show'); input.focus(); } });
|
||||
},
|
||||
|
||||
syncToolbar: function (root, editor) {
|
||||
@@ -278,12 +439,29 @@ var EnhancedImage = Image.extend({
|
||||
var btn = root.querySelector('[data-rich-command="' + name + '"]');
|
||||
if (btn) { btn.setAttribute('aria-pressed', states[name] ? 'true' : 'false'); }
|
||||
});
|
||||
var fullscreen = root.querySelector('[data-rich-command="fullscreen"]');
|
||||
if (fullscreen) {
|
||||
var expanded = root.classList.contains('rich-editor-fullscreen');
|
||||
var label = tr(expanded ? 'Свернуть редактор' : 'Развернуть редактор');
|
||||
fullscreen.setAttribute('aria-pressed', expanded ? 'true' : 'false');
|
||||
fullscreen.setAttribute('aria-label', label);
|
||||
fullscreen.setAttribute('data-tooltip', label);
|
||||
fullscreen.innerHTML = icon(expanded ? 'arrows-minimize' : 'arrows-maximize');
|
||||
}
|
||||
var blockSelect = root.querySelector('[data-rich-block-select]');
|
||||
if (blockSelect) {
|
||||
var blockValue = 'paragraph';
|
||||
for (var level = 1; level <= 6; level++) {
|
||||
if (editor.isActive('heading', { level: level })) { blockValue = 'heading-' + level; break; }
|
||||
}
|
||||
var currentOnly = blockSelect.querySelector('[data-rich-current-block]');
|
||||
if (currentOnly) { currentOnly.remove(); }
|
||||
if (!Array.from(blockSelect.options).some(function (option) { return option.value === blockValue; })) {
|
||||
var currentOption = el('option', { value: blockValue, 'data-rich-current-block': '' });
|
||||
currentOption.textContent = 'H' + blockValue.replace('heading-', '') + ' (' + tr('в тексте') + ')';
|
||||
currentOption.disabled = true;
|
||||
blockSelect.appendChild(currentOption);
|
||||
}
|
||||
blockSelect.value = blockValue;
|
||||
}
|
||||
},
|
||||
@@ -328,6 +506,7 @@ var EnhancedImage = Image.extend({
|
||||
root.classList.remove('rich-editor-source-open');
|
||||
this.setSourceControls(root, false);
|
||||
this.syncToolbar(root, editor);
|
||||
root.dispatchEvent(new Event('rich:mode-change'));
|
||||
editor.commands.focus();
|
||||
return;
|
||||
}
|
||||
@@ -352,23 +531,25 @@ var EnhancedImage = Image.extend({
|
||||
storage.value = cm.getValue();
|
||||
storage.setAttribute('data-rich-editor-dirty', '1');
|
||||
storage.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
root.dispatchEvent(new Event('rich:source-change'));
|
||||
});
|
||||
}
|
||||
|
||||
var height = Math.max(260, Math.round(surface.getBoundingClientRect().height || 0));
|
||||
root._richSourceSyncing = true;
|
||||
root._richSourceEditor.setValue(editor.getHTML());
|
||||
root._richSourceEditor.setValue(storage ? this.syncTextarea(storage, editor) : editor.getHTML());
|
||||
root._richSourceSyncing = false;
|
||||
root._richSourceEditor.setSize('100%', height);
|
||||
root.classList.add('rich-editor-source-open');
|
||||
this.setSourceControls(root, true);
|
||||
this.syncToolbar(root, editor);
|
||||
root.dispatchEvent(new Event('rich:mode-change'));
|
||||
setTimeout(function () { root._richSourceEditor.refresh(); root._richSourceEditor.focus(); }, 0);
|
||||
},
|
||||
|
||||
setSourceControls: function (root, sourceOpen) {
|
||||
root.querySelectorAll('.rich-editor-toolbar [data-rich-command], .rich-editor-toolbar [data-rich-block-select]').forEach(function (control) {
|
||||
if (control.getAttribute('data-rich-command') === 'source') { return; }
|
||||
if (['source', 'fullscreen'].indexOf(control.getAttribute('data-rich-command')) !== -1) { return; }
|
||||
control.disabled = !!sourceOpen;
|
||||
});
|
||||
},
|
||||
|
||||
+16
-3
@@ -173,6 +173,7 @@
|
||||
|
||||
$authUser && Auth::ensureBrowserToken();
|
||||
$canAdmin = $authUser !== null && Permission::checkAcp('admin_panel');
|
||||
$isAttentionPoll = $requestPath === '/dashboard/attention';
|
||||
$forcePasswordPath = in_array($requestPath, array('/account/password', '/logout', '/locale'), true);
|
||||
if ($canAdmin && !empty($authUser['must_change_password']) && !$forcePasswordPath) {
|
||||
if (Request::isAjax()) {
|
||||
@@ -235,7 +236,7 @@
|
||||
|
||||
$canClearCache = Permission::check('manage_settings');
|
||||
$cacheSizes = array();
|
||||
if ($canClearCache) {
|
||||
if ($canClearCache && !$isAttentionPoll) {
|
||||
$cacheSizes = Cache::remember('adminx.cache.sizes.' . md5((string) ADMINX_BASE), 30, function () {
|
||||
$out = array();
|
||||
foreach (\App\Adminx\Settings\Model::cacheRows() as $row) {
|
||||
@@ -252,7 +253,7 @@
|
||||
'user_name' => $authUser['name'] ?? '',
|
||||
'user_email' => $authUser['email'] ?? '',
|
||||
'user_role' => $authUser['role'] ?? '',
|
||||
'module_header_actions' => $canAdmin
|
||||
'module_header_actions' => $canAdmin && !$isAttentionPoll
|
||||
? \App\Adminx\Support\ModuleExtensions::headerActions()
|
||||
: array(),
|
||||
'can_clear_cache' => $canClearCache,
|
||||
@@ -265,7 +266,7 @@
|
||||
//-- Сводка уведомлений в шапке (новые заказы/письма) — только для админа.
|
||||
$notifications = array('total' => 0, 'orders' => 0, 'messages' => 0, 'items' => array());
|
||||
$notificationPreferences = array();
|
||||
if ($canAdmin) {
|
||||
if ($canAdmin && !$isAttentionPoll) {
|
||||
$notificationPreferences = \App\Adminx\Support\ModuleExtensions::notificationPreferences();
|
||||
$notificationKey = 'adminx.notifications.' . (int) Auth::id() . '.' . md5((string) ADMINX_BASE);
|
||||
$notifications = Cache::remember($notificationKey, 15, function () {
|
||||
@@ -280,6 +281,18 @@
|
||||
'can_manage_notification_preferences' => Permission::check('manage_settings'),
|
||||
));
|
||||
|
||||
$attention = array('items' => array());
|
||||
$dashboardModule = \App\Common\ModuleManager::get('dashboard');
|
||||
if ($canAdmin && !empty($dashboardModule['enabled'])) {
|
||||
$attention = \App\Adminx\Dashboard\Attention::summary(ADMINX_BASE);
|
||||
if ($attention['items'] && !$isAttentionPoll) {
|
||||
AdminAssets::addStyle(ADMINX_BASE . '/modules/Dashboard/assets/attention.css', 55);
|
||||
AdminAssets::addScript(ADMINX_BASE . '/modules/Dashboard/assets/attention.js', 55);
|
||||
}
|
||||
}
|
||||
|
||||
Twig::addGlobals(array('attention' => $attention, 'attention_user_id' => (int) Auth::id()));
|
||||
|
||||
if (!$isPublic && !$canAdmin) {
|
||||
if (Request::isAjax()) {
|
||||
Response::unauthorized();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="attention_heading">Needs attention</phrase>
|
||||
<phrase data="attention_empty">Nothing new</phrase>
|
||||
<phrase data="attention_wait">Oldest waiting %s</phrase>
|
||||
<phrase data="attention_waiting">Awaiting processing</phrase>
|
||||
<phrase data="attention_minutes">min</phrase>
|
||||
<phrase data="attention_hours">h</phrase>
|
||||
<phrase data="attention_days">d</phrase>
|
||||
<phrase data="attention_unavailable">Unable to refresh</phrase>
|
||||
<phrase data="attention_stale">Updates temporarily unavailable</phrase>
|
||||
<phrase data="attention_orders">New orders</phrase>
|
||||
<phrase data="attention_messages">New messages</phrase>
|
||||
<phrase data="attention_order_filter">New: awaiting processing</phrase>
|
||||
</language>
|
||||
@@ -1,5 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="rich_tool_001">Find and replace</phrase>
|
||||
<phrase data="rich_tool_002">Find</phrase>
|
||||
<phrase data="rich_tool_003">Replace with</phrase>
|
||||
<phrase data="rich_tool_004">Replace</phrase>
|
||||
<phrase data="rich_tool_005">Replace all</phrase>
|
||||
<phrase data="rich_tool_006">Match case</phrase>
|
||||
<phrase data="rich_tool_007">No matches</phrase>
|
||||
<phrase data="rich_tool_008">Previous</phrase>
|
||||
<phrase data="rich_tool_009">Next</phrase>
|
||||
<phrase data="rich_tool_010">Special characters</phrase>
|
||||
<phrase data="rich_tool_011">Non-breaking space</phrase>
|
||||
<phrase data="rich_tool_012">Degree</phrase>
|
||||
<phrase data="rich_tool_013">Multiplication</phrase>
|
||||
<phrase data="rich_tool_014">Plus/minus</phrase>
|
||||
<phrase data="rich_tool_015">Registered trademark</phrase>
|
||||
<phrase data="rich_tool_016">Copyright</phrase>
|
||||
<phrase data="rich_tool_017">Trademark</phrase>
|
||||
<phrase data="rich_tool_018">Less than or equal</phrase>
|
||||
<phrase data="rich_tool_019">Greater than or equal</phrase>
|
||||
<phrase data="rich_tool_020">Micro</phrase>
|
||||
<phrase data="rich_tool_021">Squared</phrase>
|
||||
<phrase data="rich_tool_022">Cubed</phrase>
|
||||
<phrase data="rich_tool_023">Opening quote</phrase>
|
||||
<phrase data="rich_tool_024">Closing quote</phrase>
|
||||
<phrase data="rich_tool_025">En dash</phrase>
|
||||
<phrase data="rich_tool_026">Em dash</phrase>
|
||||
<phrase data="rich_tool_027">Ellipsis</phrase>
|
||||
<phrase data="rich_tool_028">Content fragments</phrase>
|
||||
<phrase data="rich_tool_029">Warning</phrase>
|
||||
<phrase data="rich_tool_030">Important</phrase>
|
||||
<phrase data="rich_tool_031">Included items</phrase>
|
||||
<phrase data="rich_tool_032">Specifications table</phrase>
|
||||
<phrase data="rich_tool_033">Block order</phrase>
|
||||
<phrase data="rich_tool_034">Move up</phrase>
|
||||
<phrase data="rich_tool_035">Move down</phrase>
|
||||
<phrase data="rich_tool_036">Formatting check</phrase>
|
||||
<phrase data="rich_tool_037">Review formatting</phrase>
|
||||
<phrase data="rich_tool_038">H1 in content</phrase>
|
||||
<phrase data="rich_tool_039">Empty heading</phrase>
|
||||
<phrase data="rich_tool_040">Empty link</phrase>
|
||||
<phrase data="rich_tool_041">Inline formatting</phrase>
|
||||
<phrase data="rich_tool_042">Extra empty paragraph</phrase>
|
||||
<phrase data="rich_tool_043">Fix</phrase>
|
||||
<phrase data="rich_tool_044">No issues found</phrase>
|
||||
<phrase data="rich_tool_045">Save without changes</phrase>
|
||||
<phrase data="rich_tool_046">Words</phrase>
|
||||
<phrase data="rich_tool_047">Characters</phrase>
|
||||
<phrase data="rich_tool_048">Selection</phrase>
|
||||
<phrase data="rich_tool_049">in content</phrase>
|
||||
<phrase data="rich_tool_050">Insert row above</phrase>
|
||||
<phrase data="rich_tool_051">Insert row below</phrase>
|
||||
<phrase data="rich_tool_052">Delete row</phrase>
|
||||
<phrase data="rich_tool_053">Insert column left</phrase>
|
||||
<phrase data="rich_tool_054">Insert column right</phrase>
|
||||
<phrase data="rich_tool_055">Delete column</phrase>
|
||||
<phrase data="rich_tool_056">Merge cells</phrase>
|
||||
<phrase data="rich_tool_057">Split cell</phrase>
|
||||
<phrase data="rich_tool_058">Header row</phrase>
|
||||
<phrase data="rich_tool_059">Header column</phrase>
|
||||
<phrase data="rich_tool_060">Delete table</phrase>
|
||||
<phrase data="rich_tool_061">Link to document or product</phrase>
|
||||
<phrase data="rich_tool_062">Expand editor</phrase>
|
||||
<phrase data="rich_tool_063">Collapse editor</phrase>
|
||||
<phrase data="rich_tool_064">Visual editor toolbar</phrase>
|
||||
<phrase data="rich_tool_065">Full</phrase>
|
||||
<phrase data="rich_tool_066">Compact</phrase>
|
||||
<phrase data="rich_tool_067">Product description</phrase>
|
||||
<phrase data="rich_tool_068">News / article</phrase>
|
||||
<phrase data="rich_tool_069">Headings in toolbar</phrase>
|
||||
<phrase data="rich_tool_070">Use profile</phrase>
|
||||
<phrase data="rich_tool_071">No headings</phrase>
|
||||
<phrase data="rich_tool_072">Informational</phrase>
|
||||
<phrase data="rich_tool_073">Product</phrase>
|
||||
<phrase data="rich_tool_074">Hide</phrase>
|
||||
<phrase data="rich_tool_075">Hide images in toolbar</phrase>
|
||||
<phrase data="rich_tool_076">Hide tables in toolbar</phrase>
|
||||
<phrase data="rich_tool_077">Hide source code in toolbar</phrase>
|
||||
<phrase data="rich_tool_078">Hide alignment in toolbar</phrase>
|
||||
<phrase data="rich_tool_079">Hide block ordering in toolbar</phrase>
|
||||
<phrase data="action_done">Done</phrase>
|
||||
<phrase data="auto_1cc70f8e0fc03e82">Delete view</phrase>
|
||||
<phrase data="auto_86433d57abc272e9">Saved Views</phrase>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="attention_heading">Требуют внимания</phrase>
|
||||
<phrase data="attention_empty">Новых нет</phrase>
|
||||
<phrase data="attention_wait">Самое старое ждёт %s</phrase>
|
||||
<phrase data="attention_waiting">Ожидают обработки</phrase>
|
||||
<phrase data="attention_minutes">мин</phrase>
|
||||
<phrase data="attention_hours">ч</phrase>
|
||||
<phrase data="attention_days">дн</phrase>
|
||||
<phrase data="attention_unavailable">Не удалось обновить</phrase>
|
||||
<phrase data="attention_stale">Данные временно не обновляются</phrase>
|
||||
<phrase data="attention_orders">Новые заказы</phrase>
|
||||
<phrase data="attention_messages">Новые обращения</phrase>
|
||||
<phrase data="attention_order_filter">Новые: ожидают обработки</phrase>
|
||||
</language>
|
||||
@@ -1,5 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="rich_tool_001">Найти и заменить</phrase>
|
||||
<phrase data="rich_tool_002">Найти</phrase>
|
||||
<phrase data="rich_tool_003">Заменить на</phrase>
|
||||
<phrase data="rich_tool_004">Заменить</phrase>
|
||||
<phrase data="rich_tool_005">Заменить все</phrase>
|
||||
<phrase data="rich_tool_006">Учитывать регистр</phrase>
|
||||
<phrase data="rich_tool_007">Нет совпадений</phrase>
|
||||
<phrase data="rich_tool_008">Предыдущее</phrase>
|
||||
<phrase data="rich_tool_009">Следующее</phrase>
|
||||
<phrase data="rich_tool_010">Специальные символы</phrase>
|
||||
<phrase data="rich_tool_011">Неразрывный пробел</phrase>
|
||||
<phrase data="rich_tool_012">Градус</phrase>
|
||||
<phrase data="rich_tool_013">Умножение</phrase>
|
||||
<phrase data="rich_tool_014">Плюс-минус</phrase>
|
||||
<phrase data="rich_tool_015">Зарегистрированный знак</phrase>
|
||||
<phrase data="rich_tool_016">Авторское право</phrase>
|
||||
<phrase data="rich_tool_017">Товарный знак</phrase>
|
||||
<phrase data="rich_tool_018">Меньше или равно</phrase>
|
||||
<phrase data="rich_tool_019">Больше или равно</phrase>
|
||||
<phrase data="rich_tool_020">Микро</phrase>
|
||||
<phrase data="rich_tool_021">Квадрат</phrase>
|
||||
<phrase data="rich_tool_022">Куб</phrase>
|
||||
<phrase data="rich_tool_023">Открывающая кавычка</phrase>
|
||||
<phrase data="rich_tool_024">Закрывающая кавычка</phrase>
|
||||
<phrase data="rich_tool_025">Короткое тире</phrase>
|
||||
<phrase data="rich_tool_026">Длинное тире</phrase>
|
||||
<phrase data="rich_tool_027">Многоточие</phrase>
|
||||
<phrase data="rich_tool_028">Готовые фрагменты</phrase>
|
||||
<phrase data="rich_tool_029">Внимание</phrase>
|
||||
<phrase data="rich_tool_030">Важно</phrase>
|
||||
<phrase data="rich_tool_031">Комплектация</phrase>
|
||||
<phrase data="rich_tool_032">Таблица характеристик</phrase>
|
||||
<phrase data="rich_tool_033">Порядок блоков</phrase>
|
||||
<phrase data="rich_tool_034">Переместить вверх</phrase>
|
||||
<phrase data="rich_tool_035">Переместить вниз</phrase>
|
||||
<phrase data="rich_tool_036">Проверка оформления</phrase>
|
||||
<phrase data="rich_tool_037">Проверьте оформление</phrase>
|
||||
<phrase data="rich_tool_038">H1 в тексте</phrase>
|
||||
<phrase data="rich_tool_039">Пустой заголовок</phrase>
|
||||
<phrase data="rich_tool_040">Пустая ссылка</phrase>
|
||||
<phrase data="rich_tool_041">Встроенное оформление</phrase>
|
||||
<phrase data="rich_tool_042">Лишний пустой абзац</phrase>
|
||||
<phrase data="rich_tool_043">Исправить</phrase>
|
||||
<phrase data="rich_tool_044">Замечаний нет</phrase>
|
||||
<phrase data="rich_tool_045">Сохранить без исправлений</phrase>
|
||||
<phrase data="rich_tool_046">Слов</phrase>
|
||||
<phrase data="rich_tool_047">Символов</phrase>
|
||||
<phrase data="rich_tool_048">Выделено</phrase>
|
||||
<phrase data="rich_tool_049">в тексте</phrase>
|
||||
<phrase data="rich_tool_050">Строка выше</phrase>
|
||||
<phrase data="rich_tool_051">Строка ниже</phrase>
|
||||
<phrase data="rich_tool_052">Удалить строку</phrase>
|
||||
<phrase data="rich_tool_053">Столбец слева</phrase>
|
||||
<phrase data="rich_tool_054">Столбец справа</phrase>
|
||||
<phrase data="rich_tool_055">Удалить столбец</phrase>
|
||||
<phrase data="rich_tool_056">Объединить ячейки</phrase>
|
||||
<phrase data="rich_tool_057">Разделить ячейку</phrase>
|
||||
<phrase data="rich_tool_058">Строка заголовков</phrase>
|
||||
<phrase data="rich_tool_059">Столбец заголовков</phrase>
|
||||
<phrase data="rich_tool_060">Удалить таблицу</phrase>
|
||||
<phrase data="rich_tool_061">Ссылка на документ или товар</phrase>
|
||||
<phrase data="rich_tool_062">Развернуть редактор</phrase>
|
||||
<phrase data="rich_tool_063">Свернуть редактор</phrase>
|
||||
<phrase data="rich_tool_064">Панель визуального редактора</phrase>
|
||||
<phrase data="rich_tool_065">Полная</phrase>
|
||||
<phrase data="rich_tool_066">Компактная</phrase>
|
||||
<phrase data="rich_tool_067">Описание товара</phrase>
|
||||
<phrase data="rich_tool_068">Новость / статья</phrase>
|
||||
<phrase data="rich_tool_069">Заголовки в панели</phrase>
|
||||
<phrase data="rich_tool_070">По профилю</phrase>
|
||||
<phrase data="rich_tool_071">Без заголовков</phrase>
|
||||
<phrase data="rich_tool_072">Информационные</phrase>
|
||||
<phrase data="rich_tool_073">Товарные</phrase>
|
||||
<phrase data="rich_tool_074">Не показывать</phrase>
|
||||
<phrase data="rich_tool_075">Скрыть в панели: изображения</phrase>
|
||||
<phrase data="rich_tool_076">Скрыть в панели: таблицы</phrase>
|
||||
<phrase data="rich_tool_077">Скрыть в панели: исходный код</phrase>
|
||||
<phrase data="rich_tool_078">Скрыть в панели: выравнивание</phrase>
|
||||
<phrase data="rich_tool_079">Скрыть в панели: перемещение блоков</phrase>
|
||||
<phrase data="action_done">Готово</phrase>
|
||||
<phrase data="auto_1cc70f8e0fc03e82">Удалить представление</phrase>
|
||||
<phrase data="auto_86433d57abc272e9">Сохранённые представления</phrase>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Helpers\PhpSyntax;
|
||||
|
||||
class Syntax
|
||||
{
|
||||
public static function check($code, $eval)
|
||||
@@ -30,44 +32,8 @@
|
||||
);
|
||||
}
|
||||
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'ave-sysblock-');
|
||||
if ($tmp === false) {
|
||||
return self::unavailable('Не удалось создать временный файл для проверки.');
|
||||
}
|
||||
|
||||
$file = $tmp . '.php';
|
||||
@rename($tmp, $file);
|
||||
$written = @file_put_contents($file, '?>' . (string) $code . '<?php ');
|
||||
if ($written === false) {
|
||||
@unlink($file);
|
||||
return self::unavailable('Не удалось записать временный файл для проверки.');
|
||||
}
|
||||
|
||||
$php = defined('PHP_BINARY') && PHP_BINARY ? PHP_BINARY : 'php';
|
||||
$cmd = escapeshellarg($php) . ' -l ' . escapeshellarg($file) . ' 2>&1';
|
||||
$output = array();
|
||||
$status = 0;
|
||||
@exec($cmd, $output, $status);
|
||||
@unlink($file);
|
||||
|
||||
$text = trim(implode("\n", $output));
|
||||
if ($text === '') {
|
||||
return self::unavailable('PHP CLI недоступен для проверки синтаксиса.');
|
||||
}
|
||||
|
||||
$clean = str_replace($file, 'код блока', $text);
|
||||
$line = 0;
|
||||
if (preg_match('/ on line ([0-9]+)/i', $clean, $m)) {
|
||||
$line = (int) $m[1];
|
||||
}
|
||||
|
||||
return array(
|
||||
'ok' => (int) $status === 0,
|
||||
'checked' => true,
|
||||
'message' => (int) $status === 0 ? 'Синтаксис PHP без ошибок.' : 'В PHP-коде есть синтаксическая ошибка.',
|
||||
'line' => $line,
|
||||
'output' => $clean,
|
||||
);
|
||||
$result = PhpSyntax::check('<?php ?>' . (string) $code);
|
||||
return $result['checked'] ? $result : self::unavailable($result['message']);
|
||||
}
|
||||
|
||||
protected static function unavailable($message)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="runtime_c55667032216bcac">Built-in PHP syntax checking is not available on this server.</phrase>
|
||||
<phrase data="runtime_287ef8ef854ce1aa">(copy)</phrase>
|
||||
<phrase data="runtime_eb35949a5a0f8ff5">PHP CLI is not available for syntax checking.</phrase>
|
||||
<phrase data="runtime_46c24d5ef1a06bf8">PHP syntax: error in block code</phrase>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="runtime_c55667032216bcac">Встроенная проверка PHP недоступна на сервере.</phrase>
|
||||
<phrase data="runtime_287ef8ef854ce1aa">(копия)</phrase>
|
||||
<phrase data="runtime_eb35949a5a0f8ff5">PHP CLI недоступен для проверки синтаксиса.</phrase>
|
||||
<phrase data="runtime_46c24d5ef1a06bf8">PHP-синтаксис: ошибка в коде блока</phrase>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'blocks',
|
||||
'name' => 'Блоки',
|
||||
'version' => '0.1.4',
|
||||
'version' => '0.1.6',
|
||||
|
||||
'permissions' => array(
|
||||
'key' => 'blocks',
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
use App\Adminx\Rubrics\Model as RubricsModel;
|
||||
use App\Adminx\Documents\Model as DocumentsModel;
|
||||
use App\Adminx\Packages\Products\ProductIndexer;
|
||||
use App\Adminx\Packages\Products\Attributes;
|
||||
|
||||
class Model
|
||||
{
|
||||
@@ -1137,27 +1138,50 @@
|
||||
|
||||
protected static function runFilterRecompile($rubricId, $itemId)
|
||||
{
|
||||
$runner = AdminLocation::path('modules/Catalog/bin/recompile.php');
|
||||
if (!is_file($runner)) { throw new \RuntimeException('Сценарий пересборки фильтров не найден'); }
|
||||
if (!function_exists('exec')) { throw new \RuntimeException('Функция exec недоступна'); }
|
||||
$output = array(); $code = 0;
|
||||
$command = escapeshellarg(self::phpCliBinary()) . ' ' . escapeshellarg($runner) . ' ' . (int) $rubricId . ' ' . (int) $itemId . ' 2>&1';
|
||||
exec($command, $output, $code);
|
||||
if ($code !== 0) { throw new \RuntimeException(trim(implode("\n", $output)) ?: 'Пересборка завершилась с ошибкой'); }
|
||||
return array('item_id' => (int) $itemId, 'output' => trim(implode("\n", $output)));
|
||||
}
|
||||
$item = static::item((int) $itemId);
|
||||
if (!$item || (int) $item['rubric_id'] !== (int) $rubricId) { throw new \RuntimeException('Раздел каталога не найден'); }
|
||||
$settings = static::settings((int) $rubricId, (int) $item['field_id']);
|
||||
$native = isset($item['filter_runtime']) && in_array($item['filter_runtime'], array('native', 'shadow'), true);
|
||||
$products = static::productsAvailable() && isset($settings['purpose']) && $settings['purpose'] === 'commerce';
|
||||
if ($native && !$products) { throw new \RuntimeException('Для нативных фильтров включите модуль «Товары»'); }
|
||||
if (!$native && $products && !method_exists(ProductIndexer::class, 'rebuildFilterIndex')) { throw new \RuntimeException('Обновите модуль «Товары» для пересборки индекса фильтров'); }
|
||||
$requestId = isset($settings['request_id']) ? (int) $settings['request_id'] : 0;
|
||||
if (!$native && !$products && $requestId < 1) { throw new \RuntimeException('Сначала выберите запрос фильтра в настройках каталога'); }
|
||||
$owns = !DB::$transaction_in_progress;
|
||||
$previousMode = DB::$throw_exception_on_error;
|
||||
$savepoint = 'catalog_filters_' . bin2hex(random_bytes(6));
|
||||
$started = false;
|
||||
DB::$throw_exception_on_error = true;
|
||||
try {
|
||||
if ($owns) { DB::startTransaction(); } else { DB::query('SAVEPOINT ' . $savepoint); }
|
||||
$started = true;
|
||||
$result = array('item_id' => (int) $itemId, 'runtime' => $native ? 'native' : 'legacy', 'request_id' => 0, 'products' => 0, 'filters' => 0);
|
||||
if ($native) {
|
||||
$report = Attributes::rebuildShadowIndex((int) $itemId);
|
||||
$result['filters'] = (int) $report['indexed_values'];
|
||||
$result['products'] = (int) DB::query('SELECT COUNT(DISTINCT product_id) FROM ' . Attributes::table('attribute_filter_index') . ' WHERE catalog_item_id=%i', (int) $itemId)->getValue();
|
||||
} else {
|
||||
if ($requestId > 0) {
|
||||
$compiled = \App\Content\Requests\RequestConditionCompiler::compile($requestId, true);
|
||||
if (!$compiled) { throw new \RuntimeException('Запрос фильтра не найден'); }
|
||||
$result['request_id'] = $requestId;
|
||||
}
|
||||
|
||||
protected static function phpCliBinary()
|
||||
{
|
||||
$candidates = array(getenv('PHP_CLI_BINARY'));
|
||||
if (defined('PHP_BINDIR')) { $candidates[] = PHP_BINDIR . '/php'; }
|
||||
if (defined('PHP_BINARY') && preg_match('/^php(?:[0-9.]*)?$/i', basename((string) PHP_BINARY))) { $candidates[] = PHP_BINARY; }
|
||||
$candidates[] = '/usr/local/bin/php'; $candidates[] = '/usr/bin/php';
|
||||
foreach (array_unique(array_filter($candidates)) as $candidate) {
|
||||
if (is_file($candidate) && is_executable($candidate)) { return $candidate; }
|
||||
}
|
||||
if ($products) {
|
||||
$ids = array_merge(array((int) $itemId), isset($item['source_item_ids']) ? (array) $item['source_item_ids'] : array());
|
||||
$result = array_merge($result, ProductIndexer::rebuildFilterIndex($ids));
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException('CLI-интерпретатор PHP не найден');
|
||||
if ($owns) { DB::commit(); } else { DB::query('RELEASE SAVEPOINT ' . $savepoint); }
|
||||
} catch (\Throwable $e) {
|
||||
if ($started && $owns) { DB::rollback(); }
|
||||
elseif ($started) { DB::query('ROLLBACK TO SAVEPOINT ' . $savepoint); DB::query('RELEASE SAVEPOINT ' . $savepoint); }
|
||||
throw $e;
|
||||
} finally { DB::$throw_exception_on_error = $previousMode; }
|
||||
DB::afterCommit(function () { static::clearCache(); });
|
||||
$result['output'] = $native ? 'Нативный индекс фильтров пересобран.' : 'Условия запроса и индекс фильтров пересобраны.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected static function rebuildRequest($requestId)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
return array(
|
||||
'code' => 'catalog', 'name' => 'Каталог', 'version' => '0.3.9',
|
||||
'code' => 'catalog', 'name' => 'Каталог', 'version' => '0.3.10',
|
||||
'permissions' => array('key' => 'catalog', 'items' => array(
|
||||
array(
|
||||
'code' => 'view_catalog',
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
throw new \RuntimeException($lint['message'] . ($lint['line'] ? ' Строка ' . $lint['line'] . '.' : ''));
|
||||
}
|
||||
|
||||
$php = PhpCode::cliBinary();
|
||||
$dir = TemporaryDirectory::create('console');
|
||||
|
||||
$codeFile = $dir . '/snippet.php';
|
||||
@@ -70,7 +71,6 @@
|
||||
@file_put_contents($codeFile, PhpCode::source($code));
|
||||
@file_put_contents($runnerFile, $runner);
|
||||
|
||||
$php = self::resolvePhpBinary();
|
||||
$command = escapeshellarg($php)
|
||||
. ' -d display_errors=1 -d memory_limit=128M'
|
||||
. ' -d disable_functions=exec,shell_exec,system,passthru,popen'
|
||||
@@ -82,7 +82,7 @@
|
||||
0 => array('pipe', 'r'),
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
), $pipes, $dir);
|
||||
), $pipes, $dir, null, array('bypass_shell' => DIRECTORY_SEPARATOR === '\\'));
|
||||
if (!is_resource($process)) {
|
||||
self::cleanup($dir);
|
||||
throw new \RuntimeException('Не удалось запустить PHP-процесс.');
|
||||
@@ -137,99 +137,6 @@
|
||||
);
|
||||
}
|
||||
|
||||
/** Find a CLI binary compatible with the application runtime. */
|
||||
protected static function resolvePhpBinary()
|
||||
{
|
||||
$candidates = array();
|
||||
$configured = trim((string) getenv('ADMINX_PHP_BINARY'));
|
||||
if ($configured !== '') {
|
||||
$candidates[] = $configured;
|
||||
}
|
||||
|
||||
$config = PublicConfiguration::value('admin_console', array());
|
||||
if (is_array($config) && !empty($config['php_binary'])) {
|
||||
$candidates[] = trim((string) $config['php_binary']);
|
||||
}
|
||||
|
||||
if (defined('PHP_BINDIR') && PHP_BINDIR !== '') {
|
||||
$candidates[] = rtrim(PHP_BINDIR, '/\\') . DIRECTORY_SEPARATOR . 'php';
|
||||
}
|
||||
|
||||
if (defined('PHP_BINARY') && PHP_BINARY !== '') {
|
||||
$candidates[] = dirname(PHP_BINARY) . DIRECTORY_SEPARATOR . 'php';
|
||||
$candidates[] = PHP_BINARY;
|
||||
}
|
||||
|
||||
$candidates[] = 'php';
|
||||
$candidates = array_values(array_unique($candidates));
|
||||
$detected = array();
|
||||
foreach ($candidates as $candidate) {
|
||||
$version = self::probePhpBinary($candidate);
|
||||
if ($version >= 70300) {
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
if ($version > 0) {
|
||||
$detected[] = $candidate . ' (PHP ' . self::formatVersionId($version) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$message = 'Для PHP-консоли нужен CLI-интерпретатор PHP 7.3 или новее.';
|
||||
if ($detected) {
|
||||
$message .= ' Найдены несовместимые: ' . implode(', ', $detected) . '.';
|
||||
}
|
||||
|
||||
throw new \RuntimeException($message . ' Укажите путь в ADMINX_PHP_BINARY.');
|
||||
}
|
||||
|
||||
protected static function probePhpBinary($binary)
|
||||
{
|
||||
$command = escapeshellarg((string) $binary) . ' -r ' . escapeshellarg('echo PHP_VERSION_ID;');
|
||||
$pipes = array();
|
||||
$process = @proc_open($command, array(
|
||||
0 => array('pipe', 'r'),
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
), $pipes);
|
||||
if (!is_resource($process)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
fclose($pipes[0]);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
$started = microtime(true);
|
||||
$output = '';
|
||||
do {
|
||||
$output .= (string) stream_get_contents($pipes[1]);
|
||||
$status = proc_get_status($process);
|
||||
if (!$status['running']) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (microtime(true) - $started >= 2) {
|
||||
proc_terminate($process, 9);
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(10000);
|
||||
} while (true);
|
||||
$output .= (string) stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_close($process);
|
||||
|
||||
$output = trim($output);
|
||||
return preg_match('/^\d{5,6}$/', $output) === 1 ? (int) $output : 0;
|
||||
}
|
||||
|
||||
protected static function formatVersionId($version)
|
||||
{
|
||||
$version = (int) $version;
|
||||
return (int) floor($version / 10000)
|
||||
. '.' . (int) floor(($version % 10000) / 100)
|
||||
. '.' . (int) ($version % 100);
|
||||
}
|
||||
|
||||
protected static function cleanup($dir)
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
return array(
|
||||
'code' => 'console',
|
||||
'name' => 'PHP-консоль',
|
||||
'version' => '0.1.2',
|
||||
'version' => '0.1.3',
|
||||
'registry' => array('dynamic' => true),
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
return array(
|
||||
'code' => 'console',
|
||||
'name' => 'PHP-консоль',
|
||||
'version' => '0.1.2',
|
||||
'version' => '0.1.3',
|
||||
'registry' => array('dynamic' => true),
|
||||
'permissions' => array(
|
||||
'key' => 'console',
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file adminx/modules/Dashboard/Attention.php
|
||||
| @author AVE.cms <support@ave-cms.ru>
|
||||
| @copyright 2007-2026 (c) AVE.cms
|
||||
| @link https://ave-cms.ru
|
||||
| @version 3.3
|
||||
*/
|
||||
|
||||
namespace App\Adminx\Dashboard;
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Adminx\Support\AdminLocale;
|
||||
use App\Adminx\Support\ModuleExtensions;
|
||||
|
||||
class Attention
|
||||
{
|
||||
protected static $summary;
|
||||
|
||||
public static function summary($base)
|
||||
{
|
||||
if (self::$summary !== null) { return self::$summary; }
|
||||
$items = array();
|
||||
foreach (ModuleExtensions::attentionQueues() as $queue) {
|
||||
$data = isset($queue['data']) ? $queue['data'] : array();
|
||||
$item = self::normalize($data, $queue['layout_code'], $base, time());
|
||||
if ($item !== null) { $items[] = $item; }
|
||||
}
|
||||
|
||||
return self::$summary = array('items' => $items, 'updated_at' => time());
|
||||
}
|
||||
|
||||
public static function normalize(array $data, $key, $base, $now)
|
||||
{
|
||||
$url = isset($data['url']) ? (string) $data['url'] : '';
|
||||
if (empty($data['title']) || !preg_match('~^/(?!/)[a-zA-Z0-9/_?=&.%-]+$~D', $url)) { return null; }
|
||||
$available = !isset($data['available']) || $data['available'];
|
||||
$count = max(0, (int) (isset($data['count']) ? $data['count'] : 0));
|
||||
$oldest = max(0, (int) (isset($data['oldest_at']) ? $data['oldest_at'] : 0));
|
||||
$wait = '';
|
||||
if (!$available) {
|
||||
$wait = AdminLocale::text('attention_unavailable', 'Не удалось обновить');
|
||||
} elseif ($count === 0) {
|
||||
$wait = AdminLocale::text('attention_empty', 'Новых нет');
|
||||
} elseif ($oldest > 0) {
|
||||
$minutes = max(1, (int) floor(($now - $oldest) / 60));
|
||||
$wait = sprintf(AdminLocale::text('attention_wait', 'Самое старое ждёт %s'),
|
||||
$minutes < 60 ? $minutes . ' ' . AdminLocale::text('attention_minutes', 'мин')
|
||||
: ($minutes < 1440 ? (int) floor($minutes / 60) . ' ' . AdminLocale::text('attention_hours', 'ч')
|
||||
: (int) floor($minutes / 1440) . ' ' . AdminLocale::text('attention_days', 'дн')));
|
||||
} else {
|
||||
$wait = AdminLocale::text('attention_waiting', 'Ожидают обработки');
|
||||
}
|
||||
|
||||
return array('key' => (string) $key, 'title' => AdminLocale::translateMarkup($data['title']),
|
||||
'icon' => isset($data['icon']) && preg_match('/^ti ti-[a-z0-9-]+$/D', $data['icon']) ? $data['icon'] : 'ti ti-bell',
|
||||
'tone' => isset($data['tone']) && in_array($data['tone'], array('blue', 'green'), true) ? $data['tone'] : 'amber',
|
||||
'count' => $count, 'available' => (bool) $available, 'oldest_at' => $oldest,
|
||||
'latest_id' => max(0, (int) (isset($data['latest_id']) ? $data['latest_id'] : 0)),
|
||||
'url' => rtrim((string) $base, '/') . $url, 'wait' => $wait);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,16 @@
|
||||
*/
|
||||
class Controller extends BaseController
|
||||
{
|
||||
public function attention(array $params = array())
|
||||
{
|
||||
if (!\App\Common\Auth::user() || !\App\Common\Permission::checkAcp('admin_panel')) {
|
||||
return $this->error('Недостаточно прав', array(), 403);
|
||||
}
|
||||
|
||||
\App\Helpers\Request::setHeader('Cache-Control: private, no-store', true);
|
||||
return $this->success('', array('data' => Attention::summary($this->base())));
|
||||
}
|
||||
|
||||
/** GET / и /dashboard */
|
||||
public function index(array $params = [])
|
||||
{
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
.attention-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.attention-topbar[hidden],
|
||||
.attention-overview[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.attention-shortcut,
|
||||
.attention-card {
|
||||
--attention-color: var(--amber-700);
|
||||
--attention-soft: var(--amber-100);
|
||||
--attention-border: var(--amber-300);
|
||||
color: var(--text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-decoration: none;
|
||||
}
|
||||
.attention-shortcut.is-blue,
|
||||
.attention-card.is-blue {
|
||||
--attention-color: var(--blue-700);
|
||||
--attention-soft: var(--blue-100);
|
||||
--attention-border: var(--blue-300);
|
||||
}
|
||||
.attention-shortcut.is-green,
|
||||
.attention-card.is-green {
|
||||
--attention-color: var(--green-700);
|
||||
--attention-soft: var(--green-100);
|
||||
--attention-border: var(--green-300);
|
||||
}
|
||||
.attention-shortcut.has-pending,
|
||||
.attention-card.has-pending {
|
||||
color: var(--attention-color);
|
||||
background: var(--attention-soft);
|
||||
border-color: var(--attention-border);
|
||||
}
|
||||
.attention-shortcut:hover,
|
||||
.attention-card:hover {
|
||||
color: var(--attention-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.attention-shortcut:focus-visible,
|
||||
.attention-card:focus-visible {
|
||||
outline: 2px solid var(--attention-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.attention-shortcut.is-arriving,
|
||||
.attention-card.is-arriving {
|
||||
animation: attention-arrival 0.7s ease-out 2;
|
||||
}
|
||||
.attention-shortcut[hidden],
|
||||
.attention-card[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.attention-shortcut {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-card);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.attention-shortcut > .ti {
|
||||
font-size: 18px;
|
||||
}
|
||||
.attention-shortcut b {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 18px;
|
||||
min-width: 18px;
|
||||
padding: 0 4px;
|
||||
border-radius: 9px;
|
||||
border: 2px solid var(--surface-card);
|
||||
background: var(--attention-color);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.attention-overview {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.attention-heading {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.attention-heading h2 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.attention-heading > span {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.attention-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.attention-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
min-height: 140px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-card);
|
||||
}
|
||||
.attention-card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface-card);
|
||||
font-size: 24px;
|
||||
}
|
||||
.attention-card-body {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 5px;
|
||||
}
|
||||
.attention-card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.attention-card-body strong {
|
||||
font-size: 30px;
|
||||
line-height: 1.15;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.attention-wait {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.attention-card-arrow {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
font-size: 19px;
|
||||
}
|
||||
.attention-dot {
|
||||
display: none;
|
||||
flex: 0 0 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
.has-pending .attention-dot {
|
||||
display: inline-block;
|
||||
animation: attention-pulse 2.2s ease-in-out infinite;
|
||||
}
|
||||
.attention-shortcut.is-stale {
|
||||
opacity: 0.65;
|
||||
}
|
||||
@keyframes attention-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
@keyframes attention-arrival {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
@media (max-width: 680px) {
|
||||
.attention-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.attention-card {
|
||||
padding: 16px;
|
||||
min-height: 125px;
|
||||
gap: 12px;
|
||||
}
|
||||
.attention-topbar {
|
||||
gap: 7px;
|
||||
}
|
||||
.topbar.has-attention {
|
||||
padding-inline: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
.topbar.has-attention .topbar-right {
|
||||
gap: 4px;
|
||||
}
|
||||
.topbar.has-attention .topbar-site-link,
|
||||
.topbar.has-attention .env-badge {
|
||||
display: none;
|
||||
}
|
||||
.topbar.has-attention .logo-suffix {
|
||||
display: none;
|
||||
}
|
||||
.topbar.has-attention .topbar-logo .logo-img {
|
||||
width: 52px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.has-pending .attention-dot,
|
||||
.attention-shortcut.is-arriving,
|
||||
.attention-card.is-arriving {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 380px) {
|
||||
.topbar.has-attention .logo-suffix {
|
||||
display: none;
|
||||
}
|
||||
.topbar.has-attention .topbar-logo .logo-img {
|
||||
width: 52px;
|
||||
height: auto;
|
||||
}
|
||||
.attention-shortcut {
|
||||
flex-basis: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.topbar.has-attention .topbar-logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
(function (window, document) {
|
||||
'use strict';
|
||||
var A = window.Adminx || (window.Adminx = {});
|
||||
var root = document.querySelector('[data-attention-root]');
|
||||
if (!root) return;
|
||||
var endpoint = root.dataset.attentionUrl;
|
||||
var storageKey = 'ave.attention.' + endpoint + '.' + root.dataset.attentionUser;
|
||||
var timer = null, busy = false, stopped = false, lastRequest = 0, seen = {};
|
||||
try { seen = JSON.parse(sessionStorage.getItem(storageKey) || '{}') || {}; } catch (e) {}
|
||||
|
||||
function saveSeen() { try { sessionStorage.setItem(storageKey, JSON.stringify(seen)); } catch (e) {} }
|
||||
function update(payload) {
|
||||
var present = {};
|
||||
payload.items.forEach(function (item) {
|
||||
present[item.key] = true;
|
||||
var baseline = seen[item.key];
|
||||
var arriving = item.available && baseline && (item.latest_id > baseline.latest_id || item.count > baseline.count);
|
||||
if (item.available) {
|
||||
seen[item.key] = { latest_id: Math.max(item.latest_id, baseline ? baseline.latest_id : 0), count: item.count };
|
||||
}
|
||||
document.querySelectorAll('[data-attention-key]').forEach(function (el) {
|
||||
if (el.dataset.attentionKey !== item.key) return;
|
||||
el.hidden = false;
|
||||
el.href = item.url;
|
||||
el.classList.toggle('has-pending', item.available && item.count > 0);
|
||||
el.classList.toggle('is-stale', !item.available);
|
||||
var count = el.querySelector('[data-attention-count]');
|
||||
if (count) count.textContent = item.available ? (el.classList.contains('attention-shortcut') && item.count > 99 ? '99+' : item.count) : '\u2014';
|
||||
var wait = el.querySelector('[data-attention-wait]');
|
||||
if (wait) wait.textContent = item.wait;
|
||||
if (el.classList.contains('attention-shortcut')) {
|
||||
var label = item.title + ': ' + (item.available ? item.count : '\u2014') + ' \u00b7 ' + item.wait;
|
||||
el.setAttribute('aria-label', label);
|
||||
el.setAttribute('data-tooltip', label);
|
||||
}
|
||||
if (arriving) {
|
||||
el.classList.remove('is-arriving');
|
||||
void el.offsetWidth;
|
||||
el.classList.add('is-arriving');
|
||||
window.setTimeout(function () { el.classList.remove('is-arriving'); }, 1500);
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-attention-key]').forEach(function (el) {
|
||||
if (!present[el.dataset.attentionKey]) el.hidden = true;
|
||||
});
|
||||
var any = Object.keys(present).length > 0;
|
||||
root.hidden = !any;
|
||||
var overview = document.querySelector('[data-attention-overview]');
|
||||
if (overview) overview.hidden = !any;
|
||||
stale(false);
|
||||
saveSeen();
|
||||
}
|
||||
function stale(value) {
|
||||
root.classList.toggle('is-stale', value);
|
||||
document.querySelectorAll('[data-attention-health]').forEach(function (el) { el.hidden = !value; });
|
||||
if (value) root.querySelectorAll('.attention-shortcut').forEach(function (el) { el.classList.add('is-stale'); });
|
||||
}
|
||||
function schedule(delay) {
|
||||
window.clearTimeout(timer);
|
||||
if (!stopped && !document.hidden) timer = window.setTimeout(refresh, delay || 30000);
|
||||
}
|
||||
function refresh() {
|
||||
if (stopped || busy || document.hidden) return;
|
||||
if (Date.now() - lastRequest < 3000) { schedule(3000); return; }
|
||||
busy = true; lastRequest = Date.now();
|
||||
var controller = new AbortController();
|
||||
var timeout = window.setTimeout(function () { controller.abort(); }, 10000);
|
||||
fetch(endpoint, { credentials: 'same-origin', cache: 'no-store', signal: controller.signal,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } })
|
||||
.then(function (response) {
|
||||
if (response.status === 401 || response.status === 403 || response.status === 428 || response.redirected) {
|
||||
stopped = true;
|
||||
throw Error('Session unavailable');
|
||||
}
|
||||
if (!response.ok) throw Error('Refresh failed');
|
||||
return response.json();
|
||||
}).then(function (result) {
|
||||
if (!result.success || !result.data || !Array.isArray(result.data.items)) throw Error('Invalid summary');
|
||||
update(result.data);
|
||||
}).catch(function () { stale(true); })
|
||||
.finally(function () { window.clearTimeout(timeout); busy = false; schedule(); });
|
||||
}
|
||||
try { update(JSON.parse(root.dataset.attentionInitial)); } catch (e) { stale(true); }
|
||||
document.addEventListener('visibilitychange', function () { window.clearTimeout(timer); if (!document.hidden) refresh(); });
|
||||
document.addEventListener('adminx:drawer:closed', function () { schedule(1000); });
|
||||
document.addEventListener('adminx:form-saved', function () { schedule(1000); });
|
||||
window.addEventListener('pageshow', function (event) { if (event.persisted) refresh(); });
|
||||
A.Attention = { refresh: refresh };
|
||||
schedule();
|
||||
})(window, document);
|
||||
@@ -21,7 +21,7 @@
|
||||
return [
|
||||
'code' => 'dashboard',
|
||||
'name' => 'Дашборд',
|
||||
'version' => '0.1.0',
|
||||
'version' => '0.2.3',
|
||||
|
||||
'navigation' => array(
|
||||
array(
|
||||
@@ -39,6 +39,7 @@
|
||||
),
|
||||
|
||||
'routes' => array(
|
||||
array('GET', '/dashboard/attention', array(\App\Adminx\Dashboard\Controller::class, 'attention')),
|
||||
array('GET', '/', array(\App\Adminx\Dashboard\Controller::class, 'index')),
|
||||
array('GET', '/dashboard', array(\App\Adminx\Dashboard\Controller::class, 'index')),
|
||||
),
|
||||
|
||||
@@ -5,6 +5,21 @@
|
||||
|
||||
<div class="page-header dashboard-page-header"><div><h1>{{ greeting }}</h1><p class="text-secondary">{{ lang.dashboard_summary|default('Сводка системы и последние изменения') }}</p></div><div class="dashboard-date"><i class="ti ti-calendar"></i>{{ 'now'|date('d.m.Y') }}</div></div>
|
||||
|
||||
{% if attention.items %}
|
||||
<section class="attention-overview" data-attention-overview aria-labelledby="attention-heading">
|
||||
<div class="attention-heading"><h2 id="attention-heading">{{ lang.attention_heading|default('Требуют внимания') }}</h2><span data-attention-health hidden>{{ lang.attention_stale|default('Данные временно не обновляются') }}</span></div>
|
||||
<div class="attention-grid">
|
||||
{% for item in attention.items %}
|
||||
<a class="attention-card is-{{ item.tone }}{{ item.available and item.count > 0 ? ' has-pending' : '' }}" href="{{ item.url }}" data-attention-key="{{ item.key }}">
|
||||
<span class="attention-card-icon"><i class="{{ item.icon }}" aria-hidden="true"></i></span>
|
||||
<span class="attention-card-body"><span class="attention-card-title">{{ item.title }}<i class="attention-dot" aria-hidden="true"></i></span><strong data-attention-count>{{ item.available ? item.count : '—' }}</strong><span class="attention-wait" data-attention-wait>{{ item.wait }}</span></span>
|
||||
<i class="ti ti-arrow-up-right attention-card-arrow" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if dashboard_widgets %}
|
||||
<div class="dashboard-module-widgets">
|
||||
{% for dashboard_widget in dashboard_widgets %}
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
'rubrics' => Model::rubrics(),
|
||||
'document_fields' => self::$documentFields,
|
||||
'products_available' => self::productsAvailable(),
|
||||
'product_descriptions_available' => self::productDescriptionsAvailable(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,6 +79,7 @@
|
||||
{
|
||||
$filters = self::filters($input);
|
||||
$operation = self::operation($input, $filters);
|
||||
if ($operation['type'] === 'strip_description_styles') { $filters['scope'] = 'products'; }
|
||||
$matches = self::matches($filters);
|
||||
if (!$matches['ids']) {
|
||||
throw new \InvalidArgumentException('По выбранным условиям документы не найдены');
|
||||
@@ -103,10 +105,10 @@
|
||||
'title' => html_entity_decode((string) $row['document_title'], ENT_QUOTES, 'UTF-8'),
|
||||
'rubric' => html_entity_decode((string) $row['rubric_title'], ENT_QUOTES, 'UTF-8'),
|
||||
'state' => (int) $row['document_status'] === 1 ? 'Опубликован' : 'Черновик',
|
||||
'before' => self::shortValue($change['before']),
|
||||
'after' => self::shortValue($change['after']),
|
||||
'before' => $operation['type'] === 'strip_description_styles' ? mb_substr($change['before'], 0, 1000, 'UTF-8') : self::shortValue($change['before']),
|
||||
'after' => $operation['type'] === 'strip_description_styles' ? mb_substr($change['after'], 0, 1000, 'UTF-8') : self::shortValue($change['after']),
|
||||
'changed' => $change['changed'],
|
||||
'note' => $change['note'],
|
||||
'note' => \App\Common\Language::translateSource($change['note']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -211,6 +213,17 @@
|
||||
if (!$row) { return false; }
|
||||
|
||||
$type = $operation['type'];
|
||||
if ($type === 'strip_description_styles') {
|
||||
if (!self::productDescriptionsAvailable()) { throw new \InvalidArgumentException('Нет доступа к описаниям товаров'); }
|
||||
$fieldId = self::productDescriptionField($row);
|
||||
if (!$fieldId) { return false; }
|
||||
$current = self::currentValue($row, array('target_type' => 'field', 'field_id' => $fieldId));
|
||||
$next = self::changedValue($current, $operation);
|
||||
if (!$next['changed']) { return false; }
|
||||
$service->patch($documentId, array('fields' => array($fieldId => $next['value'])), $actorId, 'bulk_editor');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($operation['target_type']) && $operation['target_type'] === 'state') {
|
||||
if ($type === 'technical' && Model::isProtectedDocument($documentId)) { return false; }
|
||||
$current = (string) $row[$operation['target']];
|
||||
@@ -272,7 +285,7 @@
|
||||
{
|
||||
$type = isset($input['operation']) ? (string) $input['operation'] : '';
|
||||
$allowed = array_merge(
|
||||
array('fill', 'set', 'clear', 'replace', 'move', 'publish', 'unpublish', 'recalculate'),
|
||||
array('fill', 'set', 'clear', 'replace', 'move', 'publish', 'unpublish', 'recalculate', 'strip_description_styles'),
|
||||
array_keys(self::$stateOperations)
|
||||
);
|
||||
if (!in_array($type, $allowed, true)) {
|
||||
@@ -280,6 +293,11 @@
|
||||
}
|
||||
|
||||
$operation = array('type' => $type, 'label' => self::operationLabel($type));
|
||||
if ($type === 'strip_description_styles') {
|
||||
if (!self::productDescriptionsAvailable()) { throw new \InvalidArgumentException('Нет доступа к описаниям товаров'); }
|
||||
return array_merge($operation, array('target_type' => 'product_description', 'target_label' => 'Описание товара'));
|
||||
}
|
||||
|
||||
if (isset(self::$stateOperations[$type])) {
|
||||
$config = self::$stateOperations[$type];
|
||||
$value = isset($config['value']) ? (string) $config['value'] : trim(isset($input[$config['value_input']]) ? (string) $input[$config['value_input']] : '');
|
||||
@@ -400,6 +418,12 @@
|
||||
foreach ($valueRows ?: array() as $valueRow) {
|
||||
$currentValues[(int) $valueRow['Id']] = html_entity_decode((string) $valueRow['current_value'], ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
} elseif (isset($operation['target_type']) && $operation['target_type'] === 'product_description') {
|
||||
foreach (array_chunk($ids, 500) as $chunk) {
|
||||
$valueRows = DB::query('SELECT p.product_id,' . \App\Modules\Products\ProductRoles::valueSql('description')
|
||||
. ' current_value FROM ' . CatalogTables::table('catalog_product_index') . ' p WHERE p.product_id IN %li AND p.is_deleted=0', $chunk)->getAll();
|
||||
foreach ($valueRows ?: array() as $valueRow) { $currentValues[(int) $valueRow['product_id']] = (string) $valueRow['current_value']; }
|
||||
}
|
||||
} elseif (isset($operation['target_type']) && $operation['target_type'] === 'field') {
|
||||
$valueRows = DB::query(
|
||||
'SELECT df.document_id,CONCAT(COALESCE(df.field_value,\'\'),COALESCE(dft.field_value,\'\')) current_value'
|
||||
@@ -444,7 +468,7 @@
|
||||
}
|
||||
|
||||
if ($operation['type'] === 'move') {
|
||||
return array('before' => (string) $row['rubric_title'], 'after' => (string) $operation['target_rubric_title'], 'changed' => (int) $row['rubric_id'] !== (int) $operation['target_rubric_id'], 'note' => 'Совпадающие поля переносятся по системному имени');
|
||||
return array('before' => (string) $row['rubric_title'], 'after' => (string) $operation['target_rubric_title'], 'changed' => (int) $row['rubric_id'] !== (int) $operation['target_rubric_id'], 'note' => 'Переносятся только поля с одинаковыми системными именами и типами. Значения остальных полей не сохранятся в новой рубрике.');
|
||||
}
|
||||
|
||||
if ($operation['target_type'] === 'state') {
|
||||
@@ -460,7 +484,7 @@
|
||||
|
||||
$current = self::currentValue($row, $operation);
|
||||
$next = self::changedValue($current, $operation);
|
||||
return array('before' => $current, 'after' => $next['value'], 'changed' => $next['changed'], 'note' => $next['changed'] ? '' : 'Значение уже соответствует действию');
|
||||
return array('before' => $current, 'after' => $next['value'], 'changed' => $next['changed'], 'note' => isset($next['removed']) ? 'Удалится атрибутов style: ' . $next['removed'] : ($next['changed'] ? '' : 'Значение уже соответствует действию'));
|
||||
}
|
||||
|
||||
protected static function currentValue(array $row, array $operation)
|
||||
@@ -469,6 +493,11 @@
|
||||
return (string) $row['__bulk_current'];
|
||||
}
|
||||
|
||||
if ($operation['target_type'] === 'product_description') {
|
||||
$fieldId = self::productDescriptionField($row);
|
||||
return $fieldId ? self::currentValue($row, array('target_type' => 'field', 'field_id' => $fieldId)) : '';
|
||||
}
|
||||
|
||||
if ($operation['target_type'] === 'document' || $operation['target_type'] === 'state') {
|
||||
if (!array_key_exists($operation['target'], $row)) {
|
||||
$value = DB::query(
|
||||
@@ -496,6 +525,11 @@
|
||||
{
|
||||
$current = (string) $current;
|
||||
$type = $operation['type'];
|
||||
if ($type === 'strip_description_styles') {
|
||||
$result = \App\Helpers\Html::removeInlineStyles($current);
|
||||
return array('value' => $result['html'], 'changed' => $result['removed'] > 0, 'removed' => $result['removed']);
|
||||
}
|
||||
|
||||
if ($type === 'fill') {
|
||||
$value = trim($current) === '' ? (string) $operation['value'] : $current;
|
||||
} elseif ($type === 'clear') {
|
||||
@@ -584,10 +618,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
protected static function productDescriptionsAvailable()
|
||||
{
|
||||
$module = \App\Common\ModuleManager::get('products');
|
||||
return $module && !empty($module['enabled']) && \App\Common\Permission::check('manage_products')
|
||||
&& class_exists('App\\Modules\\Products\\ProductRoles') && self::productsAvailable();
|
||||
}
|
||||
|
||||
protected static function productDescriptionField(array $row)
|
||||
{
|
||||
$product = DB::query('SELECT rubric_id FROM ' . CatalogTables::table('catalog_product_index')
|
||||
. ' WHERE product_id=%i AND is_deleted=0', (int) $row['Id'])->getAssoc();
|
||||
if (!$product || (int) $product['rubric_id'] !== (int) $row['rubric_id']) { return 0; }
|
||||
$roles = \App\Modules\Products\ProductRoles::forRubric((int) $row['rubric_id']);
|
||||
return isset($roles['description']) ? (int) $roles['description'] : 0;
|
||||
}
|
||||
|
||||
protected static function operationLabel($type)
|
||||
{
|
||||
$labels = array(
|
||||
'fill' => 'Заполнить пустые', 'set' => 'Установить значение', 'clear' => 'Очистить',
|
||||
'strip_description_styles' => 'Удалить стили из описаний товаров',
|
||||
'replace' => 'Найти и заменить', 'move' => 'Перенести в рубрику',
|
||||
'publish' => 'Опубликовать', 'unpublish' => 'Снять с публикации',
|
||||
'recalculate' => 'Пересчитать поля и индексы',
|
||||
|
||||
@@ -442,6 +442,7 @@
|
||||
AdminAssets::addScript($this->base() . '/modules/Documents/assets/documents.js', 50);
|
||||
return $this->render('@documents/bulk-editor.twig', array(
|
||||
'bulk_options' => BulkEditor::options(),
|
||||
'bulk_clean_descriptions' => Request::getStr('operation', '') === 'strip_description_styles',
|
||||
));
|
||||
}
|
||||
|
||||
@@ -587,9 +588,11 @@
|
||||
$snapshot = (new DocumentSnapshotRepository())->find($id);
|
||||
DocumentSaveEvents::after('create', 'adminx', $id, $rubricId, Auth::id(), $input, $fieldValues, array(), is_array($snapshot) ? $snapshot : array());
|
||||
$nextMediaDraftToken = DocumentMediaDraft::issue(Auth::id(), $id);
|
||||
$editorState = Model::editorState($id);
|
||||
return $this->success('Документ создан', array('data' => array(
|
||||
'id' => $id,
|
||||
'document_version' => Model::documentVersion($id),
|
||||
'document_version' => $editorState ? $editorState['version'] : Model::documentVersion($id),
|
||||
'editor_state' => $editorState,
|
||||
'snapshot_warning' => $snapshotError,
|
||||
'media_draft_token' => $nextMediaDraftToken,
|
||||
), 'redirect' => $this->base() . '/documents/' . $id . '/edit'));
|
||||
@@ -734,9 +737,11 @@
|
||||
$snapshot = (new DocumentSnapshotRepository())->find($id);
|
||||
DocumentSaveEvents::after('update', 'adminx', $id, (int) $doc['rubric_id'], Auth::id(), $input, $fieldValues, $doc, is_array($snapshot) ? $snapshot : array());
|
||||
$mediaCleanup = DocumentMediaReplacement::cleanup($mediaReplacement, $fieldValues);
|
||||
$editorState = Model::editorState($id);
|
||||
return $this->success('Документ сохранён', array('data' => array(
|
||||
'id' => $id,
|
||||
'document_version' => Model::documentVersion($id),
|
||||
'document_version' => $editorState ? $editorState['version'] : Model::documentVersion($id),
|
||||
'editor_state' => $editorState,
|
||||
'snapshot_warning' => $snapshotError,
|
||||
'media_cleanup' => $mediaCleanup,
|
||||
'media_draft_token' => DocumentMediaDraft::issue(Auth::id(), $id),
|
||||
|
||||
@@ -728,6 +728,23 @@
|
||||
)->getValue());
|
||||
}
|
||||
|
||||
public static function editorState($id)
|
||||
{
|
||||
$row = DB::query('SELECT Id,document_title,document_status,document_deleted,document_changed,document_version FROM '
|
||||
. self::documentsTable() . ' WHERE Id=%i LIMIT 1', (int) $id)->getAssoc();
|
||||
if (!$row) { return null; }
|
||||
$label = $row['document_deleted'] ? 'Удалён' : ($row['document_status'] ? 'Опубликован' : 'Черновик');
|
||||
$label = \App\Common\Language::translateSource($label);
|
||||
return array(
|
||||
'title' => self::decode($row['document_title']),
|
||||
'version' => max(1, (int) $row['document_version']),
|
||||
'state_label' => $label,
|
||||
'state_badge' => $row['document_deleted'] ? 'gray' : ($row['document_status'] ? 'green' : 'amber'),
|
||||
'meta' => 'ID ' . (int) $row['Id'] . ', ' . $label . \App\Common\Language::translateSource(', изменён:')
|
||||
. ' ' . date('d.m.Y H:i', (int) $row['document_changed']),
|
||||
);
|
||||
}
|
||||
|
||||
public static function markChanged($id)
|
||||
{
|
||||
self::touchDocument((int) $id);
|
||||
@@ -1744,6 +1761,9 @@
|
||||
DB::Insert(self::docFieldsTable(), array(
|
||||
'rubric_field_id' => (int) $field['Id'],
|
||||
'document_id' => (int) $documentId,
|
||||
'field_value' => '',
|
||||
'field_number_value' => 0,
|
||||
'document_in_search' => '1',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1243,6 +1243,7 @@
|
||||
.documents-bulk-controls .field {
|
||||
min-width: 0;
|
||||
}
|
||||
.documents-bulk-progress-actions [hidden],
|
||||
.documents-bulk-controls .field[hidden],
|
||||
[data-bulk-technical-help][hidden] {
|
||||
display: none;
|
||||
@@ -1337,6 +1338,15 @@
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.documents-bulk-html-preview {
|
||||
display: block;
|
||||
max-height: 140px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.documents-bulk-value-cell {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1289,7 +1289,7 @@
|
||||
if (host) { allowed[String(host.getAttribute('data-rubric-field-id'))] = true; }
|
||||
});
|
||||
document.querySelectorAll('.ax-document-field[data-rubric-field-id]').forEach(function (field) {
|
||||
var hide = (limited || tokens.length > 0) && !allowed[String(field.getAttribute('data-rubric-field-id'))];
|
||||
var hide = limited && !allowed[String(field.getAttribute('data-rubric-field-id'))];
|
||||
field.classList.toggle('is-catalog-hidden', hide);
|
||||
});
|
||||
var empty = catalog.querySelector('[data-document-catalog-empty]');
|
||||
@@ -1377,9 +1377,11 @@
|
||||
});
|
||||
},
|
||||
|
||||
submit: function (stay) {
|
||||
submit: function (stay, reviewed) {
|
||||
if (!this.form || this.submitting) { return; }
|
||||
var self = this;
|
||||
if (!reviewed && Adminx.RichEditor && Adminx.RichEditor.reviewBeforeSave
|
||||
&& Adminx.RichEditor.reviewBeforeSave(this.form, function () { self.submit(stay, true); })) { return; }
|
||||
var id = parseInt(this.field('id').value, 10) || 0;
|
||||
this.clearErrors();
|
||||
this.syncEditors();
|
||||
@@ -1426,6 +1428,7 @@
|
||||
self.refreshMediaDraft(payload.data.media_draft_token, payload.data.id || id);
|
||||
}
|
||||
self.resetMediaReplacements();
|
||||
self.refreshSavedHeading(payload.data && payload.data.editor_state);
|
||||
if (self.form.getAttribute('data-quick-edit') === '1') {
|
||||
self.refreshQuickEditOpener(!stay);
|
||||
return;
|
||||
@@ -1622,7 +1625,10 @@
|
||||
}
|
||||
control.value = saved[Math.min(index, saved.length - 1)] || '';
|
||||
if (control._adminxCodeMirror) { control._adminxCodeMirror.setValue(control.value); }
|
||||
if (control._adminxTiptap) { control._adminxTiptap.commands.setContent(control.value || '<p></p>'); }
|
||||
if (control._adminxTiptap) {
|
||||
if (Adminx.RichEditor && Adminx.RichEditor.replaceHtml) { Adminx.RichEditor.replaceHtml(control, control.value); }
|
||||
else { control._adminxTiptap.commands.setContent(control.value || '<p></p>'); }
|
||||
}
|
||||
});
|
||||
});
|
||||
this.form.querySelectorAll('[data-document-term-input]').forEach(function (field) {
|
||||
@@ -1677,14 +1683,22 @@
|
||||
if (note) { note.remove(); }
|
||||
var factId = document.querySelector('[data-document-fact-id]');
|
||||
if (factId) { factId.textContent = String(id); }
|
||||
var titleField = this.field('document_title');
|
||||
},
|
||||
|
||||
refreshSavedHeading: function (state) {
|
||||
if (!state) { return; }
|
||||
var heading = document.querySelector('[data-document-edit-heading]');
|
||||
var title = titleField ? titleField.value.trim() : '';
|
||||
if (heading && title) { heading.textContent = title; }
|
||||
var status = this.field('document_status');
|
||||
var meta = document.querySelector('[data-document-edit-meta]');
|
||||
if (meta) { meta.textContent = 'ID ' + id + ', ' + (status && status.value === '1' ? 'опубликован' : 'черновик') + ', сохранён только что'; }
|
||||
if (title) { document.title = title + ' · AVE.cms'; }
|
||||
if (heading) { heading.textContent = state.title; }
|
||||
if (meta) { meta.textContent = state.meta; }
|
||||
document.querySelectorAll('[data-document-state-badge]').forEach(function (badge) {
|
||||
badge.classList.remove('badge-blue', 'badge-green', 'badge-amber', 'badge-gray');
|
||||
if (['blue', 'green', 'amber', 'gray'].indexOf(state.state_badge) !== -1) {
|
||||
badge.classList.add('badge-' + state.state_badge);
|
||||
}
|
||||
badge.textContent = state.state_label;
|
||||
});
|
||||
document.title = state.title + ' · AVE.cms';
|
||||
},
|
||||
|
||||
setSaving: function (saving) {
|
||||
@@ -3671,7 +3685,10 @@
|
||||
var selected = target.options[target.selectedIndex];
|
||||
var editsField = operation && ['fill', 'set', 'clear', 'replace'].indexOf(operation.value) !== -1;
|
||||
var message = editsField && selected ? String(selected.getAttribute('data-help') || '') : '';
|
||||
if (selected && selected.value === 'document_title' && scope && scope.value === 'products') {
|
||||
if (operation && operation.value === 'strip_description_styles') {
|
||||
message = Adminx.tr('Только поле с ролью «Описание товара». Будут удалены атрибуты style, включая отступы, цвета и размеры текста. Другие поля, классы, заголовки и блоки <style> не изменяются. HTML будет нормализован при разборе.');
|
||||
}
|
||||
if (editsField && selected && selected.value === 'document_title' && scope && scope.value === 'products') {
|
||||
message = 'Это заголовок документа в панели. Название товара на сайте обычно берётся из поля рубрики, помеченного «витрина: название товара на сайте».';
|
||||
}
|
||||
var text = help.querySelector('span');
|
||||
@@ -3702,6 +3719,12 @@
|
||||
|
||||
function updateOperation() {
|
||||
var value = operation ? operation.value : '';
|
||||
if (scope) {
|
||||
Array.prototype.forEach.call(scope.options || [], function (option) {
|
||||
option.disabled = value === 'strip_description_styles' && option.value !== 'products';
|
||||
});
|
||||
if (value === 'strip_description_styles') { scope.value = 'products'; }
|
||||
}
|
||||
var editsField = ['fill', 'set', 'clear', 'replace'].indexOf(value) !== -1;
|
||||
setVisibility('[data-bulk-target-wrap]', editsField);
|
||||
setVisibility('[data-bulk-search-wrap]', value === 'replace');
|
||||
@@ -3757,6 +3780,17 @@
|
||||
token = plan.token || '';
|
||||
var rows = root.querySelector('[data-bulk-preview-rows]');
|
||||
rows.replaceChildren();
|
||||
var htmlPreview = plan.operation && plan.operation.type === 'strip_description_styles';
|
||||
function valueCell(value) {
|
||||
var td = cell(htmlPreview ? '' : value, 'documents-bulk-value-cell');
|
||||
if (htmlPreview) {
|
||||
var code = document.createElement('code');
|
||||
code.className = 'documents-bulk-html-preview';
|
||||
code.textContent = value || '';
|
||||
td.appendChild(code);
|
||||
}
|
||||
return td;
|
||||
}
|
||||
(plan.sample || []).forEach(function (item) {
|
||||
var tr = document.createElement('tr');
|
||||
var identity = document.createElement('td');
|
||||
@@ -3768,10 +3802,15 @@
|
||||
identity.appendChild(meta);
|
||||
tr.appendChild(identity);
|
||||
tr.appendChild(cell(item.rubric));
|
||||
tr.appendChild(cell(item.before, 'documents-bulk-value-cell'));
|
||||
tr.appendChild(cell(item.after, 'documents-bulk-value-cell'));
|
||||
tr.appendChild(valueCell(item.before));
|
||||
tr.appendChild(valueCell(item.after));
|
||||
var result = cell(item.changed ? 'Изменится' : (item.note || 'Без изменений'));
|
||||
result.className = item.changed ? 'documents-bulk-result is-changed' : 'documents-bulk-result is-skipped';
|
||||
if (item.changed && item.note) {
|
||||
var note = document.createElement('small');
|
||||
note.textContent = item.note;
|
||||
result.appendChild(note);
|
||||
}
|
||||
tr.appendChild(result);
|
||||
rows.appendChild(tr);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="documents_bulk_strip_description_styles">Remove styles from product descriptions</phrase>
|
||||
<phrase data="documents_bulk_description_styles_help">Only the field assigned to the product description role. Removes style attributes, including spacing, colors and font sizes. Other fields, classes, headings and <style> elements are unchanged. HTML is normalized during parsing.</phrase>
|
||||
<phrase data="documents_bulk_move_warning">Only fields with matching aliases and types are transferred. Values of other fields will not be retained in the destination rubric.</phrase>
|
||||
<phrase data="documents_bulk_title">Bulk editor</phrase>
|
||||
<phrase data="documents_bulk_intro">Checks changes before starting and processes documents in small batches.</phrase>
|
||||
<phrase data="documents_bulk_notice_title">Review first, then apply</phrase>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="documents_bulk_strip_description_styles">Удалить стили из описаний товаров</phrase>
|
||||
<phrase data="documents_bulk_description_styles_help">Только поле с ролью «Описание товара». Будут удалены атрибуты style, включая отступы, цвета и размеры текста. Другие поля, классы, заголовки и блоки <style> не изменяются. HTML будет нормализован при разборе.</phrase>
|
||||
<phrase data="documents_bulk_move_warning">Переносятся только поля с одинаковыми системными именами и типами. Значения остальных полей не сохранятся в новой рубрике.</phrase>
|
||||
<phrase data="documents_bulk_title">Массовый редактор</phrase>
|
||||
<phrase data="documents_bulk_intro">Проверяет изменения до запуска и обрабатывает документы небольшими пакетами.</phrase>
|
||||
<phrase data="documents_bulk_notice_title">Сначала проверка, затем применение</phrase>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'documents',
|
||||
'name' => 'Документы',
|
||||
'version' => '0.4.2',
|
||||
'version' => '0.4.10',
|
||||
|
||||
'permissions' => array(
|
||||
'key' => 'documents',
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<label class="product-package-field product-package-field-title"><span>Грузовое место</span><input class="input" name="title" value="{{ package.title|default('')|e('html_attr') }}" placeholder="Коробка 1" aria-label="Название грузового места"></label>
|
||||
<label class="product-package-field"><span>Количество</span><input class="input" type="number" name="quantity" min="1" max="999" step="1" value="{{ package.quantity }}" aria-label="Количество"></label>
|
||||
<label class="product-package-field"><span>Вес, кг</span><input class="input" type="number" name="weight_kg" min="0" step="0.001" value="{{ package.weight_kg|default('') }}" placeholder="0" aria-label="Вес в килограммах"></label>
|
||||
<label class="product-package-field"><span>Длина, см</span><input class="input" type="number" name="length_cm" min="0" step="0.1" value="{{ package.length_cm|default('') }}" placeholder="0" aria-label="Длина в сантиметрах"></label>
|
||||
<label class="product-package-field"><span>Ширина, см</span><input class="input" type="number" name="width_cm" min="0" step="0.1" value="{{ package.width_cm|default('') }}" placeholder="0" aria-label="Ширина в сантиметрах"></label>
|
||||
<label class="product-package-field"><span>Высота, см</span><input class="input" type="number" name="height_cm" min="0" step="0.1" value="{{ package.height_cm|default('') }}" placeholder="0" aria-label="Высота в сантиметрах"></label>
|
||||
@@ -242,12 +242,7 @@
|
||||
<div class="product-package-list" data-package-list>
|
||||
{% for package in shipping_profile.packages %}
|
||||
<div class="product-package-row" data-package-row>
|
||||
<input class="input" name="title" value="{{ package.title|e('html_attr') }}" aria-label="Название грузового места">
|
||||
<input class="input" type="number" name="quantity" min="1" max="999" step="1" value="{{ package.quantity }}" aria-label="Количество">
|
||||
<input class="input" type="number" name="weight_kg" min="0" step="0.001" value="{{ package.weight_kg }}" aria-label="Вес в килограммах">
|
||||
<input class="input" type="number" name="length_cm" min="0" step="0.1" value="{{ package.length_cm }}" aria-label="Длина в сантиметрах">
|
||||
<input class="input" type="number" name="width_cm" min="0" step="0.1" value="{{ package.width_cm }}" aria-label="Ширина в сантиметрах">
|
||||
<input class="input" type="number" name="height_cm" min="0" step="0.1" value="{{ package.height_cm }}" aria-label="Высота в сантиметрах">
|
||||
{% include '@documents/_product_package_fields.twig' with {package: package} only %}
|
||||
<button class="btn btn-ghost btn-icon btn-sm ax-act ax-act-danger" type="button" data-package-remove data-tooltip="Удалить грузовое место" aria-label="Удалить грузовое место"><i class="ti ti-trash"></i></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -259,6 +254,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<template data-package-template><div class="product-package-row" data-package-row><input class="input" name="title" value="" placeholder="Коробка 1" aria-label="Название грузового места"><input class="input" type="number" name="quantity" min="1" max="999" step="1" value="1" aria-label="Количество"><input class="input" type="number" name="weight_kg" min="0" step="0.001" value="" placeholder="0" aria-label="Вес в килограммах"><input class="input" type="number" name="length_cm" min="0" step="0.1" value="" placeholder="0" aria-label="Длина в сантиметрах"><input class="input" type="number" name="width_cm" min="0" step="0.1" value="" placeholder="0" aria-label="Ширина в сантиметрах"><input class="input" type="number" name="height_cm" min="0" step="0.1" value="" placeholder="0" aria-label="Высота в сантиметрах"><button class="btn btn-ghost btn-icon btn-sm ax-act ax-act-danger" type="button" data-package-remove data-tooltip="Удалить грузовое место" aria-label="Удалить грузовое место"><i class="ti ti-trash"></i></button></div></template>
|
||||
<template data-package-template><div class="product-package-row" data-package-row>{% include '@documents/_product_package_fields.twig' with {package: {quantity: 1}} only %}<button class="btn btn-ghost btn-icon btn-sm ax-act ax-act-danger" type="button" data-package-remove data-tooltip="Удалить грузовое место" aria-label="Удалить грузовое место"><i class="ti ti-trash"></i></button></div></template>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
<div class="documents-bulk-controls">
|
||||
{% if bulk_options.products_available %}
|
||||
<label class="field"><span>Контур</span><select class="select" name="scope">
|
||||
<option value="all">Документы и товары</option>
|
||||
<option value="all"{{ not bulk_clean_descriptions ? ' selected' : '' }}>Документы и товары</option>
|
||||
<option value="documents">Только обычные документы</option>
|
||||
<option value="products">Только товары</option>
|
||||
<option value="products"{{ bulk_clean_descriptions ? ' selected' : '' }}>Только товары</option>
|
||||
</select></label>
|
||||
{% else %}
|
||||
<input type="hidden" name="scope" value="all">
|
||||
@@ -57,7 +57,7 @@
|
||||
</select></label>
|
||||
<label class="field"><span>Состояние</span><select class="select" name="state">
|
||||
<option value="">Опубликованные и черновики</option>
|
||||
<option value="active">Только опубликованные</option>
|
||||
<option value="active"{{ bulk_clean_descriptions ? ' selected' : '' }}>Только опубликованные</option>
|
||||
<option value="draft">Только черновики</option>
|
||||
</select></label>
|
||||
<label class="field documents-bulk-search"><span>Поиск</span><span class="input-wrap"><i class="ti ti-search"></i><input class="input" type="search" name="q" placeholder="ID, название или alias"></span></label>
|
||||
@@ -78,6 +78,7 @@
|
||||
<option value="set">Установить значение</option>
|
||||
<option value="clear">Очистить значение</option>
|
||||
<option value="replace">Найти и заменить</option>
|
||||
{% if bulk_options.product_descriptions_available %}<option value="strip_description_styles"{{ bulk_clean_descriptions ? ' selected' : '' }}>Удалить стили из описаний товаров</option>{% endif %}
|
||||
<option value="move">Перенести в другую рубрику</option>
|
||||
<option value="publish">Опубликовать</option>
|
||||
<option value="unpublish">Снять с публикации</option>
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
<span class="icon-tile documents-head-icon" style="--tile-bg:var(--sec-soft);--tile-fg:var(--sec)"><i class="ti ti-file-text"></i></span>
|
||||
<h2>Основное</h2>
|
||||
</div>
|
||||
<span class="badge badge-{{ document.state_badge }}">{{ document.state_label }}</span>
|
||||
<span class="badge badge-{{ document.state_badge }}" data-document-state-badge>{{ document.state_label }}</span>
|
||||
</div>
|
||||
<div class="documents-card-body">
|
||||
<div class="form-grid">
|
||||
|
||||
@@ -129,7 +129,12 @@
|
||||
{
|
||||
return Lock::run('media-trash', function () use ($token) {
|
||||
$entry = self::entry($token);
|
||||
$payload = self::itemsRoot() . '/' . $entry['token'] . '/payload';
|
||||
self::removeTree($payload);
|
||||
clearstatcache(true, $payload);
|
||||
if (file_exists($payload)) { throw new \RuntimeException('Не удалось удалить файлы корзины. Проверьте права доступа.'); }
|
||||
self::removeTree(self::itemsRoot() . '/' . $entry['token']);
|
||||
if (is_dir(self::itemsRoot() . '/' . $entry['token'])) { throw new \RuntimeException('Не удалось удалить запись корзины. Проверьте права доступа.'); }
|
||||
AuditLog::record('media.purged', array('actor_id' => Auth::id(), 'target_type' => 'media', 'meta' => array('path' => $entry['path'], 'token' => $entry['token'])));
|
||||
return $entry;
|
||||
});
|
||||
|
||||
@@ -1036,3 +1036,11 @@ button.media-thumb {
|
||||
color: var(--text-secondary, #667085);
|
||||
font-size: 13px;
|
||||
}
|
||||
.media-trash-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
@@ -59,10 +59,12 @@
|
||||
var audit = e.target.closest('[data-media-audit-run]');
|
||||
var trashRestore = e.target.closest('[data-media-trash-restore]');
|
||||
var trashPurge = e.target.closest('[data-media-trash-purge]');
|
||||
var trashClear = e.target.closest('[data-media-trash-clear]');
|
||||
|
||||
if (audit) { self.runAudit(audit); return; }
|
||||
if (trashRestore) { self.restoreTrash(trashRestore); return; }
|
||||
if (trashPurge) { self.purgeTrash(trashPurge); return; }
|
||||
if (trashClear) { self.clearTrash(trashClear); return; }
|
||||
if (folder) { self.createFolder(); return; }
|
||||
if (rename) { self.rename(rename); return; }
|
||||
if (del) { self.remove(del); return; }
|
||||
@@ -401,6 +403,35 @@
|
||||
});
|
||||
},
|
||||
|
||||
clearTrash: function (button) {
|
||||
var self = this;
|
||||
if (self.clearingTrash) return;
|
||||
var items = Array.from(document.querySelectorAll('[data-media-trash-purge]'));
|
||||
if (!items.length) return;
|
||||
Adminx.Confirm.open({kind: 'danger', title: 'Очистить корзину медиа?',
|
||||
message: 'Все объекты, показанные в корзине, будут удалены окончательно. Восстановить их будет нельзя.',
|
||||
confirmLabel: 'Удалить навсегда', confirmClass: 'btn-danger', onConfirm: function () {
|
||||
self.clearingTrash = true;
|
||||
var done = 0, progress = document.querySelector('[data-media-trash-progress]');
|
||||
document.querySelectorAll('[data-media-trash-clear], [data-media-trash-purge], [data-media-trash-restore]').forEach(function (el) { el.disabled = true; });
|
||||
function step() {
|
||||
progress.textContent = done + ' / ' + items.length;
|
||||
if (done === items.length) { window.location.reload(); return Promise.resolve(); }
|
||||
var item = items[done];
|
||||
return Adminx.Ajax.post(self.base() + '/media/trash/' + encodeURIComponent(item.getAttribute('data-media-trash-purge')) + '/purge', new FormData(document.getElementById('mediaActionForm'))).then(function (payload) {
|
||||
var data = payload.data || {};
|
||||
if (!data.success) throw new Error(data.message || Adminx.tr('Не удалось очистить корзину'));
|
||||
item.closest('tr').remove(); done++; return step();
|
||||
});
|
||||
}
|
||||
step().catch(function (error) {
|
||||
self.clearingTrash = false;
|
||||
document.querySelectorAll('[data-media-trash-clear], [data-media-trash-purge], [data-media-trash-restore]').forEach(function (el) { el.disabled = false; });
|
||||
Adminx.Toast.show(error.message || 'Ошибка сети', 'error');
|
||||
});
|
||||
}});
|
||||
},
|
||||
|
||||
clearThumbnails: function (button) {
|
||||
var self = this;
|
||||
var name = button.getAttribute('data-name') || button.getAttribute('data-path') || '';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="media_trash_clear">Empty trash</phrase>
|
||||
<phrase data="media_trash_clear_confirm">Empty media trash?</phrase>
|
||||
<phrase data="media_trash_clear_warning">All items currently shown in the trash will be permanently deleted. This cannot be undone.</phrase>
|
||||
<phrase data="media_trash_clear_error">Could not empty trash</phrase>
|
||||
<phrase data="media_trash_files_error">Could not delete trashed files. Check filesystem permissions.</phrase>
|
||||
<phrase data="media_trash_entry_error">Could not delete trash metadata. Check filesystem permissions.</phrase>
|
||||
</language>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="media_trash_clear">Очистить корзину</phrase>
|
||||
<phrase data="media_trash_clear_confirm">Очистить корзину медиа?</phrase>
|
||||
<phrase data="media_trash_clear_warning">Все объекты, показанные в корзине, будут удалены окончательно. Восстановить их будет нельзя.</phrase>
|
||||
<phrase data="media_trash_clear_error">Не удалось очистить корзину</phrase>
|
||||
<phrase data="media_trash_files_error">Не удалось удалить файлы корзины. Проверьте права доступа.</phrase>
|
||||
<phrase data="media_trash_entry_error">Не удалось удалить запись корзины. Проверьте права доступа.</phrase>
|
||||
</language>
|
||||
@@ -19,7 +19,7 @@
|
||||
return array(
|
||||
'code' => 'media',
|
||||
'name' => 'Медиа',
|
||||
'version' => '0.6.4',
|
||||
'version' => '0.6.5',
|
||||
|
||||
'permissions' => array(
|
||||
'key' => 'media',
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</div>
|
||||
<div class="section-header ax-panel-header"><div class="section-icon"><i class="ti ti-trash"></i></div><div><div class="section-eyebrow">Медиа</div><h2>Удалённые файлы и папки</h2><p class="section-desc">Восстановление возвращает объект по исходному пути. Окончательное удаление необратимо.</p></div></div>
|
||||
<div class="card ax-list-card media-trash-card">
|
||||
{% if items and can_manage %}<div class="media-trash-toolbar"><button class="btn btn-danger" type="button" data-media-trash-clear><i class="ti ti-trash"></i>Очистить корзину</button><span class="text-secondary" data-media-trash-progress role="status" aria-live="polite"></span></div>{% endif %}
|
||||
<div class="table-scroll"><table class="table"><thead><tr><th>Объект</th><th>Тип</th><th>Удалён</th><th class="table-actions"></th></tr></thead><tbody>
|
||||
{% for item in items %}<tr><td><b>{{ item.name }}</b><small class="table-subtitle mono">{{ item.path }}</small></td><td>{{ item.type == 'folder' ? 'Папка' : 'Файл' }}</td><td>{{ item.deleted_label }}</td><td class="table-actions"><div class="cluster">
|
||||
<button class="btn btn-ghost btn-icon ax-act ax-act-restore" type="button" data-media-trash-restore="{{ item.token }}" data-name="{{ item.name }}" data-tooltip="Восстановить" aria-label="Восстановить"><i class="ti ti-restore"></i></button>
|
||||
|
||||
@@ -441,7 +441,7 @@
|
||||
}
|
||||
|
||||
foreach ($defaults as $key => $value) {
|
||||
if (!isset($input[$key]) || trim((string) $input[$key]) === '') {
|
||||
if (!array_key_exists($key, $input)) {
|
||||
$input[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'navigation',
|
||||
'name' => 'Навигация',
|
||||
'version' => '0.2.2',
|
||||
'version' => '0.2.3',
|
||||
|
||||
'hook_definitions' => array(
|
||||
array(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file adminx/modules/Requests/migrations/015_request_description_capacity.php
|
||||
| @author AVE.cms <support@ave-cms.ru>
|
||||
| @copyright 2007-2026 (c) AVE.cms
|
||||
| @link https://ave-cms.ru
|
||||
| @version 3.3
|
||||
*/
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Content\ContentTables;
|
||||
|
||||
return function (array $context) {
|
||||
$table = ContentTables::table('request');
|
||||
$column = \DB::query('SHOW COLUMNS FROM %b LIKE %s', $table, 'request_description')->getAssoc();
|
||||
if (!$column || strtolower((string) $column['Type']) !== 'tinytext') { return 0; }
|
||||
\DB::query('ALTER TABLE %b MODIFY COLUMN `request_description` TEXT NOT NULL', $table);
|
||||
return 1;
|
||||
};
|
||||
@@ -22,7 +22,7 @@
|
||||
return [
|
||||
'code' => 'requests',
|
||||
'name' => 'Запросы',
|
||||
'version' => '0.2.0',
|
||||
'version' => '0.2.1',
|
||||
|
||||
'permissions' => [
|
||||
'key' => 'requests',
|
||||
@@ -76,6 +76,7 @@
|
||||
['id' => '012_condition_value_sources', 'file' => 'migrations/012_condition_value_sources.php'],
|
||||
['id' => '013_sort_rules', 'file' => 'migrations/013_sort_rules.php'],
|
||||
['id' => '014_request_revisions', 'file' => 'migrations/014_request_revisions.sql'],
|
||||
['id' => '015_request_description_capacity', 'file' => 'migrations/015_request_description_capacity.php'],
|
||||
],
|
||||
|
||||
'routes' => array(
|
||||
|
||||
@@ -405,6 +405,7 @@
|
||||
$preset = $kind === 'rich' ? 'full' : ($kind === 'rich_simple' ? 'simple' : 'slim');
|
||||
return '<textarea class="textarea documents-rich-editor" name="' . $this->e($this->fname($field, '[raw]'))
|
||||
. '" data-rich-editor data-rich-preset="' . $preset . '" data-editor-height="' . $editorHeight
|
||||
. '" data-rich-options="' . $this->e(json_encode(\App\Content\Fields\RichEditorSettings::options($settings), JSON_UNESCAPED_UNICODE))
|
||||
. '" spellcheck="true">' . $this->e($parsed) . '</textarea>';
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'rubrics',
|
||||
'name' => 'Рубрики и поля',
|
||||
'version' => '0.3.10',
|
||||
'version' => '0.3.11',
|
||||
'field_sets' => \App\Adminx\Rubrics\RubricFieldPresets::definitions(),
|
||||
'hook_definitions' => array(
|
||||
array(
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
return array(
|
||||
'code' => 'updates',
|
||||
'name' => 'Обновления',
|
||||
'version' => '0.2.1',
|
||||
'version' => '0.2.2',
|
||||
'notifications' => array('provider' => array(\App\Adminx\Updates\NotificationProvider::class, 'items'), 'permission' => 'view_core_updates', 'sort_order' => 45),
|
||||
'permissions' => array(
|
||||
'key' => 'updates',
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `{{public_shell_prefix}}_sessions`
|
||||
MODIFY `sesskey` VARCHAR(256) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
MODIFY `value` MEDIUMTEXT NOT NULL;
|
||||
@@ -21,7 +21,7 @@
|
||||
return [
|
||||
'code' => 'users',
|
||||
'name' => 'Пользователи',
|
||||
'version' => '0.3.1',
|
||||
'version' => '0.3.2',
|
||||
|
||||
'permissions' => [
|
||||
'key' => 'users',
|
||||
@@ -78,6 +78,7 @@
|
||||
['id' => '002_harden_public_remember_tokens', 'file' => 'migrations/002_harden_public_remember_tokens.php'],
|
||||
['id' => '003_system_user_security', 'file' => 'migrations/003_system_user_security.php'],
|
||||
['id' => '004_system_session_expiry', 'file' => 'migrations/004_system_session_expiry.php'],
|
||||
['id' => '005_public_session_capacity', 'file' => 'migrations/005_public_session_capacity.sql'],
|
||||
],
|
||||
|
||||
'admin_extension' => array(
|
||||
|
||||
+10
-1
@@ -18,11 +18,20 @@
|
||||
|
||||
<!-- ================= TOPBAR ================= -->
|
||||
<style>.topbar-help .ti{color:var(--primary,#206bc4)}.topbar-help:hover .ti{color:var(--primary,#206bc4)}</style>
|
||||
<header class="topbar">
|
||||
<header class="topbar{{ attention.items ? ' has-attention' : '' }}">
|
||||
<button class="btn btn-ghost btn-icon" id="sidebarToggle" aria-label="{{ lang.topbar_collapse_menu|default('Свернуть меню') }}" data-tooltip="{{ lang.topbar_menu|default('Меню') }}"><i class="ti ti-layout-sidebar-left-collapse"></i></button>
|
||||
<a href="{{ ADMINX_BASE }}/" class="topbar-logo" style="color:var(--text-primary)"><img class="logo-img" src="{{ asset_ver(ADMINX_BASE ~ '/assets/img/logo.svg') }}" alt="AVE" width="66" height="24"><span class="logo-text logo-suffix">.cms</span></a>
|
||||
{% if site_development_mode %}<a class="env-badge" href="{{ ADMINX_BASE }}/settings/main" data-tooltip="{{ lang.development_mode_hint|default('Сайт закрыт для посетителей') }}"><i class="ti ti-lock"></i>{{ lang.development_mode|default('Режим разработки') }}</a>{% endif %}
|
||||
<div class="topbar-right">
|
||||
{% if attention.items %}
|
||||
<div class="attention-topbar" data-attention-root data-attention-user="{{ attention_user_id }}" data-attention-url="{{ ADMINX_BASE }}/dashboard/attention" data-attention-initial="{{ attention|json_encode|e('html_attr') }}" aria-label="{{ lang.attention_heading|default('Требуют внимания') }}">
|
||||
{% for item in attention.items %}
|
||||
<a class="attention-shortcut is-{{ item.tone }}{{ item.available and item.count > 0 ? ' has-pending' : '' }}" href="{{ item.url }}" data-attention-key="{{ item.key }}" data-tooltip="{{ item.title }}: {{ item.available ? item.count : '—' }} · {{ item.wait }}" aria-label="{{ item.title }}: {{ item.available ? item.count : '—' }} · {{ item.wait }}">
|
||||
<i class="{{ item.icon }}" aria-hidden="true"></i><b data-attention-count>{{ item.available ? (item.count > 99 ? '99+' : item.count) : '—' }}</b>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<a class="btn btn-ghost btn-icon topbar-site-link" href="{{ public_url }}" target="_blank" rel="noopener" aria-label="{{ lang.topbar_visit_site|default('Перейти на сайт') }}" data-tooltip="{{ lang.topbar_visit_site|default('Перейти на сайт') }}"><i class="ti ti-external-link"></i></a>
|
||||
<button class="btn btn-ghost btn-icon topbar-command" type="button" data-command-palette-open aria-label="Быстрый переход" data-tooltip="Быстрый переход"><i class="ti ti-search"></i></button>
|
||||
{% if module_header_actions %}
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# Массовый редактор документов и товаров
|
||||
|
||||
## Очистка стилей описаний товаров
|
||||
|
||||
Из «Качества каталога» кнопка **Очистить стили описаний** открывает массовый
|
||||
редактор с действием **Удалить стили из описаний товаров**. Можно также выбрать
|
||||
это действие в самом редакторе. Контур автоматически ограничивается товарами.
|
||||
Выберите состояние (опубликованные, черновики или оба), рубрику при необходимости,
|
||||
нажмите **Проверить изменения**, просмотрите количество и фрагменты HTML до/после,
|
||||
затем подтвердите запуск. Предпросмотр ничего в товарах не меняет.
|
||||
|
||||
Удаляются все атрибуты `style`, включая `margin-left: 40px`, цвета и размеры.
|
||||
Поле определяется ролью «Описание товара», без фиксированного ID; поля других
|
||||
ролей, SEO, краткое описание и характеристики не меняются. Классы, заголовки
|
||||
H1–H6 и элементы `<style>` эта операция не удаляет. Разметка нормализуется
|
||||
HTML-парсером. Описания без inline-стилей не пересохраняются. Поэтому очередь
|
||||
«Разметка описания» может остаться непустой после очистки, если там остались H1
|
||||
или элементы `<style>`.
|
||||
|
||||
Обработка пакетами по 20, с ревизиями и штатным сбросом кеша. Удалённые
|
||||
документы исключены. Требуются права управления документами и товарами.
|
||||
|
||||
Раздел **Документы → Массовый редактор** изменяет большой набор записей через
|
||||
тот же штатный сервис сохранения, который используют редактор документа, JSON
|
||||
API, импорт и быстрое заполнение товаров.
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
Документ — конкретная страница, запись, статья или товар. Его структура задаётся
|
||||
рубрикой, а внешний вид — шаблоном рубрики и шаблоном сайта.
|
||||
|
||||
Работа с текстом: [инструменты визуального редактора](rich-editor.md).
|
||||
|
||||
## Создание документа
|
||||
|
||||
1. Откройте **Контент → Документы** и нажмите создание.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Визуальный редактор
|
||||
|
||||
## Инструменты
|
||||
|
||||
Наведите курсор на значок: подсказка показывает название действия.
|
||||
|
||||
- **Найти и заменить**: поиск по тексту, переключение регистра, переход между
|
||||
совпадениями, замена одного или всех. Разметка не участвует в поиске.
|
||||
Текст замены вставляется как текст, а не как HTML. «Заменить все» отменяется одним undo.
|
||||
- **Специальные символы**: неразрывный пробел, градус, знак умножения, плюс-минус,
|
||||
кавычки, степени и другие символы. Вставляются в позицию курсора вместо выделения.
|
||||
- **Таблица**: после установки курсора в ячейку появляется дополнительная панель.
|
||||
Можно добавлять/удалять строки и столбцы, выделить несколько ячеек и объединить их,
|
||||
разделить ячейку, назначить заголовки или удалить таблицу. Недоступные действия отключены.
|
||||
- **Ссылка на документ или товар**: выберите страницу в штатном поиске. Выделенный
|
||||
текст станет ссылкой; без выделения вставится название выбранного документа.
|
||||
- **Готовые фрагменты**: «Внимание», «Важно», «Комплектация», таблица характеристик.
|
||||
После вставки замените заготовку своим текстом. Это обычная редактируемая HTML-разметка.
|
||||
- **Порядок блоков**: отдельное окно со списком абзацев, заголовков, списков, изображений
|
||||
и таблиц верхнего уровня. Перетащите строку или используйте стрелки вверх/вниз.
|
||||
Вложенный список перемещается целиком со своим блоком. Изменения доступны в undo.
|
||||
- **Проверка оформления**: H1 при ограниченном профиле или в редакторе товара,
|
||||
пустые заголовки/ссылки, inline-стили, повторные пустые абзацы. Исправления выполняются
|
||||
только по нажатию конкретной кнопки. Перед сохранением документа можно проверить
|
||||
замечания или выбрать «Сохранить без исправлений».
|
||||
- Под редактором показаны слова и символы всего текста, а при выделении — ещё и выделения.
|
||||
- **Развернуть редактор**: полный экран; повторное нажатие или Escape возвращает обычный
|
||||
размер. Если открыто вложенное окно, Escape сначала закрывает его.
|
||||
- **Вставить из Word**: сохраняет структуру и выделения, удаляет оформление Word.
|
||||
Обычная вставка автоматически очищается при распознавании Word. Изображения из Word
|
||||
добавляйте через медиатеку отдельно.
|
||||
|
||||
## Настройки В Рубрике
|
||||
|
||||
Откройте **Рубрики и поля → нужная рубрика → текстовое поле → настройки типа**.
|
||||
Настройки доступны у форматированного, многострочного и текстового содержимого в rich-режиме.
|
||||
|
||||
1. Выберите панель: полная, компактная, описание товара или новость/статья.
|
||||
2. Выберите доступные заголовки: по профилю, H1–H6, H2–H6 или без заголовков.
|
||||
3. Выберите набор фрагментов: все, информационные, товарные или отключённые.
|
||||
4. При необходимости скройте изображения, таблицы, исходный код, выравнивание,
|
||||
перемещение блоков.
|
||||
5. Сохраните настройки поля и заново откройте документ.
|
||||
|
||||
Профили товара/новости по умолчанию убирают H1 из выбора. Уже сохранённый H1 не удаляется:
|
||||
панель покажет «H1 (в тексте)». Настройки задаются каждому полю рубрики отдельно.
|
||||
Это настройки удобства, а не запрет HTML на стороне сервера.
|
||||
|
||||
## Несохранённый Текст
|
||||
|
||||
В документах и товарах используется существующая локальная копия всей формы. Восстановление
|
||||
теперь корректно обновляет и визуальный режим, и открытый HTML-исходник.
|
||||
|
||||
В остальных редакторах с именованным полем и известным пользователем хранится локальная
|
||||
копия текста до 7 дней (не более 1 млн символов на поле). При повторном открытии можно
|
||||
посмотреть текущий серверный текст и локальную копию, восстановить или удалить копию.
|
||||
Автоматической перезаписи серверного документа нет. После восстановления нажмите сохранение
|
||||
в основной форме. В браузерах с запрещённым localStorage восстановление недоступно.
|
||||
|
||||
Локальные копии привязаны к пользователю, странице и полю. На общем компьютере удаляйте
|
||||
ненужные локальные копии и завершайте сеанс. Это не замена серверным ревизиям и резервным копиям.
|
||||
|
||||
## Оформление Фрагментов
|
||||
|
||||
Фрагменты не содержат inline-цветов и отступов. Информационные блоки используют стандартный
|
||||
`blockquote` с классами `content-callout`, `content-callout--warning` или
|
||||
`content-callout--info`. Их публичное оформление можно задать в CSS своей темы.
|
||||
Без дополнительных стилей они остаются обычными цитатами, списками и таблицами.
|
||||
@@ -3,6 +3,15 @@
|
||||
The Commerce module manages the cart, checkout, delivery, payment,
|
||||
coupons, favorites, viewed products and customer order history.
|
||||
|
||||
## Processing archived carts
|
||||
|
||||
In Orders → Abandoned carts, choose the contact or UTM archive. After handling
|
||||
the customer, click the green Processed checkmark and confirm. The cart leaves
|
||||
the pending archive queue and remains available under Processed archive.
|
||||
Contacts, cart items and attribution are retained without automatic expiry.
|
||||
The detail view shows when and by whom it was processed. Reopen returns it to
|
||||
the queue. The mark is shared by all managers and requires order management permission.
|
||||
|
||||
## Order export
|
||||
|
||||
Set the search, order state, and payment filter, then click **CSV**. The current
|
||||
@@ -472,3 +481,12 @@ The script is included in `System -> Site Users -> Registration`, in the block
|
||||
**Account after order**. The consent text, policy address and Twig-
|
||||
letter template. If public registration or this script is disabled,
|
||||
checkout continues to work in guest mode.
|
||||
# Payment after checkout
|
||||
|
||||
An online order is saved before starting payment. The confirmation page shows
|
||||
its number and amount. The Pay button is immediately available; manager approval
|
||||
is not required. Repeated clicks reuse the existing payment.
|
||||
|
||||
In **Shop settings → General → Payment notice**, edit the notice and toggle its
|
||||
visibility. Hiding the notice does not hide the Pay button. Its presentation is
|
||||
editable in the `payment_notice` shop template.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Metrica: Ecommerce
|
||||
|
||||
This optional module connects storefront activity to Yandex Metrica without
|
||||
replacing the theme or changing prices and orders. Products and Commerce must
|
||||
be installed and enabled. Cart tracking requires Commerce 1.8.10 or newer.
|
||||
|
||||
## Connect
|
||||
|
||||
1. Open **Modules → Metrica: Ecommerce → Settings** and enter your numeric
|
||||
counter ID. No account password or OAuth token is required.
|
||||
2. Select **Already installed on the website** if a template, system block or
|
||||
tag manager already loads Metrica. Enable ecommerce on that counter and use
|
||||
the same data container, normally `dataLayer`.
|
||||
3. Otherwise select **Load through this module**. The module loads and
|
||||
initializes Metrica. Webvisor is optional.
|
||||
4. Enable the integration and keep test mode on. Select the events and save.
|
||||
5. Visit the storefront in another tab, view a product, add it to the cart,
|
||||
change the quantity and remove it. Check the module's **Diagnostics** tab.
|
||||
6. Disable test mode when ready. Verify actual receipt using Metrica tools.
|
||||
|
||||
An existing ecommerce sender must not send the same events as this module.
|
||||
An existing counter alone is fine. The module never deletes template code.
|
||||
|
||||
## Events
|
||||
|
||||
- Impressions when at least half of a product card is visible; product clicks
|
||||
and detail views.
|
||||
- Add/remove events use the actual quantity difference after a successful
|
||||
cart mutation. Failed or rolled-back changes are not purchases or additions.
|
||||
- Purchase means a successfully created storefront order. Revenue is the
|
||||
discounted merchandise total, excluding shipping. Gifts have zero price;
|
||||
the order revenue includes any order-level coupon discount.
|
||||
- Create the JavaScript goals `ave_begin_checkout` and
|
||||
`ave_payment_confirmed` in Metrica. Confirmed payment is a separate goal,
|
||||
never a second purchase.
|
||||
|
||||
Large orders are split to respect Metrica's 8192-character payload limit.
|
||||
Parts use IDs such as `123-1` and `123-2`; revenue is divided, not duplicated.
|
||||
Such an order counts as multiple purchases in Metrica reports.
|
||||
|
||||
## Diagnostics and limitations
|
||||
|
||||
Test mode does not push ecommerce into the live container or call Metrica
|
||||
goals. An independently installed counter remains active. The browser-local
|
||||
log retains the latest 50 events. Queued in JavaScript does not mean that
|
||||
Yandex confirmed receipt. The log is not a delivery ledger.
|
||||
|
||||
Session queues, CSRF-protected acknowledgements and browser-side event IDs
|
||||
reduce duplicates. Storage restrictions, ad blockers or closing a page can
|
||||
still affect delivery; this is not an exactly-once server integration.
|
||||
|
||||
For external payment redirects, pending events are sent when the buyer comes
|
||||
back in the same session. Events expire after 24 hours. Only this session's
|
||||
new orders are watched for payment, for up to seven days and on subsequent
|
||||
page visits or focus. No return means no browser payment goal. Guaranteed
|
||||
server-side reporting requires a separate conversion import integration.
|
||||
Manual and imported orders are not treated as storefront activity.
|
||||
|
||||
For custom cards use `data-product-card`, an inner `data-item` product ID,
|
||||
`itemprop="name"` and `itemprop="price"` with numeric `content`. The standard
|
||||
`.product-card` is also supported. Optional `data-metrica-list` labels a list.
|
||||
AJAX cards are discovered automatically. The `commerce:cart-updated` event
|
||||
and standard mini-cart replacement trigger a queue refresh.
|
||||
|
||||
Customer names, phones, emails, addresses, order access tokens and payment
|
||||
credentials are not included. Configure your site's analytics consent policy
|
||||
separately. Disabling this package does not remove a separately installed
|
||||
counter. Uninstall uses the standard module lifecycle.
|
||||
|
||||
[Yandex payload reference](https://yandex.ru/support/metrica/en/ecommerce/data).
|
||||
@@ -3,6 +3,16 @@
|
||||
Модуль Commerce управляет корзиной, оформлением заказа, доставкой, оплатой,
|
||||
купонами, избранным, просмотренными товарами и историей заказов покупателя.
|
||||
|
||||
## Обработка архивных корзин
|
||||
|
||||
В «Заказы → Брошенные корзины» выберите архив с контактами или UTM-метками.
|
||||
После работы с покупателем нажмите зелёную галочку «Обработана» и подтвердите.
|
||||
Корзина исчезнет из очереди архива, но останется в фильтре «Обработанные в архиве».
|
||||
Контакты, состав и рекламные метки сохраняются без автоматического удаления по сроку.
|
||||
В просмотре указаны дата обработки и менеджер. Кнопка «Вернуть в работу» снимает
|
||||
отметку и возвращает корзину в очередь. Отметка общая для всех менеджеров;
|
||||
для её изменения нужно право управления заказами.
|
||||
|
||||
## Выгрузка заказов
|
||||
|
||||
В списке заказов сначала задайте поиск, состояние и признак оплаты, затем
|
||||
@@ -396,6 +406,12 @@ Commerce также дополняет общий view model товарной к
|
||||
по себе не меняет итог заказа. Фиксированная стоимость способа доставки
|
||||
продолжает работать независимо от API.
|
||||
|
||||
У каждого способа доставки можно задать «Бесплатно от, ₽». Ноль выключает
|
||||
правило. Порог сравнивается с суммой товаров после акций и купона: при достижении
|
||||
порога стоимость этого способа становится нулевой в оформлении, заказе и платеже.
|
||||
Бесплатная доставка не добавляется платной услугой в чек. Правило действует и
|
||||
для заказа, который создаёт менеджер; другие способы доставки не затрагиваются.
|
||||
|
||||
На странице оформления расчёт запускается автоматически после выбора способа
|
||||
доставки и заполнения города или индекса. Под названием способа показываются
|
||||
ориентировочная цена перевозчика и срок. Эта сумма информационная: в итог
|
||||
@@ -505,3 +521,13 @@ Commerce использует общую систему публичных по
|
||||
**Аккаунт после заказа**. Там же задаются текст согласия, адрес политики и Twig-
|
||||
шаблон письма. Если публичная регистрация или этот сценарий выключены,
|
||||
оформление заказа продолжает работать в гостевом режиме.
|
||||
|
||||
## Оплата после оформления
|
||||
|
||||
Онлайн-заказ сначала сохраняется и показывает номер и сумму. Переход к банку
|
||||
происходит по отдельной кнопке «Оплатить», доступной сразу, без ожидания
|
||||
подтверждения менеджера. Повторное нажатие не создаёт второй платёж.
|
||||
|
||||
В **Настройки магазина → Общие → Предупреждение перед оплатой** можно изменить
|
||||
текст и выключить его показ. Кнопка оплаты остаётся доступной при скрытом
|
||||
предупреждении. Оформление плашки редактируется в шаблоне `payment_notice`.
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# Чат с посетителями
|
||||
|
||||
Модуль добавляет переписку с менеджером на публичные страницы сайта.
|
||||
Регистрация посетителя не требуется. Если установлены товарный и торговый
|
||||
модули, менеджер видит товар на открытой странице, корзину и заказы покупателя.
|
||||
ИИ, файлы и внешние мессенджеры в эту версию не входят.
|
||||
|
||||
## Подключение
|
||||
|
||||
1. Откройте **Модули → Управление**, установите «Чат с посетителями».
|
||||
2. Откройте **Чат с посетителями → Настройки** и включите чат.
|
||||
3. Задайте дни, начало и окончание рабочего дня, часовой пояс.
|
||||
По умолчанию: понедельник-пятница, 09:00-16:00, Europe/Moscow.
|
||||
4. Настройте заголовок окна, текст вне сети и быстрые ответы (один на строку).
|
||||
5. В ролях предоставьте менеджерам право **Чат: просмотр и ответы**.
|
||||
Настройки и работа с чужими диалогами требуют дополнительного права
|
||||
**Чат: настройки и управление чужими диалогами**.
|
||||
|
||||
Публичные шаблоны редактировать не нужно. Модуль подключает окно при штатном
|
||||
рендеринге HTML-страниц. На служебных документах и страницах ошибок окна нет.
|
||||
На автономных HTML-файлах вне движка автоматического подключения нет.
|
||||
|
||||
## Работа менеджера
|
||||
|
||||
- Откройте вкладку **Диалоги** и включите **Принимаю чаты**.
|
||||
- Оставьте эту вкладку открытой. Присутствие подтверждается короткими запросами;
|
||||
через 90 секунд без подтверждения менеджер перестаёт считаться доступным.
|
||||
- Во **Входящих** выберите диалог и нажмите **Принять**. Он закрепится за вами
|
||||
и появится в **Моих диалогах**. Два менеджера одновременно принять его не смогут.
|
||||
- Напишите ответ или выберите быстрый ответ. Ссылка на опубликованный товар
|
||||
текущего магазина дополнится карточкой товара; исходный текст сохранится.
|
||||
Внешние ссылки остаются обычными ссылками. Товар можно также найти и отправить
|
||||
кнопкой выбора товара в переписке.
|
||||
- Режим **Внутренняя заметка** сохраняет текст только для сотрудников.
|
||||
- **Ожидание** оставляет диалог за вами. Новое сообщение посетителя возвращает
|
||||
его в работу. При завершении выберите результат; для «Связаться позже»
|
||||
укажите дату и время. Завершённый диалог попадает в архив.
|
||||
- Можно передать диалог другому менеджеру, который сейчас принимает чаты.
|
||||
- Новый вопрос посетителя после завершения создаёт отдельный диалог во входящих.
|
||||
Прежняя переписка остаётся в архиве и не смешивается с новой.
|
||||
|
||||
Фильтры: входящие, мои, в работе, ожидание, завершённые, все. По 40 диалогов
|
||||
на страницу. Уведомление о новых диалогах/непрочитанных сообщениях появляется
|
||||
в общем колокольчике. Звук включается отдельно во вкладке диалогов.
|
||||
|
||||
**Отправлено** означает запись в базу. **Прочитано** появляется после открытия
|
||||
сообщений получателем в видимой активной вкладке. Загрузка списка диалогов
|
||||
сама по себе не отмечает сообщения прочитанными.
|
||||
|
||||
## Для посетителя
|
||||
|
||||
Окно доступно с кнопки «Написать нам». В рабочее время при наличии принимающего
|
||||
менеджера показывается статус «Менеджер на связи». В остальных случаях можно
|
||||
оставить сообщение и контакт для последующего ответа: телефон **или** email.
|
||||
Имя и контакт не обязательны для отправки; без контакта ответ можно прочитать
|
||||
только при возвращении в тот же браузер. Автоматических SMS/писем модуль не шлёт.
|
||||
|
||||
Переписка сохраняется между переходами по сайту в том же браузере. Очистка cookie,
|
||||
другой браузер или истечение cookie лишают посетителя доступа к прежней переписке.
|
||||
Секрет посетителя хранится в HttpOnly cookie до 180 дней, в базе находится только
|
||||
его SHA-256. В localStorage сохраняется лишь признак наличия чата, не текст и не
|
||||
контакты. На общем компьютере нужно учитывать доступ следующего пользователя
|
||||
того же браузера к переписке.
|
||||
|
||||
## Технические границы
|
||||
|
||||
- PHP 7.3, MySQL/MariaDB, обычные AJAX-запросы. WebSocket и внешний сервис не нужны.
|
||||
- До первого открытия чата не создаются диалоги и не опрашивается очередь.
|
||||
- После начала переписки открытое окно опрашивает новые сообщения раз в 4 секунды,
|
||||
закрытое раз в 30 секунд; скрытая вкладка приостанавливает этот опрос.
|
||||
- Административная очередь обновляется раз в 5 секунд в видимой вкладке.
|
||||
- Текст до 4000 символов, без исполняемого HTML. Отправка защищена CSRF,
|
||||
лимитами по IP и скрытым полем от простых автоматических отправок.
|
||||
- Повтор запроса с тем же ключом не создаёт дубль сообщения.
|
||||
- Общие изменения пишутся в аудит без содержимого сообщений и контактов.
|
||||
- Отключение модуля сохраняет данные. Удаление модуля уничтожает его таблицы
|
||||
диалогов, сообщений и присутствия. Перед удалением сделайте резервную копию.
|
||||
- Автоматического удаления истории по сроку в этой версии нет.
|
||||
|
||||
При установке на рабочий сайт обновите информацию об обработке контактов
|
||||
в соответствии с тем, как ваша организация использует чат.
|
||||
# Дополнения версии 1.1.0
|
||||
|
||||
## Повторное посещение и контакты (1.2.1)
|
||||
|
||||
Переписка и ранее отправленные контакты восстанавливаются в том же браузере
|
||||
по cookie чата, независимо от PHP-сессии. Срок привязки продлевается на 180 дней.
|
||||
Очистка cookie или смена браузера/устройства не позволяет восстановить анонимную
|
||||
историю автоматически. Сообщения хранятся в БД, а не в localStorage.
|
||||
|
||||
В настройках есть «Контактные поля раскрыты по умолчанию»: включено — имя и
|
||||
контакт сразу видны, выключено — блок нужно раскрыть. Во время текущего
|
||||
посещения ручное переключение блока и ввод посетителя не сбрасываются опросом.
|
||||
|
||||
|
||||
## Архив 1.2.0
|
||||
|
||||
Завершённые чаты выводятся отдельным списком с фильтрами по датам завершения,
|
||||
менеджеру, результату, ответам и контактам. Поиск: имя, контакт, источник, точный номер.
|
||||
В каждой строке отдельно указаны сообщения посетителя, ответы менеджера,
|
||||
внутренние заметки и служебные события. Сводка учитывает всю выборку.
|
||||
Нажмите номер/имя для просмотра истории по дням. История не обрезается:
|
||||
длинная переписка имеет страницы по 100 сообщений. «К списку» сохраняет фильтры.
|
||||
Просмотр архива не меняет прочитанность и не включает режим приёма чатов.
|
||||
|
||||
|
||||
В версии 1.1.2 добавлены хлебные крошки и предупреждение при уходе из окна чата.
|
||||
Оно появляется, если включено «Принимаю чаты», идёт отправка или остался черновик.
|
||||
Переход по ссылке можно отменить; при закрытии/обновлении вкладки браузер показывает
|
||||
собственное предупреждение (его текст изменить нельзя). На мобильных устройствах
|
||||
закрытие приложения браузера не всегда вызывает это событие.
|
||||
Без других менеджеров онлайн чат переходит в «Ответим позже» после истечения
|
||||
присутствия (до 90 секунд). Само окно остаётся доступным и сохраняет обращения.
|
||||
|
||||
Появились отдельные вкладки «Архив», «Статистика» и «Быстрые ответы».
|
||||
В очереди видны новые чаты, ожидание ответа и назначенный менеджер.
|
||||
Чтение сообщения не снимает ожидание ответа. Завершённые чаты не удаляются
|
||||
автоматически; новое обращение того же посетителя создаётся отдельно.
|
||||
|
||||
Публичное имя отвечающего задаётся в настройках. Имена сотрудников видны только
|
||||
в панели. Источники, UTM, реферер, IP и браузер доступны внутри переписки.
|
||||
Архивная статистика отражает отдельные завершённые диалоги. Общая статистика группирует диалоги по дате
|
||||
создания, а ответы менеджеров считает за выбранный период.
|
||||
|
||||
## Товары и уведомления (1.3.0)
|
||||
|
||||
Ссылки на опубликованные товары текущего сайта превращаются в мини-карточки
|
||||
с изображением, названием, артикулом и ценой, если данные есть в каталоге.
|
||||
Модуль не запрашивает OpenGraph у внешних сайтов. Если менеджер отправляет
|
||||
ссылку на рабочий домен с локального или тестового сайта, добавьте этот домен
|
||||
в настройку «Дополнительные домены магазина для превью товаров».
|
||||
|
||||
Очередь ставит новые обращения выше уже принятых; видно время ожидания и
|
||||
недоступность назначенного менеджера. По желанию включите уведомления браузера
|
||||
кнопкой во вкладке диалогов. Для браузерных уведомлений нужна открытая вкладка
|
||||
панели и разрешение браузера. Обновление до версии 1.3.0 применяет миграцию 003.
|
||||
|
||||
Рядом с плашками заказов и форм отображается число новых/непросмотренных чатов.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Метрика: электронная коммерция
|
||||
|
||||
Модуль связывает действия покупателя с Яндекс Метрикой. Он не меняет цены,
|
||||
корзину, заказ и оформление страниц. Нужны установленные и включённые модули
|
||||
«Товары» и «Магазин»; для изменений корзины нужен «Магазин» версии 1.8.10 или новее.
|
||||
|
||||
## Подключение
|
||||
|
||||
1. Откройте **Модули → Метрика: электронная коммерция → Настройки**.
|
||||
2. Введите числовой номер счётчика из Яндекс Метрики. Пароль не нужен.
|
||||
3. Если счётчик уже подключён общим шаблоном, системным блоком или менеджером
|
||||
тегов, оставьте **Уже установлен на сайте**. Модуль не будет вставлять его
|
||||
второй раз. В настройках существующего счётчика включите электронную
|
||||
коммерцию, контейнер должен совпадать с настройкой модуля (`dataLayer`).
|
||||
4. Если счётчика нет, выберите **Подключать модулем**. В этом режиме модуль
|
||||
загружает код Яндекса и инициализирует ecommerce. Вебвизор необязателен.
|
||||
5. Выберите события, включите интеграцию и пока оставьте тестовый режим.
|
||||
6. Сохраните. В соседней вкладке откройте сайт, товар, добавьте его в корзину,
|
||||
измените количество, удалите. Вернитесь во вкладку **Проверка**.
|
||||
7. После проверки выключите тестовый режим. Для проверки реального приёма
|
||||
используйте инструменты самой Метрики, а не только журнал модуля.
|
||||
|
||||
Нельзя одновременно оставлять старый собственный скрипт отправки ecommerce
|
||||
и включать такие же события модуля: получится двойной учёт. Обычный счётчик
|
||||
оставить можно. Модуль автоматически не удаляет код из ваших шаблонов.
|
||||
|
||||
## Что учитывается
|
||||
|
||||
- Показы карточек: когда не меньше половины карточки видно на экране.
|
||||
- Переход из карточки: ссылка на основной товар, не переключение миниатюры.
|
||||
- Просмотр товара: фактическая страница товара, включая повторное открытие.
|
||||
- Добавление/удаление: только изменение количества после успешной операции.
|
||||
Увеличили с одного до трёх — добавлено два. Ошибка и откат не считаются.
|
||||
- Покупка: заказ действительно создан публичным оформлением. Берутся ID
|
||||
товаров, состав, цены с акциями и итог товаров со скидками, без доставки.
|
||||
Подарки имеют нулевую цену. Общая купонная скидка учтена в доходе заказа.
|
||||
- Начало оформления: JS-цель `ave_begin_checkout`.
|
||||
- Оплата подтверждена: JS-цель `ave_payment_confirmed`, не повторная покупка.
|
||||
|
||||
Обе JS-цели создайте в интерфейсе Метрики. ID товара остаётся ID документа,
|
||||
а ID покупки — номер заказа. Это не идентификаторы покупателя.
|
||||
|
||||
Очень большой заказ делится на части, чтобы уложиться в ограничение Яндекса
|
||||
8192 символа на объект. Части имеют номера вида `123-1`, `123-2`; доход
|
||||
распределяется между ними, не дублируется. Число покупок в отчёте в этом случае
|
||||
соответствует числу частей, а не числу исходных заказов.
|
||||
|
||||
## Проверка и ограничения
|
||||
|
||||
В тестовом режиме модуль ничего не помещает в рабочий dataLayer и не вызывает
|
||||
цели Яндекса. Уже установленный отдельно счётчик при этом продолжает работать.
|
||||
Журнал хранит последние 50 событий только в браузере; он не является журналом
|
||||
доставки. «Передано в JS» означает постановку в очередь JavaScript, не ответ Яндекса.
|
||||
|
||||
Повторная загрузка страницы и повторное чтение очереди не должны повторять
|
||||
покупку: используются ID события, подтверждение очереди с CSRF и память
|
||||
браузера. При недоступности хранилища браузера защита слабее; абсолютную
|
||||
доставку ровно один раз браузерная аналитика не гарантирует.
|
||||
|
||||
Если после заказа покупатель ушёл в банк, событие будет передано при возвращении
|
||||
на сайт или следующем открытии страницы в той же сессии. Очередь живёт сутки.
|
||||
Проверка оплаты отслеживает только заказы этой сессии, не старые заказы из БД;
|
||||
наблюдение действует до семи дней. Без возвращения посетителя оплата не попадёт
|
||||
в цель. Для гарантированного серверного учёта нужен отдельный механизм импорта
|
||||
конверсий. Ручные и импортированные заказы не считаются посещениями сайта.
|
||||
|
||||
На новом шаблоне карточки используйте штатный `data-product-card`, внутри
|
||||
`data-item` с ID товара и микроданные `itemprop="name"`, `itemprop="price"`
|
||||
с числовым `content`. Дополнительно поддерживается обычный `.product-card`.
|
||||
Необязательный `data-metrica-list` задаёт название списка. AJAX-карточки
|
||||
подхватываются автоматически. Событие `commerce:cart-updated` обновляет очередь;
|
||||
стандартная замена мини-корзины тоже поддерживается.
|
||||
|
||||
Модуль не передаёт имена, телефоны, email, адреса, токены заказа и платёжные
|
||||
ключи. Настройку согласия на аналитику и политику сайта необходимо учитывать
|
||||
отдельно. Выключение пакета прекращает его подключение; самостоятельно
|
||||
встроенный счётчик останется в шаблоне.
|
||||
|
||||
Формат событий: [документация Яндекса](https://yandex.ru/support/metrica/ru/ecommerce/data).
|
||||
+16
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
1. Откройте **Модули → Слайдеры** и нажмите **Создать слайдер**.
|
||||
2. Укажите понятное название и постоянный системный код, например `main`.
|
||||
3. Сохраните слайдер и добавьте изображения из общей медиатеки.
|
||||
3. Сохраните слайдер. Откроется его отдельная страница со вкладками «Слайды» и «Настройки слайдера».
|
||||
4. Для каждого слайда настройте текст, ссылки, кнопки, положение и затемнение.
|
||||
5. Расставьте слайды перетаскиванием за маркер слева.
|
||||
6. Скопируйте тег и вставьте его в шаблон, блок или содержимое документа:
|
||||
@@ -22,6 +22,21 @@
|
||||
Вместо кода разрешён числовой ID: `[mod_slider:3]`. Код удобнее, потому что
|
||||
остаётся понятным после переноса данных между сайтами.
|
||||
|
||||
## Редактирование и сохранение
|
||||
|
||||
Откройте слайдер из списка. Во вкладке «Слайды» видны изображения, названия
|
||||
и состояние каждого слайда. Карандаш открывает выбранный слайд в правой панели,
|
||||
а «Добавить слайд» открывает пустую форму. Предпросмотр обновляется при вводе.
|
||||
|
||||
«Сохранить слайд» записывает изменения, обновляет список и закрывает панель.
|
||||
«Сохранить и остаться» записывает изменения и оставляет этот слайд открытым.
|
||||
Копирование создаёт выключенную копию и открывает её для редактирования.
|
||||
|
||||
Общие настройки сохраняются отдельно во вкладке «Настройки слайдера».
|
||||
Кнопки внизу этой вкладки относятся только к общим настройкам.
|
||||
После изменения слайдов, их порядка или настроек публичный кеш сбрасывается
|
||||
автоматически. Переустанавливать модуль или заново вставлять тег не требуется.
|
||||
|
||||
## Что настраивается у слайдера
|
||||
|
||||
| Параметр | Что делает |
|
||||
|
||||
@@ -105,10 +105,11 @@
|
||||
'005_rebuild_native_condition_cache', '006_sargable_condition_cache',
|
||||
'007_result_contract', '008_preview_renderer', '009_native_executor',
|
||||
'010_native_audit_result', '011_order_tiebreaker', '012_condition_value_sources',
|
||||
'013_sort_rules', '014_request_revisions',
|
||||
'013_sort_rules', '014_request_revisions', '015_request_description_capacity',
|
||||
), 'backup' => array(
|
||||
'013_sort_rules' => array('{{content_prefix}}_request'),
|
||||
'014_request_revisions' => array(),
|
||||
'015_request_description_capacity' => array('{{content_prefix}}_request'),
|
||||
)),
|
||||
'rubrics' => array('directory' => 'Rubrics', 'ids' => array(
|
||||
'001_create_rubrics_tables', '002_rubric_field_settings',
|
||||
@@ -147,9 +148,11 @@
|
||||
'users' => array('directory' => 'Users', 'ids' => array(
|
||||
'001_normalize_public_session_activity', '002_harden_public_remember_tokens',
|
||||
'003_system_user_security', '004_system_session_expiry',
|
||||
'005_public_session_capacity',
|
||||
), 'backup' => array(
|
||||
'002_harden_public_remember_tokens' => array('{{public_user_prefix}}_users_session'),
|
||||
'003_system_user_security' => array(),
|
||||
'004_system_session_expiry' => array('{{prefix}}_users_session'),
|
||||
'005_public_session_capacity' => array('{{public_shell_prefix}}_sessions'),
|
||||
)),
|
||||
);
|
||||
|
||||
+1
-1
@@ -423,7 +423,7 @@
|
||||
<label><span>Имя администратора</span><input name="admin_name" value="<?= setup_escape($defaults['admin_name']) ?>" required></label>
|
||||
<label><span>Логин</span><input name="admin_login" value="<?= setup_escape($defaults['admin_login']) ?>" required></label>
|
||||
<label><span>Email</span><input name="admin_email" type="email" value="<?= setup_escape($defaults['admin_email']) ?>" required></label>
|
||||
<label><span>Пароль</span><input name="admin_password" type="password" minlength="<?= (int) $adminPasswordMinLength ?>" required><small>Не менее <?= (int) $adminPasswordMinLength ?> символов</small></label>
|
||||
<label><span>Пароль</span><input name="admin_password" type="password" minlength="<?= (int) $adminPasswordMinLength ?>" required><small><?= setup_escape(sprintf(AveSetupLocale::translate('Не менее %d символов'), (int) $adminPasswordMinLength)) ?></small></label>
|
||||
<label class="form-span-2"><span>Папка панели управления</span><input name="admin_directory" value="<?= setup_escape($defaults['admin_directory']) ?>" pattern="[A-Za-z][A-Za-z0-9_\-]{0,47}" maxlength="48" required><small>По умолчанию adminx. Можно изменить до установки; системные имена использовать нельзя.</small></label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -212,7 +212,9 @@
|
||||
'Конфигурация и кеш' => 'Configuration and cache',
|
||||
'Распаковываем AVE.cms' => 'Unpacking AVE.cms',
|
||||
'Система настроена' => 'The system is configured',
|
||||
'Не менее символов' => 'At least characters',
|
||||
'Не менее %d символов' => 'At least %d characters',
|
||||
'Публичный адрес сайта' => 'Public website URL',
|
||||
'Полный адрес с https://. Он используется для проверки домена и защищённых ссылок в письмах.' => 'Full URL starting with https://. Used to validate the domain and secure links in emails.',
|
||||
'выбранный профиль' => 'selected profile',
|
||||
'Новый сайт на AVE.cms' => 'New website on AVE.cms',
|
||||
'Таблицы и индексы' => 'Tables and indexes',
|
||||
@@ -222,7 +224,7 @@
|
||||
'Проверяем данные' => 'Checking the data',
|
||||
'Язык установщика' => 'Installer language',
|
||||
'AVE.cms установлена' => 'AVE.cms installed',
|
||||
'Core-схема найдена' => 'Core circuit found',
|
||||
'Core-схема найдена' => 'Core schema found',
|
||||
'Другой источник' => 'Other source',
|
||||
'После установки' => 'After installation',
|
||||
'Удалить объекты' => 'Delete objects',
|
||||
@@ -243,7 +245,7 @@
|
||||
'Ошибка записи:' => 'Write error:',
|
||||
'Рекомендуется' => 'Recommended',
|
||||
'Администратор' => 'Administrator',
|
||||
'Схема ядра InnoDB' => 'InnoDB core diagram',
|
||||
'Схема ядра InnoDB' => 'InnoDB core schema',
|
||||
'Установка AVE.cms' => 'Installing AVE.cms',
|
||||
'Не подключать' => 'Do not connect',
|
||||
'PHP 7.3 или новее' => 'PHP 7.3 or later',
|
||||
|
||||
+3
-3
@@ -354,9 +354,9 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_console_snippets` (
|
||||
-- Public shell and accounts ----------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `{{prefix}}_public_sessions` (
|
||||
`sesskey` VARCHAR(32) NOT NULL,
|
||||
`sesskey` VARCHAR(256) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`expiry` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`value` TEXT NOT NULL,
|
||||
`value` MEDIUMTEXT NOT NULL,
|
||||
`Ip` VARCHAR(45) NOT NULL DEFAULT '',
|
||||
`expire_datum` VARCHAR(25) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`sesskey`),
|
||||
@@ -992,7 +992,7 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_request` (
|
||||
`request_order_by_nat` INT NOT NULL DEFAULT 0,
|
||||
`request_author_id` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`request_created` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`request_description` TINYTEXT NOT NULL,
|
||||
`request_description` TEXT NOT NULL,
|
||||
`request_result_contract` MEDIUMTEXT DEFAULT NULL,
|
||||
`request_preview_renderer` VARCHAR(64) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT 'data_cards',
|
||||
`request_executor_mode` VARCHAR(16) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT 'legacy',
|
||||
|
||||
@@ -184,8 +184,10 @@
|
||||
return $value;
|
||||
}
|
||||
|
||||
$started = false;
|
||||
try {
|
||||
return Lock::run('cache-remember:' . (string) $key, function () use ($key, $ttl, $callback, $miss) {
|
||||
return Lock::run('cache-remember:' . (string) $key, function () use ($key, $ttl, $callback, $miss, &$started) {
|
||||
$started = true;
|
||||
$value = self::get($key, $miss);
|
||||
if ($value !== $miss) {
|
||||
return $value;
|
||||
@@ -196,6 +198,8 @@
|
||||
return $value;
|
||||
});
|
||||
} catch (\RuntimeException $e) {
|
||||
// Retry only lock acquisition, never a failing computation or cache write.
|
||||
if ($started) { throw $e; }
|
||||
$value = self::get($key, $miss);
|
||||
if ($value !== $miss) {
|
||||
return $value;
|
||||
@@ -225,8 +229,10 @@
|
||||
return $value;
|
||||
}
|
||||
|
||||
$started = false;
|
||||
try {
|
||||
return Lock::run('cache-remember:' . (string) $key, function () use ($key, $ttl, $tags, $callback, $miss) {
|
||||
return Lock::run('cache-remember:' . (string) $key, function () use ($key, $ttl, $tags, $callback, $miss, &$started) {
|
||||
$started = true;
|
||||
$value = self::get($key, $miss);
|
||||
if ($value !== $miss) {
|
||||
return $value;
|
||||
@@ -237,6 +243,7 @@
|
||||
return $value;
|
||||
});
|
||||
} catch (\RuntimeException $e) {
|
||||
if ($started) { throw $e; }
|
||||
$value = self::get($key, $miss);
|
||||
if ($value !== $miss) {
|
||||
return $value;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file system/App/Common/CliRuntime.php
|
||||
| @author AVE.cms <support@ave-cms.ru>
|
||||
| @copyright 2007-2026 (c) AVE.cms
|
||||
| @link https://ave-cms.ru
|
||||
| @version 3.3
|
||||
*/
|
||||
|
||||
namespace App\Common;
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
class CliRuntime
|
||||
{
|
||||
public static function php(array $preferred = array(), $minimumVersion = 70300)
|
||||
{
|
||||
if (!function_exists('proc_open')) { throw new \RuntimeException('proc_open недоступен: запуск PHP CLI невозможен.'); }
|
||||
$runtime = array(
|
||||
'binary' => defined('PHP_BINARY') ? PHP_BINARY : '',
|
||||
'bindir' => defined('PHP_BINDIR') ? PHP_BINDIR : '',
|
||||
'ini' => php_ini_loaded_file() ?: '',
|
||||
'path' => (string) getenv('PATH'),
|
||||
'windows' => DIRECTORY_SEPARATOR === '\\',
|
||||
'version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
|
||||
);
|
||||
$preferred[] = trim((string) getenv('PHP_CLI_BINARY'));
|
||||
foreach (self::phpCandidates($preferred, $runtime) as $candidate) {
|
||||
if (!self::isPhpName($candidate) || !is_file($candidate) || (!$runtime['windows'] && !is_executable($candidate))) { continue; }
|
||||
if (self::probe($candidate) >= (int) $minimumVersion) { return $candidate; }
|
||||
}
|
||||
|
||||
$version = (int) floor((int) $minimumVersion / 10000) . '.' . (int) floor(((int) $minimumVersion % 10000) / 100);
|
||||
throw new \RuntimeException('Консольный PHP ' . $version . ' или новее не найден. Укажите полный путь к php или php.exe в PHP_CLI_BINARY.');
|
||||
}
|
||||
|
||||
public static function findCommand($name, array $preferred = array())
|
||||
{
|
||||
if (!preg_match('/^[A-Za-z0-9_.-]+$/', (string) $name)) { throw new \InvalidArgumentException('Invalid command name'); }
|
||||
$windows = DIRECTORY_SEPARATOR === '\\';
|
||||
$extensions = $windows ? explode(';', (string) getenv('PATHEXT')) : array();
|
||||
$paths = explode($windows ? ';' : ':', (string) getenv('PATH'));
|
||||
foreach (self::commandCandidates($name, $paths, $extensions, $windows, $preferred) as $candidate) {
|
||||
if (is_file($candidate) && ($windows || is_executable($candidate))) { return $candidate; }
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected static function phpCandidates(array $preferred, array $runtime)
|
||||
{
|
||||
$windows = !empty($runtime['windows']);
|
||||
$separator = $windows ? '\\' : '/';
|
||||
$name = $windows ? 'php.exe' : 'php';
|
||||
$out = $preferred;
|
||||
if (self::isPhpName($runtime['binary'])) { $out[] = $runtime['binary']; }
|
||||
foreach (array($runtime['bindir'], dirname(str_replace('\\', '/', (string) $runtime['ini'])), dirname(str_replace('\\', '/', (string) $runtime['binary']))) as $directory) {
|
||||
if ($directory === '' || $directory === '.') { continue; }
|
||||
$directory = $windows ? str_replace('/', '\\', $directory) : $directory;
|
||||
$out[] = rtrim($directory, '/\\') . $separator . $name;
|
||||
$out[] = rtrim($directory, '/\\') . $separator . 'bin' . $separator . $name;
|
||||
}
|
||||
|
||||
$paths = explode($windows ? ';' : ':', (string) $runtime['path']);
|
||||
$out = array_merge($out, self::commandCandidates($name, $paths, array(), $windows));
|
||||
if ($windows && preg_match('~^(.*/modules)/~i', str_replace('\\', '/', (string) $runtime['binary']), $match)) {
|
||||
$found = array();
|
||||
foreach (array('/php/*/php.exe', '/php/*/bin/php.exe', '/PHP*/php.exe', '/PHP*/bin/php.exe') as $suffix) {
|
||||
$found = array_merge($found, glob($match[1] . $suffix) ?: array());
|
||||
}
|
||||
|
||||
$version = isset($runtime['version']) ? preg_quote($runtime['version'], '~') : '';
|
||||
if ($version !== '') {
|
||||
$version = str_replace('\\.', '[._-]', $version);
|
||||
usort($found, function ($a, $b) use ($version) { return (int) preg_match('~' . $version . '~', $b) - (int) preg_match('~' . $version . '~', $a); });
|
||||
}
|
||||
|
||||
$out = array_merge($out, $found);
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($out, function ($candidate) { return trim((string) $candidate) !== ''; })));
|
||||
}
|
||||
|
||||
protected static function commandCandidates($name, array $paths, array $extensions, $windows, array $preferred = array())
|
||||
{
|
||||
$suffixes = array('');
|
||||
if ($windows && pathinfo((string) $name, PATHINFO_EXTENSION) === '') {
|
||||
foreach (array_merge($extensions, array('.EXE', '.COM', '.BAT', '.CMD')) as $extension) {
|
||||
$extension = trim((string) $extension);
|
||||
if (preg_match('/^\.[A-Za-z0-9]+$/', $extension)) { $suffixes[] = strtolower($extension); }
|
||||
}
|
||||
}
|
||||
|
||||
$out = $preferred;
|
||||
foreach ($paths as $directory) {
|
||||
$directory = trim((string) $directory, " \t\n\r\0\x0B\"");
|
||||
if ($directory === '') { continue; }
|
||||
foreach (array_unique($suffixes) as $suffix) { $out[] = rtrim($directory, '/\\') . ($windows ? '\\' : '/') . $name . $suffix; }
|
||||
}
|
||||
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
protected static function isPhpName($path)
|
||||
{
|
||||
return preg_match('/^php(?:[0-9]+(?:\.[0-9]+)*)?(?:\.exe)?$/i', basename(str_replace('\\', '/', (string) $path))) === 1;
|
||||
}
|
||||
|
||||
protected static function probe($binary)
|
||||
{
|
||||
$command = escapeshellarg((string) $binary) . ' -n -r ' . escapeshellarg('echo PHP_SAPI,chr(124),PHP_VERSION_ID;');
|
||||
$pipes = array();
|
||||
try {
|
||||
$process = @proc_open($command, array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $pipes, null, null, array('bypass_shell' => DIRECTORY_SEPARATOR === '\\'));
|
||||
} catch (\Throwable $e) { return 0; }
|
||||
if (!is_resource($process)) { return 0; }
|
||||
fclose($pipes[0]);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
$start = microtime(true); $stdout = ''; $stderr = ''; $exitCode = null; $timedOut = false;
|
||||
do {
|
||||
$stdout .= (string) stream_get_contents($pipes[1]);
|
||||
$stderr .= (string) stream_get_contents($pipes[2]);
|
||||
$status = proc_get_status($process);
|
||||
if (!$status['running']) { $exitCode = (int) $status['exitcode']; break; }
|
||||
if (microtime(true) - $start >= 2 || strlen($stdout) + strlen($stderr) > 8192) { $timedOut = true; proc_terminate($process, 9); break; }
|
||||
usleep(20000);
|
||||
} while (true);
|
||||
$stdout .= (string) stream_get_contents($pipes[1]);
|
||||
$stderr .= (string) stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]); fclose($pipes[2]);
|
||||
$closed = proc_close($process);
|
||||
$exitCode = $exitCode !== null && $exitCode >= 0 ? $exitCode : $closed;
|
||||
if ($timedOut || $exitCode !== 0 || trim($stderr) !== '' || !preg_match('/^cli\|([0-9]{5,6})$/', trim($stdout), $match)) { return 0; }
|
||||
return (int) $match[1];
|
||||
}
|
||||
}
|
||||
@@ -216,6 +216,12 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Installation defaults become project-owned and must survive core updates. */
|
||||
public static function preservedPath($logicalPath)
|
||||
{
|
||||
return in_array(strtolower(self::normalizeLogicalPath($logicalPath)), array('.htaccess', 'robots.txt'), true);
|
||||
}
|
||||
|
||||
public static function lockFile()
|
||||
{
|
||||
return BASEPATH . '/storage/installed.lock';
|
||||
|
||||
@@ -103,11 +103,11 @@
|
||||
|
||||
foreach (isset($requirements['extensions']) ? (array) $requirements['extensions'] : array() as $extension) { if (!extension_loaded((string) $extension)) { $errors[] = 'Отсутствует PHP-расширение ' . $extension; } }
|
||||
$conflicts = array();
|
||||
foreach ($manifest['files'] as $file) {
|
||||
foreach (self::managedEntries($manifest['files']) as $file) {
|
||||
if (!self::writableTarget(ReleaseState::resolve($file['path']))) { $errors[] = 'Нет прав записи: ' . $file['path']; }
|
||||
}
|
||||
|
||||
foreach ($manifest['deletions'] as $file) {
|
||||
foreach (self::managedEntries($manifest['deletions']) as $file) {
|
||||
$target = ReleaseState::resolve($file['path']);
|
||||
if (is_file($target) && !is_writable($target)) { $errors[] = 'Нет прав удаления: ' . $file['path']; }
|
||||
}
|
||||
@@ -118,7 +118,7 @@
|
||||
$required = max(10485760, (int) filesize(self::directory($job['id']) . '/patch.zip') * 4 + (int) ceil($databaseBytes * 1.25));
|
||||
$free = @disk_free_space(self::root());
|
||||
if ($free !== false && $free < $required) { $errors[] = 'Недостаточно свободного места для staging и отката'; }
|
||||
return array('ready' => !$errors && !$conflicts, 'errors' => $errors, 'conflicts' => $conflicts, 'files' => count($manifest['files']), 'deletions' => count($manifest['deletions']), 'migrations' => count($manifest['migrations']), 'required_bytes' => $required);
|
||||
return array('ready' => !$errors && !$conflicts, 'errors' => $errors, 'conflicts' => $conflicts, 'files' => count(self::managedEntries($manifest['files'])), 'deletions' => count(self::managedEntries($manifest['deletions'])), 'preserved' => self::preservedEntries($manifest), 'migrations' => count($manifest['migrations']), 'required_bytes' => $required);
|
||||
}
|
||||
|
||||
public static function rollback($id, $restoreDatabase = true)
|
||||
@@ -162,7 +162,7 @@
|
||||
@mkdir($directory, 0750, true);
|
||||
$job['journal'] = array();
|
||||
$paths = array();
|
||||
foreach (array_merge($job['manifest']['files'], $job['manifest']['deletions']) as $file) { $paths[$file['path']] = $file['path']; }
|
||||
foreach (self::managedEntries(array_merge($job['manifest']['files'], $job['manifest']['deletions'])) as $file) { $paths[$file['path']] = $file['path']; }
|
||||
$paths['storage/installed.lock'] = 'storage/installed.lock';
|
||||
foreach ($paths as $logical) {
|
||||
$actual = $logical === 'storage/installed.lock' ? ReleaseState::lockFile() : ReleaseState::resolve($logical);
|
||||
@@ -243,13 +243,13 @@
|
||||
self::assertMaintenanceAvailable($job['id']);
|
||||
self::writeMaintenance($job);
|
||||
try {
|
||||
foreach ($job['manifest']['files'] as $file) {
|
||||
foreach (self::managedEntries($job['manifest']['files']) as $file) {
|
||||
$source = self::directory($job['id']) . '/stage/payload/' . $file['path'];
|
||||
$target = ReleaseState::resolve($file['path']);
|
||||
self::replaceFile($source, $target, $file['sha256'], $file['mode']);
|
||||
}
|
||||
|
||||
foreach ($job['manifest']['deletions'] as $file) { $target = ReleaseState::resolve($file['path']); if (is_file($target) && !@unlink($target)) { throw new \RuntimeException('Не удалось удалить устаревший файл: ' . $file['path']); } }
|
||||
foreach (self::managedEntries($job['manifest']['deletions']) as $file) { $target = ReleaseState::resolve($file['path']); if (is_file($target) && !@unlink($target)) { throw new \RuntimeException('Не удалось удалить устаревший файл: ' . $file['path']); } }
|
||||
$migrations = isset($job['pending_migrations']) && is_array($job['pending_migrations'])
|
||||
? $job['pending_migrations']
|
||||
: $job['manifest']['migrations'];
|
||||
@@ -279,15 +279,31 @@
|
||||
{
|
||||
try {
|
||||
if (!ReleaseState::matches($job['manifest']['to'])) { throw new \RuntimeException('Реестр версии не подтвердил обновление'); }
|
||||
foreach ($job['manifest']['files'] as $file) { $target = ReleaseState::resolve($file['path']); if (!is_file($target) || !hash_equals($file['sha256'], hash_file('sha256', $target))) { throw new \RuntimeException('Контрольная проверка файла не пройдена: ' . $file['path']); } }
|
||||
foreach (self::managedEntries($job['manifest']['files']) as $file) { $target = ReleaseState::resolve($file['path']); if (!is_file($target) || !hash_equals($file['sha256'], hash_file('sha256', $target))) { throw new \RuntimeException('Контрольная проверка файла не пройдена: ' . $file['path']); } }
|
||||
self::verifyRuntime();
|
||||
self::removeMaintenance(); $job['status'] = 'completed'; $job['progress'] = 100; $job['message'] = 'Обновление установлено'; $job['recovery_token'] = ''; $job['updated_at'] = date(DATE_ATOM); self::save($job); return $job;
|
||||
$job['preserved_files'] = self::preservedEntries($job['manifest']);
|
||||
self::removeMaintenance(); $job['status'] = 'completed'; $job['progress'] = 100; $job['message'] = 'Обновление установлено' . ($job['preserved_files'] ? '. Сохранены файлы проекта: ' . implode(', ', $job['preserved_files']) : ''); $job['recovery_token'] = ''; $job['updated_at'] = date(DATE_ATOM); self::save($job); return $job;
|
||||
} catch (\Throwable $e) {
|
||||
$job['status'] = 'verification_failed'; $job['error'] = $e->getMessage(); $job['message'] = 'Контрольная проверка не пройдена, требуется откат'; $job['updated_at'] = date(DATE_ATOM); self::save($job); throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
protected static function withPreflight(array $job) { $job['preflight'] = self::preflight($job); return $job; }
|
||||
protected static function managedEntries(array $entries)
|
||||
{
|
||||
return array_values(array_filter($entries, function ($file) { return !ReleaseState::preservedPath($file['path']); }));
|
||||
}
|
||||
|
||||
protected static function preservedEntries(array $manifest)
|
||||
{
|
||||
$paths = array();
|
||||
foreach (array_merge($manifest['files'], $manifest['deletions']) as $file) {
|
||||
if (ReleaseState::preservedPath($file['path'])) { $paths[] = $file['path']; }
|
||||
}
|
||||
|
||||
return array_values(array_unique($paths));
|
||||
}
|
||||
|
||||
protected static function writableTarget($path) { $path = (string) $path; if (is_file($path)) { return is_writable($path) && is_writable(dirname($path)); } $directory = dirname($path); while (!is_dir($directory) && $directory !== dirname($directory)) { $directory = dirname($directory); } return is_dir($directory) && is_writable($directory); }
|
||||
public static function databaseRequirementError($databaseVersion, array $requirements)
|
||||
{
|
||||
@@ -350,7 +366,7 @@
|
||||
}
|
||||
|
||||
protected static function replaceFile($source, $target, $checksum, $mode) { if (!is_file($source) || !hash_equals($checksum, hash_file('sha256', $source))) { throw new \RuntimeException('Staging-файл повреждён'); } $directory = dirname($target); if (!is_dir($directory) && !@mkdir($directory, 0755, true) && !is_dir($directory)) { throw new \RuntimeException('Не удалось создать каталог обновления'); } $temporary = $target . '.update-' . bin2hex(random_bytes(4)); if (!copy($source, $temporary) || !@rename($temporary, $target)) { @unlink($temporary); throw new \RuntimeException('Не удалось заменить файл: ' . $target); } @chmod($target, $mode ?: 0644); if (function_exists('opcache_invalidate')) { @opcache_invalidate($target, true); } }
|
||||
protected static function restoreFiles(array $job) { foreach (array_reverse(isset($job['journal']) ? $job['journal'] : array()) as $entry) { if (!empty($entry['existed'])) { self::replaceFile($entry['backup'], $entry['actual'], hash_file('sha256', $entry['backup']), fileperms($entry['backup']) & 0777); } elseif (is_file($entry['actual'])) { @unlink($entry['actual']); } } }
|
||||
protected static function restoreFiles(array $job) { foreach (array_reverse(isset($job['journal']) ? $job['journal'] : array()) as $entry) { if (ReleaseState::preservedPath($entry['path'])) { continue; } if (!empty($entry['existed'])) { self::replaceFile($entry['backup'], $entry['actual'], hash_file('sha256', $entry['backup']), fileperms($entry['backup']) & 0777); } elseif (is_file($entry['actual'])) { @unlink($entry['actual']); } } }
|
||||
protected static function clearRuntimeCaches()
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
$currentMethod = $method;
|
||||
$currentBody = $body;
|
||||
$currentHeaders = $options['headers'];
|
||||
$redirectCookies = array();
|
||||
$started = microtime(true);
|
||||
$requestId = substr(hash('sha256', $method . '|' . $currentUrl . '|' . microtime(true)), 0, 24);
|
||||
self::observe('http.outbound.requesting', 'requesting', $requestId, array(
|
||||
@@ -139,6 +140,7 @@
|
||||
'redirects' => $redirect,
|
||||
'duration_ms' => (int) round((microtime(true) - $started) * 1000),
|
||||
), array('status' => (int) $response['status'], 'bytes' => (int) $response['bytes']), $options['source']);
|
||||
unset($response['set_cookies']);
|
||||
return $response;
|
||||
}
|
||||
|
||||
@@ -148,12 +150,26 @@
|
||||
|
||||
$nextUrl = self::redirectUrl($target['url'], (string) $response['headers']['location']);
|
||||
$nextTarget = self::resolve($nextUrl, $options);
|
||||
if (self::origin($target) !== self::origin($nextTarget)) {
|
||||
$sameOrigin = self::origin($target) === self::origin($nextTarget);
|
||||
if (!$sameOrigin && $options['same_origin_redirects']) {
|
||||
throw new \RuntimeException('Удалённый сервер перенаправил запрос на другой хост');
|
||||
}
|
||||
|
||||
if (!$sameOrigin) {
|
||||
$currentHeaders = self::withoutSensitiveHeaders($currentHeaders);
|
||||
$redirectCookies = array();
|
||||
} elseif ($options['redirect_cookies']) {
|
||||
foreach (self::redirectCookiePairs($response['set_cookies']) as $name => $value) {
|
||||
$redirectCookies[$name] = $value;
|
||||
}
|
||||
|
||||
if ($redirectCookies) {
|
||||
$currentHeaders = self::withCookieHeader($currentHeaders, $redirectCookies);
|
||||
}
|
||||
}
|
||||
|
||||
$currentUrl = $nextTarget['url'];
|
||||
if ($response['status'] === 303 || (($response['status'] === 301 || $response['status'] === 302) && $currentMethod === 'POST')) {
|
||||
if ($response['status'] === 303 || (($response['status'] === 301 || ($response['status'] === 302 && !$options['preserve_post_on_302'])) && $currentMethod === 'POST')) {
|
||||
$currentMethod = 'GET';
|
||||
$currentBody = null;
|
||||
}
|
||||
@@ -178,6 +194,7 @@
|
||||
$bytes = 0;
|
||||
$overflow = false;
|
||||
$responseHeaders = array();
|
||||
$setCookies = array();
|
||||
$headerOverflow = false;
|
||||
$file = null;
|
||||
if ($options['download_path'] !== '' && $method !== 'HEAD') {
|
||||
@@ -200,10 +217,11 @@
|
||||
CURLOPT_USERAGENT => $options['user_agent'],
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RESOLVE => array($target['host'] . ':' . $target['port'] . ':' . $resolveIp),
|
||||
CURLOPT_HEADERFUNCTION => function ($curl, $line) use (&$responseHeaders, &$headerOverflow, $options) {
|
||||
CURLOPT_HEADERFUNCTION => function ($curl, $line) use (&$responseHeaders, &$setCookies, &$headerOverflow, $options) {
|
||||
$length = strlen($line);
|
||||
if (stripos($line, 'HTTP/') === 0) {
|
||||
$responseHeaders = array();
|
||||
$setCookies = array();
|
||||
return $length;
|
||||
}
|
||||
|
||||
@@ -214,6 +232,7 @@
|
||||
|
||||
$name = strtolower(trim(substr($line, 0, $position)));
|
||||
$value = trim(substr($line, $position + 1));
|
||||
if ($name === 'set-cookie') { $setCookies[] = $value; }
|
||||
if ($name === 'content-length' && ctype_digit($value) && (int) $value > $options['max_bytes']) {
|
||||
$headerOverflow = true;
|
||||
return 0;
|
||||
@@ -266,7 +285,7 @@
|
||||
throw new \RuntimeException('Удалённый сервер изменил проверенный сетевой адрес');
|
||||
}
|
||||
|
||||
return array('status' => $status, 'headers' => $responseHeaders, 'body' => $buffer, 'bytes' => $bytes);
|
||||
return array('status' => $status, 'headers' => $responseHeaders, 'set_cookies' => $setCookies, 'body' => $buffer, 'bytes' => $bytes);
|
||||
}
|
||||
|
||||
protected static function resolve($url, array $options)
|
||||
@@ -443,6 +462,9 @@
|
||||
'source' => 'runtime',
|
||||
'download_path' => '',
|
||||
'allow_http_errors' => false,
|
||||
'preserve_post_on_302' => false,
|
||||
'same_origin_redirects' => false,
|
||||
'redirect_cookies' => false,
|
||||
'allowed_private_hosts' => array(),
|
||||
), $options);
|
||||
$options['allowed_schemes'] = array_values(array_intersect(array('http', 'https'), array_map('strtolower', (array) $options['allowed_schemes'])));
|
||||
@@ -453,6 +475,9 @@
|
||||
$options['max_bytes'] = max(1, min(104857600, (int) $options['max_bytes']));
|
||||
$options['download_path'] = isset($options['download_path']) ? (string) $options['download_path'] : '';
|
||||
$options['allow_http_errors'] = !empty($options['allow_http_errors']);
|
||||
$options['preserve_post_on_302'] = !empty($options['preserve_post_on_302']);
|
||||
$options['same_origin_redirects'] = !empty($options['same_origin_redirects']);
|
||||
$options['redirect_cookies'] = !empty($options['redirect_cookies']);
|
||||
$options['allowed_private_hosts'] = array_values(array_unique(array_filter(array_map(function ($host) {
|
||||
try { return self::asciiHost($host); } catch (\Throwable $e) { return ''; }
|
||||
}, (array) $options['allowed_private_hosts']))));
|
||||
@@ -527,6 +552,33 @@
|
||||
}));
|
||||
}
|
||||
|
||||
protected static function redirectCookiePairs(array $headers)
|
||||
{
|
||||
$pairs = array();
|
||||
foreach ($headers as $header) {
|
||||
$part = trim(strtok((string) $header, ';'));
|
||||
$position = strpos($part, '=');
|
||||
if ($position === false) { continue; }
|
||||
$name = trim(substr($part, 0, $position));
|
||||
$value = trim(substr($part, $position + 1));
|
||||
if (!preg_match('/^[A-Za-z0-9!#$%&\x27*+.^_`|~-]+$/D', $name) || preg_match('/[\r\n;]/', $value)) { continue; }
|
||||
$pairs[$name] = $value;
|
||||
}
|
||||
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
protected static function withCookieHeader(array $headers, array $cookies)
|
||||
{
|
||||
$headers = array_values(array_filter($headers, function ($header) {
|
||||
return !preg_match('/^cookie\s*:/i', (string) $header);
|
||||
}));
|
||||
$pairs = array();
|
||||
foreach ($cookies as $name => $value) { $pairs[] = $name . '=' . $value; }
|
||||
$headers[] = 'Cookie: ' . implode('; ', $pairs);
|
||||
return $headers;
|
||||
}
|
||||
|
||||
protected static function origin(array $target)
|
||||
{
|
||||
return $target['scheme'] . '://' . $target['host'] . ':' . $target['port'];
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file system/App/Content/Fields/RichEditorSettings.php
|
||||
| @author AVE.cms <support@ave-cms.ru>
|
||||
| @copyright 2007-2026 (c) AVE.cms
|
||||
| @link https://ave-cms.ru
|
||||
| @version 3.3
|
||||
*/
|
||||
|
||||
namespace App\Content\Fields;
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
class RichEditorSettings
|
||||
{
|
||||
public static function schema()
|
||||
{
|
||||
$schema = array(
|
||||
array('key' => 'rich_profile', 'type' => 'select', 'label' => 'Панель визуального редактора',
|
||||
'options' => array('full' => 'Полная', 'compact' => 'Компактная', 'product' => 'Описание товара', 'news' => 'Новость / статья'), 'default' => 'full'),
|
||||
array('key' => 'rich_headings', 'type' => 'select', 'label' => 'Заголовки в панели',
|
||||
'options' => array('profile' => 'По профилю', 'all' => 'H1–H6', 'no_h1' => 'H2–H6', 'none' => 'Без заголовков'), 'default' => 'profile'),
|
||||
array('key' => 'rich_snippets', 'type' => 'select', 'label' => 'Готовые фрагменты',
|
||||
'options' => array('all' => 'Все', 'info' => 'Информационные', 'product' => 'Товарные', 'none' => 'Не показывать'), 'default' => 'all'),
|
||||
);
|
||||
foreach (array('image' => 'изображения', 'table' => 'таблицы', 'source' => 'исходный код', 'align' => 'выравнивание', 'blocks' => 'перемещение блоков') as $key => $label) {
|
||||
$schema[] = array('key' => 'rich_hide_' . $key, 'type' => 'bool', 'label' => 'Скрыть в панели: ' . $label, 'default' => false);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
public static function options(array $settings)
|
||||
{
|
||||
$result = array();
|
||||
foreach (self::schema() as $desc) {
|
||||
$key = $desc['key'];
|
||||
$value = isset($settings[$key]) ? $settings[$key] : $desc['default'];
|
||||
if ($desc['type'] === 'select' && (!is_scalar($value) || !isset($desc['options'][$value]))) { $value = $desc['default']; }
|
||||
$result[substr($key, 5)] = $desc['type'] === 'bool' ? (bool) $value : (string) $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
public function settingsSchema()
|
||||
{
|
||||
return array(
|
||||
return array_merge(array(
|
||||
array(
|
||||
'key' => 'mode',
|
||||
'type' => 'select',
|
||||
@@ -80,7 +80,7 @@
|
||||
'label' => 'Высота редактора, px',
|
||||
'default' => 300,
|
||||
),
|
||||
);
|
||||
), \App\Content\Fields\RichEditorSettings::schema());
|
||||
}
|
||||
|
||||
public function renderEdit(FieldContext $ctx)
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
|
||||
public function settingsSchema()
|
||||
{
|
||||
return array(
|
||||
return array_merge(array(
|
||||
array('key' => 'editor', 'type' => 'select', 'label' => 'Редактор',
|
||||
'options' => array('rich', 'plain'), 'default' => 'rich'),
|
||||
array('key' => 'height', 'type' => 'int', 'label' => 'Высота редактора, px', 'default' => 300),
|
||||
);
|
||||
), \App\Content\Fields\RichEditorSettings::schema());
|
||||
}
|
||||
|
||||
public function renderEdit(FieldContext $ctx)
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
|
||||
public function settingsSchema()
|
||||
{
|
||||
return array(
|
||||
return array_merge(array(
|
||||
array('key' => 'height', 'type' => 'int', 'label' => 'Высота редактора, px', 'default' => 300),
|
||||
);
|
||||
), \App\Content\Fields\RichEditorSettings::schema());
|
||||
}
|
||||
|
||||
public function renderEdit(FieldContext $ctx)
|
||||
|
||||
@@ -16,5 +16,8 @@
|
||||
{% endif %}
|
||||
{% include '@system_auth/phone.twig' %}
|
||||
{% include '@system_auth/oauth.twig' %}
|
||||
{% if not password_login_enabled and not phone_providers and not oauth_providers %}
|
||||
<p role="status" data-auth-unavailable>Вход временно недоступен. Попробуйте позже.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,5 +24,8 @@
|
||||
{% if phone_registration_enabled and registration_phone_providers %}
|
||||
{% include '@system_auth/phone.twig' with {'phone_providers': registration_phone_providers, 'phone_auth_registration': true, 'return_url': auth_urls.profile} %}
|
||||
{% endif %}
|
||||
{% if not email_registration_enabled and not (phone_registration_enabled and registration_phone_providers) %}
|
||||
<p role="status" data-auth-unavailable>Регистрация сейчас недоступна.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
$params
|
||||
))->getValue();
|
||||
$pages = max(1, (int) ceil($total / $limit));
|
||||
if ($paginate) { PublicPageContext::recordPagination(PublicPageContext::documentId(), 'page', $pages); }
|
||||
$page = min($page, $pages);
|
||||
$sql = 'SELECT d.Id,d.document_title,d.document_alias,d.document_excerpt,d.document_published,d.document_count_view'
|
||||
. ' FROM %b d WHERE ' . $condition . ' ORDER BY d.document_published DESC,d.Id DESC'
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
$xml = self::declaration() . "\n<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";
|
||||
for ($part = 1; $part <= $parts; $part++) {
|
||||
$xml .= " <sitemap><loc>" . self::escape(rtrim(HOST, '/') . '/sitemap-' . $part . '.xml')
|
||||
. '</loc><lastmod>' . date('c') . "</lastmod></sitemap>\n";
|
||||
. "</loc></sitemap>\n";
|
||||
}
|
||||
|
||||
return $xml . '</sitemapindex>';
|
||||
@@ -69,7 +69,8 @@
|
||||
$frequencies = array('always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never');
|
||||
$xml = self::declaration() . "\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";
|
||||
if ($includeHome) {
|
||||
$xml .= self::url(rtrim(HOST, '/') . '/', time(), 'weekly', '0.8');
|
||||
$home = Repository::home();
|
||||
if ($home) { array_unshift($rows, $home); }
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
@@ -79,8 +80,8 @@
|
||||
: 'weekly';
|
||||
$changed = !empty($row['document_changed']) ? (int) $row['document_changed'] : (int) $row['document_published'];
|
||||
$xml .= self::url(
|
||||
rtrim(HOST, '/') . '/' . ltrim((string) $row['document_alias'], '/') . URL_SUFF,
|
||||
$changed > 0 ? $changed : time(),
|
||||
(int) $row['Id'] === 1 ? rtrim(HOST, '/') . '/' : rtrim(HOST, '/') . '/' . ltrim((string) $row['document_alias'], '/') . URL_SUFF,
|
||||
$changed,
|
||||
$frequency,
|
||||
(string) $row['document_sitemap_pr']
|
||||
);
|
||||
@@ -91,8 +92,9 @@
|
||||
|
||||
protected static function url($location, $timestamp, $frequency, $priority)
|
||||
{
|
||||
return ' <url><loc>' . self::escape($location) . '</loc><lastmod>' . date('c', $timestamp)
|
||||
. '</lastmod><changefreq>' . self::escape($frequency) . '</changefreq><priority>'
|
||||
return ' <url><loc>' . self::escape($location) . '</loc>'
|
||||
. ($timestamp > 0 ? '<lastmod>' . date('c', $timestamp) . '</lastmod>' : '')
|
||||
. '<changefreq>' . self::escape($frequency) . '</changefreq><priority>'
|
||||
. self::escape($priority) . "</priority></url>\n";
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,16 @@
|
||||
)->getAll() ?: array();
|
||||
}
|
||||
|
||||
protected static function baseSql($select)
|
||||
public static function home()
|
||||
{
|
||||
return DB::query(self::baseSql(
|
||||
'doc.Id, doc.document_alias, doc.document_published, doc.document_changed,'
|
||||
. ' doc.document_sitemap_freq, doc.document_sitemap_pr',
|
||||
true
|
||||
) . ' LIMIT 1')->getAssoc() ?: null;
|
||||
}
|
||||
|
||||
protected static function baseSql($select, $home = false)
|
||||
{
|
||||
$documents = ContentTables::table('documents');
|
||||
$rubrics = ContentTables::table('rubrics');
|
||||
@@ -57,7 +66,8 @@
|
||||
. " WHERE doc.document_status = '1' AND doc.document_deleted = '0'"
|
||||
. " AND doc.document_in_sitemap = 1"
|
||||
. ' AND ' . DocumentVisibility::publicSql('doc', 'rub')
|
||||
. " AND doc.Id != '1' AND doc.Id != '" . (int) PAGE_NOT_FOUND_ID . "'"
|
||||
. ($home ? ' AND doc.Id = 1' : ' AND doc.Id != 1')
|
||||
. ' AND doc.Id != ' . (defined('PAGE_NOT_FOUND_ID') ? (int) PAGE_NOT_FOUND_ID : (int) \App\Frontend\PublicSettings::get('page_not_found_id', 0))
|
||||
. " AND doc.document_meta_robots NOT LIKE '%noindex%'"
|
||||
. " AND (rub.rubric_template != '' OR EXISTS (SELECT 1 FROM `" . $templates . "` rt"
|
||||
. " WHERE rt.rubric_id = rub.Id AND rt.template != ''))"
|
||||
|
||||
@@ -21,6 +21,33 @@
|
||||
|
||||
class Html
|
||||
{
|
||||
/** Remove inline CSS from an HTML fragment, leaving other attributes and elements intact. */
|
||||
public static function removeInlineStyles($html)
|
||||
{
|
||||
$html = (string) $html;
|
||||
if (stripos($html, 'style') === false) { return array('html' => $html, 'removed' => 0); }
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
try {
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
if (!$dom->loadHTML('<?xml encoding="UTF-8"><html><body>' . $html . '</body></html>', LIBXML_NONET)) {
|
||||
throw new \InvalidArgumentException('Не удалось разобрать HTML описания');
|
||||
}
|
||||
|
||||
$nodes = (new \DOMXPath($dom))->query('//*[@style]');
|
||||
$count = $nodes->length;
|
||||
if (!$count) { return array('html' => $html, 'removed' => 0); }
|
||||
foreach ($nodes as $node) { $node->removeAttribute('style'); }
|
||||
$body = $dom->getElementsByTagName('body')->item(0);
|
||||
if (!$body) { throw new \InvalidArgumentException('Не удалось разобрать HTML описания'); }
|
||||
$result = '';
|
||||
foreach ($body->childNodes as $node) { $result .= $dom->saveHTML($node); }
|
||||
return array('html' => $result, 'removed' => $count);
|
||||
} finally {
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
}
|
||||
}
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
//
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file system/App/Helpers/PhpSyntax.php
|
||||
| @author AVE.cms <support@ave-cms.ru>
|
||||
| @copyright 2007-2026 (c) AVE.cms
|
||||
| @link https://ave-cms.ru
|
||||
| @version 3.3
|
||||
*/
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
class PhpSyntax
|
||||
{
|
||||
public static function check($source, $lineOffset = 0)
|
||||
{
|
||||
if (!function_exists('token_get_all') || !defined('TOKEN_PARSE')) {
|
||||
return array('ok' => false, 'checked' => false, 'message' => 'Встроенная проверка PHP недоступна на сервере.', 'output' => '', 'line' => 0);
|
||||
}
|
||||
|
||||
try {
|
||||
token_get_all((string) $source, TOKEN_PARSE);
|
||||
} catch (\ParseError $e) {
|
||||
$line = max(1, (int) $e->getLine() + (int) $lineOffset);
|
||||
return array('ok' => false, 'checked' => true, 'message' => 'В PHP-коде есть синтаксическая ошибка.', 'output' => $e->getMessage() . ' (строка ' . $line . ')', 'line' => $line);
|
||||
}
|
||||
|
||||
return array('ok' => true, 'checked' => true, 'message' => 'Синтаксис PHP без ошибок.', 'output' => '', 'line' => 0);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -17,5 +17,5 @@
|
||||
return array(
|
||||
'product' => 'AVE.cms',
|
||||
'version' => '3.3',
|
||||
'build' => '0.41',
|
||||
'build' => '0.42',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user