mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-09-10 02:01:52 +00:00
release: AVE.cms 3.3 build 0.41
This commit is contained in:
+39
-1
@@ -11,7 +11,45 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
Новые изменения будут добавлены после build 0.40.
|
||||
Новые изменения будут добавлены после build 0.41.
|
||||
|
||||
## 3.3 build 0.41 — 7 сентября 2026
|
||||
|
||||
### Контент и каталог
|
||||
|
||||
- Уточнены роли товарных полей и связи между разделами каталога, документами и
|
||||
нативными характеристиками; редакторы получили более понятные переходы к
|
||||
связанным настройкам.
|
||||
- Массовые и быстрые изменения документов используют единый безопасный путь
|
||||
записи и корректно обновляют производные данные и кеш.
|
||||
- Улучшены диагностика качества, выборки товаров и совместимость запросов с
|
||||
MariaDB 10.3.
|
||||
|
||||
### Публичный сайт и фиды
|
||||
|
||||
- Фиды корректно обновляются после изменения товара, устойчивее генерируются
|
||||
параллельно и учитывают видимость служебного контента.
|
||||
- Поиск и товарные выборки точнее обрабатывают доступность, роли полей и
|
||||
специальные условия показа.
|
||||
|
||||
### Панель управления
|
||||
|
||||
- Раздел 404 поддерживает безопасный редирект на произвольный локальный путь и
|
||||
выбор любого доступного публичного документа, а таблицы журнала адаптированы
|
||||
для мобильного экрана.
|
||||
- Исправлено мобильное отображение журнала событий; виджет заказов по статусам
|
||||
теперь использует тот же период 14 дней, что и общая статистика.
|
||||
- Дополнены переводы, подсказки и пользовательская справка для изменённых
|
||||
разделов.
|
||||
|
||||
### Надёжность
|
||||
|
||||
- Операции корзины, оформления заказа, оплаты и доставки получили дополнительные
|
||||
проверки целостности и регрессионные тесты.
|
||||
- Для build 0.41 подготовлены чистая core-сборка, компактный установщик,
|
||||
накопительное обновление с build 0.20 и обновлённый каталог модулей.
|
||||
- Проектные торговые расширения и ИИ-помощник не входят в публичную core-сборку
|
||||
и репозиторий модулей.
|
||||
|
||||
## 3.3 build 0.40 — 25 августа 2026
|
||||
|
||||
|
||||
+155
-121
@@ -2,35 +2,35 @@
|
||||
"format": "ave-core-package-v2",
|
||||
"product": "AVE.cms",
|
||||
"version": "3.3",
|
||||
"build": "0.40",
|
||||
"build": "0.41",
|
||||
"package": "core",
|
||||
"built_at": "2026-08-25T11:42:07+00:00",
|
||||
"built_at": "2026-09-07T04:07:54+00:00",
|
||||
"installed_modules": 0,
|
||||
"bundled_optional_modules": [],
|
||||
"optional_after_install": [
|
||||
"setup/"
|
||||
],
|
||||
"core_fingerprint": "045c60e55daab45dcf75d86b72e01dd6aed8ca7103efa4293dd1676f0be349ab",
|
||||
"core_fingerprint": "7896669cffd40910cf19bca24b6784de5653e80ca4feee3c182c309971f9dcb8",
|
||||
"files": {
|
||||
".gitignore": "20d4fb177d47da6944a7a1c2dbf5918fd62766881aa52b00901f9054b1db4b49",
|
||||
".htaccess": "71c06b7bc41b09c347a562ca72768acaa0ee1ef8388c749fc67956c793d8a826",
|
||||
"CHANGELOG.md": "4adedd967bb28883655dfb981ebcc7fdc1e32367a79efb4e4417e3fd70d4f04e",
|
||||
"CHANGELOG.md": "ae00742770c10c9ed32f028b7df22df1ce55627635569b38de6f2043e3bcd093",
|
||||
"README.md": "19b820f1b092780039aae011e133dc6a1e5f1cf213b435e0ff1962b592b9095b",
|
||||
"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": "e42cdc1157d8a55760278ab46d603316039fad0680eadf7c95824984f45f63ea",
|
||||
"adminx/Support/ModuleExtensions.php": "36de2286496c8327ee22d806f98a6bc9521a2d81cd1bb4182122c7925ed8d0a7",
|
||||
"adminx/Support/InterfaceSettings.php": "752bd345a6595604b3ef87bd93bd0259a76f144ed887c7ff443a4fe53651494d",
|
||||
"adminx/Support/ModuleExtensions.php": "e7925a287351ef338a3733551e36646cb914c3ce069bed5f3a0c24aec7596b9a",
|
||||
"adminx/Support/Notifications.php": "ab08805d571cd3d8a4b2a09fdc6ce96fb4c7b5ec75691322cf8a4e1a142112a3",
|
||||
"adminx/Support/PhpCode.php": "5cdd56c7deddd713dd06412b64da729c8a411aa99f46958fc29121055740cd07",
|
||||
"adminx/Support/PhpCode.php": "a9b53565414c9784919ca06ce6d3926f939ddbb42b097f62bb8111b8d235dd71",
|
||||
"adminx/Support/ReAuth.php": "f8c2146fb4b8f03b7d0cd2f3576e3f39fe2749823c5d6f1dca494268dcaba303",
|
||||
"adminx/Support/Roles.php": "6be335dd643317f77bd7c2db92f25895e601fe72818befd62f6aeb3ecac9fce7",
|
||||
"adminx/Support/RouteGuard.php": "e9e20c097c0db354851546585be72bfc71a5d14652871f8bd1160e845d4dab1b",
|
||||
"adminx/Support/SavedViews.php": "4548ca129f270184e61cb670e7f44a1c103099a6a6c2820bed1a11bc18f227c4",
|
||||
"adminx/Support/SectionHelp.php": "4ce72b8895ad4f813d3a49e56a895c20f2821ad7f546c87f6e891793592dee4c",
|
||||
"adminx/Support/SensitiveRoutes.php": "d14d809310b43fdb66ec141b0573ec1020aea3720d20269d39f201f411d9f9c5",
|
||||
"adminx/Support/SectionHelp.php": "91d58793f2d6db4d78fd8f45fd6602dc3a5bd67a97456befdba156afabfc365f",
|
||||
"adminx/Support/SensitiveRoutes.php": "3359bb50323e98433ff1e830e9c62645c5b346027dea869c58fc1dbc13e4d84b",
|
||||
"adminx/Support/SystemFeatures.php": "f18ada623710f8c3efc6588c037227f6074e75bad9a3820aeef60b4d8d4b20a4",
|
||||
"adminx/Support/SystemHealth.php": "286aee452e90f71ad64437bd39d6dd985d312eb85bbb711490d3a0b631a16aea",
|
||||
"adminx/Support/Twig/AdminConstantExpression.php": "44bb7d2d978e3c873d457ba1ca7b8021c6f5292e1a4f8b74e49348d01dc84b1d",
|
||||
@@ -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": "0883ec02682b3069765388c61e0d9f8262786599fbd8bec51701affbab585a5e",
|
||||
"adminx/assets/css/adminkit.css": "5cbcb6bf1e506d35afa6ffccbbc56385c8feb31a5eec52af5054c315335b71a2",
|
||||
"adminx/assets/fonts/NunitoSans-Bold.woff2": "2d48580b7e1be60fa94b8092ebe86471fe0adfe5ca75242a7af2042ac9da825f",
|
||||
"adminx/assets/fonts/NunitoSans-ExtraBold.woff2": "66612ee9d1cdc341c9eec8a43104bbf515e7cc52821558bfc7dfcf33c8cb0cc1",
|
||||
"adminx/assets/fonts/NunitoSans-Regular.woff2": "878adc25278872933f004783f0ebbe092cebb3f9161aa30cffb2b9a8a07db040",
|
||||
@@ -47,7 +47,7 @@
|
||||
"adminx/assets/fonts/OpenSans-Regular.woff2": "a26ed465673123933c5510730a7286f49016a40fa94062021d1dc4c9bfeddbd3",
|
||||
"adminx/assets/fonts/OpenSans-SemiBold.woff2": "7d963e1d2f61c11c2e8bd2eb528777b5a55569708d50a7ee7bd097d7fafe6da4",
|
||||
"adminx/assets/img/logo.svg": "685d9bf037b33fbcfc200947d5051c5325762f8ab8a29ac9e1e587076321e463",
|
||||
"adminx/assets/js/adminx.js": "34d38f1291372e5ce10d051d9cbc144f61020f833fce263f5041fdc6926c20d1",
|
||||
"adminx/assets/js/adminx.js": "0f2efdf3ba50652f1e979bfc546d562e38e44e13f2bfb7c42f666e8b5d944a17",
|
||||
"adminx/assets/js/bulk-actions.js": "d7ee80678e6703aff2418a4679ba8e5027e7220cb089848bcced6d9f100b3aae",
|
||||
"adminx/assets/js/command-palette.js": "f62f0bad5ddf2b6e813e7eda094d73df63dce09a6e581025971e8f10430e8d9f",
|
||||
"adminx/assets/js/editor-codemirror.js": "90a04c219e84f5391a40b0d1fc807ecfe4ca582a198bc202bdc5aa6dccec51fa",
|
||||
@@ -142,13 +142,13 @@
|
||||
"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": "bf4c543d4b32cf24df073318151ae6a7192132897ccbb81089e7c78398247462",
|
||||
"adminx/language/en/client.xml": "de712c66a939fcae607c087e0941dfbf824152a41be85aeae0a0406cc0ef3053",
|
||||
"adminx/index.php": "267b4eb5ee65f64e1388acaf6e64ce27608b4b405744ff4fed6eda5f656bc7fb",
|
||||
"adminx/language/en/client.xml": "7a6a03448ca15df594f38e3ccb63f11cabc9c3c5ce301022074a14490e3dbee5",
|
||||
"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": "b69308cbe26e7da2c10084ce7163e3aacb24cf35447a70a3474963ab0a29ff77",
|
||||
"adminx/language/ru/client.xml": "606dac2a0fec6fdb5fbd4ca8201d1fef5f6607556a27d249688646c3dc595fe4",
|
||||
"adminx/language/ru/common.xml": "d27d863bf82591dbe857317d97a888a23980c87529e2b4c1d4125a6c0ba20966",
|
||||
"adminx/language/ru/interface.xml": "03fa6351745d2e47c2b3407da65c8b7b048b8e862409dc9bb934e939b9439c75",
|
||||
"adminx/language/ru/runtime.xml": "3c411a18695af7a63dd0685a8351ac3a51695aea7d1fa2550fb24cc9e3e82be6",
|
||||
@@ -185,24 +185,37 @@
|
||||
"adminx/modules/Blocks/migrations/012_merge_visual_blocks.php": "5c9cd6efd66a027134621859da1059b2c403bacb732885d6b405699cbce8fc14",
|
||||
"adminx/modules/Blocks/module.php": "53eb434566d820814f5b25382200b75ed02b0067c066c8f8cb62bee13529b964",
|
||||
"adminx/modules/Blocks/view/index.twig": "39f9225b6a95c65549d0c861afb7db7879ca338c251290653dded6ab7cb715cc",
|
||||
"adminx/modules/Catalog/Controller.php": "dea3f7ac5cdb8bd1757e18f95bcbeb2dd433123536d6f0766bc584bb1c09fcbf",
|
||||
"adminx/modules/Catalog/Model.php": "d524730981ad388928cfc29208a821ed1d474bd01d6db1749fd37fdfb4e4958e",
|
||||
"adminx/modules/Catalog/assets/catalog-drawer.css": "b3e4f798d7bdf329aa91987ae5be435910ddccfeb8a32211059846ab9dc6b7c4",
|
||||
"adminx/modules/Catalog/Controller.php": "f9bf3bf72e15cf0b886157a9b421c4f6a2364b35a70464e7285259a151d51bbe",
|
||||
"adminx/modules/Catalog/EditorContextLinks.php": "0319a6f237aa3aa627aba1f44a3efe4c6de007b26a9e75ba0df6b3b2ccc5478e",
|
||||
"adminx/modules/Catalog/Model.php": "937cec97bcb67aad4ce4da40e9da2b1ad691de6969ae5b12bbec2118b997653d",
|
||||
"adminx/modules/Catalog/assets/catalog-drawer.css": "27af3ab3f1b3cfe6425467528350489a870388b34427a79b0b2c4e94af9f1cd6",
|
||||
"adminx/modules/Catalog/assets/catalog.css": "49e7a33fc444f8330806e1cc5cbb0d523b673c64faafd25dbf0fe33067643efd",
|
||||
"adminx/modules/Catalog/assets/catalog.js": "1fc4acc30b05fd702dee9ff62958141aeab7593ca38d6f6360567af7c3dc9742",
|
||||
"adminx/modules/Catalog/language/en/client.xml": "9b156a155a1e6f1a2024f4be5ab8d1051b1ec2adf4b9a0ede71afe11e6d4558f",
|
||||
"adminx/modules/Catalog/assets/catalog.js": "fe5b3f7df7c8c70348badc433dab5b91268a69bf081a2bc87e52dd253923fad4",
|
||||
"adminx/modules/Catalog/assets/context-links.css": "71a563572960db0574e99fe1c81bf1227e1851ab902afdac7ffeb3b5a0d8e877",
|
||||
"adminx/modules/Catalog/language/en/client.xml": "775af8a1a4deb887ef9671e64f97e9f43b69b045c3bbcf2a52c3ab184dfd4ec5",
|
||||
"adminx/modules/Catalog/language/en/context-links.xml": "1e465bcee8e3331ef7ea978b530a657bf1087ff1484d909354f0f8366ff99331",
|
||||
"adminx/modules/Catalog/language/en/interface.xml": "1007d3fb9158846dd36f519d13909502e16d3c0a9ca2c49dc24bec088c487ea7",
|
||||
"adminx/modules/Catalog/language/en/product-roles.xml": "054da578bcc51de37ed41222eb40f758f5cf5045d0ad97e6fc6e931af76886d8",
|
||||
"adminx/modules/Catalog/language/en/product-selection.xml": "8150677ed1b3f6aa192acdecb27958c69880c390768ac7e5a60ae37aa3f82c33",
|
||||
"adminx/modules/Catalog/language/en/runtime.xml": "f30a0b64665a64de1682251402c720bc9e526138f682c5dd2e9fbf3ffb87f2b0",
|
||||
"adminx/modules/Catalog/language/ru/client.xml": "a4747370c4510ad0a56344ce3ee79d460585363253e983053b6743e613132a51",
|
||||
"adminx/modules/Catalog/language/ru/client.xml": "3091a480bff89529fbe28efb55aa87b59944f7605f38f614d71780103cbe63e0",
|
||||
"adminx/modules/Catalog/language/ru/context-links.xml": "33a0cd281d0586e85ac6144933767067540f62919df57c3dbf41c5bdfb439ec0",
|
||||
"adminx/modules/Catalog/language/ru/interface.xml": "15d8de452eef296375ba40d557ce15ac0363881a7b763b7111c8cc221e9861ff",
|
||||
"adminx/modules/Catalog/language/ru/product-roles.xml": "9f80012acbb4d98ae26d88edb484ee6f446748a41b6549fcebdbf08d85af5cf2",
|
||||
"adminx/modules/Catalog/language/ru/product-selection.xml": "42773ca81d5f35780b8a791868bea289d2d04f0e9eba579cae23094406ff1f5e",
|
||||
"adminx/modules/Catalog/language/ru/runtime.xml": "fe59945f4ceb2bf2d85699623369e7e5889613b8cecd8551a390c78023531d14",
|
||||
"adminx/modules/Catalog/migrations/005_add_catalog_purpose.sql": "006ff237e25042a4fa9b9f74d5079741eeee1aa52db0cd996ea4c8b817e6700f",
|
||||
"adminx/modules/Catalog/module.php": "c36086c3162d1eba433af05c879dcfb9cff830ee3c53da38705a72cc19b3777b",
|
||||
"adminx/modules/Catalog/view/edit.twig": "13bd9ebabdc8bb470a6c8ceb813b122d33da5c5604a23bb7ad3a840cc061cde2",
|
||||
"adminx/modules/Catalog/migrations/006_add_item_sources.php": "97a023140d1380ef2ef6a2ab1dd1a86b0901689fa2f1ccdf0fc76608dd18e546",
|
||||
"adminx/modules/Catalog/module.php": "f393b86b20c962f5262840381a397533e34e4c7f5b8bb786ae9aaa6de70f5d05",
|
||||
"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",
|
||||
"adminx/modules/Catalog/view/_product-selection-preview.twig": "9942891a9991a28f99072ef66ea7a8910ff3548972c2e601510e5abda250fba4",
|
||||
"adminx/modules/Catalog/view/edit.twig": "776e68b63576fc389487d4f03467df6a259f3903d2bd2b59ac7017e18b8b5cac",
|
||||
"adminx/modules/Catalog/view/index.twig": "754c7c4dc81176e8c99b3d9e8c1fda2f62f03df341c3df1f7393f72d43c6f8e2",
|
||||
"adminx/modules/Console/Controller.php": "af0309a50d7f4366279be8ea12959cf7718a3c68b895a65ec5846554fd6a4857",
|
||||
"adminx/modules/Console/Model.php": "53aad480b8d4e5d68c4e9345d219ea5cbff6d04eee7ce4aa15deab6f00dac1cc",
|
||||
"adminx/modules/Console/Runner.php": "bd65deeeaea97f4a7ee47826148775e9488a2edac80f5f8640ada6ac96f6b26b",
|
||||
"adminx/modules/Console/Runner.php": "6c8c216003d8228b091683acaae99791f612a93d4150542c2ff52c67dd029a33",
|
||||
"adminx/modules/Console/assets/console.css": "e2645bf876bfeafc15e6f4e5ad9889adb773aea35ddc42ff151d8ed1f8866eef",
|
||||
"adminx/modules/Console/assets/console.js": "f0f2cec892c4b5cdeb11730bf42e9cf68ff779974338086cdf92618ebcacdb10",
|
||||
"adminx/modules/Console/language/en/client.xml": "02bef78a30b83340eb8968297639b6283dc642236e853bef413e5b7d2ef70315",
|
||||
@@ -212,12 +225,12 @@
|
||||
"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": "cb95fd418b5f7970dea53d5bcdd0478bb0560c8b2688a95d5492ab053f324de8",
|
||||
"adminx/modules/Console/view/index.twig": "c64fe9632db751357ed98cf2ddca524001e71200297e1b50f0d5c827483933af",
|
||||
"adminx/modules/Customers/Controller.php": "8f64a15c6bb87eb6ef3535bac2c5364a3bb64ad6d1575eb0ae82c56a0bd04462",
|
||||
"adminx/modules/Customers/CustomerCenter.php": "02eca82e2d1e2d7998fe6203458ee535421913197e76fb28a6e674231fb989fd",
|
||||
"adminx/modules/Customers/GlobalSearchProvider.php": "6be85ba385ade05cef1167a6acf146a7e078d176b72db773e0d26d2ab8ea65de",
|
||||
"adminx/modules/Customers/Model.php": "fb84df79d1ef9b403f16e44100325d26db5c144b6cbc6a69e3425a9a3f29a043",
|
||||
"adminx/modules/Console/module.php": "c6c883cc51ee1a27460f71eca88edd44726f2be493934b84adeee1b7d8ccf8dc",
|
||||
"adminx/modules/Console/view/index.twig": "54f877d5feea19bc4f4b618eea7cf7523a2266bb2e3909f3fb723edd16d4a326",
|
||||
"adminx/modules/Customers/Controller.php": "0d9b8dfaf794c378ef8da5ddf2084e461b54ce0ed7eb490bda62c30574f05763",
|
||||
"adminx/modules/Customers/CustomerCenter.php": "af34954e6330bc9036c04fb56b439b143f591e95b83124d95a034d87d6870bdf",
|
||||
"adminx/modules/Customers/GlobalSearchProvider.php": "d76002b2baefa8395d2da3e603b547938b9ead8e692775562cc7d7a65e9601fa",
|
||||
"adminx/modules/Customers/Model.php": "f9ec0574ae68a3cdc120cd5be9444067e205d650608f64e071f199190c876792",
|
||||
"adminx/modules/Customers/assets/customers.css": "2542f8ba075ce20f4fa48a966eda5de82b5e574a17f5bebc88727b608eb47bb8",
|
||||
"adminx/modules/Customers/assets/customers.js": "7ef3b863eff78c83bce7d78a7cdd67fc715d26fe8d9d9239af8d43f27bbb45c9",
|
||||
"adminx/modules/Customers/language/en/center.xml": "0ebf643e8a2e00fdf07a290adfd108d0d7e53781d15c37f44d55564c7476b738",
|
||||
@@ -235,10 +248,10 @@
|
||||
"adminx/modules/Customers/migrations/005_checkout_registration.sql": "4e5ac6d4d5868f9e1c91b896fc03ad0982d5716287101e721a3820d21abe1744",
|
||||
"adminx/modules/Customers/migrations/006_phone_identity.php": "97e8aa457fd8f8ade781a03a98bdc7ee03cac5734782f9e72fb0b76bdbd2cdbb",
|
||||
"adminx/modules/Customers/migrations/007_customer_center.php": "7adde1ce2b92f24b7f1d1d483aca4304bad918d06ae2f213226eca96dcff344e",
|
||||
"adminx/modules/Customers/module.php": "ef1055dbaf8d23748b492f0fcd8a7ce184b534d72714744d684875735f4b7f73",
|
||||
"adminx/modules/Customers/module.php": "85424c3ed22c4def6b79af1454ce79ceae4987fd0212b53451e4d95f43af354c",
|
||||
"adminx/modules/Customers/view/customer-center-detail.twig": "2239602955041c4eebe704c70e835fd45272f721abd28bf254904901bd77a4eb",
|
||||
"adminx/modules/Customers/view/customer-center.twig": "223e657861275fbfe482ce624a6216a91e8df893cd1c5fe58a2999606468addf",
|
||||
"adminx/modules/Customers/view/index.twig": "61b25d07799e50b3a06c5ab91b4895dd685b54a29085f896f9c51aebbfdb3d7b",
|
||||
"adminx/modules/Customers/view/index.twig": "20868f4edf7f9ca7770a733d4593eb56ffb225199940212d60aa709f2c760070",
|
||||
"adminx/modules/Dashboard/Controller.php": "7e6ad0610df7581bb010cee574d8d093c4737af439bd2b0763142f50ddee1c54",
|
||||
"adminx/modules/Dashboard/Widgets.php": "56fc7ba7c1f9e30d7eae8c61524b6441b1c706a2cd56b1db883bb8bff7186eed",
|
||||
"adminx/modules/Dashboard/assets/dashboard.css": "8e28b56cac2e55d9b57304ea6e265fd94a52400674c082fbdd78f1eed1872424",
|
||||
@@ -283,23 +296,23 @@
|
||||
"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": "6ca33e4ee73f83e043b13363bddb391199a1da902e74dfa7709a10ac32be574b",
|
||||
"adminx/modules/Documents/BulkEditor.php": "24a2c33a50e5025e4f8a15426bf8d4f6ac21e6574ef636f30fb5c6fad0249133",
|
||||
"adminx/modules/Documents/Controller.php": "283aec04707c7b7fdd01e2ba69c1961b925f259655e9a455889bdd5dfc66f4fe",
|
||||
"adminx/modules/Documents/DocumentHookRunner.php": "7e0cd24ed5791b6d5010e4a05351b709be09b6df1e700a3ff06685b9078b2b1d",
|
||||
"adminx/modules/Documents/DocumentHookRunner.php": "eb915de91391858987386925da37d943318fea1f46893de1b3fcc0957ce43e07",
|
||||
"adminx/modules/Documents/DocumentMediaReplacement.php": "e97d960c852162e655a4389cc5f477f0178597da50e733b00bdf5bcab076c65d",
|
||||
"adminx/modules/Documents/EditConflict.php": "ebc3ebb92c3fc3ccf3909d2ff517014fc888ee20576a520b4979f68bde632b97",
|
||||
"adminx/modules/Documents/Model.php": "5287c9adc603c942d1ffae135df360717ee18b1d1b2b9c29bb35d183b477b54a",
|
||||
"adminx/modules/Documents/Revisions.php": "8caef88f7456000b001757cff13393ae36991b33c4c3070c27f3eaa4e8bf6492",
|
||||
"adminx/modules/Documents/assets/documents.css": "396d4cc7625b3331a41d94f040ac3fabafd9c0b993181b490ceea8f365e1a5ad",
|
||||
"adminx/modules/Documents/assets/documents.js": "dce151189d8244be41fb6b1bbb85a26241c788d91111ccb5314c361150024c3d",
|
||||
"adminx/modules/Documents/Model.php": "ff03bcfe54618d1b83a6780c57cb86816ea25879351f1beb4e847cbb1ae4c9a5",
|
||||
"adminx/modules/Documents/Revisions.php": "f17f0f7ebb4fc3ccc44695e4a1ca7093dfa3f4fe3f9c895e17fa8631567769fe",
|
||||
"adminx/modules/Documents/assets/documents.css": "88c0911854e90ed0d71b005ec95560520ba8db2f44c8e3a457c074a3b5aa323d",
|
||||
"adminx/modules/Documents/assets/documents.js": "9d0abe758f84ed6fe4f262c335628ffc07678a012b579aacb5787a250cac8065",
|
||||
"adminx/modules/Documents/assets/redirects.js": "95ca395c6b4fd3814da37450c27da351e75f509e26421c975dd8e0ee5f657efd",
|
||||
"adminx/modules/Documents/language/en/bulk-editor.xml": "b7af92bd151c7899a56cc461f4a09edaa842213cb6e6b4b4363af31aee228e50",
|
||||
"adminx/modules/Documents/language/en/client.xml": "9ccb6f2aa9a83862abff2c1fa1c6b10760600c3202293fd7e42073c9cfb5fae4",
|
||||
"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/client.xml": "3e14cb51de149aca6c3f309ffb3973a47f80f638805325ff0a5cc23dd1025302",
|
||||
"adminx/modules/Documents/language/ru/client.xml": "e265a5a3c8f6bf973dbde8f3f8970ab9cc88557cd71df6449f3b4a93dbbaffec",
|
||||
"adminx/modules/Documents/language/ru/interface.xml": "f8d60edba8ac5b6d0eb6deaa3b9b89733cd0b8d3eb951c105a9874ea6769c305",
|
||||
"adminx/modules/Documents/language/ru/runtime.xml": "6c3e9a2dbf862bef5a76ff0bb600084fcd9d5d1a7f0cbfd02377242b74fcd759",
|
||||
"adminx/modules/Documents/language/ru/zz-interface.xml": "2d819845ff2ae5246203374a685f8a5ad881ba9067166161eb96d5c9271026dd",
|
||||
@@ -319,18 +332,19 @@
|
||||
"adminx/modules/Documents/migrations/014_document_relation_edges.sql": "3491ade2987535f8db1b3b00ff27f5a2062b02a3a1cb2e707d2752ff446b566c",
|
||||
"adminx/modules/Documents/migrations/015_document_creation_presets.sql": "a123f8867ab6f6346002d08317acac37062c2792913ad14102c81812d45571bd",
|
||||
"adminx/modules/Documents/migrations/016_reconcile_document_content_columns.php": "06671bb26bf29479dce24888fb3712c0d20e2aac5b3d70196604ce4097c875fc",
|
||||
"adminx/modules/Documents/module.php": "dc7642c19f3fabda2ce0577dbf2002de389ee2f5616c26a6f6cdd88c639f91c9",
|
||||
"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/view/api.twig": "a035a2d566298587ce8530c98915e80e4e7f9b0f41a60e0e55d4547b996a6c40",
|
||||
"adminx/modules/Documents/view/bulk-editor.twig": "27d9baca8cf591dc2ee81b26fda3efc9017c6809c1d23168a749d01bddfebdfa",
|
||||
"adminx/modules/Documents/view/edit.twig": "94eb351309fb3d6fdab844e253ab2d7e3b03e1d846b35da19b7e498733ad27a2",
|
||||
"adminx/modules/Documents/view/index.twig": "8bb67e66a55007fda1374d83a3bc0b19fdc518a47d191a04913772d8c8ce132c",
|
||||
"adminx/modules/Documents/view/bulk-editor.twig": "270d6a2e52aa6aa8efed73b4b5c311e8e8f7d8e0cf82ef1e510e7e6fb4d3f029",
|
||||
"adminx/modules/Documents/view/edit.twig": "cb4b983e868912bd3d072e3cbc6b57f21616e7d35cabacec7dca8b9e40ce09ef",
|
||||
"adminx/modules/Documents/view/index.twig": "ccf2bd0d384091f8a98ead4e93a8ad389a515176d17e5b5ee661e7e3ea0908de",
|
||||
"adminx/modules/Documents/view/redirects.twig": "4a4903edc81e4770220852d9be9f02a983ddde6f829a4360ea01d3b68456582b",
|
||||
"adminx/modules/Documents/view/views.twig": "821f19085ba3997b6ef8f005e838f6fd449f0b32bce0d2804271311856cce8f0",
|
||||
"adminx/modules/Events/Controller.php": "c57fc25e815c47c14626371d044079e6711f0a4bce2004d2b9256db62f0d0e85",
|
||||
"adminx/modules/Events/Model.php": "a3a09370aa47766c7c4e70ec002af98a13bfcad3151b9ecb96a6ffb6cddf7ae2",
|
||||
"adminx/modules/Events/assets/events.css": "91457d384056a6ea96bdc6bf012c033c19e0cb21401460ae9b0f9398493258fc",
|
||||
"adminx/modules/Events/assets/events.js": "d4c6cc636473ecd81bd1551c37df5b96434e7e35fbb251f93b3647a25f52d336",
|
||||
"adminx/modules/Events/assets/events.css": "775a6dd0099f2846319d5d7d8f742d2a637e59d9b0cac86c0203f41b9684f63b",
|
||||
"adminx/modules/Events/assets/events.js": "a9b8fe56cced63eb6778c883e1339a8e3615d7b5a717bd44528f15f43e1716e5",
|
||||
"adminx/modules/Events/language/en/client.xml": "ea0be743665d923eed666b97590a63ad078caa2d439f18ccf78c22f040ab9e24",
|
||||
"adminx/modules/Events/language/en/interface.xml": "8ba72b194174cff23b1678a34930d0fcc8fbb51465a6e0457cd503c2d0119dc1",
|
||||
"adminx/modules/Events/language/en/runtime.xml": "e6d14b92215da7d85bc8460f2e42410c0005f51c1245533b9d57c62fc211a457",
|
||||
@@ -338,8 +352,8 @@
|
||||
"adminx/modules/Events/language/ru/interface.xml": "701b5862e628fd5e4d64ac4df5983d1f40b8aa1b2fc67b4eccde0a1143338e27",
|
||||
"adminx/modules/Events/language/ru/runtime.xml": "339a6e6c117e3e14e871acd38ab398ec4f54f0bee3785a9c3f5d90048899a9ec",
|
||||
"adminx/modules/Events/migrations/001_materialize_event_logs.sql": "59500ca428021c9dce543ff9a7711e2b729f7e9566ec6277be09939e78b9ddd0",
|
||||
"adminx/modules/Events/module.php": "d506c314dfc71abe7216ec80243f46549a833a6cc3371333d1e7a6435312c46b",
|
||||
"adminx/modules/Events/view/index.twig": "7057cee5abe6ace177509c54c2d1736ab88b96f9f17f6db2bec0553b4eb1ac5f",
|
||||
"adminx/modules/Events/module.php": "6958762304e3d31b51fb68a670b87509a23242cd2c4c9f9cfe6ec6cccabde5d8",
|
||||
"adminx/modules/Events/view/index.twig": "077d09060c5f1ff8abefb4bfd1532f0bee28fed275bd5aa7b06dc7077a142286",
|
||||
"adminx/modules/GlobalSearch/Controller.php": "6a109bbe2ca833df90138af056cb6cc32c313677d3bda071b3638448ca7e33fa",
|
||||
"adminx/modules/GlobalSearch/Model.php": "b81c82cc43c6f90e411645b3f934b42d7457eaa7eea1f5afd7c8efa362f82d97",
|
||||
"adminx/modules/GlobalSearch/language/en/interface.xml": "a025b7a82786864c5d74bea07880e3c7b3f0cd7e52d8142ea5be8db346ae1a3e",
|
||||
@@ -351,11 +365,11 @@
|
||||
"adminx/modules/Groups/Setup.php": "b7140a87474e343455fa81809f8f8f2dfe7e1ee3640c5f024a1bb06ebd783865",
|
||||
"adminx/modules/Groups/assets/groups.css": "403f4555599cf1637ec2b5a2e6248a1fdde7b1bf599646c1c103e816fc3fa85f",
|
||||
"adminx/modules/Groups/assets/groups.js": "235a058028abd1d9559b915f8a36da9333bd7b381b0113f9c98b28305c593188",
|
||||
"adminx/modules/Groups/language/en/client.xml": "dbceb0c80a844d05f3c370dbc495a71142818c0c9e3ff94c33586bea89f07831",
|
||||
"adminx/modules/Groups/language/en/client.xml": "e633c67dddf6f8d1d07d8f4bff18d0b0a527dc074073a5b79d033cf10f24aa98",
|
||||
"adminx/modules/Groups/language/en/interface.xml": "ca7dfd681c7f1e8580914cafdfd259d5d3be84868e7ee97288fa36ab3f785de7",
|
||||
"adminx/modules/Groups/language/en/runtime.xml": "7ab07b963b2b29f046f6c363f04c1104de6d5914f5e70b6b8cd42f5770847168",
|
||||
"adminx/modules/Groups/language/en/simulator.xml": "de91b32f039df2dd87ecd47e27725a6dffe018867a53a72df2d21e3394e63146",
|
||||
"adminx/modules/Groups/language/ru/client.xml": "320b9b55d6eaf6f4d3643f3fa4675544a779540ec34b3b94e03deac6f57579b0",
|
||||
"adminx/modules/Groups/language/ru/client.xml": "f19cd9563c2f5cc46bab9468cce38e316d17d670e9f9e98817f593696eb3f8b8",
|
||||
"adminx/modules/Groups/language/ru/interface.xml": "6f9da63ae65ecc8890ea16dbe7e879bdf5520d5f74c0a6204285b19d580f15e3",
|
||||
"adminx/modules/Groups/language/ru/runtime.xml": "eea7d7d2b88befb79b50d00c89dc63d0bdfa95883483d9606ce0605c03c92402",
|
||||
"adminx/modules/Groups/language/ru/simulator.xml": "14d8db8fa0e8a71206ad93817fbf2fac71cc023003977e03ffb2b7e581d16659",
|
||||
@@ -398,7 +412,7 @@
|
||||
"adminx/modules/Modules/Controller.php": "de3c800b32a05ab41dac31d4964242fb0635aef6ef32f69a332509159b8e755d",
|
||||
"adminx/modules/Modules/ModuleArchiveInstaller.php": "22f015a6695de9f4fea12a580adfb76837e59ee4e9de92c153782c5a40a97560",
|
||||
"adminx/modules/Modules/ModuleRepository.php": "29ced1e9d5c36dab4f0018c10fab1b80282b8d225805bf13f075e148016c45f9",
|
||||
"adminx/modules/Modules/NativeModuleAdapter.php": "b753875997fcabd019e4dc7420f5b43f3b75b6c673c24ee98462e6809f00b665",
|
||||
"adminx/modules/Modules/NativeModuleAdapter.php": "0bd4f968dc72afcf6342628901f0ee132127f5d4f945a5aea57284a133486248",
|
||||
"adminx/modules/Modules/NotificationProvider.php": "55571216fe84feca3484b06937f988748d00ec9bb68b45d0e85d16a3c3836b25",
|
||||
"adminx/modules/Modules/assets/modules.css": "ba3f1f6721dd1e529903254abe8fd24e20873b6c00e0a2d2d2d7bcc65170d884",
|
||||
"adminx/modules/Modules/assets/modules.js": "7ed1e03f0114ff942b4db057ca57586c2cc94ed55a2d10f5ca5dafa908acabb5",
|
||||
@@ -408,17 +422,17 @@
|
||||
"adminx/modules/Modules/language/ru/client.xml": "176d4ebdae73e7533082676c2e56199d47a43336ace47fbeb31392adad26d212",
|
||||
"adminx/modules/Modules/language/ru/interface.xml": "ee33c8914bb44478a8801ae23522954c860a6ecb007b848bb956fc07d5be4ce7",
|
||||
"adminx/modules/Modules/language/ru/runtime.xml": "271e31a8ae1586ed045861933a6db150427c78be7560942185633f35cf90686a",
|
||||
"adminx/modules/Modules/module.php": "aad8c5f3a11dec21eb14a5d9a83e30c88cb4ea1206e78bc758804d015a828456",
|
||||
"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/Revisions.php": "165a2e2d8ad218c1437fffbed9297b7be25d0ae6567f0d72aa78da872cefa1b8",
|
||||
"adminx/modules/Navigation/assets/navigation.css": "e7ad029b253b19546ed2c0eb2a3527dbb3b03a56db13c6f3089c3da9c12933ca",
|
||||
"adminx/modules/Navigation/assets/navigation.js": "72d68867419fab1b3e7e822a42a7fbcd866ccdbef8fa3051a245e23f23f5ec30",
|
||||
"adminx/modules/Navigation/language/en/client.xml": "9a0ad0248941cd6377ada049666387581bea0dd76353d34779b4c1343b74ba50",
|
||||
"adminx/modules/Navigation/language/en/client.xml": "251de8adc3c264bd01b816ba8f9ff18d9266ccc061c0a996755a039782c4141b",
|
||||
"adminx/modules/Navigation/language/en/interface.xml": "2a6bfcf0fdfc36fd87d666842008635dc12b73c3038fa486e45a4d117956a53e",
|
||||
"adminx/modules/Navigation/language/en/runtime.xml": "341b984ede453a135b6e155fd29c08d9d5a2ab16e62046379f982d9a56dc4a81",
|
||||
"adminx/modules/Navigation/language/ru/client.xml": "46f18714dea8e751f33baa049f3684d824109e49e23b1b325ef37c8a043f1c7e",
|
||||
"adminx/modules/Navigation/language/ru/client.xml": "0cefd219fe8f8f3726a07e8e82c5d1a4fd07244f79c1b6d2ce16c12ed7962f9d",
|
||||
"adminx/modules/Navigation/language/ru/interface.xml": "6b460827a6f170057ca6c4310d747a73f67d8468eb98c12cf4238a54ab6c6918",
|
||||
"adminx/modules/Navigation/language/ru/runtime.xml": "45d994c39fa7fa092525ed4a61d6775008f3993f4cdcdd615d61e5f15a749cff",
|
||||
"adminx/modules/Navigation/migrations/001_create_navigation_tables.sql": "43c937b8d7d7f5b70569c591db58ee8592386b988e8cf148fae723b1f68b3012",
|
||||
@@ -432,14 +446,14 @@
|
||||
"adminx/modules/PublicSite/PublicViewTemplates.php": "d1d7f7bff22703500f17807d5fe6b02d42a2fcc4cb6d7e7224fe0a47f7dc1ffa",
|
||||
"adminx/modules/PublicSite/TemplateMap.php": "22bc2649771e972ea23f2387c6bd9abb02ea3ee89054f38e5492e9401b400959",
|
||||
"adminx/modules/PublicSite/assets/presentations.css": "fddb0b4300129f7d7c33853b7e31bce131eb6f867e577443429dcafac8cc67d4",
|
||||
"adminx/modules/PublicSite/assets/presentations.js": "3a74e7c7ae3269b98eca52d2d61db1f4f21871c6f4a5425cc7bab58d24efd963",
|
||||
"adminx/modules/PublicSite/assets/presentations.js": "0e9f9c392360773f1220cd417d303e23a60e643092d60ea46563029543f30b0f",
|
||||
"adminx/modules/PublicSite/assets/public-site.css": "b61c2c699e85ef1ae36e306d83d7d9508bde41702f06037a7ec8c6e69744cb67",
|
||||
"adminx/modules/PublicSite/language/en/client.xml": "5a93fd6babc20670e54795211cf44ed155417aa933e5d4a50be21aebfa7bd510",
|
||||
"adminx/modules/PublicSite/language/en/interface.xml": "e5e868af542e9fe6c981dff18e31a0050d2d7ae2b8b217e7b71bd98b496b5f5b",
|
||||
"adminx/modules/PublicSite/language/ru/client.xml": "f49060544db65a516b2a643420871571b4da5911ae800075c01ae35e55a5d639",
|
||||
"adminx/modules/PublicSite/language/ru/interface.xml": "67a7d1a651d8dd4ca3edc6c281d68c15146a8e84cb18833f9f4041fbbd57a619",
|
||||
"adminx/modules/PublicSite/migrations/001_create_presentations.sql": "023d97f077af57107549dd9e420f5ed55a278fb8318caeb8d86f2dc7750dbfbb",
|
||||
"adminx/modules/PublicSite/module.php": "46694327d2ac6315da3f9ae02b87a038cfea4da4314ef30c1bc451efe6b279b7",
|
||||
"adminx/modules/PublicSite/module.php": "607b8329ba3cf5dabce53fcb3b0ac56e6cd3f1412a41214d1e410c4b6a99dc5d",
|
||||
"adminx/modules/PublicSite/view/_placement_pagination.twig": "b10413bdba07695500803625486b56ae5283c5a0a6dff83c668c8f93d1eab182",
|
||||
"adminx/modules/PublicSite/view/_tabs.twig": "92db2936b265d3b07056ea3aea8ab9dc30e05849b6b3f7d04bf86231fc273643",
|
||||
"adminx/modules/PublicSite/view/diagnostics.twig": "242682325bd7c99c75f9694d0e7ff6217124bf093364c027d13f609db660f4b7",
|
||||
@@ -453,10 +467,8 @@
|
||||
"adminx/modules/Requests/Revisions.php": "78483f6667f25c050cbb2d224baa8ccf8d977b30eb84538c3cfb18c7af764bdc",
|
||||
"adminx/modules/Requests/assets/requests.css": "bf29e90ce2211bbd4ce3b61b29bd3e60d5e5d8f75c9e4ffd99402cb000f27685",
|
||||
"adminx/modules/Requests/assets/requests.js": "da981f0941d3008ceb4042540ecbfde1385431395f2f56eb94836b4139feaa0e",
|
||||
"adminx/modules/Requests/language/en/client.xml": "544406edaf2b568062e697f7264b5c5fbe3fc45c27e019615601e831612db0bc",
|
||||
"adminx/modules/Requests/language/en/interface.xml": "766190d3f850f6fc414a4646d786f79f394ee8f03524b3a2ae820fbe792d4602",
|
||||
"adminx/modules/Requests/language/en/runtime.xml": "8f618793f78ea2941557d9eb9c253f8b12ac6b60b76e7158801461b30dbfea28",
|
||||
"adminx/modules/Requests/language/ru/client.xml": "9dc1d047bd12190cce6197b20e4bc4b3a1597c387873ffd99d59710d7bbe60d9",
|
||||
"adminx/modules/Requests/language/ru/interface.xml": "2a5e72c60d70a0a7e7243dba02bab846807269da888a9468976719987b50284c",
|
||||
"adminx/modules/Requests/language/ru/runtime.xml": "6e5b2487c9b3aeefebcf1c93e4458b0f902d1a334117d935dd9a0631d3c4547e",
|
||||
"adminx/modules/Requests/migrations/001_native_database_calls.sql": "cdab530043491203e61b5e5ed325da707e99797d3737019b27f36f0495639344",
|
||||
@@ -490,7 +502,7 @@
|
||||
"adminx/modules/Rubrics/FieldEditors/RelationFieldEditor.php": "9504549a3b81893ec8a114a286a960450bbd302cb2f8f9af97660c8332f91925",
|
||||
"adminx/modules/Rubrics/FieldEditors/SimpleFieldEditor.php": "e961e7ab2bf3a16d6d9131513f11d2d4b9520ecee5572ae7e1b7ae8475d1cacb",
|
||||
"adminx/modules/Rubrics/FieldTypes.php": "c6cc64b0e1b1fb29b65275d337e5747b9f9f68ca46171e7e7f07129cd27b8409",
|
||||
"adminx/modules/Rubrics/Model.php": "21f19765b1ca8e4ca7bd43fa8bd9737e26008cc152e77737215bbcc0572855c7",
|
||||
"adminx/modules/Rubrics/Model.php": "85aac47a064c1d2d95cc3e620e106c03989ebe9cf237cb4516253c338e2f49a9",
|
||||
"adminx/modules/Rubrics/RubricFieldPresets.php": "80109bd9f066f077ad19e48d7c7599e4dbf7b1d0c06c54db9fc921992bca2e77",
|
||||
"adminx/modules/Rubrics/RubricFieldSetLinks.php": "41cd27e5947680eb184dafba5fd95f707ccad0c20fab2ca948cf882ada0adefe",
|
||||
"adminx/modules/Rubrics/RubricRevisions.php": "83d3659ae0b1d2ce75fe02b0ebfc11d04857556ff352dc2b5408a1481291c658",
|
||||
@@ -499,7 +511,7 @@
|
||||
"adminx/modules/Rubrics/assets/field-sets.css": "fe5253279caa882f9623b058c92ab461c476c080baf2399425e522a36b75336c",
|
||||
"adminx/modules/Rubrics/assets/field-sets.js": "efbb011014f756dd59ac1357441d06c9b69b278a9d934f7cd9d2d1d7398cd313",
|
||||
"adminx/modules/Rubrics/assets/rubrics.css": "72898787697fd0e26c61105c6f1fe5b509d731234e60b438742e30cda8fd6cc9",
|
||||
"adminx/modules/Rubrics/assets/rubrics.js": "d0384f3fedd7a765a5d0006a1ca5080a9cc37fade556e95c2561927d197ea2ca",
|
||||
"adminx/modules/Rubrics/assets/rubrics.js": "2d85e91a7426c17e2f79f9f71dbbe37318c9fb5f020be5bf3a7e53062c1e3dc0",
|
||||
"adminx/modules/Rubrics/language/en/interface.xml": "7edfdc6fe447448dbed987bfbfb8244557b2a7fab89a2ff3dd1e29eca57a6033",
|
||||
"adminx/modules/Rubrics/language/en/runtime.xml": "4cad3d97c6a2695d038286cedc2c4509230c3bc9fa789dba2da9db89e8d9c93d",
|
||||
"adminx/modules/Rubrics/language/ru/interface.xml": "7df4b3f148b367ca3185174c0feb3129f7ce28f99791a2f49de256d78bae7730",
|
||||
@@ -520,24 +532,26 @@
|
||||
"adminx/modules/Rubrics/migrations/014_rubric_purpose.php": "2913a1a7bb6f4c5665ad05704dcbbde78c7ab6b75b0ad7643fd00e482af69411",
|
||||
"adminx/modules/Rubrics/migrations/015_merge_directory_permissions.php": "648b3e232e9426f84d69238295515342e42695e46288b901950cde2e8979b0e6",
|
||||
"adminx/modules/Rubrics/migrations/016_rubric_trash.sql": "09385e8d14d5d4d977db59c60035338404019783cc44c6c784cc2172faab08f0",
|
||||
"adminx/modules/Rubrics/module.php": "ae3e957e9c3e099e66fcd14d9ddcf76a172207ff68d77d6279ca7a4c15c37c36",
|
||||
"adminx/modules/Rubrics/migrations/017_rubric_public_visibility.sql": "2a6ef9686235b453807701c24d1cd8dc81ddfdbb80824a7ff10a70ca3f949827",
|
||||
"adminx/modules/Rubrics/module.php": "a7ebe05a45eb4270f057f2d59f717b2f06df0350a0e86041f2319c70895b9a5b",
|
||||
"adminx/modules/Rubrics/view/_content_tabs.twig": "e4c0a9b899b5085aeaec2e1793b8eeac4a7619959fffc7ce39151af1250e0d9f",
|
||||
"adminx/modules/Rubrics/view/field-sets.twig": "911dd7cc9546411e1b29724466328c7d2db8db630b2b52d33e8443a066b16810",
|
||||
"adminx/modules/Rubrics/view/index.twig": "4c01e7b020d16528b2a2df68e35bb2e4639efabe30adc4f19508d85701e788e3",
|
||||
"adminx/modules/Security/Controller.php": "c7f2263f460057191402dacf8948b822f9b33bbacbe94e6e9ad871613147fc86",
|
||||
"adminx/modules/Security/assets/security.css": "fd8c477d07c38450587153402380040b72fa9a92c60edd532c4b21046cfe375a",
|
||||
"adminx/modules/Security/assets/security.js": "f270dd0b910b9aa4815680b8d80d66ffb31bc7862f74e628cc60efba4205a99b",
|
||||
"adminx/modules/Security/language/en/client.xml": "f474d114b7a8dc3dd0b8438109e9ddb4dfb89717807c6733965f5b410270475c",
|
||||
"adminx/modules/Rubrics/view/index.twig": "d25a68bbbde3bf2da618c465cc2ce6cdaa70b557388db6633c719e0d6deff163",
|
||||
"adminx/modules/Security/Controller.php": "e5d785a8c907757c23785eac8d7ad90932424b9a6bdca86f197fdc6a2f6a6605",
|
||||
"adminx/modules/Security/assets/security.css": "463dcdc2449fcbe569d5f189c95ea54bd0dd5d7bed08568f67f30fad1bcda6a3",
|
||||
"adminx/modules/Security/assets/security.js": "a24fae3873ce9c41fca452981b9ecf60284eaefb0a5c1eba44ab4cbeefa71f5e",
|
||||
"adminx/modules/Security/language/en/client.xml": "c43f9a474b5bc3d6a2c010416bb0dd74cfe70561db098c447a9fc81b905796f5",
|
||||
"adminx/modules/Security/language/en/interface.xml": "1a64dbe2ea97b26131f6d7237d9cc89d55cc236dbdfaeee125fe5ba359c35928",
|
||||
"adminx/modules/Security/language/en/runtime.xml": "968b9f0fe17b31d8ea7d3ef942aad19d123ae4abe19f111fc1efdd89249af7da",
|
||||
"adminx/modules/Security/language/ru/client.xml": "77e5f3aeb0d673b31735840b5915bcd8508365f93bbb2337cfb5cc2fd06d6b94",
|
||||
"adminx/modules/Security/language/ru/client.xml": "37088b5db55df21d618f241ee9106a1826ef5751b01123c808bfc4ee7c40136f",
|
||||
"adminx/modules/Security/language/ru/interface.xml": "934110ec57234db479a8764143250553a1b355d120fb4b7d9d866879e887ae61",
|
||||
"adminx/modules/Security/language/ru/runtime.xml": "cbeafc1cbc995206868fa3a3c3ee8d64d2dab21c1360c38ce6f69611d0d09a9e",
|
||||
"adminx/modules/Security/migrations/001_create_ip_blocks.sql": "832aff733e2c25a43164c60a601dba08c21fa29ac9e826568055dcdb65417a76",
|
||||
"adminx/modules/Security/module.php": "557308dd9dbe114e5c7129e1cfbc06f25ded104e08c473513e2f388ba945198f",
|
||||
"adminx/modules/Security/view/index.twig": "b70bb298a3e1aed377d31bc6dd6b7937cbdcb71cd7098113b8b026ba9dbfab84",
|
||||
"adminx/modules/Security/migrations/002_create_user_agent_blocks.php": "1afce9594d058d8bc60532d09001c87213aa133eb9086624b80cff146bd53a62",
|
||||
"adminx/modules/Security/module.php": "c8e42bac300bc4ae73cd7997a8d820bcd721291360d54bc28301caf5e142c1c6",
|
||||
"adminx/modules/Security/view/index.twig": "3954407173cc5bedbf58b47dd527acc548e2138f838b4a9a2921e34cdd807c81",
|
||||
"adminx/modules/Settings/Constants.php": "5f8d15924c33f2e23e6f57f2934229e9c16333334f8ce3679793b35e5acbe0c2",
|
||||
"adminx/modules/Settings/Controller.php": "90aff255d936753ab763ec534a097e9988f6c737bd5be2ba1194a7700a372e72",
|
||||
"adminx/modules/Settings/Controller.php": "dadaa11752c61da85c099732491630e44c8a4dd6691e2ac4e91d996f3792d810",
|
||||
"adminx/modules/Settings/Model.php": "9e16004c78a03d39db1bdfd5e23b88b77e8afc7b8f57a2a21997602ace277440",
|
||||
"adminx/modules/Settings/ProductionDiagnostics.php": "b23d473a5f6e7c231c68abe4d48159b177699b5f0c23020e96252b240f86c87d",
|
||||
"adminx/modules/Settings/Schema.php": "7367374ace3e25fd86e5a829ebedb0358ba38bf5be0cfab9b859f1e5d3cf0525",
|
||||
@@ -556,7 +570,7 @@
|
||||
"adminx/modules/Settings/migrations/005_order_system_navigation.php": "df82c4966f28ff3b7d9cf184013477c60a4ae635345f21809bd58b94310e8957",
|
||||
"adminx/modules/Settings/migrations/006_create_admin_saved_views.sql": "69cf1cc085a31b2310f82fe55ad6e0abbb6f14707715205e5a53f06334759a99",
|
||||
"adminx/modules/Settings/migrations/007_public_site_url.sql": "7aab89b5e7ea5f68b519322341c90d42b424b7f994d706f949aa326e571f6cbe",
|
||||
"adminx/modules/Settings/module.php": "b5de078a2dd6e0b766ac0d5456f7fa60ded80741c29053e6689ee3e48b539593",
|
||||
"adminx/modules/Settings/module.php": "5f9365f2bfb8d2b7553a8723b83a19936dfc51ad906c8f3e1e1b809ad1d1e913",
|
||||
"adminx/modules/Settings/view/index.twig": "7d64c286f8e580ae51b1529253fd816cdcabf8122a564fcc428ced98bd7b14d6",
|
||||
"adminx/modules/Templates/Controller.php": "00b1758860293b56b052a8c41f7174d0d97ba1d78e012b80f8e9f29194eb77d4",
|
||||
"adminx/modules/Templates/Model.php": "4f7a9082f462170533c0c140ef085f6a33111d20061b464864eb86066d50289f",
|
||||
@@ -603,17 +617,17 @@
|
||||
"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": "13dcee85d0558141f4305c2b0dd37401706dc4cbf193564153a0eaecf5dbc681",
|
||||
"adminx/modules/Updates/module.php": "d3507184cdf713263cbeb20c6ac8761c941ac021853dbb40f27a8a851e65cd3a",
|
||||
"adminx/modules/Updates/view/index.twig": "8d975051ca96fd99902714bd60b6a85131d6304578ea65a3dbf732523b638ece",
|
||||
"adminx/modules/Users/Controller.php": "dd8e11ec359064b37437057f0e1295dd1f20239186d5726119ee0764bce2fd10",
|
||||
"adminx/modules/Users/Model.php": "afd334340bbbeed9ac638421bc490262a21173007734740d2724d4ddc8cace63",
|
||||
"adminx/modules/Users/NotificationProvider.php": "460ab91a010e473937318542903a30053e08303b512b94ffe43559a85da8c57d",
|
||||
"adminx/modules/Users/assets/users.css": "1322f3db7bbfa13e548a8c019ac6580f947de5786c5f460435db2561dc7b1f28",
|
||||
"adminx/modules/Users/assets/users.js": "d7d2cdeda4a0bf569ef14eae2bdbb6feb593ea65951382cc2208052518fafd10",
|
||||
"adminx/modules/Users/language/en/client.xml": "a84c325dcf979712bf3eb5cfe1cae2f2d4a9bf698702d003bfeb66954eaee159",
|
||||
"adminx/modules/Users/language/en/client.xml": "4a8ba2493d2697518531b57fbe5a2bf9c304d1f114048a39539bc94e06a25d9e",
|
||||
"adminx/modules/Users/language/en/interface.xml": "4434abf4230c2a616004dbfcfd4d79f43ccc60f32030842d0eade5bc3bfc28eb",
|
||||
"adminx/modules/Users/language/en/runtime.xml": "2f4b8e5ae2ed591f03bc5d10e4e305b2404dee89f75ee4480f0daead10cc58f5",
|
||||
"adminx/modules/Users/language/ru/client.xml": "64054dfdd9c2bd70084b283e776597c86b1899a603fb5a07935832ff5960a3ba",
|
||||
"adminx/modules/Users/language/ru/client.xml": "7ba843fb0b54bb2245816291b83772bb404f4e47776e51ffad6ee4dac2a606d6",
|
||||
"adminx/modules/Users/language/ru/interface.xml": "9df72a9413ddbde9513a6ad9b5d04a0bd68a828fc328197bdd180a5a77d4b274",
|
||||
"adminx/modules/Users/language/ru/runtime.xml": "5bb9db970f2e2da5849e20272d700ec4c70de65443ab4bf10b960aea79195842",
|
||||
"adminx/modules/Users/migrations/001_normalize_public_session_activity.sql": "e72f1cacea2cff5f74f7baebe3cb1f154b87fd7368541b1a3a5d2300a0deae86",
|
||||
@@ -627,7 +641,7 @@
|
||||
"adminx/view/_saved_views.twig": "51217212a3b0ef7437de4dd55a07fdf55aaa3138155b00f65fd85af686412da1",
|
||||
"adminx/view/auth.twig": "e44eb6337248470418be7015886e6e5d7bf5b0021fc917f4cc0dcd98c6492d85",
|
||||
"adminx/view/error.twig": "1187e6d0c325e0bbf6a7bd9836a5ef6d96cb8f72a0599050d3da7120683a6202",
|
||||
"adminx/view/main.twig": "4aa00f5a2c50e3cc4d339c0ad9377526db6e8a65575843be59dfd20df5210344",
|
||||
"adminx/view/main.twig": "6845837fe7c9adab56c611dcbdd713f910d65cbee0c7b62eff7ed7bcf571d6f5",
|
||||
"configs/db.config.example.php": "e42bdb47aa7d1bc9cd85cb36e05acf940cafd951d6220664c485d1b971f18809",
|
||||
"configs/index.php": "48e6b249e3d83530187c095ede0198af498c59f384bd458db74f9e0c2306eba9",
|
||||
"configs/public.config.php": "882ea6a03ee8df191be8ea68ba78d39ae600d918a0880ebdff158fc38f534aa6",
|
||||
@@ -650,7 +664,7 @@
|
||||
"help/content/README.md": "273451d12b6360df56cc58323bfb8fd7521b4e3daf8f7ad93c874fe6877af048",
|
||||
"help/content/blocks.md": "1d55be2a9f1a48df426e5856a54307d77b352d0037d68f08ed7e16a322d9ef8c",
|
||||
"help/content/bulk-editor.md": "821107322b125c9344bd04346037fbd19f8ccd0ad6da822fe78af3299d01f746",
|
||||
"help/content/catalog.md": "8ca86447bc45f8f14688d7f74521569b50d4176b9ec2ecca0bad25d91a0ef0b8",
|
||||
"help/content/catalog.md": "12434610186c9d797270ee0bd802abe79b67e976a88a94dbb10f2ee1fb4dbdbf",
|
||||
"help/content/content-model-tools.md": "baaf738aec454b1855ea110b8f5e1ef3ceaaad51cb920d4551cd088daeab7e76",
|
||||
"help/content/content-studio.md": "91ef485e614a9a71a07d4e6c143377f7052ddf3b00d3f2f0d0a32258bdb14dbe",
|
||||
"help/content/directories.md": "ff6c5ea9bd7fe1fb9d0aa1875d03fc3bc00b2bd2b0ec83d6496cd55168db294c",
|
||||
@@ -699,7 +713,7 @@
|
||||
"help/en/content/README.md": "7c871512fea7f1bf635d5a816e322161dedbd8c158d520cd0385467e8dadd1ff",
|
||||
"help/en/content/blocks.md": "34657f0a7ab977bf78d5b7645bf80a0755f40830bab4d4620ca7f39ea5992210",
|
||||
"help/en/content/bulk-editor.md": "c99d5eb5058d6714f0281b84bfdf9020fc63ef27519c095af5f92fa2d5e96b44",
|
||||
"help/en/content/catalog.md": "8f12199f4d727e0ad715f91e980de638357fe1fa5af0702351fd3ee30afe29e1",
|
||||
"help/en/content/catalog.md": "82769c6156f6dbe1bbe7cf5b3d0a8d4a55fc1be7389fe1e3945f0fe745200654",
|
||||
"help/en/content/content-model-tools.md": "fc50f8c334a129d3ead4a58503a7659abc2b1fdea7c9d277feff1e3ba4bb7b6b",
|
||||
"help/en/content/content-studio.md": "194fe41a1eefdf7da3c9a47b8539ed3ed9d785d2a75816a39da6d2784ec40925",
|
||||
"help/en/content/directories.md": "cfadf2361ead3bed9b63435041af8c91e24ec8b492906f0d9b976047edc123fb",
|
||||
@@ -780,7 +794,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": "e31cbf7ead9d758401e35d7723f0189a354ce818a4fcf5a0118c73795088dcf0",
|
||||
"help/en/modules/commerce.md": "e0a156c6ba634fe4c5beeae5eca0e0d592adddedbdab7e444da647609dbe01f7",
|
||||
"help/en/modules/commerceml.md": "610f6b2f9abbeeac604da0408192ae52dae7d6f3fb09e0d321e14d1cc190b808",
|
||||
"help/en/modules/contacts.md": "81c818e9cbcd1d61f34d9199219ea160a04bf88af110b32c3527c65fba65d39e",
|
||||
"help/en/modules/content-packages.md": "4b89df6d05fa75e484e38e404d53fa7ecc6b61aa6930fbf74cf9b6611bfbf532",
|
||||
@@ -879,7 +893,7 @@
|
||||
"help/modules/benchmark.md": "6c31f16fc3f083f29c65398c38b78609a3d94da168d30a93825af8ee9650eb0d",
|
||||
"help/modules/cache-warmup.md": "56c05ec252b44a1a0d1c4b09efa90cf9d450b0b138787903c898f56b377b8749",
|
||||
"help/modules/comments.md": "04bcdea5aa7740e3749b51dcc9fc2954e98b22298d3e4fcdcc9865d94e1d0376",
|
||||
"help/modules/commerce.md": "0255c08bdf10f7db8e530c15e987c5d137ee115434ca0a0d71a831cc9fbfad9f",
|
||||
"help/modules/commerce.md": "d398d0dfc1edca5010d81ae660d412b4b1ed8d4df448fc78f237ad0ee2cc731a",
|
||||
"help/modules/commerceml.md": "60a0a5c541b560b5a6a4ed8213d9145caace68c15be534af13f5f817eb5251ba",
|
||||
"help/modules/contacts.md": "cb4284709f7b469164e3ef4c817893cdcaf4eba1b460164fb36f75a798bc457d",
|
||||
"help/modules/content-packages.md": "8dde0ebc897b7891a6afadb6491f4797bef26622918addab67f0f4ebfd719678",
|
||||
@@ -943,16 +957,16 @@
|
||||
"setup/language/en.php": "963dd284ee244e6d006dede8522c1118dc0909974dcd24d477fd06b7e1965fa1",
|
||||
"setup/language/ru.php": "8522b13c4f147804538eaa857585296db811c736e1361e6b403daecea05f34f8",
|
||||
"setup/recovery.php": "359ae9b79508a382bdde175824b18a5bdb0fd69ddb7776c7d18ac9eb826d127a",
|
||||
"setup/schema.sql": "f3a291d2dacebf1b1141254bd899bb522f20ff8c5c9ef5b76e04c324e0de6d73",
|
||||
"setup/schema.sql": "4942144465f8854cce3c3e6f06bc9d2ebd11ad1239d4a086feda318a0ae70448",
|
||||
"storage/.htaccess": "ca761a92f2b6eb47feffcf200a8fd846e7fdd8abe065008d29beff0319ca3496",
|
||||
"system/App/Common/AdminAssets.php": "dcd58f19fda48263b2912a10dfdc24e3f12967683649c4f614a8d54c62d51b86",
|
||||
"system/App/Common/AdminLocation.php": "d2729b0d627ddbdeb7f0c4b110664ed949f373946f80929c51688391c64352de",
|
||||
"system/App/Common/ApiTokenRepository.php": "0795e1add3723708240949b7aeada364fbeadd74366884b57e9142efcfaf617c",
|
||||
"system/App/Common/AuditLog.php": "5306d21085ff7ef7313f5798fbd416872825ff7d2831f94cd9744de010e1cc2b",
|
||||
"system/App/Common/AuditLog.php": "9609b82e4d8b9ef2e1ea7268cc8b4636b1ff38080e63455777ea7d8329b9801f",
|
||||
"system/App/Common/Auth.php": "647421cae2ec8bbf273d687ac1fda134c7bcc28db4977e01f5896c79e7c2c83d",
|
||||
"system/App/Common/Auth/IdentityLinker.php": "7da4d902405bb4ead2278e5bc3578cd0318422fe82318bec94257ed44d5ba98c",
|
||||
"system/App/Common/Auth/IdentityLinker.php": "26c85deda5918b413f2ee4fc48ef00d5268aa6ef1081095b801b2428d61979f7",
|
||||
"system/App/Common/Auth/PublicUserNames.php": "0345d1f452e0a9accaa9b98eea71f27a428692a5b4f2ad225a10ad8bcf777684",
|
||||
"system/App/Common/Auth/PublicUserProvider.php": "163449ae80d412a605fc60380e97107b8710d6e14d06baf042d9e63d35d75c29",
|
||||
"system/App/Common/Auth/PublicUserProvider.php": "5d3690dcbc0e9cace7e09350d283cba541f15cf8a346c9bedcbd9f6cec58d65f",
|
||||
"system/App/Common/Cache.php": "bb322246cbfa12cbcdd67c16a24c55715ad67b31d356b4f18091c0aa1d00eb9a",
|
||||
"system/App/Common/CacheKey.php": "47400cd90d1880d061995bfea585b56210e18a870607d9b1cf7f0eeaccf15032",
|
||||
"system/App/Common/CompiledModuleRegistry.php": "9ab055fa759f4041944bb94bfc2e28361986a09b742210981928bd6ee15f758e",
|
||||
@@ -988,7 +1002,7 @@
|
||||
"system/App/Common/FileCacheInvalidator.php": "24d34113c9909e3c56d74ea35ed61a1744c20f7fa7ecaef49761f2a28a774a18",
|
||||
"system/App/Common/Flash.php": "8fcdafb920b374a6f346c10a42e3f846003fccba97f4bb48fa7028aae2a0091b",
|
||||
"system/App/Common/HookCatalog.php": "52e151f1b41856436d13cb6fd6610ffdec264225a0ff7bd29761ad405ece92a8",
|
||||
"system/App/Common/IpBlocker.php": "5536f4802233588671fa5aaace1c017ae8f52590b64213d5d9681a608ff3c3b9",
|
||||
"system/App/Common/IpBlocker.php": "356afbf56805056a4e9ca3ccd614e4be1726995702b8659b5226104fcf497d2c",
|
||||
"system/App/Common/Language.php": "587672d76ade8b5e1b30e98796e3656a833515db4d09b44717da443e0fa7ac1f",
|
||||
"system/App/Common/Lifecycle.php": "4a59040f213232dfc9cc5c3a9bee491eab5916b65accb47c9972ecfb2573b68e",
|
||||
"system/App/Common/LifecycleEvent.php": "68a94bcf55a62dc88f0b464ae2ed13c4056f2722fa550cf31f4633f313c8e359",
|
||||
@@ -1004,8 +1018,8 @@
|
||||
"system/App/Common/ModuleSettings.php": "b3280ad547e59bfcefb4a338339a36905fab5db1548345b0f00e257d275a6294",
|
||||
"system/App/Common/ModuleTagRegistry.php": "9e68b64303825a25ef4f71f31a7adedee9d2752e4d1d06162605da1dc510a6a5",
|
||||
"system/App/Common/Navigation.php": "1a1b0a3552d941dc0fc59c334ab705e86a73ee96ee42871dc958012bec1d81f6",
|
||||
"system/App/Common/NotFoundLog.php": "3b1cdeefd05ae250e90d1a05dd9e1bc0ba2cb0c2cbb89d504fe009151f20452a",
|
||||
"system/App/Common/OutboundHttpClient.php": "1be0216c2147b35a329e18d78911e7c5aa2cc225141b749ccba8c1bd9222a26d",
|
||||
"system/App/Common/NotFoundLog.php": "f5630e839aee069c1122452da42f42ecae5d2082fe582dda7be6760059f3eb71",
|
||||
"system/App/Common/OutboundHttpClient.php": "dc03f3093e70438ba92cd8d15ae690141186b6cf7916a863bd6f7de4c15be0bd",
|
||||
"system/App/Common/PackageModuleRuntime.php": "5ad492b9053bcea4d3bd4398b11fb2d976272e2036d379bdc24c19dd5fe7486f",
|
||||
"system/App/Common/Permission.php": "8e2241bca829de0104741dc2518860cd15489866dc310437e07aeab2bf0f0355",
|
||||
"system/App/Common/Proxy.php": "707564208ec8c24ac2ffb5bf41cba34f1b39059ce6bda31a69aaf4b116d864e2",
|
||||
@@ -1021,7 +1035,7 @@
|
||||
"system/App/Common/Ref.php": "e5e55093871022e86a7c8e8841e2e150922a7454500668dfb6f30fd50aa6044b",
|
||||
"system/App/Common/ReferrerLog.php": "a2da447a548011c9ae6f8be2ef4aacac2a3d4c38b62a19d7ea5442589cb7febe",
|
||||
"system/App/Common/Registry.php": "7b4ddbb3f96a7ad54a8d43e151cb595e598d3620b368d21721927ce9d54e1ea3",
|
||||
"system/App/Common/ResponseSecurityHeaders.php": "b159a7105d3c1fb3ffdefbbc5997741d7b0f83bfb26fabcac96f61b53969364f",
|
||||
"system/App/Common/ResponseSecurityHeaders.php": "54d181e1ec49f328e63851d6d2d693d8318fdf82684efc8d3f4cd5d394c03e0d",
|
||||
"system/App/Common/Router.php": "4b0dd3fd9f732177070ce3326b691a8decfbe698aa0d8cf687a18f6c02b73d91",
|
||||
"system/App/Common/RuntimeConstantSchema.php": "e67a2de96a2496e0463a3b9e9d66fa6b2ef0e4b7dff768469ef472ba639797d7",
|
||||
"system/App/Common/RuntimeConstants.php": "decb1277dada75ee2b5c7d531f064db7ffecef5b3ad71d306111b2a45121d34f",
|
||||
@@ -1035,7 +1049,7 @@
|
||||
"system/App/Common/SignedEventProof.php": "732c3b27f360dcbadcef405cac3d2cec5827e02cc789dc203001627750efcec8",
|
||||
"system/App/Common/SiteOrigin.php": "d0471ed37ff0c8e08b4ab9e12138e793996d6bfe0a467ab020e82e301c773d19",
|
||||
"system/App/Common/StoredPhpRuntime.php": "2bd4fa83c970cfa0b8002b11cf36bd0def7b37ea19c025f7c6b2f37f0de38286",
|
||||
"system/App/Common/SystemTables.php": "85e95425b64d35bd8a3a5d4ddb14c1367ecbaa64ff6aaf4378055a1019895806",
|
||||
"system/App/Common/SystemTables.php": "3c95b8902bb344f1f7fb50ab4ff355cb17791f4ac4a716b40eec0bb279fcfe74",
|
||||
"system/App/Common/Template.php": "5a0cacde5059fa9918321e6baa71a09a754a024055dedffae161486c51902f45",
|
||||
"system/App/Common/TemporaryDirectory.php": "4aad5e498b0ef5483427c18ec6eaaf9db64ffd1348a0ee3f445da080987e4bd1",
|
||||
"system/App/Common/TrafficAttribution.php": "cef0bce9cbd2ca3d8dd297cf1b969669ed1cb996cfa175841ecc6836ca7eb8e0",
|
||||
@@ -1056,7 +1070,7 @@
|
||||
"system/App/Content/Documents/ContentCacheInvalidator.php": "d2bfd72d4df1a05e281b88543fabb8356ffc9799d37286c3767c6a0173ea91b1",
|
||||
"system/App/Content/Documents/DocumentAliasRegistry.php": "803ccfa70c3648e2e4c625aaa827597431d4ea54dbf83618b904f699f9560846",
|
||||
"system/App/Content/Documents/DocumentAliasTemplate.php": "47bbcead460d5d4160daea31ebfd36ecc3657f74f905162f20ee0d1eae78e3db",
|
||||
"system/App/Content/Documents/DocumentCreationPresetRepository.php": "a58de0560e194d8386932a4e2e0e807a097ba1bad1f56cc8d11587f6c20e54c3",
|
||||
"system/App/Content/Documents/DocumentCreationPresetRepository.php": "840ac8e512260323394ecf648f67d6c0aeace68810f989601f7532b3f7c9a708",
|
||||
"system/App/Content/Documents/DocumentExcerpt.php": "8c5742f976a492531af95f2291e8c824c3a9804a54a27997f9f51a716a57f19d",
|
||||
"system/App/Content/Documents/DocumentHookContext.php": "021d6e15aad1bcfb9168149189ff7cae13b1f88020a76c07d27f22ecd7d6b5a7",
|
||||
"system/App/Content/Documents/DocumentHookException.php": "a2158338b544e5ba49226efa3a80c52b4f60c88d1112a3ca1a72f50043abe0a0",
|
||||
@@ -1065,17 +1079,18 @@
|
||||
"system/App/Content/Documents/DocumentMediaFinalization.php": "5ea218f79f835a4fc25da0199aab87137e620b60c31d8e2f587a1b911b3af6b1",
|
||||
"system/App/Content/Documents/DocumentMediaFinalizer.php": "785487028e221726c6bc15662f83ed985701a8e5d21e9fc9605e816ef4c835b7",
|
||||
"system/App/Content/Documents/DocumentMediaPath.php": "23b40c98d3cf9a73df3b7f7083b58b60dd80b8bda71e3e3a6354342cc72322a8",
|
||||
"system/App/Content/Documents/DocumentMutationService.php": "868e1301cf822516debe1f441f80eb1897d0a1617f8d27a6a2a1dced8bafa6f1",
|
||||
"system/App/Content/Documents/DocumentMutationService.php": "84a05df5bd0aa77e65896a4d74be4b1a83e3fd3719c758039218aa18018a2ca8",
|
||||
"system/App/Content/Documents/DocumentPickerRepository.php": "0e8efb5b42aadd74725213b8302b53b5147d7e0eb2e83d301a5f073bb0c13142",
|
||||
"system/App/Content/Documents/DocumentRevisionPayload.php": "c7f83350de98b6d98531508eafecc4a2b5a0551c7f2ba77a9e5b954fd1287ea0",
|
||||
"system/App/Content/Documents/DocumentRevisionPayload.php": "84cbd5fa39bd5b2beee18018e75361288a6db42920371f1d6aa37ab57cecc93c",
|
||||
"system/App/Content/Documents/DocumentRubricCodeRunner.php": "a9c04196b76ab91c7b1f8b53afbd45fc6727805c953fb5fe5ff36b69846d389f",
|
||||
"system/App/Content/Documents/DocumentSaveEvent.php": "ad30286d10b734e090732f84f0e6a77bff050acf653949140049418dda8663bf",
|
||||
"system/App/Content/Documents/DocumentSaveEvents.php": "79a9fa78819938f179bc24121992e7d44306c51612734d9ef10839b764a5d394",
|
||||
"system/App/Content/Documents/DocumentSaveRejected.php": "4ebdc1c5aa622f6389b107320a7786b8acde89438f6c56a660317deb623cec5f",
|
||||
"system/App/Content/Documents/DocumentSearch.php": "ba8d1c7a28878e3b7e2c96aff07c59c2ca9b5b5eabb4eda99de6362b4274d54e",
|
||||
"system/App/Content/Documents/DocumentSnapshotBuilder.php": "1d9437433d721198fbd18c6cb141657da93c569fabba46bc59466981b68b5ee3",
|
||||
"system/App/Content/Documents/DocumentSnapshotBuilder.php": "cb741de6e24d3ffae47d1dd1c5a202afa8454858ef8deddda1cbb1e66ae1fc34",
|
||||
"system/App/Content/Documents/DocumentSnapshotRepository.php": "54faed521c064f84cc4017e65ea0dba8b9bb34571240a90c083eb076c9a11b1a",
|
||||
"system/App/Content/Documents/DocumentSnapshotStore.php": "e61a7873204ba76b50d2ae7f087a59442cb93fa10ad5c3f0ac0087d941dd2075",
|
||||
"system/App/Content/Documents/DocumentVisibility.php": "9a8b17058d47f40d4eb5e7c6a96f15d7f234c02b275ba1f6a46aadf58251324f",
|
||||
"system/App/Content/Documents/FieldTemplateManifest.php": "8eca7f9c9056ad5492b2b22cc55b407d8a65b32f5889775a5ecf6b4caae7ab8b",
|
||||
"system/App/Content/Documents/RubricSchemaBuilder.php": "a391211bbdbde83a6b8f4ce2ccf98bad2ad2f5d39f35edb5e262782729dd86f2",
|
||||
"system/App/Content/ExtensionTables.php": "7e59c929815afc570dbd6e0af754339ee94a1cb32b928edbc6d50cf34bcbe085",
|
||||
@@ -1161,7 +1176,7 @@
|
||||
"system/App/Content/Packages/ContentPackageWriter.php": "dea33706a29e11d2a04ca6904bad49f8ff6c062b2cc1eeef1bf9ba18f49adb00",
|
||||
"system/App/Content/Packages/PackageDependencyContext.php": "8a1f8ca6bbb7add9d8af11d28cdf7137c9a9eaf25b528b0259cf9bd0be00c8b7",
|
||||
"system/App/Content/Packages/PackageModuleProvisioner.php": "28a922ac5471d86ea0c467de11164c97fecd0ee432814e79927bb875995629cc",
|
||||
"system/App/Content/PaymentPrograms.php": "d233564780b9ab9655f8c41c490aad207c95731f36e6967fb52ac54b9c89e52e",
|
||||
"system/App/Content/PaymentPrograms.php": "a34c9ed42bfc0d465ac4f37fad28d02ab0145164702e2cbed35991da501f9e3a",
|
||||
"system/App/Content/Presentation/DocumentPresentationBridge.php": "be90d44accf2f75435a62e8be5e79184e095708a3601784daaf87aa4e05ba501",
|
||||
"system/App/Content/Presentation/PresentationAssignmentRepository.php": "a24059e979dfacf7d06f72a46a0322f50b921d0361ed8a426a64b393ca387cca",
|
||||
"system/App/Content/Presentation/PresentationDataRegistry.php": "7159542177443ab7e84ce85e52764bfd7df4448320059175dcbb151ef40eb38c",
|
||||
@@ -1170,7 +1185,8 @@
|
||||
"system/App/Content/Presentation/PresentationRepository.php": "bc5fb81cb6068ac54551463e563637ad9d75fc5a571d27e8642ed19e7a43b118",
|
||||
"system/App/Content/Presentation/PresentationRevisionRepository.php": "061af7ebd841a44a24be91e6ae3d3b497347bb5333819c74712348b81c40c2df",
|
||||
"system/App/Content/Presentation/PresentationSyntax.php": "aa15536fc06559d34b9aa6b248136e8314f75997b467083dcfff4f1445fe44f9",
|
||||
"system/App/Content/ProductShipping.php": "8a92b23991b502a97ee3d718add3e87e05382ff80aa83e3c657f54400c3745bc",
|
||||
"system/App/Content/ProductDerivedCacheInvalidator.php": "6a15cec08f3de799ec9826034fdb2260dcddbfee1987141f5e670235495e6a48",
|
||||
"system/App/Content/ProductShipping.php": "54f8add77a6cb75515a6eb8191893888da43aaf36a167a699f3d7e946a858349",
|
||||
"system/App/Content/PublicShellTables.php": "f89cd224436da9d1122c988a3a0ac19e5bef85351c51649506fe16a05c514cd9",
|
||||
"system/App/Content/PublicUserTables.php": "f44a184075c9f978564f4b413cb3e89cda5f17528459d52258f458cef87941ab",
|
||||
"system/App/Content/Requests/NativeRequestExecutor.php": "988a1635bb4602b55edb89a1fb8dd26ee0537c074c77fc86b00ecbd8800517ba",
|
||||
@@ -1192,8 +1208,8 @@
|
||||
"system/App/Content/Rubrics/RubricMutationService.php": "e3190ef1ddd94bdaf077148ce84c5758b2a32683369730a2b847dbbd0d72a74c",
|
||||
"system/App/Content/Templates/TemplateMutationService.php": "8c7e52202454e35eb0c8176325cc23ba760ac40ff0430cbba5e1446c83cf7a87",
|
||||
"system/App/Frontend/Auth/AuthService.php": "1cef24cffd7773f3783ec96479b8e7daa1808b03a34dfc8da37b84e6e09144b8",
|
||||
"system/App/Frontend/Auth/Controller.php": "8002201011ff8e1c477dc879ec57d2138bf325621978ccbc9a56921c4eff7f9d",
|
||||
"system/App/Frontend/Auth/Feature.php": "7910b5f938854a4a32feed637b757182b12f4a7178cad97a1498509173571776",
|
||||
"system/App/Frontend/Auth/Controller.php": "248d7c8018c643622fe41cc679f186d2915017da5250d343415e7a223803c496",
|
||||
"system/App/Frontend/Auth/Feature.php": "b8ff790cce56875274c372095b80d52c0d54fe12c518573e7a02c583744a3389",
|
||||
"system/App/Frontend/Auth/FormTemplateRepository.php": "bcd8797e63cb67803aef2267a2c62e3ad368b5fd947057a768adbb1288ee9281",
|
||||
"system/App/Frontend/Auth/OAuth/HttpClient.php": "b09acf92b312bd05ca78500fccaac3b77d36a1046d64d3615a00d60af4e3594f",
|
||||
"system/App/Frontend/Auth/OAuth/IdentityRepository.php": "35ab143e0d536185125c685f17f5f4bb0ba0cc72daba62552ef6f82370e38ec1",
|
||||
@@ -1203,32 +1219,33 @@
|
||||
"system/App/Frontend/Auth/OAuth/ProviderRegistry.php": "e0ff4bc12ef2349d4f914299c78715438d9fbd44ab98e00937534786baa07800",
|
||||
"system/App/Frontend/Auth/Phone/ChallengeRepository.php": "2bfd7ad7b4640e653d3ad0e1ed5f1779ad8df2b569bee071c040d44996c15ea8",
|
||||
"system/App/Frontend/Auth/Phone/Controller.php": "bb07bfb74a89742c2dbeeb7edf79f02ae43ef6a252d02a3bb7c3c78144cf280d",
|
||||
"system/App/Frontend/Auth/Phone/PhoneAuthService.php": "4f31afa6d600a754cf76805b2a475bf47ffbf7a540b65fb99df098f227211a5a",
|
||||
"system/App/Frontend/Auth/Phone/PhoneAuthService.php": "b06155f0cfd10b006e26f181300500c19553dfb9ef2482af8674b24f1c4457e2",
|
||||
"system/App/Frontend/Auth/Phone/ProviderInterface.php": "55107c8f6a89bea01b91314dfe523b684bccf5bf9f1a3c8f047fd3a509aa6d00",
|
||||
"system/App/Frontend/Auth/Phone/ProviderRegistry.php": "fbeb488c9b63843085b680500ecafd61389a29f55e0d76692550939e378d2f16",
|
||||
"system/App/Frontend/Auth/ProfileRepository.php": "9cd7be35b048faf407a925e6369a5895b8bfafe6cf756eb3c4321ea4979a9af6",
|
||||
"system/App/Frontend/Auth/ProfileRepository.php": "7d18e219821994cff6fe9b4c5339a8ff206a08b478e99338d0804688663d4a31",
|
||||
"system/App/Frontend/Auth/Registration/CheckoutRegistrationGateInterface.php": "6ce038a47e21b594a47bee16075dbf102775b493efd5f2304fa8f80341d34bec",
|
||||
"system/App/Frontend/Auth/Registration/EmailRegistrationGate.php": "ce6dcad98a944aade0fd9a80444c183b4f29bc50e1f119aa3d1d72bdfc3fa732",
|
||||
"system/App/Frontend/Auth/Registration/RegistrationGateInterface.php": "99e73d0d6db972bb5b2c2a7a21dd50823a9bcef7d05937c525d0e7096775aedd",
|
||||
"system/App/Frontend/Auth/Registration/RegistrationGateRegistry.php": "bc7c8546780daacd23243d16778062384f043661674956d1a5a47a8b6440eb44",
|
||||
"system/App/Frontend/Auth/Renderer.php": "502cfc65cc32e653bae3be85a0a93299365fcec56143cc6e1c7f2bb4d53023ab",
|
||||
"system/App/Frontend/Auth/Renderer.php": "b814406bf6935064761de83c83bccc35f653e4f2361c8eb1b507d21345adbe26",
|
||||
"system/App/Frontend/Auth/Tags.php": "92fc1c466e720be36a0adce5a735e47c94bf88d9a3e70ba9d62196ef63e639f5",
|
||||
"system/App/Frontend/Auth/TokenRepository.php": "5e09b154d4747bfb3d2795a9b449ae4aefffe47508edef92d1a7b392c0371207",
|
||||
"system/App/Frontend/Auth/UserRepository.php": "07a35cb3a82201465360b7f75e4be1534c88d53789ad2a5b1ef617be97a060e3",
|
||||
"system/App/Frontend/Auth/assets/auth.css": "a16949511a17462d9157945f2ceccb445254b34243e9e368796dd3137aac49c8",
|
||||
"system/App/Frontend/Auth/assets/auth.js": "a619f46dfbdf5dbe783a316ac5964a6ed40b89f872b3bd7a5dd88d5825c1acfe",
|
||||
"system/App/Frontend/Auth/view/login.twig": "87ca03cc43b778ded40a8a6c8481d0463977219c1b48424896a18fd6f0ec84cd",
|
||||
"system/App/Frontend/Auth/UserRepository.php": "ad924aa73f6c78de471ac3f4be5e2a7f0348a6b5834042475df616c1fb1a14d4",
|
||||
"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/message.twig": "4e9a660af7d0deae9424a07550816c401902e75279414b11657e0eddf573deea",
|
||||
"system/App/Frontend/Auth/view/oauth.twig": "7ccb2c7ae36a378586334887c32fda96909cb6fa235601f1f697d4c8049af648",
|
||||
"system/App/Frontend/Auth/view/oauth_connections.twig": "e2d845ce9d906ca12d3f642e92ede1aa415203f7dc2ac472006f40e80eb8bf27",
|
||||
"system/App/Frontend/Auth/view/overview.twig": "97ad0cd5f72f63b6c92139f7cba54041840131975fdca417352dacf7f64ddb68",
|
||||
"system/App/Frontend/Auth/view/panel.twig": "de638d18587b6c6671d1ba9db4f9eb3c8cc7765be560aee55ceda4625cd9eb50",
|
||||
"system/App/Frontend/Auth/view/panel.twig": "5c32483dff6f349ac0a2b8f88c4ff1da6d89cf5e191b5cbf07e15999b843c672",
|
||||
"system/App/Frontend/Auth/view/password.twig": "099d7c1e48649faba57eb50df9bd9a091c79dad12e9c1fee9b38c3f2e025ed80",
|
||||
"system/App/Frontend/Auth/view/phone.twig": "f84dc89e54a9d2197cb6a9f8b3c4b838f008805e109061add189a43232dc427e",
|
||||
"system/App/Frontend/Auth/view/profile.twig": "71032aeeebc80036301181932c90ee004d9440bbc64b1b3f79ba03c0e3037a87",
|
||||
"system/App/Frontend/Auth/view/register.twig": "92f78ab66ba8c16e0b7c3727ac80ae239e01f696399b72049f19a1087d3f3cc9",
|
||||
"system/App/Frontend/Auth/view/remember.twig": "fb170509256e4ecf13ebe5f64cae8cbd6ad8ef143abe645f4bc1f269e0d8a0fe",
|
||||
"system/App/Frontend/Auth/view/reset.twig": "5dff5378aa0e2203fd67fe9f1dd61586ee51e3d3f6138011b9d2b5dec4da31aa",
|
||||
"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/remember.twig": "9872a6f4812aeb7d8f04870156e0427e9a73be4277c59b1480dfb81d9dc0dc5d",
|
||||
"system/App/Frontend/Auth/view/reset.twig": "a8d13ea135ca16cc20af3d1940cd8ed5cb13143043992e2ec73b5c7d2c60c8cf",
|
||||
"system/App/Frontend/BlockRenderer.php": "1d23d7ff85ba1052f896e204d1885f99ec2c0f6382e01d5d1903d69fe661305b",
|
||||
"system/App/Frontend/Bootstrap.php": "7cdbfc2855a819b80e742eaa15dbbbf9d1f29ef47fda18860c82ea4541fdb8a4",
|
||||
"system/App/Frontend/BreadcrumbRenderer.php": "6db4cbc31bc4e887a8ab4222c0359008dcc8d08bca969e9e3c3a098db12794b4",
|
||||
@@ -1239,14 +1256,14 @@
|
||||
"system/App/Frontend/Debug/PublicDebugCollector.php": "6755ad710e9d045b2bfa74745354f717c101236b5d64ad5530fd779468969a00",
|
||||
"system/App/Frontend/Debug/PublicDebugToolbar.php": "7a92630b59483e63d623fa769e13c246e6bd657c6d027a5d14ab8f95986da0de",
|
||||
"system/App/Frontend/Debug/PublicLayoutInspector.php": "8a7f5ea4cff9834ad13cac7e0d2380d878978e406e71860e1ad9ca183e427dab",
|
||||
"system/App/Frontend/DocumentAccessPolicy.php": "13102e35dada9eb0e174189b976605257b48a32756e801d1ec5c358d0c0912b8",
|
||||
"system/App/Frontend/DocumentAccessPolicy.php": "4a0d67a678708904694b957d553a6e454ea0fcf9aa62075760d5cc754bda6f07",
|
||||
"system/App/Frontend/DocumentApi/Controller.php": "7c5ffecf7e1515710931d7c31b5b2c5f555f8d35778a1fc202a8136b07c2490c",
|
||||
"system/App/Frontend/DocumentApi/Feature.php": "afe7143ed3739b704064927d08fa9b2c79e7050e19e08aca5e4e6638ba5c6994",
|
||||
"system/App/Frontend/DocumentFieldRenderer.php": "1b9eeeb27e40274f8f73bac1a34da993b799731575fef36e790a7f5d87fa7e65",
|
||||
"system/App/Frontend/DocumentFieldRepository.php": "fc8e80455feca50e4bb5bff3566eb6814fcca1994f1167bd8dd7972f1c87ac69",
|
||||
"system/App/Frontend/DocumentPaginationRenderer.php": "560c9441fb758bc760b14f37bd88c39c36207c1717a18f43b5c57599a61231ed",
|
||||
"system/App/Frontend/DocumentRenderCache.php": "9dd0e9e666021e00eda8c4d09cf8f197f17468b8ad545b4d357654bf646bfc15",
|
||||
"system/App/Frontend/DocumentRepository.php": "f5388b21eee81b3536ef32f239a521dd3187410c9874adbdbdc416949409c27a",
|
||||
"system/App/Frontend/DocumentRepository.php": "bef6b4c740d5b4071433d39aada6564e39107510c90dd82fd112f751f7d71caa",
|
||||
"system/App/Frontend/DocumentRevisionPreview.php": "372b751f673411722c2de5f04ee1fac57b745e327a2259916313781a3f175e89",
|
||||
"system/App/Frontend/DocumentRevisionRepository.php": "bbfb1c5a77c979b9998cc647a0e410150ca33727f6caf46f7c49f6ed01774f45",
|
||||
"system/App/Frontend/DocumentRouteRepository.php": "68b7a9c4972613c7fae886c74273cced26d5749d9242f0cf7463176c1ac0d854",
|
||||
@@ -1256,11 +1273,11 @@
|
||||
"system/App/Frontend/Feeds/Controller.php": "2d446f4bb973c781ce01bf4831721b3e5fac1f6e1642f1862d86da8e8f4c271d",
|
||||
"system/App/Frontend/Feeds/Feature.php": "1322bf61d4296c79ad8825671ed556cca79bc9b2eed46cbee8b4f70d19f6ca6b",
|
||||
"system/App/Frontend/Feeds/FeedPresets.php": "bff8edea9f095a63fa2c127ee6afb35c5a3ce4434ca309e9a8a7dd047ba16428",
|
||||
"system/App/Frontend/Feeds/Generator.php": "4234369a293bad237fe0d36dd3c78da616d03ce37070ac72988893f796773da9",
|
||||
"system/App/Frontend/Feeds/Generator.php": "a2147e1443ececc8e2fe4ec3ddfb37bead2021198b779d2b09c4437f35768a94",
|
||||
"system/App/Frontend/Feeds/PathController.php": "faf72ea5ba08256c8566a5ee9a73adfcabd59ef0f510c108a6003b659659635b",
|
||||
"system/App/Frontend/Feeds/Repository.php": "3d36c493b3714780ed078537ecc8788c7f403a6a6eebb022128bd3e9b3e6dfa1",
|
||||
"system/App/Frontend/Feeds/Repository.php": "78576a9bb53eb1e982ee29f2ebd755ce882c24bf6e8204512adeba4db7e197b1",
|
||||
"system/App/Frontend/Feeds/Schema.php": "a4603347d631e6a748e0b245901f952736754e4e5e73c96289ed17a1998f5a31",
|
||||
"system/App/Frontend/Feeds/Service.php": "3d0f3badbc07a9d47e5902c106be270a24a71e2d64a636ca909b2393bdeb59f7",
|
||||
"system/App/Frontend/Feeds/Service.php": "2188a059be3b46131010bc68af39b74da59d135a4b7c8af2bf6d5bcbe6816cba",
|
||||
"system/App/Frontend/FieldEndpoint.php": "0bb0200a52169ecb77498df32d6ee3053324844cfa2e9200d6b3b819dd4c6aa8",
|
||||
"system/App/Frontend/HiddenContentRenderer.php": "62d34471f729ee37811b17f3eb80a4c6031dddc04f7b7bcc377cb79cc353649d",
|
||||
"system/App/Frontend/Media/Controller.php": "1a6102b612e14f7f595a0279d1ec47e1fbd828cb87f0bee0f619e6ca2213845e",
|
||||
@@ -1276,7 +1293,7 @@
|
||||
"system/App/Frontend/ModuleTagRenderer.php": "0b41289126478a1aeda6cf7119f8f3d5f89b43bb7684c92ab5447d5dab408795",
|
||||
"system/App/Frontend/NavigationRenderer.php": "d176186b06d56832e640a8b3d53ccad4aff04e3834b261d83670d675271e4d00",
|
||||
"system/App/Frontend/NavigationRepository.php": "3714ca40102621191837fbed7511873327250ecb186c54bb0cce89781bdb0db1",
|
||||
"system/App/Frontend/PageComponentRenderer.php": "4176d2748b73b1c7cb72963cf67cfafd66246d93119728d9d688e6a48b882485",
|
||||
"system/App/Frontend/PageComponentRenderer.php": "83260a9d4d83603a99bf7aa8b620e06b78911f994b807b5fc179d3fd939b0812",
|
||||
"system/App/Frontend/PageShellRenderer.php": "e87c2ca686fbf6fd721a35656e752aaf590633a196c93715f75d36ddd78245fb",
|
||||
"system/App/Frontend/PageTemplateRepository.php": "58f73d71e6bb394f725aee0201f436236214acca343dc75bacdcfa752f3e8caf",
|
||||
"system/App/Frontend/PageTemplateResolver.php": "1af594b212cd4e30d5de361415a8e238493995d8bf955df9291e40169afe621c",
|
||||
@@ -1293,14 +1310,14 @@
|
||||
"system/App/Frontend/PublicNotFoundResponder.php": "52f165cd61251cea4189f6fa876e77b4934cf25cd973e7be14eb592f0999a390",
|
||||
"system/App/Frontend/PublicNotice.php": "b0eca5f46e64d4b0f33feadd126c81c8072a7ca959c13c2194defb8a3a01f027",
|
||||
"system/App/Frontend/PublicPageContext.php": "cc41038f45307b8abbd18b843cd0d82e1f77e8bc36d2807fef17c5a49b4f5c49",
|
||||
"system/App/Frontend/PublicPageLifecycle.php": "e973e9648354eb36c8bd00c84c6a124910c82f5eba9c456c042960927e1b485f",
|
||||
"system/App/Frontend/PublicPageLifecycle.php": "74f9b301d6d7bdc336aa490f3080705c5098ae155b99d164ea5df3708fa9e4eb",
|
||||
"system/App/Frontend/PublicPageRenderer.php": "10cb6a6b8f6e00998619ca4e92b4c85324cf19ccf955806647ccd09e94971a5a",
|
||||
"system/App/Frontend/PublicProfiler.php": "81d276cae6c51474987d9099e1f916a7e24b0ed03abf0819e3a9f08152196c52",
|
||||
"system/App/Frontend/PublicRedirectResponder.php": "67b0915e9820fb5d0f4270d86ea39d99502acc49c86997aef7fe0d9ef3fe7b60",
|
||||
"system/App/Frontend/PublicRequestApi.php": "803113eaeaf272d423ba39331e0dfa25a540251aab4961d2b796a6734215ed37",
|
||||
"system/App/Frontend/PublicSettings.php": "e70b9a478cefe4eb9f55ba0cde37ed14e270be345d08773e8a6b736be3bef321",
|
||||
"system/App/Frontend/QuickEdit/PublicQuickEdit.php": "11e2c721e676d3ddba73c6f8cbe11ac9a9ad15acfd4e97ddfd88ba7f8f43dfd7",
|
||||
"system/App/Frontend/RequestFieldRenderer.php": "01d6e96d630727f740874334e7ffbdfbdc076d896a6d8a390723c8aaaec41c97",
|
||||
"system/App/Frontend/RequestFieldRenderer.php": "5065a2bb8b630eeffb4e3ac978e6abf15f296e523b6a0a3421c4f3a870258cdf",
|
||||
"system/App/Frontend/RequestItemContext.php": "28f70292d7feeb77b88246d1d3299ff54447afc1aa6c525211835a4cfd51ebb4",
|
||||
"system/App/Frontend/RequestItemRenderer.php": "6426d63609073d85938dbaef0f77935d856a57eca225503d812e31f07ab98790",
|
||||
"system/App/Frontend/RequestListReadModel.php": "7eedb83e42382cada17e60484108702a52f8a890b59d93cd921e194025af6064",
|
||||
@@ -1313,10 +1330,10 @@
|
||||
"system/App/Frontend/SiteAccess.php": "a1cc5a86ab9f88c22214b9cb642556d7e6ffe73b00084de988ddd730805410fc",
|
||||
"system/App/Frontend/Sitemap/Controller.php": "7a1504cb00897bba10ba0d6d297c7338f8c9eda8782577ef1ef8f8316a35846a",
|
||||
"system/App/Frontend/Sitemap/Feature.php": "7ff7afcc57c8533acdd1050a40a0c4ca65815668f5e8c72d4b02bdd6c92ae8fd",
|
||||
"system/App/Frontend/Sitemap/Repository.php": "f7d645987328e97bd01b8204cea3f40fe22921c7f07806ebeba2fe53be80f02f",
|
||||
"system/App/Frontend/Sitemap/Repository.php": "2dff32e665493d6cd7e6946952541ad414d460d8c1b0d873161b134e24ed1ffe",
|
||||
"system/App/Frontend/StoredTemplateApi.php": "042e21a93a6c914ac6cf53465b5292833a8240645870ac1bcf29e6fed0a2288f",
|
||||
"system/App/Frontend/ThemeAssets.php": "5870bace8823561820b64752e5dba16e434449e12de317ad4085087b31232396",
|
||||
"system/App/Frontend/ThemePageShellRenderer.php": "483e2cf8ef91099b5ff3b3c1d70aa3a4d2ddf354add2bff650ec523150384ea9",
|
||||
"system/App/Frontend/ThemePageShellRenderer.php": "86638d6559c6b57f540ff2705ea3ba7607001ee46c94e9398ece474a34b05f52",
|
||||
"system/App/Frontend/ThemeSettings.php": "77509ac00183373005f2456a58abf2c8608d6677751f7540d8cb8f5db36284a4",
|
||||
"system/App/Frontend/ThemeSettingsTags.php": "13fa6b59c8518dca33f6b3482e0f9a00c14a248d430756f62a9b0c5175346fab",
|
||||
"system/App/Frontend/view/content/list.twig": "60db68391fa334c46694d3d646a0458e661e147770feb86e7be174b62e1f1f65",
|
||||
@@ -1354,7 +1371,7 @@
|
||||
"system/language/ru/lang.xml": "7d46cc9822f064a1af4d6528ccfe2dd4a9cc318912e327fd807befd2ca3dfd11",
|
||||
"system/preload.php": "66bfb1aaf36ba85945b729b09a37e96ead0be1cc2749ebd9abd4b9da4f25ba0e",
|
||||
"system/public.php": "3f72b5bf44bc0ba5ba6fb6b3adc2e2f9d5e9902d9cdfa3d7b748f4aebd179b87",
|
||||
"system/release.php": "313f3e82832ba2ed320647a9a3eee63b54cda5b390b8b1916fb9ceb7d2344764",
|
||||
"system/release.php": "96de420b08cdefac694c48399015fdbccc9bc81c5992d1052254934c8e0d2f2e",
|
||||
"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",
|
||||
@@ -2118,18 +2135,31 @@
|
||||
"adminx/modules/Blocks/module.php": "core",
|
||||
"adminx/modules/Blocks/view/index.twig": "core",
|
||||
"adminx/modules/Catalog/Controller.php": "core",
|
||||
"adminx/modules/Catalog/EditorContextLinks.php": "core",
|
||||
"adminx/modules/Catalog/Model.php": "core",
|
||||
"adminx/modules/Catalog/assets/catalog-drawer.css": "core",
|
||||
"adminx/modules/Catalog/assets/catalog.css": "core",
|
||||
"adminx/modules/Catalog/assets/catalog.js": "core",
|
||||
"adminx/modules/Catalog/assets/context-links.css": "core",
|
||||
"adminx/modules/Catalog/language/en/client.xml": "core",
|
||||
"adminx/modules/Catalog/language/en/context-links.xml": "core",
|
||||
"adminx/modules/Catalog/language/en/interface.xml": "core",
|
||||
"adminx/modules/Catalog/language/en/product-roles.xml": "core",
|
||||
"adminx/modules/Catalog/language/en/product-selection.xml": "core",
|
||||
"adminx/modules/Catalog/language/en/runtime.xml": "core",
|
||||
"adminx/modules/Catalog/language/ru/client.xml": "core",
|
||||
"adminx/modules/Catalog/language/ru/context-links.xml": "core",
|
||||
"adminx/modules/Catalog/language/ru/interface.xml": "core",
|
||||
"adminx/modules/Catalog/language/ru/product-roles.xml": "core",
|
||||
"adminx/modules/Catalog/language/ru/product-selection.xml": "core",
|
||||
"adminx/modules/Catalog/language/ru/runtime.xml": "core",
|
||||
"adminx/modules/Catalog/migrations/005_add_catalog_purpose.sql": "core",
|
||||
"adminx/modules/Catalog/migrations/006_add_item_sources.php": "core",
|
||||
"adminx/modules/Catalog/module.php": "core",
|
||||
"adminx/modules/Catalog/view/_context-links.twig": "core",
|
||||
"adminx/modules/Catalog/view/_product-role-preview.twig": "core",
|
||||
"adminx/modules/Catalog/view/_product-roles.twig": "core",
|
||||
"adminx/modules/Catalog/view/_product-selection-preview.twig": "core",
|
||||
"adminx/modules/Catalog/view/edit.twig": "core",
|
||||
"adminx/modules/Catalog/view/index.twig": "core",
|
||||
"adminx/modules/Console/Controller.php": "core",
|
||||
@@ -2251,6 +2281,7 @@
|
||||
"adminx/modules/Documents/migrations/014_document_relation_edges.sql": "core",
|
||||
"adminx/modules/Documents/migrations/015_document_creation_presets.sql": "core",
|
||||
"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_panels.twig": "core",
|
||||
"adminx/modules/Documents/view/api.twig": "core",
|
||||
@@ -2385,10 +2416,8 @@
|
||||
"adminx/modules/Requests/Revisions.php": "core",
|
||||
"adminx/modules/Requests/assets/requests.css": "core",
|
||||
"adminx/modules/Requests/assets/requests.js": "core",
|
||||
"adminx/modules/Requests/language/en/client.xml": "core",
|
||||
"adminx/modules/Requests/language/en/interface.xml": "core",
|
||||
"adminx/modules/Requests/language/en/runtime.xml": "core",
|
||||
"adminx/modules/Requests/language/ru/client.xml": "core",
|
||||
"adminx/modules/Requests/language/ru/interface.xml": "core",
|
||||
"adminx/modules/Requests/language/ru/runtime.xml": "core",
|
||||
"adminx/modules/Requests/migrations/001_native_database_calls.sql": "core",
|
||||
@@ -2452,6 +2481,7 @@
|
||||
"adminx/modules/Rubrics/migrations/014_rubric_purpose.php": "core",
|
||||
"adminx/modules/Rubrics/migrations/015_merge_directory_permissions.php": "core",
|
||||
"adminx/modules/Rubrics/migrations/016_rubric_trash.sql": "core",
|
||||
"adminx/modules/Rubrics/migrations/017_rubric_public_visibility.sql": "core",
|
||||
"adminx/modules/Rubrics/module.php": "core",
|
||||
"adminx/modules/Rubrics/view/_content_tabs.twig": "core",
|
||||
"adminx/modules/Rubrics/view/field-sets.twig": "core",
|
||||
@@ -2466,6 +2496,7 @@
|
||||
"adminx/modules/Security/language/ru/interface.xml": "core",
|
||||
"adminx/modules/Security/language/ru/runtime.xml": "core",
|
||||
"adminx/modules/Security/migrations/001_create_ip_blocks.sql": "core",
|
||||
"adminx/modules/Security/migrations/002_create_user_agent_blocks.php": "core",
|
||||
"adminx/modules/Security/module.php": "core",
|
||||
"adminx/modules/Security/view/index.twig": "core",
|
||||
"adminx/modules/Settings/Constants.php": "core",
|
||||
@@ -3008,6 +3039,7 @@
|
||||
"system/App/Content/Documents/DocumentSnapshotBuilder.php": "core",
|
||||
"system/App/Content/Documents/DocumentSnapshotRepository.php": "core",
|
||||
"system/App/Content/Documents/DocumentSnapshotStore.php": "core",
|
||||
"system/App/Content/Documents/DocumentVisibility.php": "core",
|
||||
"system/App/Content/Documents/FieldTemplateManifest.php": "core",
|
||||
"system/App/Content/Documents/RubricSchemaBuilder.php": "core",
|
||||
"system/App/Content/ExtensionTables.php": "core",
|
||||
@@ -3102,6 +3134,7 @@
|
||||
"system/App/Content/Presentation/PresentationRepository.php": "core",
|
||||
"system/App/Content/Presentation/PresentationRevisionRepository.php": "core",
|
||||
"system/App/Content/Presentation/PresentationSyntax.php": "core",
|
||||
"system/App/Content/ProductDerivedCacheInvalidator.php": "core",
|
||||
"system/App/Content/ProductShipping.php": "core",
|
||||
"system/App/Content/PublicShellTables.php": "core",
|
||||
"system/App/Content/PublicUserTables.php": "core",
|
||||
@@ -3149,6 +3182,7 @@
|
||||
"system/App/Frontend/Auth/UserRepository.php": "core",
|
||||
"system/App/Frontend/Auth/assets/auth.css": "core",
|
||||
"system/App/Frontend/Auth/assets/auth.js": "core",
|
||||
"system/App/Frontend/Auth/assets/phone-mask.js": "core",
|
||||
"system/App/Frontend/Auth/view/login.twig": "core",
|
||||
"system/App/Frontend/Auth/view/message.twig": "core",
|
||||
"system/App/Frontend/Auth/view/oauth.twig": "core",
|
||||
|
||||
@@ -75,6 +75,46 @@
|
||||
return $saved;
|
||||
}
|
||||
|
||||
/** Синхронизировать переключатель из карточки модуля с общей раскладкой главной. */
|
||||
public static function setModuleDashboardVisibility($moduleCode, $visible, array $definitions)
|
||||
{
|
||||
$moduleCode = (string) $moduleCode;
|
||||
$targetCodes = array();
|
||||
foreach ($definitions as $definition) {
|
||||
if (!is_array($definition) || !isset($definition['module_code'], $definition['layout_code'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((string) $definition['module_code'] === $moduleCode) {
|
||||
$targetCodes[(string) $definition['layout_code']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$targetCodes) {
|
||||
return self::config('dashboard');
|
||||
}
|
||||
|
||||
$config = self::config('dashboard');
|
||||
$seen = array();
|
||||
foreach ($config as &$row) {
|
||||
$code = (string) $row['code'];
|
||||
if (isset($targetCodes[$code])) {
|
||||
$row['visible'] = (bool) $visible;
|
||||
$seen[$code] = true;
|
||||
}
|
||||
}
|
||||
|
||||
unset($row);
|
||||
foreach (array_keys($targetCodes) as $code) {
|
||||
if (!isset($seen[$code])) {
|
||||
$config[] = array('code' => $code, 'visible' => (bool) $visible);
|
||||
}
|
||||
}
|
||||
|
||||
Settings::set(self::key('dashboard'), $config, 'json');
|
||||
return $config;
|
||||
}
|
||||
|
||||
protected static function prepare(array $items, $codeKey)
|
||||
{
|
||||
$prepared = array();
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
public static function notifications($base, array $summary)
|
||||
{
|
||||
foreach (self::contributions('notifications') as $contribution) {
|
||||
$moduleCode = isset($contribution['module_code']) ? (string) $contribution['module_code'] : '';
|
||||
$data = isset($contribution['data']) ? $contribution['data'] : array();
|
||||
$items = isset($data['items']) && is_array($data['items']) ? $data['items'] : (self::isList($data) ? $data : array());
|
||||
foreach ($items as $item) {
|
||||
@@ -113,6 +114,11 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = isset($item['key']) ? trim((string) $item['key']) : '';
|
||||
if ($key !== '' && !self::notificationEnabled($moduleCode, $key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = isset($item['count']) ? max(0, (int) $item['count']) : 1;
|
||||
if ($count < 1) {
|
||||
continue;
|
||||
@@ -137,8 +143,7 @@
|
||||
'title' => AdminLocale::translateMarkup(isset($item['title']) ? (string) $item['title'] : ''),
|
||||
));
|
||||
$summary['total'] += $count;
|
||||
if (!empty($item['key'])) {
|
||||
$key = (string) $item['key'];
|
||||
if ($key !== '') {
|
||||
$summary[$key] = isset($summary[$key]) ? (int) $summary[$key] + $count : $count;
|
||||
}
|
||||
}
|
||||
@@ -147,6 +152,80 @@
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/** Настраиваемые элементы колокольчика без запуска их провайдеров. */
|
||||
public static function notificationPreferences()
|
||||
{
|
||||
self::boot();
|
||||
$out = array();
|
||||
foreach (self::$extensions as $code => $extension) {
|
||||
$module = self::currentModule($code, $extension['module']);
|
||||
if (empty($module['installed']) || empty($module['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$config = $extension['config'];
|
||||
if (empty($config['notifications'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (self::normalizeItems($config['notifications']) as $item) {
|
||||
if (!is_array($item) || !self::allowed($item) || empty($item['preferences']) || !is_array($item['preferences'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($item['preferences'] as $preference) {
|
||||
if (!is_array($preference)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = isset($preference['key']) ? trim((string) $preference['key']) : '';
|
||||
$label = isset($preference['label']) ? trim((string) $preference['label']) : '';
|
||||
if (!preg_match('/^[a-z][a-z0-9_]{0,63}$/', $key) || $label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$default = !empty($preference['default']);
|
||||
$out[] = array(
|
||||
'module_code' => (string) $code,
|
||||
'key' => $key,
|
||||
'label' => AdminLocale::translateMarkup($label),
|
||||
'description' => AdminLocale::translateMarkup(isset($preference['description']) ? (string) $preference['description'] : ''),
|
||||
'enabled' => (bool) ModuleSettings::get(self::notificationSettingKey($key), $default, $code),
|
||||
'default' => $default,
|
||||
'sort_order' => isset($preference['sort_order']) ? (int) $preference['sort_order'] : 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($out, function ($left, $right) {
|
||||
if ($left['sort_order'] === $right['sort_order']) {
|
||||
return strcmp($left['label'], $right['label']);
|
||||
}
|
||||
|
||||
return $left['sort_order'] <=> $right['sort_order'];
|
||||
});
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Сохранить один объявленный модулем переключатель уведомлений. */
|
||||
public static function setNotificationPreference($moduleCode, $key, $enabled)
|
||||
{
|
||||
$moduleCode = strtolower(trim((string) $moduleCode));
|
||||
$key = strtolower(trim((string) $key));
|
||||
foreach (self::notificationPreferences() as $preference) {
|
||||
if ($preference['module_code'] !== $moduleCode || $preference['key'] !== $key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ModuleSettings::set(self::notificationSettingKey($key), (bool) $enabled, $moduleCode, 'bool');
|
||||
$preference['enabled'] = (bool) $enabled;
|
||||
return $preference;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Настройка уведомления не найдена');
|
||||
}
|
||||
|
||||
public static function catalog()
|
||||
{
|
||||
self::boot();
|
||||
@@ -410,4 +489,20 @@
|
||||
{
|
||||
return (bool) ModuleSettings::get('adminx_' . $type . '_enabled', true, $code);
|
||||
}
|
||||
|
||||
protected static function notificationEnabled($moduleCode, $key)
|
||||
{
|
||||
foreach (self::notificationPreferences() as $preference) {
|
||||
if ($preference['module_code'] === (string) $moduleCode && $preference['key'] === (string) $key) {
|
||||
return !empty($preference['enabled']);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function notificationSettingKey($key)
|
||||
{
|
||||
return 'adminx_notification_' . preg_replace('/[^a-z0-9_]+/', '_', strtolower((string) $key)) . '_enabled';
|
||||
}
|
||||
}
|
||||
|
||||
+117
-7
@@ -16,8 +16,12 @@
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Common\PublicConfiguration;
|
||||
|
||||
class PhpCode
|
||||
{
|
||||
const LINT_TIMEOUT = 3;
|
||||
|
||||
public static function source($code)
|
||||
{
|
||||
$code = trim((string) $code);
|
||||
@@ -41,12 +45,27 @@
|
||||
return array('ok' => false, 'message' => 'Не удалось записать временный файл.', 'output' => '', 'line' => 0);
|
||||
}
|
||||
|
||||
$php = defined('PHP_BINARY') && PHP_BINARY ? PHP_BINARY : 'php';
|
||||
$output = array();
|
||||
$status = 0;
|
||||
@exec(escapeshellarg($php) . ' -l ' . escapeshellarg($file) . ' 2>&1', $output, $status);
|
||||
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);
|
||||
$text = trim(implode("\n", $output));
|
||||
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)) {
|
||||
@@ -54,10 +73,101 @@
|
||||
}
|
||||
|
||||
return array(
|
||||
'ok' => $status === 0 && $text !== '',
|
||||
'message' => $status === 0 ? 'Синтаксис PHP без ошибок.' : 'В PHP-коде есть синтаксическая ошибка.',
|
||||
'ok' => $result['exit_code'] === 0 && $text !== '',
|
||||
'message' => $result['exit_code'] === 0 ? 'Синтаксис PHP без ошибок.' : 'В PHP-коде есть синтаксическая ошибка.',
|
||||
'output' => $text,
|
||||
'line' => $line,
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
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 : ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
protected static function definitions()
|
||||
{
|
||||
return array(
|
||||
self::item('#^/documents/bulk-editor$#', 'Массовый редактор', 'Меняет одинаковые настройки сразу у выбранной группы документов и сначала показывает точный предпросмотр.', array('Служебный документ остаётся доступен полям, запросам и связям, но его публичный адрес возвращает 404.', 'Служебные документы автоматически исключаются из публичного поиска, sitemap, SEO-задач и проверок качества.', 'Внутренний поиск, robots и участие в sitemap можно менять отдельными действиями, не скрывая саму страницу.'), 'Сначала выберите рубрику или другой узкий фильтр, проверьте примеры и только затем запускайте изменение.'),
|
||||
self::item('#^/documents/redirects$#', 'Редиректы документов', 'Управляет старыми адресами страниц и перенаправляет посетителей на актуальные документы.', array('Ищите редирект по старому URL, названию или ID документа.', 'Коды 301/308 подходят для постоянного переноса, 302/307 — для временного.', 'Перед удалением убедитесь, что старый адрес больше не используется во внешних ссылках.'), 'Каждый старый URL должен быть уникальным и вести на существующий документ.'),
|
||||
self::item('#^/documents/views$#', 'Статистика просмотров', 'Показывает посещаемость документов по дням и помогает находить наиболее востребованные страницы.', array('Период влияет только на подневную статистику.', 'Общий счётчик документа хранится отдельно и не очищается вместе с дневными данными.', 'Очистка статистики необратима и предназначена для обслуживания.'), 'Для бизнес-аналитики используйте внешнюю систему аналитики; здесь хранится внутренняя статистика CMS.'),
|
||||
self::item('#^/documents/(?:create|\d+/edit)$#', 'Редактор документа', 'Создаёт и изменяет страницу сайта: её адрес, публикацию, SEO и значения полей выбранной рубрики.', array('Набор полей и их группы задаются рубрикой.', 'Изменение alias может автоматически создать редирект со старого URL.', 'Ctrl+S сохраняет изменения и оставляет редактор открытым; ревизии позволяют вернуться к прошлой версии.'), 'Смена рубрики существующего документа запрещена без отдельной миграции полей.'),
|
||||
@@ -133,12 +134,12 @@
|
||||
self::item('#^/settings/benchmark$#', 'Производительность', 'Измеряет скорость PHP, базы данных, файловых операций и кеша на текущем сервере.', array('Результаты нужны для сравнения одного окружения во времени, а не разных проектов.', 'Повторяйте тест после обновления PHP, БД или инфраструктуры.', 'Тест создаёт кратковременную нагрузку и сохраняет историю запусков.'), 'Оценивайте не только общий балл, но и конкретный медленный этап.'),
|
||||
self::item('#^/settings#', 'Настройки системы', 'Собирает общие параметры AVE.cms, интерфейс, константы, пагинацию, обслуживание и диагностику.', array('Каждый подраздел имеет собственный URL и назначение.', 'Типизированные значения проверяются перед сохранением.', 'Настройки устанавливаемых модулей хранятся вместе с соответствующим модулем.'), 'Меняйте только параметры, назначение которых понятно и проверено на текущем сайте.', 'administration/settings'),
|
||||
self::item('#^/events#', 'Системные события', 'Объединяет аудит действий, ошибки, SQL-журнал, 404 и статистику внешних переходов.', array('Цвет и состояние помогают отличать успешные операции, предупреждения и ошибки.', 'Переходы группируются по посетителю, странице входа и источнику; открытые IP и произвольные query-параметры не сохраняются.', 'Фильтры и CSV-экспорт работают отдельно для каждого источника.'), 'Очистка журнала необратима и не исправляет причину ошибки.'),
|
||||
self::item('#^/security/ip-blocks#', 'IP-блокировки', 'Управляет ручными блокировками адресов, а автоматические ограничения выполняет RateLimiter.', array('Блокировка может быть постоянной или иметь дату окончания.', 'Причина видна администраторам и записывается в аудит.', 'Разблокировка возвращает доступ сразу.'), 'Не блокируйте адрес reverse proxy без проверки реального клиентского IP.'),
|
||||
self::item('#^/security/ip-blocks#', 'Блокировки запросов', 'Ограничивает доступ к публичному сайту по IP-адресу или характерной строке User-Agent.', array('IP и User-Agent находятся на отдельных вкладках.', 'Правило может быть постоянным или иметь дату окончания.', 'Для робота используйте его уникальное имя, а не общие слова Mozilla, Chrome или Safari.', 'Причина и действия блокировки записываются в аудит.'), 'Перед блокировкой User-Agent проверьте предложенный фрагмент: слишком общее правило может затронуть обычных посетителей.'),
|
||||
self::item('#^/database#', 'База данных', 'Показывает рабочие таблицы AVE.cms, выполняет обслуживание, применяет миграции ядра и управляет полными резервными копиями.', array('OPTIMIZE применяется только к выбранной таблице или ко всей рабочей схеме после подтверждения.', 'Применение миграций не восстанавливает данные: это отдельная операция развития структуры ядра.', 'Новые дампы используют {{prefix}}, поэтому могут восстанавливаться с другим dbpref.', 'Перед восстановлением система проверяет файл и автоматически создаёт страховочную копию.'), 'Восстановление полностью заменяет таблицы текущей установки; рабочую копию дополнительно храните вне сервера.', 'database'),
|
||||
self::item('#^/kanban$#', 'Канбан', 'Организует рабочие карточки по колонкам и показывает доску на дашборде.', array('Карточки и порядок сохраняются без перезагрузки.', 'Колонки можно настроить под собственный процесс.', 'Карточке можно назначить ответственного, срок и связанный документ.', 'Командная карточка видна коллегам, а назначенная отдельно появляется у ответственного.'), 'Колонки принадлежат владельцу доски: назначенный сотрудник видит карточку в общем списке, но не перестраивает чужую доску.'),
|
||||
self::item('#^/notes$#', 'Заметки', 'Хранит личные текстовые заметки сотрудника и закрепляет важные записи на дашборде.', array('Заметки видны только их владельцу.', 'Закрепление влияет на порядок выдачи.', 'Быстрое создание доступно из шапки, если вклад модуля включён.'), 'Не храните в заметках пароли, токены и другие секреты.'),
|
||||
self::item('#^/reminders$#', 'Напоминания', 'Хранит личные задачи с датой и выводит просроченные пункты в общем колокольчике.', array('Срок определяет состояние напоминания.', 'Выполнение сохраняется сразу.', 'Просроченный счётчик формируется отдельно для текущего сотрудника.'), 'Удаление напоминания необратимо; выполненный пункт можно вернуть в работу.'),
|
||||
self::item('#^/notfound$#', 'Ошибки 404', 'Показывает несуществующие публичные адреса и помогает создать документ или редирект.', array('Сначала проверьте источник переходов и частоту запроса.', 'Для старого адреса существующей страницы создайте постоянный редирект.', 'Случайные сканирующие URL можно удалить из журнала без создания страницы.'), 'Редирект должен вести на действительно соответствующий документ, а не всегда на главную.'),
|
||||
self::item('#^/notfound$#', 'Ошибки 404', 'Показывает несуществующие публичные адреса, источник перехода, IP и программу или робота, который сделал запрос.', array('Кнопка «Подробнее» раскрывает полный реферер, User-Agent и время первого и последнего обращения.', 'Для старого адреса существующей страницы создайте постоянный редирект.', 'Нежелательного робота можно отправить в блокировки прямо из записи.', 'CSV учитывает текущий поиск и переключатель нерешённых записей.', 'Очистка всего журнала необратима, но новые ошибки начнут собираться снова.'), 'Не блокируйте обычный браузер только из-за одного ошибочного адреса; сначала оцените источник и частоту обращений.'),
|
||||
self::item('#^/seo-audit$#', 'SEO-аудит', 'Проверяет документы на технические и редакционные проблемы, влияющие на поисковое представление.', array('Фильтры помогают сначала разобрать критичные и массовые проблемы.', 'Оценка документа складывается из нескольких независимых проверок.', 'Исправления выполняются в редакторе документа и проявляются после повторного аудита.'), 'Автоматическая оценка является подсказкой и не заменяет проверку содержания страницы.', 'modules/seo-audit'),
|
||||
self::item('#^/todo$#', 'Todo', 'Хранит рабочие задачи сотрудников со сроками и связями с документами.', array('Задачу можно создать из выпадающего списка в шапке на любой странице панели управления.', 'Ответственный видит назначенную задачу и может отметить её выполненной.', 'Режим «Команда» открывает задачу всем сотрудникам с правом просмотра Todo.', 'Связанный документ открывается прямо из строки задачи.'), 'Изменять и удалять задачу может её создатель; выполнение доступно создателю и ответственному.'),
|
||||
self::item('#^/modules/search#', 'Поиск по сайту', 'Индексирует опубликованные документы и формирует обычную страницу результатов и JSON для живого поиска.', array('Области позволяют отдельно искать по товарам, новостям, статьям и другим наборам рубрик.', 'Алгоритм определяет разбор строки и вес точного совпадения, заголовка и содержимого.', 'Штатный HTML можно заменить шаблонами модуля или существующим запросом документов.'), 'После изменения рубрик или индексируемых полей выполните полную переиндексацию.', 'modules/search'),
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
'legacy_migration.write' => 'Изменение реквизитов исходной системы',
|
||||
'legacy_migration.execute' => 'Очистка стартовых данных и миграция старой AVE.cms',
|
||||
'database.restore' => 'Полная замена рабочей схемы данными из резервной копии',
|
||||
'external_connection.write' => 'Изменение адреса или секрета внешнего подключения',
|
||||
);
|
||||
protected static $required = array(
|
||||
'POST /modules/archive/install' => array('module.code', 'install_module_code'),
|
||||
|
||||
@@ -537,6 +537,16 @@ hr {
|
||||
.notif-head b {
|
||||
font-size: 13px;
|
||||
}
|
||||
.notif-head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.notif-head-actions .btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
.notif-list {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
@@ -591,6 +601,66 @@ hr {
|
||||
margin-bottom: 6px;
|
||||
color: var(--color-success);
|
||||
}
|
||||
.notif-settings {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
background: var(--background-subtle);
|
||||
border-top: 1px solid var(--border-default);
|
||||
}
|
||||
.notif-settings-copy {
|
||||
padding: 8px 8px 6px;
|
||||
}
|
||||
.notif-settings-copy b,
|
||||
.notif-settings-copy small {
|
||||
display: block;
|
||||
}
|
||||
.notif-settings-copy b {
|
||||
font-size: 12px;
|
||||
}
|
||||
.notif-settings-copy small {
|
||||
margin-top: 2px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.notif-setting-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 44px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 56px;
|
||||
padding: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-card);
|
||||
cursor: pointer;
|
||||
}
|
||||
.notif-setting-row + .notif-setting-row {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.notif-setting-row > span:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
.notif-setting-row b,
|
||||
.notif-setting-row small {
|
||||
display: block;
|
||||
}
|
||||
.notif-setting-row b {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.notif-setting-row small {
|
||||
margin-top: 2px;
|
||||
color: var(--text-muted);
|
||||
font-size: 10.5px;
|
||||
line-height: 1.3;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.notif-setting-row .switch {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
justify-content: center;
|
||||
}
|
||||
/* Заголовок в меню пользователя */
|
||||
#userMenu > .dropdown-menu {
|
||||
width: 280px;
|
||||
@@ -3137,18 +3207,27 @@ table.table {
|
||||
gap: 12px;
|
||||
padding: var(--space-5) var(--space-5) 0;
|
||||
}
|
||||
.modal-header > div {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.modal-header h3 {
|
||||
flex: 1;
|
||||
}
|
||||
.modal-close {
|
||||
display: grid;
|
||||
flex: 0 0 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
place-items: center;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
padding: 4px;
|
||||
padding: 0;
|
||||
border-radius: var(--radius-xs);
|
||||
margin: -4px -4px 0 0;
|
||||
margin: -4px -4px 0 auto;
|
||||
}
|
||||
.modal-close:hover {
|
||||
color: var(--text-primary);
|
||||
|
||||
@@ -488,6 +488,53 @@
|
||||
}
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Видимость отдельных элементов в колокольчике.
|
||||
// ------------------------------------------------------------------ //
|
||||
Adminx.NotificationPreferences = {
|
||||
init: function () {
|
||||
document.addEventListener('click', function (event) {
|
||||
var toggle = event.target.closest('[data-notification-settings-toggle]');
|
||||
if (!toggle) { return; }
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var menu = toggle.closest('.notif-menu');
|
||||
var panel = menu ? menu.querySelector('[data-notification-settings]') : null;
|
||||
var list = menu ? menu.querySelector('.notif-list') : null;
|
||||
if (!panel) { return; }
|
||||
var opening = panel.hidden;
|
||||
panel.hidden = !opening;
|
||||
if (list) { list.hidden = opening; }
|
||||
toggle.setAttribute('aria-expanded', opening ? 'true' : 'false');
|
||||
});
|
||||
|
||||
document.addEventListener('change', function (event) {
|
||||
var input = event.target.closest('[data-notification-preference]');
|
||||
if (!input) { return; }
|
||||
var previous = !input.checked;
|
||||
var data = new FormData();
|
||||
data.append('module_code', input.getAttribute('data-module-code') || '');
|
||||
data.append('key', input.getAttribute('data-key') || '');
|
||||
data.append('enabled', input.checked ? '1' : '0');
|
||||
input.disabled = true;
|
||||
Adminx.Ajax.post(Adminx.base() + '/settings/notifications', data).then(function (payload) {
|
||||
var response = Adminx.Ajax.handle(payload);
|
||||
if (!payload.ok || response.success === false) {
|
||||
input.checked = previous;
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
}).catch(function () {
|
||||
input.checked = previous;
|
||||
Adminx.Toast.show(Adminx.t('server_unavailable', 'Сервер недоступен. Повторите попытку.'), 'error');
|
||||
}).then(function () {
|
||||
input.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Меню пользователя в подвале сайдбара. Сайдбар имеет overflow:hidden,
|
||||
// поэтому всплывающее меню позиционируем fixed вручную (над триггером),
|
||||
@@ -977,6 +1024,7 @@
|
||||
Adminx.Sidebar.init();
|
||||
Adminx.Theme.init();
|
||||
Adminx.Dropdown.init();
|
||||
Adminx.NotificationPreferences.init();
|
||||
Adminx.SidebarUser.init();
|
||||
Adminx.Tabs.init();
|
||||
Adminx.Drawer.init();
|
||||
|
||||
+22
-1
@@ -62,6 +62,7 @@
|
||||
//-- Ядро: автозагрузчик, БД, сессии, настройки, Twig, хуки.
|
||||
include BASEPATH . DS . 'system' . DS . 'bootstrap.php';
|
||||
App::init();
|
||||
\App\Common\ResponseSecurityHeaders::apply(array('http://127.0.0.1:*', 'http://localhost:*'));
|
||||
|
||||
use App\Common\AdminAssets;
|
||||
use App\Common\Auth;
|
||||
@@ -207,6 +208,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($navFlat['modules']['children'])) {
|
||||
usort($navFlat['modules']['children'], function ($left, $right) {
|
||||
$leftRegistry = isset($left['code']) && (string) $left['code'] === 'modules_registry';
|
||||
$rightRegistry = isset($right['code']) && (string) $right['code'] === 'modules_registry';
|
||||
if ($leftRegistry !== $rightRegistry) { return $leftRegistry ? -1 : 1; }
|
||||
$leftLabel = isset($left['label']) ? (string) $left['label'] : '';
|
||||
$rightLabel = isset($right['label']) ? (string) $right['label'] : '';
|
||||
$leftCyrillic = preg_match('/^\p{Cyrillic}/u', $leftLabel) === 1;
|
||||
$rightCyrillic = preg_match('/^\p{Cyrillic}/u', $rightLabel) === 1;
|
||||
if ($leftCyrillic !== $rightCyrillic) { return $leftCyrillic ? -1 : 1; }
|
||||
return strnatcasecmp($leftLabel, $rightLabel);
|
||||
});
|
||||
}
|
||||
|
||||
$commandPaletteItems = array();
|
||||
foreach ($navigationItems as $navItem) {
|
||||
if (!isset($navItem['url']) || (string) $navItem['url'] === '#') { continue; }
|
||||
@@ -249,7 +264,9 @@
|
||||
|
||||
//-- Сводка уведомлений в шапке (новые заказы/письма) — только для админа.
|
||||
$notifications = array('total' => 0, 'orders' => 0, 'messages' => 0, 'items' => array());
|
||||
$notificationPreferences = array();
|
||||
if ($canAdmin) {
|
||||
$notificationPreferences = \App\Adminx\Support\ModuleExtensions::notificationPreferences();
|
||||
$notificationKey = 'adminx.notifications.' . (int) Auth::id() . '.' . md5((string) ADMINX_BASE);
|
||||
$notifications = Cache::remember($notificationKey, 15, function () {
|
||||
$summary = \App\Adminx\Support\Notifications::summary(ADMINX_BASE);
|
||||
@@ -257,7 +274,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
Twig::addGlobal('notifications', $notifications);
|
||||
Twig::addGlobals(array(
|
||||
'notifications' => $notifications,
|
||||
'notification_preferences' => $notificationPreferences,
|
||||
'can_manage_notification_preferences' => Permission::check('manage_settings'),
|
||||
));
|
||||
|
||||
if (!$isPublic && !$canAdmin) {
|
||||
if (Request::isAjax()) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<phrase data="btn_cancel">Cancel</phrase>
|
||||
<phrase data="btn_close">Close</phrase>
|
||||
<phrase data="btn_confirm">Confirm</phrase>
|
||||
<phrase data="btn_create">Create</phrase>
|
||||
<phrase data="btn_delete">Delete</phrase>
|
||||
<phrase data="btn_save">Save</phrase>
|
||||
<phrase data="btn_show">Show</phrase>
|
||||
@@ -34,6 +35,7 @@
|
||||
<phrase data="topbar_theme">Theme</phrase>
|
||||
<phrase data="ui_loading">Loading...</phrase>
|
||||
<phrase data="ui_main">General</phrase>
|
||||
<phrase data="ui_name">Name</phrase>
|
||||
<phrase data="ui_nothing_found">Nothing found</phrase>
|
||||
<phrase data="ui_remove">Remove</phrase>
|
||||
<phrase data="ui_search">Search</phrase>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<phrase data="btn_cancel">Отмена</phrase>
|
||||
<phrase data="btn_close">Закрыть</phrase>
|
||||
<phrase data="btn_confirm">Подтвердить</phrase>
|
||||
<phrase data="btn_create">Создать</phrase>
|
||||
<phrase data="btn_delete">Удалить</phrase>
|
||||
<phrase data="btn_save">Сохранить</phrase>
|
||||
<phrase data="btn_show">Показать</phrase>
|
||||
@@ -34,6 +35,7 @@
|
||||
<phrase data="topbar_theme">Тема</phrase>
|
||||
<phrase data="ui_loading">Загрузка...</phrase>
|
||||
<phrase data="ui_main">Основное</phrase>
|
||||
<phrase data="ui_name">Название</phrase>
|
||||
<phrase data="ui_nothing_found">Ничего не найдено</phrase>
|
||||
<phrase data="ui_remove">Убрать</phrase>
|
||||
<phrase data="ui_search">Поиск</phrase>
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
if (!Permission::check('view_products')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); }
|
||||
if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); }
|
||||
$this->assets();
|
||||
$state = Request::getStr('state', 'active');
|
||||
if (!in_array($state, array('active', 'inactive', 'all'), true)) { $state = 'active'; }
|
||||
$stats = Model::productQualityStats();
|
||||
$issues = Model::productQualityIssues($stats);
|
||||
$allowed = array();
|
||||
@@ -130,11 +132,15 @@
|
||||
$filters = array(
|
||||
'q' => Request::getStr('q', ''),
|
||||
'issue' => $issue,
|
||||
'state' => $state,
|
||||
'page' => Request::getInt('page', 1),
|
||||
'per_page' => Request::getInt('per_page', 25),
|
||||
'include_quality' => true,
|
||||
'quality_scope' => true,
|
||||
'quality_scope' => $state === 'active',
|
||||
);
|
||||
$aiModule = ModuleManager::get('ai_assistant');
|
||||
$aiAvailable = $aiModule && !empty($aiModule['installed']) && !empty($aiModule['enabled'])
|
||||
&& class_exists('\App\Modules\AiAssistant\Repository') && Permission::check('manage_ai_assistant');
|
||||
return $this->render('@products/product-quality.twig', array(
|
||||
'result' => Model::products($filters),
|
||||
'stats' => $stats,
|
||||
@@ -142,6 +148,8 @@
|
||||
'filters' => $filters,
|
||||
'saved_views' => SavedViews::all('catalog_quality', Auth::id(), $this->savedViewFields('quality')),
|
||||
'can_manage' => Permission::check('manage_products'),
|
||||
'can_toggle' => Permission::check('manage_products') && Permission::check('manage_documents'),
|
||||
'ai_available' => (bool) $aiAvailable,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1367,6 +1375,7 @@
|
||||
'shipping_profile' => $shippingProfile,
|
||||
'package_templates' => ShippingPackageTemplates::all(),
|
||||
'native_attributes' => $nativeAttributes,
|
||||
'product_context_links' => EditorContextLinks::product($document, $nativeAttributes),
|
||||
'product_readiness' => ProductReadiness::build($product, $nativeAttributes, $shippingProfile, $variantGroup),
|
||||
'product_promotions_available' => Promotions::available(),
|
||||
'product_promotions' => Promotions::forProduct($id),
|
||||
@@ -1692,9 +1701,12 @@
|
||||
$settings['filters_default_ids'] = $this->ids(isset($settings['filters_default']) ? $settings['filters_default'] : '');
|
||||
$styles = @unserialize(isset($settings['filters_default_settings']) ? (string) $settings['filters_default_settings'] : '', array('allowed_classes' => false));
|
||||
$settings['filter_styles'] = is_array($styles) ? $styles : array();
|
||||
$flat = Model::items($rubricId, $fieldId);
|
||||
return $this->render('@catalog/edit.twig', array(
|
||||
'catalog' => $catalog, 'settings' => $settings,
|
||||
'tree' => Model::tree($rubricId, $fieldId), 'flat' => Model::items($rubricId, $fieldId),
|
||||
'tree' => Model::tree($rubricId, $fieldId), 'flat' => $flat,
|
||||
'source_options' => Model::sourceOptions($rubricId, $fieldId),
|
||||
'source_schema_available' => Model::sourceItemsAvailable(),
|
||||
'fields' => $fields,
|
||||
'field_groups' => array_values($fieldGroups),
|
||||
'request_options' => Model::requestOptions(),
|
||||
@@ -1704,15 +1716,44 @@
|
||||
'filter_template_options' => $filterTemplatesAvailable ? FilterTemplates::options() : array(),
|
||||
'filter_templates_available' => $filterTemplatesAvailable,
|
||||
'products_available' => $productsAvailable,
|
||||
'product_roles' => $productsAvailable ? \App\Adminx\Packages\Products\ProductRoleSettings::view($rubricId, $settings, Request::getInt('product_preview', 0)) : array(),
|
||||
'can_manage' => Permission::check('manage_catalog'),
|
||||
));
|
||||
}
|
||||
|
||||
public function productPreview(array $params = array())
|
||||
{
|
||||
$rubricId = isset($params['rubric']) ? (int) $params['rubric'] : 0;
|
||||
$fieldId = isset($params['field']) ? (int) $params['field'] : 0;
|
||||
if (!Permission::check('view_catalog') || !$this->productsAvailable()
|
||||
|| !\App\Adminx\Packages\Products\ProductRoleSettings::canReadRubric($rubricId)) {
|
||||
return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403);
|
||||
}
|
||||
|
||||
$catalog = Model::catalog($rubricId, $fieldId);
|
||||
if (!$catalog) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Каталог не найден'), 404); }
|
||||
$settings = Model::settings($rubricId, $fieldId);
|
||||
$selection = array();
|
||||
foreach (array('context', 'category', 'feed', 'scope', 'query', 'source', 'limit') as $key) {
|
||||
$selection[$key] = Request::getStr('selection_' . $key, $key === 'context' ? 'catalog' : ($key === 'limit' ? '8' : ''));
|
||||
}
|
||||
|
||||
return $this->partial('@catalog/_product-role-preview.twig', array(
|
||||
'catalog' => $catalog, 'settings' => $settings,
|
||||
'product_roles' => \App\Adminx\Packages\Products\ProductRoleSettings::view($rubricId, $settings,
|
||||
Request::getInt('product_preview', 0), $selection, Request::getInt('selection_check', 0) === 1),
|
||||
));
|
||||
}
|
||||
|
||||
public function item(array $params = array())
|
||||
{
|
||||
if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); }
|
||||
$item = Model::item(isset($params['id']) ? (int) $params['id'] : 0);
|
||||
if ($item) { $item['condition_context'] = Model::conditionContext($item['id']); }
|
||||
if ($item) {
|
||||
$item['condition_context'] = Model::conditionContext($item['id']);
|
||||
$item['context_links'] = EditorContextLinks::section($item, $this->productsAvailable());
|
||||
}
|
||||
|
||||
return $item ? $this->success('', array('data' => $item)) : $this->error('Раздел не найден', array(), 404);
|
||||
}
|
||||
|
||||
@@ -1818,6 +1859,7 @@
|
||||
return $this->success($id > 0 ? 'Раздел сохранён' : 'Раздел создан', array('data' => array(
|
||||
'id' => $saved,
|
||||
'condition_context' => Model::conditionContext($saved),
|
||||
'context_links' => EditorContextLinks::section(Model::item($saved), $this->productsAvailable()),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -1893,7 +1935,7 @@
|
||||
protected function savedViewFields($scope)
|
||||
{
|
||||
return $scope === 'quality'
|
||||
? array('q', 'issue', 'per_page')
|
||||
? array('q', 'issue', 'state', 'per_page')
|
||||
: array('q', 'state', 'issue', 'payment', 'per_page', 'view');
|
||||
}
|
||||
|
||||
@@ -1934,6 +1976,7 @@
|
||||
{
|
||||
AdminAssets::addStyle($this->base() . '/modules/Catalog/assets/catalog.css', 50);
|
||||
AdminAssets::addStyle($this->base() . '/modules/Catalog/assets/catalog-drawer.css', 51);
|
||||
AdminAssets::addStyle($this->base() . '/modules/Catalog/assets/context-links.css', 52);
|
||||
AdminAssets::addScript($this->base() . '/modules/Catalog/assets/catalog.js', 50);
|
||||
if ($this->productsAvailable()) {
|
||||
if (is_file(BASEPATH . '/modules/products/admin/assets/products.css')) {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file adminx/modules/Catalog/EditorContextLinks.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\Catalog;
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Adminx\Support\AdminLocale;
|
||||
use App\Common\AdminLocation;
|
||||
use App\Common\Permission;
|
||||
use App\Content\CatalogTables;
|
||||
use App\Content\ContentTables;
|
||||
use App\Content\Presentation\PresentationRenderer;
|
||||
use App\Helpers\Html;
|
||||
use App\Modules\Products\CardSettings;
|
||||
use App\Modules\Products\CardTemplateRepository;
|
||||
use DB;
|
||||
|
||||
/** Saved editor context, linking to HTML parent pages rather than JSON drawer endpoints. */
|
||||
class EditorContextLinks
|
||||
{
|
||||
public static function product(array $document, array $attributes)
|
||||
{
|
||||
$links = static::page($document);
|
||||
$sections = DB::query(
|
||||
'SELECT DISTINCT i.id,i.rubric_id,i.field_id,i.name,i.filter_runtime FROM %b cp JOIN %b i ON i.id=cp.catalog_item_id WHERE cp.product_id=%i ORDER BY i.name,i.id',
|
||||
CatalogTables::table('catalog_category_products'), CatalogTables::table('module_catalog_items'), (int) $document['Id']
|
||||
)->getAll() ?: array();
|
||||
$sets = array();
|
||||
foreach ($attributes['groups'] as $group) {
|
||||
$id = (int) $group['set_id'];
|
||||
if (isset($sets[$id])) { continue; }
|
||||
$sets[$id] = true;
|
||||
static::add($links, 'attribute_set', '/catalog/attribute-sets/' . $id, array(), 'view_products', $group['set_name']);
|
||||
}
|
||||
|
||||
if (!$sections) { static::card($links, (int) $document['rubric_id'], array(), array()); }
|
||||
$catalogSettings = array();
|
||||
foreach ($sections as $section) {
|
||||
$key = (int) $section['rubric_id'] . ':' . (int) $section['field_id'];
|
||||
if (!isset($catalogSettings[$key])) {
|
||||
$catalogSettings[$key] = Model::settings((int) $section['rubric_id'], (int) $section['field_id']);
|
||||
}
|
||||
|
||||
static::filters($links, $section);
|
||||
static::card($links, (int) $document['rubric_id'], $section, $catalogSettings[$key]);
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
public static function section(array $section, $productsAvailable = true)
|
||||
{
|
||||
$document = !empty($section['document_id']) ? DB::query(
|
||||
'SELECT Id,rubric_id,rubric_tmpl_id FROM %b WHERE Id=%i LIMIT 1',
|
||||
ContentTables::table('documents'), (int) $section['document_id']
|
||||
)->getAssoc() : array();
|
||||
$links = $document ? static::page($document) : array();
|
||||
static::filters($links, $section, $productsAvailable);
|
||||
if (!$productsAvailable) { return $links; }
|
||||
$settings = Model::settings((int) $section['rubric_id'], (int) $section['field_id']);
|
||||
if (!empty($section['attribute_set_id'])) {
|
||||
static::add($links, 'attribute_set', '/catalog/attribute-sets/' . (int) $section['attribute_set_id'], array(), 'view_products', '#' . (int) $section['attribute_set_id']);
|
||||
}
|
||||
|
||||
if (isset($settings['purpose']) && $settings['purpose'] === 'commerce') {
|
||||
static::card($links, (int) $section['rubric_id'], $section, $settings);
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
protected static function filters(array &$links, array $section, $productsAvailable = true)
|
||||
{
|
||||
$native = isset($section['filter_runtime']) && $section['filter_runtime'] === 'native';
|
||||
if ($native && !$productsAvailable) { return; }
|
||||
if ($native) {
|
||||
static::add($links, 'filters', '/catalog/attributes', array('view' => 'filters', 'section_id' => (int) $section['id']), 'view_products', $section['name']);
|
||||
} elseif (static::allowed('manage_catalog')) {
|
||||
static::add($links, 'filters', '/catalog/' . (int) $section['rubric_id'] . '/' . (int) $section['field_id'], array('item' => (int) $section['id'], 'tab' => 'filters'), 'view_catalog', $section['name']);
|
||||
}
|
||||
}
|
||||
|
||||
protected static function card(array &$links, $rubricId, array $section, array $settings)
|
||||
{
|
||||
$targets = array('rubric' => (string) $rubricId, 'module' => 'products');
|
||||
if (!empty($section['rubric_id'])) { $targets['rubric'] = (string) $section['rubric_id']; }
|
||||
if (!empty($section['id'])) { $targets['catalog'] = (string) $section['id']; }
|
||||
if (!empty($settings['request_id'])) { $targets['request'] = (string) $settings['request_id']; }
|
||||
$scope = isset($section['name']) ? $section['name'] : '';
|
||||
$presentation = static::presentation($targets);
|
||||
if ($presentation && trim((string) $presentation['published_item_markup']) !== '') {
|
||||
if (static::allowed('manage_public_presentations')) {
|
||||
static::add($links, 'card', '/public-site/presentations', array('edit' => (int) $presentation['presentation_id']), 'view_public_site', $scope);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$template = static::cardTemplate($rubricId);
|
||||
if ($template && trim((string) $template['published_markup']) !== '') {
|
||||
if (static::allowed('manage_products')) {
|
||||
static::add($links, 'card', '/catalog/card-templates', array('edit' => (int) $template['id']), 'view_products', $scope);
|
||||
}
|
||||
} else {
|
||||
static::add($links, 'card', '/catalog/public-templates', array('template' => 'product-card'), 'view_products', $scope);
|
||||
}
|
||||
}
|
||||
|
||||
protected static function page(array $document)
|
||||
{
|
||||
$links = array();
|
||||
if (!static::allowed('view_rubrics') || !static::allowed('manage_rubrics')) { return $links; }
|
||||
$query = array('templates' => (int) $document['rubric_id']);
|
||||
$alternate = !empty($document['rubric_tmpl_id']) ? static::alternate($document) : null;
|
||||
// DocumentRepository falls back to the rubric template when the alternate is empty.
|
||||
if ($alternate && trim((string) $alternate['template']) !== '') { $query['template'] = (int) $alternate['id']; }
|
||||
static::add($links, 'page', '/rubrics', $query, 'view_rubrics');
|
||||
return $links;
|
||||
}
|
||||
|
||||
protected static function alternate(array $document)
|
||||
{
|
||||
return DB::query('SELECT id,template FROM %b WHERE id=%i AND rubric_id=%i LIMIT 1', ContentTables::table('rubric_templates'), (int) $document['rubric_tmpl_id'], (int) $document['rubric_id'])->getAssoc();
|
||||
}
|
||||
|
||||
protected static function presentation(array $targets) { return PresentationRenderer::resolve('content_list', $targets); }
|
||||
|
||||
protected static function cardTemplate($rubricId)
|
||||
{
|
||||
$settings = CardSettings::forRubric($rubricId);
|
||||
return CardTemplateRepository::published((int) $settings['template_id']);
|
||||
}
|
||||
|
||||
protected static function allowed($permission) { return Permission::check($permission); }
|
||||
|
||||
protected static function add(array &$links, $kind, $path, array $query, $permission, $scope = '')
|
||||
{
|
||||
if (!static::allowed($permission)) { return; }
|
||||
$url = AdminLocation::url($path);
|
||||
$links[] = array(
|
||||
'kind' => $kind,
|
||||
'url' => $query ? Html::appendQuery($url, $query) : $url,
|
||||
'label' => AdminLocale::text('catalog_context_' . $kind),
|
||||
'scope' => (string) $scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,14 @@
|
||||
use App\Common\DatabaseSchema;
|
||||
use App\Common\FileCacheInvalidator;
|
||||
use App\Common\ModuleManager;
|
||||
use App\Common\ModuleSettings;
|
||||
use App\Common\Registry;
|
||||
use App\Modules\Products\ProductRoles;
|
||||
use App\Content\CatalogTables;
|
||||
use App\Content\ContentTables;
|
||||
use App\Content\Fields\FieldValueCodec;
|
||||
use App\Content\Requests\RequestConditionValue;
|
||||
use App\Frontend\Feeds\Service as FeedService;
|
||||
use App\Helpers\Json;
|
||||
use App\Adminx\Rubrics\Model as RubricsModel;
|
||||
use App\Adminx\Documents\Model as DocumentsModel;
|
||||
@@ -146,8 +150,21 @@
|
||||
}
|
||||
|
||||
public static function saveSettings($rubricId, $fieldId, array $input)
|
||||
{
|
||||
if (DB::$transaction_in_progress) {
|
||||
throw new \LogicException('Catalog settings must be saved outside an existing transaction');
|
||||
}
|
||||
|
||||
$throwOnError = DB::$throw_exception_on_error;
|
||||
DB::$throw_exception_on_error = true;
|
||||
try { return self::saveSettingsOwned($rubricId, $fieldId, $input); }
|
||||
finally { DB::$throw_exception_on_error = $throwOnError; }
|
||||
}
|
||||
|
||||
protected static function saveSettingsOwned($rubricId, $fieldId, array $input)
|
||||
{
|
||||
self::requireCatalog($rubricId, $fieldId);
|
||||
$existing = self::settings($rubricId, $fieldId);
|
||||
$data = array();
|
||||
$data['purpose'] = isset($input['purpose']) && $input['purpose'] === 'commerce' ? 'commerce' : 'content';
|
||||
if ($data['purpose'] === 'commerce' && !self::productsAvailable()) {
|
||||
@@ -162,7 +179,24 @@
|
||||
throw new \InvalidArgumentException('У рубрики уже есть товарный каталог');
|
||||
}
|
||||
|
||||
foreach(array('product_title_field_id','product_article_field_id','product_price_field_id','product_old_price_field_id','product_stock_field_id','product_images_field_id') as $key){$data[$key]=isset($input[$key])?max(0,(int)$input[$key]):0;if($data[$key]&&!DB::query('SELECT Id FROM '.self::fieldsTable().' WHERE Id=%i AND rubric_id=%i LIMIT 1',$data[$key],(int)$rubricId)->getValue()){throw new \InvalidArgumentException('Коммерческое поле не принадлежит рубрике');}}
|
||||
foreach (array('product_title_field_id','product_article_field_id','product_price_field_id','product_old_price_field_id','product_stock_field_id','product_images_field_id') as $key) {
|
||||
$data[$key] = isset($input[$key]) ? max(0, (int) $input[$key]) : (int) $existing[$key];
|
||||
if (isset($input[$key]) && $data[$key] && !DB::query('SELECT Id FROM ' . self::fieldsTable() . ' WHERE Id=%i AND rubric_id=%i LIMIT 1', $data[$key], (int) $rubricId)->getValue()) {
|
||||
throw new \InvalidArgumentException('Коммерческое поле не принадлежит рубрике');
|
||||
}
|
||||
}
|
||||
|
||||
$roleInput = null;
|
||||
if ($data['purpose'] === 'commerce' && isset($input['product_roles']) && is_array($input['product_roles'])) {
|
||||
$roleFields = ProductRoles::fields($rubricId);
|
||||
$roleInput = array_replace(ProductRoles::configured($rubricId), ProductRoles::validate($input['product_roles'], $roleFields));
|
||||
$roleSettings = self::settings($rubricId, $fieldId);
|
||||
$resolved = ProductRoles::resolve($roleFields, $roleSettings, $roleInput);
|
||||
foreach (ProductRoles::definitions() as $role => $definition) {
|
||||
if (isset($definition['column'])) { $data[$definition['column']] = $resolved[$role]; }
|
||||
}
|
||||
}
|
||||
|
||||
$data['product_card_template_id'] = isset($input['product_card_template_id']) ? max(0, (int) $input['product_card_template_id']) : 0;
|
||||
if ($data['product_card_template_id'] > 0 && !DB::query(
|
||||
'SELECT id FROM ' . CatalogTables::table('catalog_card_templates') . ' WHERE id=%i LIMIT 1',
|
||||
@@ -215,30 +249,41 @@
|
||||
);
|
||||
}
|
||||
|
||||
$existing = self::settings($rubricId, $fieldId);
|
||||
if ((int) $existing['id'] > 0) {
|
||||
DB::Update(self::settingsTable(), $data, 'id = %i', (int) $existing['id']);
|
||||
if ($data['purpose'] !== $existing['purpose'] && self::productsAvailable()) {
|
||||
try { ProductIndexer::rebuild(); }
|
||||
catch (\Throwable $e) { DB::Update(self::settingsTable(), array('purpose'=>$existing['purpose']), 'id=%i', (int) $existing['id']); throw $e; }
|
||||
$reindex = $data['purpose'] !== $existing['purpose'];
|
||||
foreach ($data as $key => $value) {
|
||||
if (preg_match('/^product_.*_field_id$/', $key) && (int) $value !== (int) $existing[$key]) { $reindex = true; }
|
||||
}
|
||||
|
||||
if ($roleInput !== null && $resolved !== ProductRoles::resolve($roleFields, $existing, ProductRoles::configured($rubricId))) { $reindex = true; }
|
||||
$registrySettings = Registry::get('settings');
|
||||
DB::startTransaction();
|
||||
try {
|
||||
$settingsId = (int) $existing['id'];
|
||||
if ($settingsId > 0) {
|
||||
DB::Update(self::settingsTable(), $data, 'id=%i', $settingsId);
|
||||
} else {
|
||||
$data['rubric_id'] = (int) $rubricId;
|
||||
$data['field_id'] = (int) $fieldId;
|
||||
DB::Insert(self::settingsTable(), $data);
|
||||
$settingsId = (int) DB::insertId();
|
||||
}
|
||||
|
||||
Cache::forgetTag(CacheKey::tag('catalog'));
|
||||
FileCacheInvalidator::publicPresentation();
|
||||
return (int) $existing['id'];
|
||||
if ($roleInput !== null) { ModuleSettings::set('field_roles_' . (int) $rubricId, $roleInput, 'products', 'json'); }
|
||||
if (self::productsAvailable()) { ProductRoles::reset(); }
|
||||
if ($reindex && self::productsAvailable()) { ProductIndexer::reindexRubric($rubricId, $existing['purpose'] === 'commerce' && $data['purpose'] === 'commerce'); }
|
||||
DB::afterCommit(function () {
|
||||
DB::clearTags(array('settings'));
|
||||
Cache::forgetTag(CacheKey::tag('catalog'));
|
||||
FileCacheInvalidator::publicPresentation();
|
||||
});
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollback();
|
||||
Registry::set('settings', $registrySettings);
|
||||
if (self::productsAvailable()) { ProductRoles::reset(); }
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$data['rubric_id'] = (int) $rubricId;
|
||||
$data['field_id'] = (int) $fieldId;
|
||||
DB::Insert(self::settingsTable(), $data);
|
||||
$settingsId = (int) DB::insertId();
|
||||
if ($data['purpose'] === 'commerce') {
|
||||
try { ProductIndexer::rebuild(); }
|
||||
catch (\Throwable $e) { DB::Delete(self::settingsTable(), 'id=%i', $settingsId); throw $e; }
|
||||
}
|
||||
|
||||
Cache::forgetTag(CacheKey::tag('catalog'));
|
||||
FileCacheInvalidator::publicPresentation();
|
||||
return $settingsId;
|
||||
}
|
||||
|
||||
@@ -358,6 +403,33 @@
|
||||
return $tree;
|
||||
}
|
||||
|
||||
public static function sourceOptions($rubricId, $fieldId)
|
||||
{
|
||||
$items = self::items($rubricId, $fieldId);
|
||||
$map = array();
|
||||
foreach ($items as $item) { $map[(int) $item['id']] = $item; }
|
||||
$out = array();
|
||||
foreach ($items as $item) {
|
||||
$parts = array((string) $item['name']);
|
||||
$parentId = (int) $item['parent_id'];
|
||||
$visited = array((int) $item['id'] => true);
|
||||
while ($parentId > 0 && isset($map[$parentId]) && empty($visited[$parentId])) {
|
||||
$visited[$parentId] = true;
|
||||
array_unshift($parts, (string) $map[$parentId]['name']);
|
||||
$parentId = (int) $map[$parentId]['parent_id'];
|
||||
}
|
||||
|
||||
$out[] = array(
|
||||
'id' => (int) $item['id'],
|
||||
'name' => (string) $item['name'],
|
||||
'path' => implode(' / ', $parts),
|
||||
'status' => (int) $item['status'],
|
||||
);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function item($id)
|
||||
{
|
||||
$row = DB::query(
|
||||
@@ -385,6 +457,12 @@
|
||||
if ($documentId > 0 && !self::documentExists($documentId)) { throw new \RuntimeException('Выбранный документ не найден'); }
|
||||
$filterIds = self::ids(isset($input['filters_use']) ? $input['filters_use'] : array());
|
||||
$filterIds = self::orderedIds(isset($input['filters_order']) ? $input['filters_order'] : array(), $filterIds);
|
||||
$sourceIds = self::validSourceItemIds(
|
||||
isset($input['source_item_ids']) ? $input['source_item_ids'] : array(),
|
||||
$id,
|
||||
$rubricId,
|
||||
$fieldId
|
||||
);
|
||||
$data = array(
|
||||
'rubric_id' => (int) $rubricId, 'field_id' => (int) $fieldId,
|
||||
'name' => $name, 'parent_id' => $parentId,
|
||||
@@ -396,6 +474,12 @@
|
||||
'filters_use' => implode(',', $filterIds),
|
||||
'filters_settings' => serialize(self::filterStyles(isset($input['filter_style']) ? $input['filter_style'] : array(), $filterIds)),
|
||||
);
|
||||
if (self::sourceItemsAvailable()) {
|
||||
$data['source_item_ids'] = implode(',', $sourceIds);
|
||||
} elseif ($sourceIds) {
|
||||
throw new \RuntimeException('Сначала примените миграцию каталога для источников товаров');
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
DB::Update(self::itemsTable(), $data, 'id = %i', $id);
|
||||
} else {
|
||||
@@ -608,7 +692,7 @@
|
||||
return array(
|
||||
'total'=>0,'quality_total'=>0,'active'=>0,'inactive'=>0,'without_category'=>0,
|
||||
'without_image'=>0,'without_article'=>0,'without_price'=>0,'without_stock'=>0,
|
||||
'without_excerpt'=>0,'without_seo'=>0,'stale'=>0,'shipping_incomplete'=>0,
|
||||
'without_description'=>0,'without_seo'=>0,'stale'=>0,'shipping_incomplete'=>0,
|
||||
'shipping_disabled'=>0,'duplicate_articles'=>0,'legacy_attributes'=>0,
|
||||
'filter_index_errors'=>0,'registration_incomplete'=>0,'without_video'=>0,
|
||||
'variant_errors'=>0,'description_markup'=>0,'sfr'=>0,'issues'=>0,'recommendations'=>0,'indexed_at'=>0,
|
||||
@@ -1084,6 +1168,7 @@
|
||||
protected static function clearCache()
|
||||
{
|
||||
DB::clearTags(array('documents', 'requests', 'modules', 'catalog'));
|
||||
(new FeedService())->clearAll();
|
||||
Cache::forgetTag(CacheKey::tag('catalog'));
|
||||
}
|
||||
|
||||
@@ -1124,6 +1209,28 @@
|
||||
|
||||
protected static function idsString($value) { return implode(',', self::ids($value)); }
|
||||
|
||||
public static function sourceItemsAvailable()
|
||||
{
|
||||
return DatabaseSchema::columnExists(self::itemsTable(), 'source_item_ids');
|
||||
}
|
||||
|
||||
protected static function validSourceItemIds($value, $itemId, $rubricId, $fieldId)
|
||||
{
|
||||
$ids = array_values(array_diff(self::ids($value), array((int) $itemId)));
|
||||
if (!$ids) { return array(); }
|
||||
$rows = DB::query(
|
||||
'SELECT id FROM ' . self::itemsTable()
|
||||
. ' WHERE rubric_id=%i AND field_id=%i AND id IN (' . implode(',', $ids) . ')',
|
||||
(int) $rubricId,
|
||||
(int) $fieldId
|
||||
)->getAll() ?: array();
|
||||
$allowed = array();
|
||||
foreach ($rows as $row) { $allowed[(int) $row['id']] = true; }
|
||||
return array_values(array_filter($ids, function ($id) use ($allowed) {
|
||||
return isset($allowed[(int) $id]);
|
||||
}));
|
||||
}
|
||||
|
||||
protected static function orderedIds($order, array $selected)
|
||||
{
|
||||
$selectedMap = array_fill_keys($selected, true); $out = array();
|
||||
@@ -1164,6 +1271,7 @@
|
||||
'level' => (int) $row['level'], 'position' => (int) $row['position'],
|
||||
'fields_use' => self::ids(isset($row['fields_use']) ? $row['fields_use'] : ''),
|
||||
'filters_use' => self::ids(isset($row['filters_use']) ? $row['filters_use'] : ''),
|
||||
'source_item_ids' => self::ids(isset($row['source_item_ids']) ? $row['source_item_ids'] : ''),
|
||||
'filter_styles' => is_array($styles) ? $styles : array(),
|
||||
'attribute_set_id' => isset($row['attribute_set_id']) ? (int) $row['attribute_set_id'] : 0,
|
||||
'attributes_runtime' => isset($row['attributes_runtime']) ? (string) $row['attributes_runtime'] : 'legacy',
|
||||
|
||||
@@ -739,6 +739,45 @@
|
||||
grid-template-columns: minmax(0, 1fr) 40px;
|
||||
gap: 6px;
|
||||
}
|
||||
.catalog-source-picker {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 40px;
|
||||
gap: 6px;
|
||||
}
|
||||
.catalog-source-picker [data-catalog-source-inputs] {
|
||||
display: none;
|
||||
}
|
||||
.catalog-source-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
padding: 8px 11px;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--background-card);
|
||||
color: var(--text-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.catalog-source-value:hover:not(:disabled) {
|
||||
border-color: var(--blue-300);
|
||||
background: var(--blue-50);
|
||||
}
|
||||
.catalog-source-value:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.catalog-source-value i {
|
||||
flex: 0 0 auto;
|
||||
color: var(--blue-600);
|
||||
}
|
||||
.catalog-source-value span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.catalog-document-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -832,6 +871,74 @@
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
}
|
||||
.catalog-source-modal {
|
||||
width: min(66.666vw, 1000px);
|
||||
height: min(72vh, 760px);
|
||||
max-height: calc(100vh - 40px);
|
||||
}
|
||||
.catalog-source-modal .modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.catalog-source-modal .modal-header > div {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.catalog-source-modal .modal-header p {
|
||||
margin: 3px 0 0;
|
||||
}
|
||||
.catalog-source-options {
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
align-content: start;
|
||||
gap: 6px;
|
||||
min-height: 0;
|
||||
margin-top: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.catalog-source-option {
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 54px;
|
||||
padding: 8px 11px;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--background-card);
|
||||
cursor: pointer;
|
||||
}
|
||||
.catalog-source-option:hover {
|
||||
border-color: var(--blue-300);
|
||||
background: var(--blue-50);
|
||||
}
|
||||
.catalog-source-option[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.catalog-source-option > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
.catalog-source-option b,
|
||||
.catalog-source-option small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.catalog-source-option small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
.catalog-source-option em {
|
||||
font-style: normal;
|
||||
}
|
||||
.catalog-source-empty {
|
||||
padding: 28px 0;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.catalog-condition-toolbar {
|
||||
align-items: stretch;
|
||||
@@ -885,6 +992,16 @@
|
||||
.catalog-document-result em {
|
||||
display: none;
|
||||
}
|
||||
.catalog-source-modal {
|
||||
width: calc(100vw - 20px);
|
||||
height: calc(100vh - 20px);
|
||||
}
|
||||
.catalog-source-option {
|
||||
grid-template-columns: 20px minmax(0, 1fr);
|
||||
}
|
||||
.catalog-source-option em {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.catalog-behavior-grid {
|
||||
@@ -956,3 +1073,123 @@
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
.catalog-role-preview {
|
||||
margin-top: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-preview {
|
||||
margin: 20px 0;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid var(--border-color, #e3e5e8);
|
||||
border-bottom: 1px solid var(--border-color, #e3e5e8);
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-preview h4 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-preview [hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
|
||||
align-items: end;
|
||||
gap: 12px;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-controls .field {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-controls .select,
|
||||
.catalog-role-preview .catalog-selection-controls .input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-controls .btn {
|
||||
min-height: 40px;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-result {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-result ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-result li {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin: 6px 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-result li .ti {
|
||||
flex: 0 0 16px;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-result li span {
|
||||
min-width: 0;
|
||||
}
|
||||
.catalog-role-preview .catalog-selection-result b {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.catalog-role-preview h3,
|
||||
.catalog-role-preview p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.catalog-role-preview .table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.catalog-role-preview th:first-child {
|
||||
width: 24%;
|
||||
}
|
||||
.catalog-role-preview th:nth-child(2) {
|
||||
width: 32%;
|
||||
}
|
||||
.catalog-role-preview td {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.catalog-role-preview .btn-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.catalog-role-preview .btn {
|
||||
white-space: normal;
|
||||
min-height: 40px;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.catalog-role-preview .table-wrap {
|
||||
overflow: visible;
|
||||
}
|
||||
.catalog-role-preview .table,
|
||||
.catalog-role-preview tbody,
|
||||
.catalog-role-preview tr,
|
||||
.catalog-role-preview td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.catalog-role-preview thead {
|
||||
display: none;
|
||||
}
|
||||
.catalog-role-preview tr {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.catalog-role-preview td {
|
||||
padding: 4px 8px;
|
||||
border: 0;
|
||||
}
|
||||
.catalog-role-preview td:first-child {
|
||||
font-weight: 600;
|
||||
}
|
||||
.catalog-role-preview td[data-label]::before {
|
||||
content: attr(data-label) ': ';
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
'use strict';
|
||||
var Adminx = window.Adminx || (window.Adminx = {});
|
||||
Adminx.Catalog = {
|
||||
itemForm: null, settingsForm: null, dragItem: null, dragGroup: [], dragPlaceholder: null, dragGhost: null, dragPointerId: null, dragStartX: 0, dragStartY: 0, dragStarted: false, dragOrderSnapshot: '', orderSaving: false, filterOrder: [], itemSaveTimer: null, settingsSaveTimer: null, conditionContext: null,
|
||||
itemForm: null, settingsForm: null, dragItem: null, dragGroup: [], dragPlaceholder: null, dragGhost: null, dragPointerId: null, dragStartX: 0, dragStartY: 0, dragStarted: false, dragOrderSnapshot: '', orderSaving: false, filterOrder: [], sourceItemIds: [], sourceOptionsData: null, itemSaveTimer: null, settingsSaveTimer: null, conditionContext: null,
|
||||
init: function () {
|
||||
this.itemForm = document.getElementById('catalogItemForm');
|
||||
this.settingsForm = document.getElementById('catalogSettingsForm');
|
||||
var self = this;
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.closest('[data-product-role-refresh]')) { self.refreshRolePreview(false); return; }
|
||||
if (e.target.closest('[data-product-selection-check]')) { self.refreshRolePreview(true); return; }
|
||||
var pageTab = e.target.closest('[data-catalog-page-tab]');
|
||||
if (pageTab) { self.tab('[data-catalog-page-tab]', '[data-catalog-page-panel]', pageTab.getAttribute('data-catalog-page-tab')); }
|
||||
var drawerTab = e.target.closest('[data-catalog-drawer-tab]');
|
||||
@@ -27,6 +29,8 @@
|
||||
if (treeRow && !e.target.closest('button, input, label, a, select')) { self.editItem(treeRow.getAttribute('data-catalog-builder-item')); return; }
|
||||
if (e.target.closest('[data-catalog-document-pick]')) { self.openDocumentPicker(); }
|
||||
if (e.target.closest('[data-catalog-document-clear]')) { self.setDocument(null); self.scheduleItemSave(); }
|
||||
if (e.target.closest('[data-catalog-source-open]')) { self.openSourcePicker(); }
|
||||
if (e.target.closest('[data-catalog-source-clear]')) { self.setSources([]); self.scheduleItemSave(); }
|
||||
var conditionView = e.target.closest('[data-catalog-condition-view]');
|
||||
if (conditionView) { self.showCondition(conditionView.getAttribute('data-catalog-condition-view')); }
|
||||
var conditionSync = e.target.closest('[data-catalog-condition-sync]');
|
||||
@@ -37,9 +41,11 @@
|
||||
if (e.target.closest('[data-catalog-recompile-all]')) { self.recompileAll(); }
|
||||
});
|
||||
document.addEventListener('input', function (e) {
|
||||
if (e.target.closest('[data-product-role-preview]')) { self.selectionChanged(); return; }
|
||||
if (e.target.matches('[data-catalog-tree-search]')) { self.search(e.target.value); }
|
||||
});
|
||||
document.addEventListener('change', function (e) {
|
||||
if (e.target.closest('[data-product-role-preview]')) { self.selectionChanged(); return; }
|
||||
if (e.target.matches('[data-catalog-item-status]')) { self.setTreeStatus(e.target); return; }
|
||||
if (self.itemForm && e.target.closest('#catalogItemForm')) {
|
||||
if (e.target.matches('[name="filters_use[]"]')) { self.syncFilterOrderSelection(); }
|
||||
@@ -59,6 +65,44 @@
|
||||
document.addEventListener('pointercancel', function () { self.treeDragEnd(); });
|
||||
this.normalizeTreeHierarchy();
|
||||
this.openRequestedItem();
|
||||
if (window.location.hash === '#settings') { this.tab('[data-catalog-page-tab]', '[data-catalog-page-panel]', 'settings'); }
|
||||
},
|
||||
selectionChanged: function () {
|
||||
this.rolePreviewSequence = (this.rolePreviewSequence || 0) + 1;
|
||||
var root = document.querySelector('[data-product-role-preview]');
|
||||
if (!root) { return; }
|
||||
var context = root.querySelector('[data-selection="context"]');
|
||||
root.querySelectorAll('[data-selection-context]').forEach(function (el) { el.hidden = !context || el.getAttribute('data-selection-context') !== context.value; });
|
||||
var result = root.querySelector('[data-selection-result]'), stale = root.querySelector('[data-selection-stale]');
|
||||
if (result) { result.hidden = true; if (stale) { stale.hidden = false; } }
|
||||
},
|
||||
refreshRolePreview: function (check) {
|
||||
var input = document.querySelector('[data-product-role-document]');
|
||||
if (!input || !input.checkValidity()) { if (input) { input.reportValidity(); } return; }
|
||||
var root = input.closest('[data-product-role-preview]');
|
||||
var invalid = Array.prototype.find.call(root.querySelectorAll('[data-selection-context]:not([hidden]) input'), function (el) { return !el.checkValidity(); });
|
||||
if (invalid) { invalid.reportValidity(); return; }
|
||||
var self = this, sequence = (this.rolePreviewSequence || 0) + 1;
|
||||
var errorMessage = root.getAttribute('data-error');
|
||||
this.rolePreviewSequence = sequence;
|
||||
var url = new URL(this.url() + '/product-preview', window.location.origin);
|
||||
url.searchParams.set('product_preview', input.value || '0');
|
||||
url.searchParams.set('selection_check', check === true ? '1' : '0');
|
||||
root.querySelectorAll('[data-selection]').forEach(function (el) { url.searchParams.set('selection_' + el.getAttribute('data-selection'), el.value); });
|
||||
root.setAttribute('aria-busy', 'true');
|
||||
root.querySelectorAll('button').forEach(function (el) { el.disabled = true; });
|
||||
fetch(url.toString(), { credentials: 'same-origin' }).then(function (response) {
|
||||
if (!response.ok) { throw new Error(errorMessage); }
|
||||
return response.text();
|
||||
}).then(function (html) {
|
||||
if (sequence !== self.rolePreviewSequence) { return; }
|
||||
var preview = new DOMParser().parseFromString(html, 'text/html').querySelector('[data-product-role-preview]');
|
||||
var current = document.querySelector('[data-product-role-preview]');
|
||||
if (!preview || !current) { throw new Error(errorMessage); }
|
||||
current.replaceWith(preview);
|
||||
}).catch(function (error) { if (sequence === self.rolePreviewSequence) { Adminx.Toast.show(error.message, 'error'); } }).finally(function () {
|
||||
root.removeAttribute('aria-busy'); root.querySelectorAll('button').forEach(function (el) { el.disabled = false; });
|
||||
});
|
||||
},
|
||||
base: function () { return (this.itemForm || this.settingsForm).getAttribute('data-base'); },
|
||||
rubric: function () { return (this.itemForm || this.settingsForm).getAttribute('data-rubric'); },
|
||||
@@ -76,6 +120,7 @@
|
||||
document.querySelectorAll(panels).forEach(function (el) { el.hidden = el.getAttribute(panels.indexOf('drawer') >= 0 ? 'data-catalog-drawer-panel' : 'data-catalog-page-panel') !== value; });
|
||||
},
|
||||
newItem: function (parentId) {
|
||||
this.renderContextLinks([]);
|
||||
this.itemForm.reset(); this.itemForm.querySelector('[name="id"]').value = ''; this.itemForm.querySelector('[name="status"]').checked = true;
|
||||
this.itemForm.querySelectorAll('[name="parent_id"] option').forEach(function (option) { option.disabled = false; });
|
||||
var parent = this.itemForm.querySelector('[name="parent_id"]');
|
||||
@@ -84,7 +129,7 @@
|
||||
this.checkValues('fields_use[]', defaultFields); this.checkValues('filters_use[]', defaultFilters);
|
||||
defaultFilters.forEach(function (id) { var source = this.settingsForm.querySelector('[name="filter_style[' + id + ']"]'), target = this.itemForm.querySelector('[name="filter_style[' + id + ']"]'); if (source && target) { target.value = source.value; } }, this);
|
||||
this.filterOrder = defaultFilters.slice(); this.syncFilterOrderInput();
|
||||
this.setDocument(null); this.setState('item', ''); this.renderConditionContext(null);
|
||||
this.setDocument(null); this.setSources([]); this.setState('item', ''); this.renderConditionContext(null);
|
||||
document.querySelector('[data-catalog-drawer-title]').textContent = 'Новый раздел'; this.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', 'main'); this.updateCounts();
|
||||
Adminx.Drawer.open('catalogItemDrawer');
|
||||
},
|
||||
@@ -105,10 +150,12 @@
|
||||
this.editItem(itemId, params.get('tab') || 'main');
|
||||
},
|
||||
fill: function (item) {
|
||||
this.renderContextLinks(item.context_links || []);
|
||||
this.itemForm.reset(); this.itemForm.querySelector('[name="id"]').value = item.id || ''; this.itemForm.querySelector('[name="name"]').value = item.name || '';
|
||||
this.itemForm.querySelectorAll('[name="parent_id"] option').forEach(function (option) { option.disabled = false; });
|
||||
this.itemForm.querySelector('[name="parent_id"]').value = item.parent_id || 0; this.itemForm.querySelector('[name="status"]').checked = Number(item.status) === 1;
|
||||
this.setDocument(item.document_id ? { id: item.document_id, title: item.document_title || '', alias: item.document_alias || '' } : null);
|
||||
this.setSources(item.source_item_ids || []);
|
||||
this.filterOrder = (item.filters_use || []).map(function (id) { return String(id); }); this.syncFilterOrderInput(); this.checkValues('fields_use[]', item.fields_use || []); this.checkValues('filters_use[]', item.filters_use || []);
|
||||
Object.keys(item.filter_styles || {}).forEach(function (id) { var select = this.itemForm.querySelector('[name="filter_style[' + id + ']"]'); if (select) { select.value = item.filter_styles[id]; } }, this);
|
||||
var own = this.itemForm.querySelector('[name="parent_id"] option[value="' + item.id + '"]'); if (own) { own.disabled = true; }
|
||||
@@ -125,6 +172,20 @@
|
||||
syncFilterOrderSelection: function () { var selected = {}, next = []; this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').forEach(function (input) { selected[input.value] = true; }); this.filterOrder.forEach(function (id) { if (selected[id]) { next.push(id); delete selected[id]; } }); this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').forEach(function (input) { if (selected[input.value]) { next.push(input.value); delete selected[input.value]; } }); this.filterOrder = next; this.syncFilterOrderInput(); },
|
||||
syncFilterOrderInput: function () { var input = this.itemForm && this.itemForm.querySelector('[name="filters_order"]'); if (input) { input.value = this.filterOrder.join(','); } },
|
||||
openFilterOrder: function () { var self = this; this.syncFilterOrderSelection(); if (this.filterOrder.length < 2) { return; } var overlay = document.createElement('div'), rows = ''; this.filterOrder.forEach(function (id) { var input = self.itemForm.querySelector('[name="filters_use[]"][value="' + id + '"]'), row = input.closest('.catalog-filter-row'), title = row.querySelector('.catalog-choice-copy b').textContent.trim(), type = row.querySelector('.catalog-field-type b').textContent.trim(); rows += '<div class="catalog-filter-order-row" data-filter-order-id="' + self.esc(id) + '"><button class="catalog-filter-order-handle" type="button" draggable="true" aria-label="Перетащить"><i class="ti ti-grip-vertical"></i></button><span><b>' + self.esc(title) + '</b><small>' + self.esc(type) + ' · #' + self.esc(id) + '</small></span></div>'; }); overlay.className = 'overlay catalog-filter-order-overlay'; overlay.innerHTML = '<div class="modal catalog-filter-order-modal" role="dialog" aria-modal="true"><div class="modal-header"><span class="dialog-icon info"><i class="ti ti-arrows-sort"></i></span><div><h3>Порядок фильтров</h3><p class="text-secondary">' + this.filterOrder.length + ' включённых фильтров.</p></div><button class="modal-close" type="button" data-filter-order-close aria-label="Закрыть"><i class="ti ti-x"></i></button></div><div class="modal-body"><div class="catalog-filter-order-list">' + rows + '</div></div><div class="modal-footer"><button class="btn btn-ghost" type="button" data-filter-order-close>Закрыть</button><button class="btn btn-primary" type="button" data-filter-order-save><i class="ti ti-device-floppy"></i>Сохранить порядок</button></div></div>'; document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); }); var dragged = null, close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); }; overlay.addEventListener('dragstart', function (e) { var handle = e.target.closest('.catalog-filter-order-handle'), row = handle ? handle.closest('[data-filter-order-id]') : null; if (!row) { e.preventDefault(); return; } dragged = row; row.classList.add('is-dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', row.getAttribute('data-filter-order-id')); }); overlay.addEventListener('dragover', function (e) { var target = e.target.closest('[data-filter-order-id]'); if (!dragged || !target || target === dragged) { return; } e.preventDefault(); var rect = target.getBoundingClientRect(), reference = e.clientY < rect.top + rect.height / 2 ? target : target.nextSibling; if (reference !== dragged && dragged.nextSibling !== reference) { target.parentNode.insertBefore(dragged, reference); } }); overlay.addEventListener('dragend', function () { if (dragged) { dragged.classList.remove('is-dragging'); dragged = null; } }); overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-filter-order-close]')) { close(); return; } if (e.target.closest('[data-filter-order-save]')) { self.filterOrder = Array.prototype.map.call(overlay.querySelectorAll('[data-filter-order-id]'), function (row) { return row.getAttribute('data-filter-order-id'); }); self.syncFilterOrderInput(); self.scheduleItemSave(); close(); } }); },
|
||||
renderContextLinks: function (links) {
|
||||
var panel = this.itemForm && this.itemForm.querySelector('[data-catalog-context-links]');
|
||||
if (!panel) { return; }
|
||||
var list = panel.querySelector('nav');
|
||||
list.textContent = '';
|
||||
(links || []).forEach(function (link) {
|
||||
var anchor = document.createElement('a'), icon = document.createElement('i'), label = document.createElement('span');
|
||||
anchor.href = link.url; anchor.target = '_blank'; anchor.rel = 'noopener'; anchor.setAttribute('data-context-kind', link.kind);
|
||||
icon.className = 'ti ti-external-link'; icon.setAttribute('aria-hidden', 'true'); label.textContent = link.label;
|
||||
if (link.scope) { var scope = document.createElement('small'); scope.textContent = link.scope; label.appendChild(scope); }
|
||||
anchor.appendChild(icon); anchor.appendChild(label); list.appendChild(anchor);
|
||||
});
|
||||
panel.hidden = !list.children.length;
|
||||
},
|
||||
renderConditionContext: function (context) {
|
||||
this.conditionContext = context || { request: { id: 0, title: '' }, states: {}, summary: { synced: 0, staged: 0, missing: 0, different: 0, unsupported: 0 } };
|
||||
var request = this.conditionContext.request || {}, summary = this.conditionContext.summary || {}, requestEl = document.querySelector('[data-catalog-condition-request]'), summaryEl = document.querySelector('[data-catalog-condition-summary]'), bulk = document.querySelector('[data-catalog-conditions-sync]');
|
||||
@@ -172,16 +233,61 @@
|
||||
saveItem: function (auto) {
|
||||
var id = this.itemForm.querySelector('[name="id"]').value; var self = this;
|
||||
this.ajax(this.url() + '/items' + (id ? '/' + id : ''), new FormData(this.itemForm), function (payload) {
|
||||
if (payload.data && payload.data.context_links) { self.renderContextLinks(payload.data.context_links); }
|
||||
if (auto) { self.setState('item', 'Сохранено', 'ok'); self.syncTreeRow(id); if (payload.data && payload.data.condition_context) { self.renderConditionContext(payload.data.condition_context); } return; }
|
||||
Adminx.Toast.show(payload.message, 'success'); if (Adminx.Drawer) { Adminx.Drawer.close('catalogItemDrawer'); } window.location.reload();
|
||||
}, { quiet: !!auto, fail: function () { self.setState('item', 'Не сохранено', 'error'); } });
|
||||
},
|
||||
saveSettings: function (auto) { var self = this; this.ajax(this.url() + '/settings', new FormData(this.settingsForm), function (payload) { if (auto) { self.setState('settings', 'Сохранено', 'ok'); } else { self.setState('settings', 'Сохранено', 'ok'); Adminx.Toast.show(payload.message, 'success'); } }, { quiet: !!auto, fail: function () { self.setState('settings', 'Не сохранено', 'error'); } }); },
|
||||
saveSettings: function (auto) { var self = this; this.ajax(this.url() + '/settings', new FormData(this.settingsForm), function (payload) { self.refreshRolePreview(); if (auto) { self.setState('settings', 'Сохранено', 'ok'); } else { self.setState('settings', 'Сохранено', 'ok'); Adminx.Toast.show(payload.message, 'success'); } }, { quiet: !!auto, fail: function () { self.setState('settings', 'Не сохранено', 'error'); } }); },
|
||||
setState: function (type, message, state) { var el = document.querySelector('[data-catalog-' + type + '-state]'); if (!el) { return; } el.textContent = message || ''; el.classList.toggle('is-ok', state === 'ok'); el.classList.toggle('is-error', state === 'error'); },
|
||||
syncTreeRow: function (id) { var row = document.querySelector('[data-catalog-item][data-id="' + id + '"]'); if (!row) { return; } var toggle = row.querySelector('[data-catalog-item-status]'), active = this.itemForm.querySelector('[name="status"]').checked, filterCount = this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').length, name = row.querySelector('.catalog-tree-name b'); if (toggle) { toggle.checked = active; this.updateTreeStatus(toggle); } if (name) { name.textContent = this.itemForm.querySelector('[name="name"]').value || 'Без названия'; } row.setAttribute('data-filter-count', String(filterCount)); var count = row.querySelector('.catalog-tree-count'); if (count) { count.textContent = this.itemForm.querySelectorAll('[name="fields_use[]"]:checked').length + ' / ' + filterCount; } },
|
||||
updateTreeStatus: function (input) { var item = input.closest('[data-catalog-item]'), active = input.checked, label = item.querySelector('[data-catalog-item-status-label]'), control = input.closest('.switch'); item.classList.toggle('is-inactive', !active); if (label) { label.textContent = active ? 'активен' : 'скрыт'; } input.setAttribute('aria-label', active ? 'Скрыть раздел' : 'Включить раздел'); if (control) { control.setAttribute('data-tooltip', active ? 'Скрыть раздел' : 'Включить раздел'); } },
|
||||
setTreeStatus: function (input) { var self = this, id = input.getAttribute('data-catalog-item-status'), previous = !input.checked, data = new FormData(); this.updateTreeStatus(input); input.disabled = true; data.append('_csrf', this.csrf()); data.append('status', input.checked ? '1' : '0'); this.ajax(this.url() + '/items/' + encodeURIComponent(id) + '/status', data, function (payload) { input.checked = Number(payload.data.status) === 1; input.disabled = false; self.updateTreeStatus(input); }, { quiet: true, fail: function () { input.checked = previous; input.disabled = false; self.updateTreeStatus(input); } }); },
|
||||
setDocument: function (item) { if (!this.itemForm) { return; } var input = this.itemForm.querySelector('[name="document_id"]'); var label = this.itemForm.querySelector('[data-catalog-document-label]'); var clear = this.itemForm.querySelector('[data-catalog-document-clear]'); var id = item && item.id ? Number(item.id) : 0; input.value = id || ''; if (label) { label.textContent = id ? ('#' + id + ' · ' + (item.title || item.alias || 'Без названия')) : 'Документ не выбран'; } if (clear) { clear.disabled = !id; } },
|
||||
sourceOptions: function () {
|
||||
if (this.sourceOptionsData !== null) { return this.sourceOptionsData; }
|
||||
var node = document.querySelector('[data-catalog-source-options]');
|
||||
try { this.sourceOptionsData = node ? JSON.parse(node.textContent || '[]') : []; }
|
||||
catch (e) { this.sourceOptionsData = []; }
|
||||
return this.sourceOptionsData;
|
||||
},
|
||||
setSources: function (values) {
|
||||
if (!this.itemForm) { return; }
|
||||
var ownId = Number(this.itemForm.querySelector('[name="id"]').value) || 0, allowed = {}, selected = [];
|
||||
this.sourceOptions().forEach(function (item) { if (Number(item.id) !== ownId) { allowed[String(item.id)] = item; } });
|
||||
(values || []).forEach(function (id) { id = String(Number(id) || 0); if (allowed[id] && selected.indexOf(id) === -1) { selected.push(id); } });
|
||||
this.sourceItemIds = selected;
|
||||
var inputs = this.itemForm.querySelector('[data-catalog-source-inputs]'), label = this.itemForm.querySelector('[data-catalog-source-label]'), clear = this.itemForm.querySelector('[data-catalog-source-clear]');
|
||||
if (inputs) {
|
||||
inputs.innerHTML = '';
|
||||
selected.forEach(function (id) { var input = document.createElement('input'); input.type = 'hidden'; input.name = 'source_item_ids[]'; input.value = id; inputs.appendChild(input); });
|
||||
}
|
||||
if (label) {
|
||||
var names = selected.map(function (id) { return allowed[id].name; });
|
||||
label.textContent = names.length === 0 ? 'Товары только из этого раздела' : (names.slice(0, 2).join(', ') + (names.length > 2 ? ' · ещё ' + (names.length - 2) : ''));
|
||||
}
|
||||
if (clear) { clear.disabled = selected.length === 0; }
|
||||
},
|
||||
openSourcePicker: function () {
|
||||
var self = this, ownId = Number(this.itemForm.querySelector('[name="id"]').value) || 0;
|
||||
var options = this.sourceOptions().filter(function (item) { return Number(item.id) !== ownId; });
|
||||
var selected = {}; this.sourceItemIds.forEach(function (id) { selected[String(id)] = true; });
|
||||
var overlay = document.createElement('div'), rows = '';
|
||||
options.forEach(function (item) {
|
||||
var id = String(item.id), search = (id + ' ' + (item.path || item.name || '')).toLowerCase();
|
||||
rows += '<label class="catalog-source-option" data-catalog-source-option data-search="' + self.esc(search) + '"><input type="checkbox" value="' + self.esc(id) + '"' + (selected[id] ? ' checked' : '') + '><span><b>' + self.esc(item.name || 'Без названия') + '</b><small>' + self.esc(item.path || ('Раздел #' + id)) + '</small></span><em class="badge ' + (Number(item.status) === 1 ? 'badge-green' : 'badge-gray') + '">' + (Number(item.status) === 1 ? 'активен' : 'скрыт') + '</em></label>';
|
||||
});
|
||||
overlay.className = 'overlay catalog-source-overlay';
|
||||
overlay.innerHTML = '<div class="modal catalog-source-modal" role="dialog" aria-modal="true"><div class="modal-header"><span class="dialog-icon info"><i class="ti ti-folders"></i></span><div><h3>Источники товаров</h3><p class="text-secondary">Товары выбранных разделов появятся на текущей странице.</p></div><button class="modal-close" type="button" data-catalog-source-close aria-label="Закрыть"><i class="ti ti-x"></i></button></div><div class="modal-body"><label class="input-wrap"><i class="ti ti-search"></i><input class="input" type="search" placeholder="Найти раздел по названию или ID" data-catalog-source-search></label><div class="catalog-source-options">' + rows + '</div><div class="catalog-source-empty" data-catalog-source-empty' + (options.length ? ' hidden' : '') + '>Подходящих разделов нет</div></div><div class="modal-footer"><span class="mf-left text-secondary text-sm" data-catalog-source-count></span><button class="btn btn-ghost" type="button" data-catalog-source-close>Закрыть</button><button class="btn btn-primary" type="button" data-catalog-source-save><i class="ti ti-check"></i>Применить</button></div></div>';
|
||||
document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); });
|
||||
var search = overlay.querySelector('[data-catalog-source-search]'), count = overlay.querySelector('[data-catalog-source-count]'), empty = overlay.querySelector('[data-catalog-source-empty]');
|
||||
var update = function () { var checked = overlay.querySelectorAll('[data-catalog-source-option] input:checked').length; count.textContent = checked ? ('Выбрано: ' + checked) : 'Источники не выбраны'; };
|
||||
var filter = function () { var query = (search.value || '').trim().toLowerCase(), visible = 0; overlay.querySelectorAll('[data-catalog-source-option]').forEach(function (row) { var show = !query || row.getAttribute('data-search').indexOf(query) !== -1; row.hidden = !show; if (show) { visible++; } }); empty.hidden = visible > 0; };
|
||||
var close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); };
|
||||
overlay.addEventListener('change', update);
|
||||
overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-catalog-source-close]')) { close(); return; } if (e.target.closest('[data-catalog-source-save]')) { self.setSources(Array.prototype.map.call(overlay.querySelectorAll('[data-catalog-source-option] input:checked'), function (input) { return input.value; })); self.scheduleItemSave(); close(); } });
|
||||
search.addEventListener('input', filter); update(); search.focus();
|
||||
},
|
||||
openDocumentPicker: function () {
|
||||
var self = this, overlay = document.createElement('div');
|
||||
overlay.className = 'overlay catalog-document-overlay';
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
.catalog-context-links {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
min-width: 0;
|
||||
}
|
||||
.catalog-context-links h3 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.catalog-context-links nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 24px;
|
||||
}
|
||||
.catalog-context-links a {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.catalog-context-links a > i {
|
||||
flex: none;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.catalog-context-links a > span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.catalog-context-links small {
|
||||
display: block;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.catalog-item-form > .catalog-context-links {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
<phrase data="auto_944019ea5d01e63a">hidden</phrase>
|
||||
<phrase data="auto_9729bca62f6c558e">Request Error</phrase>
|
||||
<phrase data="auto_99d406eeba4f9acb">Section</phrase>
|
||||
<phrase data="auto_9b62723a4333bb4a">Find section</phrase>
|
||||
<phrase data="auto_9f8bfc6bf2339426"><div class="alert alert-error"><i class="ti ti-alert-triangle"></i><div>Errors:</phrase>
|
||||
<phrase data="auto_a456fd20d54a422c"></p></div><button class="modal-close" type="button" data-catalog-recompile-close aria-label="Close" disabled><i class="ti ti-x"></i></button></div><div class="modal-body"><div class="catalog-recompile-status" data-catalog-recompile-status>Preparing...</div><div class="progress"><div class="progress-bar" data-catalog-recompile-progress style="width:0%;--progress-color:var(--blue-500)"></div></div><div class="catalog-recompile-errors" data-catalog-recompile-errors></div></div><div class="modal-footer"><button class="btn btn-ghost" type="button" data-catalog-recompile-stop>Stop</button><button class="btn btn-primary" type="button" data-catalog-recompile-close disabled>Close</button></div></div></phrase>
|
||||
<phrase data="auto_a4c0ae2fa076f2fd">Hide section</phrase>
|
||||
@@ -36,6 +37,11 @@
|
||||
<phrase data="auto_f61de80bf0ca15a4">No document selected</phrase>
|
||||
<phrase data="auto_f6bde2876efe2658">Not included</phrase>
|
||||
<phrase data="auto_fda18205701b609b">Replace condition?</phrase>
|
||||
<phrase data="catalog_roles_more">more</phrase>
|
||||
<phrase data="catalog_roles_no">No</phrase>
|
||||
<phrase data="catalog_selection_catalog">Catalog</phrase>
|
||||
<phrase data="catalog_selection_category">Category</phrase>
|
||||
<phrase data="catalog_selection_search">Search</phrase>
|
||||
<phrase data="runtime_6291ecb1784ea2de">Catalog created</phrase>
|
||||
<phrase data="runtime_6b3607219bc8c7f0">different</phrase>
|
||||
<phrase data="runtime_d0ef3540b088bbf6">Catalog</phrase>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="catalog_context_title">Related Settings</phrase>
|
||||
<phrase data="catalog_context_filters">Section Filters</phrase>
|
||||
<phrase data="catalog_context_attribute_set">Attribute Set</phrase>
|
||||
<phrase data="catalog_context_card">Catalog Card Template</phrase>
|
||||
<phrase data="catalog_context_page">Page Template</phrase>
|
||||
</language>
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="catalog_roles_description">Product Description</phrase>
|
||||
<phrase data="catalog_roles_warranty">Warranty</phrase>
|
||||
<phrase data="catalog_roles_registration_text">Registration Document Text</phrase>
|
||||
<phrase data="catalog_roles_registration_number">Registration Document Number</phrase>
|
||||
<phrase data="catalog_roles_title">Product Field Roles</phrase>
|
||||
<phrase data="catalog_roles_saved">Saved Mapping</phrase>
|
||||
<phrase data="catalog_roles_document">Document #</phrase>
|
||||
<phrase data="catalog_roles_sample">Sample Product</phrase>
|
||||
<phrase data="catalog_roles_check">Check</phrase>
|
||||
<phrase data="catalog_roles_optional">Optional Project Fields</phrase>
|
||||
<phrase data="catalog_roles_auto">Automatic:</phrase>
|
||||
<phrase data="catalog_roles_unassigned">Not Assigned</phrase>
|
||||
<phrase data="catalog_roles_name">Name</phrase>
|
||||
<phrase data="catalog_roles_article">SKU</phrase>
|
||||
<phrase data="catalog_roles_price">Price</phrase>
|
||||
<phrase data="catalog_roles_old_price">Old Price</phrase>
|
||||
<phrase data="catalog_roles_stock">Stock Quantity</phrase>
|
||||
<phrase data="catalog_roles_images">Images</phrase>
|
||||
<phrase data="catalog_roles_manufacturer">Manufacturer</phrase>
|
||||
<phrase data="catalog_roles_caption">Card Caption</phrase>
|
||||
<phrase data="catalog_roles_hide_price">Hide Price</phrase>
|
||||
<phrase data="catalog_roles_exclude">Exclude From Price List</phrase>
|
||||
<phrase data="catalog_roles_new">New Product</phrase>
|
||||
<phrase data="catalog_roles_discount">Discount</phrase>
|
||||
<phrase data="catalog_roles_gift">Gift</phrase>
|
||||
<phrase data="catalog_roles_video">Video</phrase>
|
||||
<phrase data="catalog_roles_instruction">Video Instructions</phrase>
|
||||
<phrase data="catalog_roles_square">Square Image</phrase>
|
||||
<phrase data="catalog_roles_banner">Cover Image</phrase>
|
||||
<phrase data="catalog_roles_registration">Registration Document</phrase>
|
||||
<phrase data="catalog_roles_program">Payment Program Eligibility</phrase>
|
||||
<phrase data="catalog_roles_program_code">Payment Program Code</phrase>
|
||||
<phrase data="catalog_roles_published">Published</phrase>
|
||||
<phrase data="catalog_roles_not_public">Not Publicly Available</phrase>
|
||||
<phrase data="catalog_roles_orderable">Available to Order</phrase>
|
||||
<phrase data="catalog_roles_not_orderable">Not Available to Order</phrase>
|
||||
<phrase data="catalog_roles_unpublished">Not Published</phrase>
|
||||
<phrase data="catalog_roles_deleted">Deleted</phrase>
|
||||
<phrase data="catalog_roles_technical">Technical Document or Rubric</phrase>
|
||||
<phrase data="catalog_roles_scheduled">Publication Has Not Started</phrase>
|
||||
<phrase data="catalog_roles_expired">Publication Has Ended</phrase>
|
||||
<phrase data="catalog_roles_missing_price">No Positive Price</phrase>
|
||||
<phrase data="catalog_roles_hidden_price">Price Is Hidden</phrase>
|
||||
<phrase data="catalog_roles_excluded">Price List Exclusion Is Enabled.</phrase>
|
||||
<phrase data="catalog_roles_role">Role</phrase>
|
||||
<phrase data="catalog_roles_field">Field</phrase>
|
||||
<phrase data="catalog_roles_value">Value</phrase>
|
||||
<phrase data="catalog_roles_yes">Yes</phrase>
|
||||
<phrase data="catalog_roles_no">No</phrase>
|
||||
<phrase data="catalog_roles_empty">Not Filled</phrase>
|
||||
<phrase data="catalog_roles_more">more</phrase>
|
||||
<phrase data="catalog_roles_missing_document">No Document Found in This Rubric.</phrase>
|
||||
<phrase data="catalog_roles_request">Request Conditions</phrase>
|
||||
<phrase data="catalog_roles_template">Page Template</phrase>
|
||||
<phrase data="catalog_roles_card">Product Card</phrase>
|
||||
<phrase data="catalog_roles_filters">filters</phrase>
|
||||
<phrase data="catalog_roles_set">Attribute Set</phrase>
|
||||
<phrase data="catalog_roles_error">Could Not Check the Mapping</phrase>
|
||||
<phrase data="catalog_roles_invalid">Invalid Field Role</phrase>
|
||||
<phrase data="catalog_roles_wrong_rubric">Role Field Does Not Belong to the Product Rubric</phrase>
|
||||
</language>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="catalog_selection_check_title">Selection Check</phrase>
|
||||
<phrase data="catalog_selection_selection">Selection</phrase>
|
||||
<phrase data="catalog_selection_catalog">Catalog</phrase>
|
||||
<phrase data="catalog_selection_recommendation">Recommendations</phrase>
|
||||
<phrase data="catalog_selection_search">Search</phrase>
|
||||
<phrase data="catalog_selection_feed">Feed</phrase>
|
||||
<phrase data="catalog_selection_category">Category</phrase>
|
||||
<phrase data="catalog_selection_none">Not Selected</phrase>
|
||||
<phrase data="catalog_selection_scope">Search Scope</phrase>
|
||||
<phrase data="catalog_selection_everywhere">Everywhere</phrase>
|
||||
<phrase data="catalog_selection_query">Search Query</phrase>
|
||||
<phrase data="catalog_selection_source">Source Product #</phrase>
|
||||
<phrase data="catalog_selection_limit">Limit</phrase>
|
||||
<phrase data="catalog_selection_check">Check Selection</phrase>
|
||||
<phrase data="catalog_selection_stale">Parameters Changed; Result Is Outdated</phrase>
|
||||
<phrase data="catalog_selection_yes">Included in Selection</phrase>
|
||||
<phrase data="catalog_selection_no">Not Included in Selection</phrase>
|
||||
<phrase data="catalog_selection_unknown">Check Incomplete</phrase>
|
||||
<phrase data="catalog_selection_denied">Insufficient Permissions</phrase>
|
||||
<phrase data="catalog_selection_unknown_context">Unknown Selection</phrase>
|
||||
<phrase data="catalog_selection_error">Check Unavailable: Review Settings and Index Health</phrase>
|
||||
<phrase data="catalog_selection_catalog_note">Eligibility Before Grouping and Pagination, Without Visitor Filters</phrase>
|
||||
<phrase data="catalog_selection_category_missing">Category Not Found</phrase>
|
||||
<phrase data="catalog_selection_category_page">Published Category Page</phrase>
|
||||
<phrase data="catalog_selection_request_denied">Insufficient Permissions for Request Conditions</phrase>
|
||||
<phrase data="catalog_selection_catalog_base">Index, Publication, Category Sources and Variant Status</phrase>
|
||||
<phrase data="catalog_selection_conditions">Saved Request Conditions</phrase>
|
||||
<phrase data="catalog_selection_group">Product or Its Group Is Eligible</phrase>
|
||||
<phrase data="catalog_selection_search_note">Search Before Pagination Using the Effective Query, Including Synonyms and Typo Correction</phrase>
|
||||
<phrase data="catalog_selection_effective_query">Effective Search Query</phrase>
|
||||
<phrase data="catalog_selection_ambiguous_query">Ambiguous Correction: No Replacement Selected</phrase>
|
||||
<phrase data="catalog_selection_missing_plan">Search Handler Did Not Return a Verification Plan</phrase>
|
||||
<phrase data="catalog_selection_pipeline_results">Search Pipeline Returned Results</phrase>
|
||||
<phrase data="catalog_selection_search_unavailable">Search Unavailable</phrase>
|
||||
<phrase data="catalog_selection_scope_missing">Search Scope Not Found</phrase>
|
||||
<phrase data="catalog_selection_short_query">Search Query Is Too Short</phrase>
|
||||
<phrase data="catalog_selection_search_base">Search Index, Text and Public Visibility</phrase>
|
||||
<phrase data="catalog_selection_search_rubrics">Search Scope Rubrics</phrase>
|
||||
<phrase data="catalog_selection_search_conditions">Search Scope Conditions</phrase>
|
||||
<phrase data="catalog_selection_feed_note">Export Eligibility Under Saved Settings, Without File Generation</phrase>
|
||||
<phrase data="catalog_selection_feed_missing">Feed Not Found</phrase>
|
||||
<phrase data="catalog_selection_feed_active">Feed Is Enabled</phrase>
|
||||
<phrase data="catalog_selection_feed_categories">Allowed Feed Categories</phrase>
|
||||
<phrase data="catalog_selection_feed_base">Index, Publication, Price Visibility and Feed Categories</phrase>
|
||||
<phrase data="catalog_selection_feed_conditions">Price and Price List Inclusion Under Feed Settings</phrase>
|
||||
<phrase data="catalog_selection_rec_note">Actual Recommendations Including Limit, Ranking and Cache</phrase>
|
||||
<phrase data="catalog_selection_source_missing">Source Product Unavailable</phrase>
|
||||
<phrase data="catalog_selection_rec_result">Native Recommendation Result</phrase>
|
||||
<phrase data="catalog_selection_rec_policy">Source Categories, Active Status, Primary Variant, Price, Stock and Limit Apply</phrase>
|
||||
</language>
|
||||
@@ -20,6 +20,7 @@
|
||||
<phrase data="auto_944019ea5d01e63a">скрыт</phrase>
|
||||
<phrase data="auto_9729bca62f6c558e">Ошибка запроса</phrase>
|
||||
<phrase data="auto_99d406eeba4f9acb">Раздел</phrase>
|
||||
<phrase data="auto_9b62723a4333bb4a">Найти раздел</phrase>
|
||||
<phrase data="auto_9f8bfc6bf2339426"><div class="alert alert-error"><i class="ti ti-alert-triangle"></i><div>Ошибки:</phrase>
|
||||
<phrase data="auto_a456fd20d54a422c"></p></div><button class="modal-close" type="button" data-catalog-recompile-close aria-label="Закрыть" disabled><i class="ti ti-x"></i></button></div><div class="modal-body"><div class="catalog-recompile-status" data-catalog-recompile-status>Подготовка...</div><div class="progress"><div class="progress-bar" data-catalog-recompile-progress style="width:0%;--progress-color:var(--blue-500)"></div></div><div class="catalog-recompile-errors" data-catalog-recompile-errors></div></div><div class="modal-footer"><button class="btn btn-ghost" type="button" data-catalog-recompile-stop>Остановить</button><button class="btn btn-primary" type="button" data-catalog-recompile-close disabled>Закрыть</button></div></div></phrase>
|
||||
<phrase data="auto_a4c0ae2fa076f2fd">Скрыть раздел</phrase>
|
||||
@@ -36,6 +37,11 @@
|
||||
<phrase data="auto_f61de80bf0ca15a4">Документ не выбран</phrase>
|
||||
<phrase data="auto_f6bde2876efe2658">Не включён</phrase>
|
||||
<phrase data="auto_fda18205701b609b">Заменить условие?</phrase>
|
||||
<phrase data="catalog_roles_more">ещё</phrase>
|
||||
<phrase data="catalog_roles_no">Нет</phrase>
|
||||
<phrase data="catalog_selection_catalog">Каталог</phrase>
|
||||
<phrase data="catalog_selection_category">Раздел</phrase>
|
||||
<phrase data="catalog_selection_search">Поиск</phrase>
|
||||
<phrase data="runtime_6291ecb1784ea2de">Каталог создан</phrase>
|
||||
<phrase data="runtime_6b3607219bc8c7f0">отличается</phrase>
|
||||
<phrase data="runtime_d0ef3540b088bbf6">Каталог</phrase>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="catalog_context_title">Связанные настройки</phrase>
|
||||
<phrase data="catalog_context_filters">Фильтры раздела</phrase>
|
||||
<phrase data="catalog_context_attribute_set">Набор характеристик</phrase>
|
||||
<phrase data="catalog_context_card">Шаблон карточки в каталоге</phrase>
|
||||
<phrase data="catalog_context_page">Шаблон страницы</phrase>
|
||||
</language>
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="catalog_roles_description">Описание товара</phrase>
|
||||
<phrase data="catalog_roles_warranty">Гарантия</phrase>
|
||||
<phrase data="catalog_roles_registration_text">Текст регистрационного документа</phrase>
|
||||
<phrase data="catalog_roles_registration_number">Номер регистрационного документа</phrase>
|
||||
<phrase data="catalog_roles_title">Назначение полей товара</phrase>
|
||||
<phrase data="catalog_roles_saved">Сохранённое сопоставление</phrase>
|
||||
<phrase data="catalog_roles_document">Документ #</phrase>
|
||||
<phrase data="catalog_roles_sample">Образец товара</phrase>
|
||||
<phrase data="catalog_roles_check">Проверить</phrase>
|
||||
<phrase data="catalog_roles_optional">Дополнительные поля проекта</phrase>
|
||||
<phrase data="catalog_roles_auto">Автоматически:</phrase>
|
||||
<phrase data="catalog_roles_unassigned">Не назначено</phrase>
|
||||
<phrase data="catalog_roles_name">Название</phrase>
|
||||
<phrase data="catalog_roles_article">Артикул</phrase>
|
||||
<phrase data="catalog_roles_price">Цена</phrase>
|
||||
<phrase data="catalog_roles_old_price">Старая цена</phrase>
|
||||
<phrase data="catalog_roles_stock">Остаток</phrase>
|
||||
<phrase data="catalog_roles_images">Изображения</phrase>
|
||||
<phrase data="catalog_roles_manufacturer">Производитель</phrase>
|
||||
<phrase data="catalog_roles_caption">Подпись карточки</phrase>
|
||||
<phrase data="catalog_roles_hide_price">Скрывать цену</phrase>
|
||||
<phrase data="catalog_roles_exclude">Исключить из прайса</phrase>
|
||||
<phrase data="catalog_roles_new">Новинка</phrase>
|
||||
<phrase data="catalog_roles_discount">Скидка</phrase>
|
||||
<phrase data="catalog_roles_gift">Подарок</phrase>
|
||||
<phrase data="catalog_roles_video">Видео</phrase>
|
||||
<phrase data="catalog_roles_instruction">Видеоинструкция</phrase>
|
||||
<phrase data="catalog_roles_square">Квадратное изображение</phrase>
|
||||
<phrase data="catalog_roles_banner">Обложка</phrase>
|
||||
<phrase data="catalog_roles_registration">Регистрационный документ</phrase>
|
||||
<phrase data="catalog_roles_program">Участие в программе оплаты</phrase>
|
||||
<phrase data="catalog_roles_program_code">Код программы оплаты</phrase>
|
||||
<phrase data="catalog_roles_published">Опубликован</phrase>
|
||||
<phrase data="catalog_roles_not_public">Не доступен публично</phrase>
|
||||
<phrase data="catalog_roles_orderable">Доступен к покупке</phrase>
|
||||
<phrase data="catalog_roles_not_orderable">Покупка недоступна</phrase>
|
||||
<phrase data="catalog_roles_unpublished">Не опубликован</phrase>
|
||||
<phrase data="catalog_roles_deleted">Удалён</phrase>
|
||||
<phrase data="catalog_roles_technical">Служебный документ или рубрика</phrase>
|
||||
<phrase data="catalog_roles_scheduled">Публикация ещё не началась</phrase>
|
||||
<phrase data="catalog_roles_expired">Публикация завершена</phrase>
|
||||
<phrase data="catalog_roles_missing_price">Нет положительной цены</phrase>
|
||||
<phrase data="catalog_roles_hidden_price">Цена скрыта</phrase>
|
||||
<phrase data="catalog_roles_excluded">Исключение из прайса включено.</phrase>
|
||||
<phrase data="catalog_roles_role">Назначение</phrase>
|
||||
<phrase data="catalog_roles_field">Поле</phrase>
|
||||
<phrase data="catalog_roles_value">Значение</phrase>
|
||||
<phrase data="catalog_roles_yes">Да</phrase>
|
||||
<phrase data="catalog_roles_no">Нет</phrase>
|
||||
<phrase data="catalog_roles_empty">Не заполнено</phrase>
|
||||
<phrase data="catalog_roles_more">ещё</phrase>
|
||||
<phrase data="catalog_roles_missing_document">Документ этой рубрики не найден.</phrase>
|
||||
<phrase data="catalog_roles_request">Условия запроса</phrase>
|
||||
<phrase data="catalog_roles_template">Шаблон страницы</phrase>
|
||||
<phrase data="catalog_roles_card">Карточка товара</phrase>
|
||||
<phrase data="catalog_roles_filters">фильтры</phrase>
|
||||
<phrase data="catalog_roles_set">Набор характеристик</phrase>
|
||||
<phrase data="catalog_roles_error">Не удалось проверить сопоставление</phrase>
|
||||
<phrase data="catalog_roles_invalid">Некорректное назначение поля</phrase>
|
||||
<phrase data="catalog_roles_wrong_rubric">Поле роли не принадлежит рубрике товара</phrase>
|
||||
</language>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="catalog_selection_check_title">Проверка выборки</phrase>
|
||||
<phrase data="catalog_selection_selection">Выборка</phrase>
|
||||
<phrase data="catalog_selection_catalog">Каталог</phrase>
|
||||
<phrase data="catalog_selection_recommendation">Рекомендации</phrase>
|
||||
<phrase data="catalog_selection_search">Поиск</phrase>
|
||||
<phrase data="catalog_selection_feed">Фид</phrase>
|
||||
<phrase data="catalog_selection_category">Раздел</phrase>
|
||||
<phrase data="catalog_selection_none">Не выбран</phrase>
|
||||
<phrase data="catalog_selection_scope">Область поиска</phrase>
|
||||
<phrase data="catalog_selection_everywhere">Везде</phrase>
|
||||
<phrase data="catalog_selection_query">Поисковая строка</phrase>
|
||||
<phrase data="catalog_selection_source">Исходный товар #</phrase>
|
||||
<phrase data="catalog_selection_limit">Лимит</phrase>
|
||||
<phrase data="catalog_selection_check">Проверить выборку</phrase>
|
||||
<phrase data="catalog_selection_stale">Параметры изменены, результат не актуален</phrase>
|
||||
<phrase data="catalog_selection_yes">Входит в выборку</phrase>
|
||||
<phrase data="catalog_selection_no">Не входит в выборку</phrase>
|
||||
<phrase data="catalog_selection_unknown">Проверка не завершена</phrase>
|
||||
<phrase data="catalog_selection_denied">Недостаточно прав</phrase>
|
||||
<phrase data="catalog_selection_unknown_context">Неизвестная выборка</phrase>
|
||||
<phrase data="catalog_selection_error">Проверка недоступна: проверьте настройки и состояние индексов</phrase>
|
||||
<phrase data="catalog_selection_catalog_note">Допуск до группировки и страниц, без выбранных посетителем фильтров</phrase>
|
||||
<phrase data="catalog_selection_category_missing">Раздел не найден</phrase>
|
||||
<phrase data="catalog_selection_category_page">Опубликованная страница раздела</phrase>
|
||||
<phrase data="catalog_selection_request_denied">Недостаточно прав для условий запроса</phrase>
|
||||
<phrase data="catalog_selection_catalog_base">Индекс, публикация, источники раздела и активность варианта</phrase>
|
||||
<phrase data="catalog_selection_conditions">Сохранённые условия запроса</phrase>
|
||||
<phrase data="catalog_selection_group">Допуск товара или его группы в выдачу</phrase>
|
||||
<phrase data="catalog_selection_search_note">Поиск до пагинации по фактической строке, с синонимами и исправлением опечаток</phrase>
|
||||
<phrase data="catalog_selection_effective_query">Фактическая поисковая строка</phrase>
|
||||
<phrase data="catalog_selection_ambiguous_query">Неоднозначное исправление: поиск не выбрал замену</phrase>
|
||||
<phrase data="catalog_selection_missing_plan">Поисковый обработчик не вернул план проверки</phrase>
|
||||
<phrase data="catalog_selection_pipeline_results">Поисковый pipeline вернул результаты</phrase>
|
||||
<phrase data="catalog_selection_search_unavailable">Поиск недоступен</phrase>
|
||||
<phrase data="catalog_selection_scope_missing">Область поиска не найдена</phrase>
|
||||
<phrase data="catalog_selection_short_query">Поисковый запрос слишком короткий</phrase>
|
||||
<phrase data="catalog_selection_search_base">Поисковый индекс, текст и публичная доступность</phrase>
|
||||
<phrase data="catalog_selection_search_rubrics">Рубрики области поиска</phrase>
|
||||
<phrase data="catalog_selection_search_conditions">Условия области поиска</phrase>
|
||||
<phrase data="catalog_selection_feed_note">Допуск в экспорт по сохранённым настройкам, без генерации файла</phrase>
|
||||
<phrase data="catalog_selection_feed_missing">Фид не найден</phrase>
|
||||
<phrase data="catalog_selection_feed_active">Фид включён</phrase>
|
||||
<phrase data="catalog_selection_feed_categories">Разрешённые разделы фида</phrase>
|
||||
<phrase data="catalog_selection_feed_base">Индекс, публикация, видимость цены и разделы фида</phrase>
|
||||
<phrase data="catalog_selection_feed_conditions">Цена и включение в прайс по настройкам фида</phrase>
|
||||
<phrase data="catalog_selection_rec_note">Фактическая выдача рекомендаций с учётом лимита, ранжирования и кеша</phrase>
|
||||
<phrase data="catalog_selection_source_missing">Исходный товар недоступен</phrase>
|
||||
<phrase data="catalog_selection_rec_result">Результат штатных рекомендаций</phrase>
|
||||
<phrase data="catalog_selection_rec_policy">Учитываются категории исходного товара, активность, основной вариант, цена, остаток и лимит</phrase>
|
||||
</language>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file adminx/modules/Catalog/migrations/006_add_item_sources.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\Common\DatabaseSchema;
|
||||
use App\Content\CatalogTables;
|
||||
|
||||
return function () {
|
||||
$table = CatalogTables::table('module_catalog_items');
|
||||
if (!DatabaseSchema::tableExists($table) || DatabaseSchema::columnExists($table, 'source_item_ids')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
DB::query('ALTER TABLE %b ADD COLUMN `source_item_ids` TEXT NULL AFTER `filters_settings`', $table);
|
||||
return 1;
|
||||
};
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
return array(
|
||||
'code' => 'catalog', 'name' => 'Каталог', 'version' => '0.3.5',
|
||||
'code' => 'catalog', 'name' => 'Каталог', 'version' => '0.3.9',
|
||||
'permissions' => array('key' => 'catalog', 'items' => array(
|
||||
array(
|
||||
'code' => 'view_catalog',
|
||||
@@ -51,6 +51,7 @@
|
||||
array('GET', '/catalog/documents', array(\App\Adminx\Catalog\Controller::class, 'documents')),
|
||||
array('GET', '/catalog/items/{id}', array(\App\Adminx\Catalog\Controller::class, 'item')),
|
||||
array('GET', '/catalog/{rubric}/{field}', array(\App\Adminx\Catalog\Controller::class, 'edit')),
|
||||
array('GET', '/catalog/{rubric}/{field}/product-preview', array(\App\Adminx\Catalog\Controller::class, 'productPreview'), array('permission' => 'view_products')),
|
||||
array('POST', '/catalog/{rubric}/{field}/items', array(\App\Adminx\Catalog\Controller::class, 'storeItem')),
|
||||
array('POST', '/catalog/{rubric}/{field}/items/{id}', array(\App\Adminx\Catalog\Controller::class, 'updateItem')),
|
||||
array('POST', '/catalog/{rubric}/{field}/items/{id}/conditions/sync', array(\App\Adminx\Catalog\Controller::class, 'syncMissingConditions')),
|
||||
@@ -63,6 +64,7 @@
|
||||
),
|
||||
'migrations' => array(
|
||||
array('id' => '005_add_catalog_purpose', 'file' => 'migrations/005_add_catalog_purpose.sql'),
|
||||
array('id' => '006_add_item_sources', 'file' => 'migrations/006_add_item_sources.php'),
|
||||
),
|
||||
'view_globals' => array('module_code' => 'catalog'),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<section class="catalog-context-links" data-catalog-context-links{{ context_links ? '' : ' hidden' }} aria-label="{{ lang.catalog_context_title|default('Связанные настройки') }}">
|
||||
<h3>{{ lang.catalog_context_title|default('Связанные настройки') }}</h3>
|
||||
<nav>{% for link in context_links %}<a href="{{ link.url }}" target="_blank" rel="noopener" data-context-kind="{{ link.kind }}"><i class="ti ti-external-link" aria-hidden="true"></i><span>{{ link.label }}{% if link.scope %}<small>{{ link.scope }}</small>{% endif %}</span></a>{% endfor %}</nav>
|
||||
</section>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="catalog-role-preview" data-product-role-preview data-error="{{ 'Не удалось проверить сопоставление'|admin_trans }}" aria-live="polite">
|
||||
<h3>{{ 'Сохранённое сопоставление'|admin_trans }}</h3>
|
||||
{% if product_roles.denied %}<p class="text-secondary">{{ 'Недостаточно прав'|admin_trans }}</p>{% else %}
|
||||
<div class="form-grid">
|
||||
<label class="field col-4"><span class="field-label">{{ 'Документ #'|admin_trans }}</span><input class="input" type="number" min="1" value="{{ product_roles.document_id ?: '' }}" data-product-role-document></label>
|
||||
<div class="field col-4"><span class="field-label">{{ 'Образец товара'|admin_trans }}</span><button type="button" class="btn btn-secondary" data-product-role-refresh><i class="ti ti-refresh"></i>{{ 'Проверить'|admin_trans }}</button></div>
|
||||
</div>
|
||||
{% include '@catalog/_product-selection-preview.twig' %}
|
||||
{% if product_roles.snapshot %}
|
||||
{% set sample = product_roles.snapshot %}
|
||||
<p>{% if product_roles.can_edit_product %}<a href="{{ ADMINX_BASE }}/catalog/products/{{ sample.id }}/edit">{{ sample.title }} · #{{ sample.id }}</a>{% else %}{{ sample.title }} · #{{ sample.id }}{% endif %}</p>
|
||||
<p><span class="badge {{ sample.published ? 'badge-green' : 'badge-gray' }}">{{ (sample.published ? 'Опубликован' : 'Не доступен публично')|admin_trans }}</span> <span class="badge {{ sample.orderable ? 'badge-green' : 'badge-gray' }}">{{ (sample.orderable ? 'Доступен к покупке' : 'Покупка недоступна')|admin_trans }}</span></p>
|
||||
{% for reason in product_roles.reasons %}<p class="text-secondary">{{ reason|admin_trans }}</p>{% endfor %}
|
||||
{% if sample.exclude_from_price %}<p class="text-secondary">{{ 'Исключение из прайса включено.'|admin_trans }}</p>{% endif %}
|
||||
<div class="table-wrap"><table class="table"><thead><tr><th>{{ 'Назначение'|admin_trans }}</th><th>{{ 'Поле'|admin_trans }}</th><th>{{ 'Значение'|admin_trans }}</th></tr></thead><tbody>
|
||||
{% for role,definition in product_roles.definitions %}{% if not definition.optional or definition.field_id %}
|
||||
<tr><td>{{ definition.label|admin_trans }}</td><td data-label="{{ 'Поле'|admin_trans }}">{% if definition.field %}<a href="{{ ADMINX_BASE }}/rubrics?fields={{ catalog.rubric_id }}&field={{ definition.field_id }}">{{ definition.field.title ?: definition.field.alias }} · #{{ definition.field_id }}</a>{% else %}{{ 'Не назначено'|admin_trans }}{% endif %}</td><td data-label="{{ 'Значение'|admin_trans }}">{% set value = attribute(sample,role) %}{% if value is same as(true) %}{{ 'Да'|admin_trans }}{% elseif value is same as(false) %}{{ 'Нет'|admin_trans }}{% elseif value is iterable %}{% if value is empty %}{{ 'Не заполнено'|admin_trans }}{% else %}{{ value|slice(0, 3)|join(', ') }}{% if value|length > 3 %} · {{ 'ещё'|admin_trans }} {{ value|length - 3 }}{% endif %}{% endif %}{% elseif value is same as('') %}{{ 'Не заполнено'|admin_trans }}{% else %}{{ value }}{% endif %}</td></tr>
|
||||
{% endif %}{% endfor %}
|
||||
</tbody></table></div>
|
||||
{% else %}<p class="text-secondary">{{ 'Документ этой рубрики не найден.'|admin_trans }}</p>{% endif %}
|
||||
<div class="btn-group">
|
||||
{% if settings.request_id and product_roles.can_requests %}<a class="btn btn-ghost" href="{{ ADMINX_BASE }}/requests/{{ settings.request_id }}"><i class="ti ti-filter"></i>{{ 'Условия запроса'|admin_trans }}</a>{% endif %}
|
||||
{% if product_roles.can_rubrics %}<a class="btn btn-ghost" href="{{ ADMINX_BASE }}/rubrics?templates={{ catalog.rubric_id }}{% if product_roles.snapshot.document.rubric_tmpl_id %}&template={{ product_roles.snapshot.document.rubric_tmpl_id }}{% endif %}"><i class="ti ti-template"></i>{{ 'Шаблон страницы'|admin_trans }}</a>{% endif %}
|
||||
<a class="btn btn-ghost" href="{{ ADMINX_BASE }}/catalog/{% if product_roles.card_template_id %}card-templates?edit={{ product_roles.card_template_id }}{% else %}public-templates?template=product-card{% endif %}"><i class="ti ti-layout-cards"></i>{{ 'Карточка товара'|admin_trans }}</a>
|
||||
</div>
|
||||
{% for section in product_roles.sections %}<p><a href="{{ ADMINX_BASE }}/catalog/{{ section.rubric_id }}/{{ section.field_id }}?item={{ section.id }}&tab=filters">{{ section.name }}: {{ 'фильтры'|admin_trans }}</a>{% if section.attribute_set_id %} · <a href="{{ ADMINX_BASE }}/catalog/attribute-sets/{{ section.attribute_set_id }}">{{ 'Набор характеристик'|admin_trans }} #{{ section.attribute_set_id }}</a>{% endif %}</p>{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<section class="catalog-commerce-fields" data-catalog-commerce-fields>
|
||||
<div class="catalog-behavior-head"><span class="icon-tile catalog-behavior-icon is-green"><i class="ti ti-shopping-bag"></i></span><h3>{{ 'Назначение полей товара'|admin_trans }}</h3></div>
|
||||
<div class="form-grid">
|
||||
{% for role,definition in product_roles.definitions %}{% if not definition.optional %}
|
||||
<label class="field col-4"><span class="field-label">{{ definition.label|admin_trans }}</span><select class="select" name="product_roles[{{ role }}]">
|
||||
<option value="-1"{{ definition.selected == -1 ? ' selected' : '' }}>{{ 'Автоматически:'|admin_trans }} {{ definition.alias }}</option><option value="0"{{ definition.selected == 0 ? ' selected' : '' }}>{{ 'Не назначено'|admin_trans }}</option>
|
||||
{% for group in field_groups %}{% for field in group.items %}<option value="{{ field.id }}"{{ definition.selected == field.id ? ' selected' : '' }}>{{ field.title ?: field.alias }} · #{{ field.id }}</option>{% endfor %}{% endfor %}
|
||||
</select></label>
|
||||
{% endif %}{% endfor %}
|
||||
</div>
|
||||
<details><summary>{{ 'Дополнительные поля проекта'|admin_trans }}</summary><div class="form-grid">
|
||||
{% for role,definition in product_roles.definitions %}{% if definition.optional %}
|
||||
<label class="field col-4"><span class="field-label">{{ definition.label|admin_trans }}</span><select class="select" name="product_roles[{{ role }}]">
|
||||
<option value="-1"{{ definition.selected == -1 ? ' selected' : '' }}>{{ 'Автоматически:'|admin_trans }} {{ definition.alias }}</option><option value="0"{{ definition.selected == 0 ? ' selected' : '' }}>{{ 'Не назначено'|admin_trans }}</option>
|
||||
{% for group in field_groups %}{% for field in group.items %}<option value="{{ field.id }}"{{ definition.selected == field.id ? ' selected' : '' }}>{{ field.title ?: field.alias }} · #{{ field.id }}</option>{% endfor %}{% endfor %}
|
||||
</select></label>
|
||||
{% endif %}{% endfor %}
|
||||
</div></details>
|
||||
{% include '@catalog/_product-role-preview.twig' %}
|
||||
</section>
|
||||
@@ -0,0 +1,24 @@
|
||||
{% set selection = product_roles.selection %}
|
||||
{% set options = product_roles.selection_options %}
|
||||
<section class="catalog-selection-preview" data-product-selection>
|
||||
<h4>{{ 'Проверка выборки'|admin_trans }}</h4>
|
||||
<div class="catalog-selection-controls">
|
||||
<label class="field"><span class="field-label">{{ 'Выборка'|admin_trans }}</span><select class="select" data-selection="context">{% for code,label in options.contexts %}<option value="{{ code }}"{{ selection.context == code ? ' selected' : '' }}>{{ label|admin_trans }}</option>{% endfor %}</select></label>
|
||||
<label class="field" data-selection-context="catalog"{{ selection.context != 'catalog' ? ' hidden' : '' }}><span class="field-label">{{ 'Раздел'|admin_trans }}</span><select class="select" data-selection="category"><option value="0">{{ 'Не выбран'|admin_trans }}</option>{% for item in options.categories %}<option value="{{ item.id }}"{{ selection.category == item.id ? ' selected' : '' }}>{{ item.name }} #{{ item.id }}</option>{% endfor %}</select></label>
|
||||
<label class="field" data-selection-context="feed"{{ selection.context != 'feed' ? ' hidden' : '' }}><span class="field-label">{{ 'Фид'|admin_trans }}</span><select class="select" data-selection="feed"><option value="0">{{ 'Не выбран'|admin_trans }}</option>{% for item in options.feeds %}<option value="{{ item.id }}"{{ selection.feed == item.id ? ' selected' : '' }}>{{ item.name }} #{{ item.id }}</option>{% endfor %}</select></label>
|
||||
<label class="field" data-selection-context="search"{{ selection.context != 'search' ? ' hidden' : '' }}><span class="field-label">{{ 'Область поиска'|admin_trans }}</span><select class="select" data-selection="scope">{% for item in options.scopes %}<option value="{{ item.code }}"{{ selection.scope == item.code ? ' selected' : '' }}>{{ item.label|admin_trans }}</option>{% endfor %}</select></label>
|
||||
<label class="field" data-selection-context="search"{{ selection.context != 'search' ? ' hidden' : '' }}><span class="field-label">{{ 'Поисковая строка'|admin_trans }}</span><input class="input" type="search" maxlength="200" value="{{ selection.query }}" data-selection="query"></label>
|
||||
<label class="field" data-selection-context="recommendation"{{ selection.context != 'recommendation' ? ' hidden' : '' }}><span class="field-label">{{ 'Исходный товар #'|admin_trans }}</span><input class="input" type="number" min="1" value="{{ selection.source ?: '' }}" data-selection="source"></label>
|
||||
<label class="field" data-selection-context="recommendation"{{ selection.context != 'recommendation' ? ' hidden' : '' }}><span class="field-label">{{ 'Лимит'|admin_trans }}</span><input class="input" type="number" min="1" max="24" value="{{ selection.limit ?: 8 }}" data-selection="limit"></label>
|
||||
<button class="btn btn-secondary" type="button" data-product-selection-check><i class="ti ti-list-search" aria-hidden="true"></i>{{ 'Проверить выборку'|admin_trans }}</button>
|
||||
</div>
|
||||
<p class="text-secondary" data-selection-stale hidden>{{ 'Параметры изменены, результат не актуален'|admin_trans }}</p>
|
||||
{% if product_roles.selection_result %}
|
||||
{% set result = product_roles.selection_result %}
|
||||
<div class="catalog-selection-result" data-selection-result>
|
||||
<p><span class="badge {{ result.matched is same as(true) ? 'badge-green' : 'badge-gray' }}">{{ (result.matched is same as(true) ? 'Входит в выборку' : (result.matched is same as(false) ? 'Не входит в выборку' : 'Проверка не завершена'))|admin_trans }}</span></p>
|
||||
<ul>{% for step in result.steps %}<li data-selection-step="{{ step.code }}"><i class="ti {{ step.pass is same as(true) ? 'ti-check text-success' : (step.pass is same as(false) ? 'ti-x text-danger' : 'ti-info-circle') }}" aria-hidden="true"></i><span>{{ step.label|admin_trans }}{% if step.detail != '' %}: <b>{{ step.detail }}</b>{% endif %}<span class="sr-only"> {{ (step.pass is same as(true) ? 'Да' : (step.pass is same as(false) ? 'Нет' : ''))|admin_trans }}</span></span></li>{% endfor %}</ul>
|
||||
{% if result.note %}<p class="text-secondary text-sm">{{ result.note|admin_trans }}</p>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
@@ -42,7 +42,7 @@
|
||||
<label class="field col-3"><span class="field-label">Навигация в шапке</span><select class="select" name="navi_id"><option value="0">Не связана</option>{% for item in navigation_options %}<option value="{{ item.id }}"{{ settings.navi_id == item.id ? ' selected' : '' }}>{{ item.title ?: item.alias }}{% if item.alias and item.alias != item.title %} · {{ item.alias }}{% endif %} (#{{ item.id }})</option>{% endfor %}</select><span class="field-hint">Дополнительные ссылки и подборки перед разделами каталога.</span></label>
|
||||
<label class="field col-3"><span class="field-label">Рубрика разделов</span><select class="select" name="rub_cat_id"><option value="0">Не выбрана</option>{% for item in rubric_options %}<option value="{{ item.id }}"{{ settings.rub_cat_id == item.id ? ' selected' : '' }}>{{ item.title ?: item.alias }}{% if item.alias and item.alias != item.title %} · {{ item.alias }}{% endif %} (#{{ item.id }})</option>{% endfor %}</select><span class="field-hint">Рубрика документов, связанных с разделами каталога.</span></label>
|
||||
</div>
|
||||
{% if products_available %}<section class="catalog-commerce-fields" data-catalog-commerce-fields><div class="catalog-behavior-head"><span class="icon-tile catalog-behavior-icon is-green"><i class="ti ti-shopping-bag"></i></span><h3>Поля товарного представления</h3></div><div class="form-grid">{% set commerce_fields={'product_title_field_id':'Название товара','product_article_field_id':'Артикул','product_price_field_id':'Цена','product_old_price_field_id':'Старая цена','product_stock_field_id':'Остаток','product_images_field_id':'Изображения'} %}{% for key,label in commerce_fields %}<label class="field col-4"><span class="field-label">{{ label }}</span><select class="select" name="{{ key }}"><option value="0">Не назначено</option>{% for group in field_groups %}{% for field in group.items %}<option value="{{ field.id }}"{{ attribute(settings,key) == field.id ? ' selected' : '' }}>{{ field.title ?: field.alias }} · #{{ field.id }}</option>{% endfor %}{% endfor %}</select></label>{% endfor %}</div></section>
|
||||
{% if products_available %}{% include '@catalog/_product-roles.twig' %}
|
||||
<section class="catalog-product-card-settings" data-catalog-commerce-fields>
|
||||
<div class="catalog-behavior-head"><span class="icon-tile catalog-behavior-icon is-blue"><i class="ti ti-layout-cards"></i></span><div><h3>Карточка товара</h3><p class="text-secondary text-sm">Единый вид для главной страницы и разделов товарного каталога.</p></div></div>
|
||||
<div class="catalog-product-card-layout">
|
||||
@@ -95,11 +95,12 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if can_manage %}<aside class="drawer drawer-right drawer-xl catalog-drawer" id="catalogItemDrawer" role="dialog" aria-modal="true" hidden>
|
||||
{% if can_manage %}<script type="application/json" data-catalog-source-options>{{ source_options|json_encode|raw }}</script><aside class="drawer drawer-right drawer-xl catalog-drawer" id="catalogItemDrawer" role="dialog" aria-modal="true" hidden>
|
||||
<div class="drawer-header"><div><h3 data-catalog-drawer-title>Раздел каталога</h3><p class="text-secondary text-sm">Свойства, поля документа и фильтры.</p></div><button class="btn btn-ghost btn-icon" type="button" data-close-drawer aria-label="Закрыть"><i class="ti ti-x"></i></button></div>
|
||||
<form id="catalogItemForm" class="drawer-body catalog-item-form" data-base="{{ ADMINX_BASE }}" data-rubric="{{ catalog.rubric_id }}" data-field="{{ catalog.field_id }}"><input type="hidden" name="_csrf" value="{{ csrf_token }}"><input type="hidden" name="id" value=""><input type="hidden" name="filters_order" value="">
|
||||
<div class="tabs catalog-drawer-tabs"><button class="tab is-active" type="button" data-catalog-drawer-tab="main">Основное</button><button class="tab" type="button" data-catalog-drawer-tab="fields">Поля <span class="tab-count" data-catalog-fields-count>0</span></button><button class="tab" type="button" data-catalog-drawer-tab="filters">Фильтры <span class="tab-count" data-catalog-filters-count>0</span></button></div>
|
||||
<section data-catalog-drawer-panel="main"><div class="form-grid"><label class="field col-12"><span class="field-label">Название</span><input class="input" type="text" name="name" maxlength="255" required><span class="field-error" data-error="name"></span></label><label class="field col-6"><span class="field-label">Родитель</span><select class="select" name="parent_id"><option value="0">Верхний уровень</option>{% for item in flat %}<option value="{{ item.id }}" data-level="{{ item.level }}">{{ item.name }}</option>{% endfor %}</select></label><div class="field col-6"><span class="field-label">Связанный документ</span><input type="hidden" name="document_id" value=""><div class="catalog-document-picker"><button class="catalog-document-value" type="button" data-catalog-document-pick><i class="ti ti-file-search"></i><span data-catalog-document-label>Документ не выбран</span></button><button class="btn btn-ghost btn-icon" type="button" data-catalog-document-clear data-tooltip="Убрать связь" aria-label="Убрать связанный документ"><i class="ti ti-x"></i></button></div></div><div class="field col-12 catalog-toggle-row"><span><b>Раздел активен</b><small>Доступен в поле документа и на сайте.</small></span><label class="switch"><input type="checkbox" name="status" value="1" aria-label="Раздел активен"></label></div></div></section>
|
||||
<section data-catalog-drawer-panel="main"><div class="form-grid"><label class="field col-12"><span class="field-label">Название</span><input class="input" type="text" name="name" maxlength="255" required><span class="field-error" data-error="name"></span></label><label class="field col-6"><span class="field-label">Родитель</span><select class="select" name="parent_id"><option value="0">Верхний уровень</option>{% for item in flat %}<option value="{{ item.id }}" data-level="{{ item.level }}">{{ item.name }}</option>{% endfor %}</select></label><div class="field col-6"><span class="field-label">Связанный документ</span><input type="hidden" name="document_id" value=""><div class="catalog-document-picker"><button class="catalog-document-value" type="button" data-catalog-document-pick><i class="ti ti-file-search"></i><span data-catalog-document-label>Документ не выбран</span></button><button class="btn btn-ghost btn-icon" type="button" data-catalog-document-clear data-tooltip="Убрать связь" aria-label="Убрать связанный документ"><i class="ti ti-x"></i></button></div></div><div class="field col-12"><span class="field-label">Источники товаров</span><div class="catalog-source-picker" data-catalog-source-picker><div data-catalog-source-inputs></div><button class="catalog-source-value" type="button" data-catalog-source-open{{ not source_schema_available ? ' disabled' : '' }}><i class="ti ti-folders"></i><span data-catalog-source-label>Товары только из этого раздела</span></button><button class="btn btn-ghost btn-icon" type="button" data-catalog-source-clear data-tooltip="Очистить источники" aria-label="Очистить источники" disabled><i class="ti ti-x"></i></button></div><span class="field-hint">Выберите другие разделы, если эта страница должна объединять их товары. Сами разделы и их место в каталоге не изменятся.</span>{% if not source_schema_available %}<span class="field-hint text-danger">Примените миграцию модуля «Каталог», чтобы включить эту настройку.</span>{% endif %}</div><div class="field col-12 catalog-toggle-row"><span><b>Раздел активен</b><small>Включённый раздел доступен в списках и меню; связанный документ можно открыть по прямому адресу независимо от этой настройки.</small></span><label class="switch"><input type="checkbox" name="status" value="1" aria-label="Раздел активен"></label></div></div></section>
|
||||
{% include '@catalog/_context-links.twig' with {context_links: []} %}
|
||||
<section data-catalog-drawer-panel="fields" hidden>
|
||||
<div class="catalog-choice-head catalog-fields-grid"><span>Поле</span><span>Тип</span><span>Включено</span></div>
|
||||
<div class="catalog-field-groups">{% for group in field_groups %}<section class="catalog-field-group" data-catalog-field-group="fields"><div class="catalog-field-group-head"><div class="catalog-field-group-title"><span class="icon-tile catalog-field-group-icon"><i class="ti ti-category-2"></i></span><span class="catalog-field-group-copy"><small>Группа полей</small><b>{{ group.title }}</b></span></div><div class="catalog-field-group-meta"><span>Всего: {{ group.items|length }}</span><span class="badge badge-blue" data-catalog-group-selected>Включено: 0</span></div></div><div class="catalog-check-list">{% for field in group.items %}<div class="catalog-choice-row catalog-fields-grid"><span class="catalog-choice-copy"><b>{{ field.title ?: field.alias }}</b><small>{{ field.alias ?: 'field_' ~ field.id }} · #{{ field.id }}</small></span><span class="catalog-field-type"><b>{{ field.type_title }}</b><small>{{ field.type }}</small></span><label class="switch"><input type="checkbox" name="fields_use[]" value="{{ field.id }}" aria-label="Использовать поле {{ field.title ?: field.alias }}"></label></div>{% endfor %}</div></section>{% endfor %}</div>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Adminx\Support\PhpCode;
|
||||
use App\Common\PublicConfiguration;
|
||||
use App\Common\TemporaryDirectory;
|
||||
|
||||
class Runner
|
||||
@@ -31,7 +32,8 @@
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
return false;
|
||||
$config = PublicConfiguration::value('admin_console', array());
|
||||
return is_array($config) && !empty($config['enabled']);
|
||||
}
|
||||
|
||||
public static function execute($code)
|
||||
@@ -144,6 +146,11 @@
|
||||
$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';
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
return array(
|
||||
'code' => 'console',
|
||||
'name' => 'PHP-консоль',
|
||||
'version' => '0.1.1',
|
||||
'version' => '0.1.2',
|
||||
'registry' => array('dynamic' => true),
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
return array(
|
||||
'code' => 'console',
|
||||
'name' => 'PHP-консоль',
|
||||
'version' => '0.1.1',
|
||||
'version' => '0.1.2',
|
||||
'registry' => array('dynamic' => true),
|
||||
'permissions' => array(
|
||||
'key' => 'console',
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<h1>PHP-консоль</h1>
|
||||
<p class="text-secondary">Системный инструмент · изолированный PHP-процесс</p>
|
||||
</div>
|
||||
<span class="badge badge-{{ runner_enabled ? 'orange' : 'red' }} badge-dot">{{ runner_enabled ? 'Локальное выполнение' : 'Выполнение отключено' }}</span>
|
||||
<span class="badge badge-{{ runner_enabled ? 'orange' : 'red' }} badge-dot">{{ runner_enabled ? 'Выполнение включено' : 'Выполнение отключено' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -102,7 +102,9 @@
|
||||
public function toggle(array $params=array())
|
||||
{
|
||||
if(($e=$this->guard())!==null){return $e;}
|
||||
try{$active=Model::toggle(isset($params['id'])?$params['id']:0,Auth::id());}catch(\InvalidArgumentException $e){return $this->error($e->getMessage(),array(),422);}
|
||||
$id=isset($params['id'])?(int)$params['id']:0;
|
||||
try{$active=Model::toggle($id,Auth::id());}catch(\InvalidArgumentException $e){return $this->error($e->getMessage(),array(),422);}
|
||||
AuditLog::record('customers.status_changed',array('actor_id'=>Auth::id(),'target_type'=>'public_user','target_id'=>$id,'meta'=>array('active'=>$active)));
|
||||
return $this->success($active?'Пользователь включён':'Пользователь отключён');
|
||||
}
|
||||
|
||||
@@ -119,6 +121,7 @@
|
||||
$id=isset($params['id'])?(int)$params['id']:0;$input=Request::postAll();
|
||||
if(!Permission::check('manage_users')){$current=Model::customer($id,Auth::id());$input['admin_access']=!empty($current['system']['is_active'])?'1':'';$input['admin_role']=!empty($current['system']['role'])?(string)$current['system']['role']:'manager';}
|
||||
try{$customer=Model::updateCustomer($id,$input,Auth::id());}catch(\InvalidArgumentException $e){return $this->error($e->getMessage(),array(),422);}catch(\Throwable $e){return $this->error('Не удалось сохранить пользователя',array(),500);}
|
||||
AuditLog::record('customers.updated',array('actor_id'=>Auth::id(),'target_type'=>'public_user','target_id'=>$id));
|
||||
return $this->success('Профиль пользователя сохранён',array('data'=>$customer));
|
||||
}
|
||||
|
||||
@@ -128,14 +131,17 @@
|
||||
return $e;
|
||||
}
|
||||
|
||||
$id = isset($params['id']) ? (int) $params['id'] : 0;
|
||||
try {
|
||||
Model::deleteCustomer(isset($params['id']) ? $params['id'] : 0, Auth::id());
|
||||
Model::deleteCustomer($id, Auth::id());
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage(), array(), 422);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('Не удалось удалить пользователя', array(), 500);
|
||||
}
|
||||
|
||||
AuditLog::record('customers.deleted', array('actor_id' => Auth::id(), 'target_type' => 'public_user', 'target_id' => $id));
|
||||
|
||||
return $this->success('Пользователь удалён', array('reload' => true));
|
||||
}
|
||||
|
||||
@@ -143,10 +149,10 @@
|
||||
public function deleteField(array $params=array()){if(($e=$this->guard())!==null){return $e;}Model::deleteField(isset($params['id'])?$params['id']:0);return $this->success('Поле удалено',array('reload'=>true));}
|
||||
public function toggleField(array $params=array()){if(($e=$this->guard())!==null){return $e;}$active=Model::toggleField(isset($params['id'])?$params['id']:0);return $this->success($active?'Поле включено':'Поле скрыто',array('data'=>array('is_active'=>$active?1:0)));}
|
||||
public function reorderFields(array $params=array()){if(($e=$this->guard())!==null){return $e;}$ids=json_decode(Request::postStr('order','[]'),true);if(!is_array($ids)){return $this->error('Некорректный порядок полей',array(),422);}$count=Model::reorderFields($ids);return $this->success('Порядок полей сохранён',array('data'=>array('count'=>$count)));}
|
||||
public function saveAuthSettings(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$settings=Model::saveAuthSettings(Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Настройки регистрации сохранены',array('data'=>array('settings'=>$settings)));}
|
||||
public function saveAuthPages(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$settings=Model::saveAuthPages(Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Страницы входа сохранены',array('data'=>array('settings'=>$settings)));}
|
||||
public function saveAuthSettings(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$settings=Model::saveAuthSettings(Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}AuditLog::record('customers.auth_settings_updated',array('actor_id'=>Auth::id(),'target_type'=>'public_auth'));return $this->success('Настройки регистрации сохранены',array('data'=>array('settings'=>$settings)));}
|
||||
public function saveAuthPages(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$settings=Model::saveAuthPages(Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}AuditLog::record('customers.auth_pages_updated',array('actor_id'=>Auth::id(),'target_type'=>'public_auth'));return $this->success('Страницы входа сохранены',array('data'=>array('settings'=>$settings)));}
|
||||
public function authForm(array $params=array()){if(!Permission::check('view_customers')){return $this->error('Недостаточно прав',array(),403);}try{$form=Model::authForm(isset($params['key'])?(string)$params['key']:'');}catch(\Throwable $e){return $this->error($e->getMessage(),array(),404);}return $this->success('',array('data'=>$form));}
|
||||
public function saveAuthForm(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$template=Model::saveAuthForm(isset($params['key'])?(string)$params['key']:'',Request::postStr('template',''),Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array('template'=>$e->getMessage()),422);}return $this->success('Шаблон формы сохранён',array('data'=>array('template'=>$template,'customized'=>1)));}
|
||||
public function resetAuthForm(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$template=Model::resetAuthForm(isset($params['key'])?(string)$params['key']:'');}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Восстановлен штатный шаблон формы',array('data'=>array('template'=>$template,'customized'=>0)));}
|
||||
public function saveAuthForm(array $params=array()){if(($e=$this->guard())!==null){return $e;}$key=isset($params['key'])?(string)$params['key']:'';try{$template=Model::saveAuthForm($key,Request::postStr('template',''),Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array('template'=>$e->getMessage()),422);}AuditLog::record('customers.auth_form_updated',array('actor_id'=>Auth::id(),'target_type'=>'public_auth_form','meta'=>array('form'=>$key)));return $this->success('Шаблон формы сохранён',array('data'=>array('template'=>$template,'customized'=>1)));}
|
||||
public function resetAuthForm(array $params=array()){if(($e=$this->guard())!==null){return $e;}$key=isset($params['key'])?(string)$params['key']:'';try{$template=Model::resetAuthForm($key);}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}AuditLog::record('customers.auth_form_reset',array('actor_id'=>Auth::id(),'target_type'=>'public_auth_form','meta'=>array('form'=>$key)));return $this->success('Восстановлен штатный шаблон формы',array('data'=>array('template'=>$template,'customized'=>0)));}
|
||||
protected function guard(){return $this->guardPermission('manage_customers');}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
. ($orderJoin !== '' ? 'COALESCE(orders.orders_count,0)' : '0') . ' orders_count,'
|
||||
. ($orderJoin !== '' ? 'COALESCE(orders.orders_total,0)' : '0') . ' orders_total,'
|
||||
. ($orderJoin !== '' ? 'COALESCE(orders.last_order_at,0)' : '0') . ' last_order_at'
|
||||
. ' FROM ' . $users . ' u' . $orderJoin . ' WHERE u.deleted!=%s';
|
||||
. ' FROM ' . $users . ' u' . $orderJoin . ' WHERE COALESCE(u.deleted,0)!=%s';
|
||||
$args = array('1');
|
||||
if ($query !== '') {
|
||||
$sql .= ' AND (u.email LIKE %ss OR u.firstname LIKE %ss OR u.lastname LIKE %ss OR u.user_name LIKE %ss OR u.phone LIKE %ss OR u.company LIKE %ss';
|
||||
@@ -78,7 +78,7 @@
|
||||
$users = PublicUserTables::table('users');
|
||||
$orders = BasketTables::table('module_basket_history');
|
||||
$result = array(
|
||||
'total' => (int) DB::query('SELECT COUNT(*) FROM ' . $users . ' WHERE deleted!=%s', '1')->getValue(),
|
||||
'total' => (int) DB::query('SELECT COUNT(*) FROM ' . $users . ' WHERE COALESCE(deleted,0)!=%s', '1')->getValue(),
|
||||
'buyers' => 0, 'repeat' => 0, 'duplicates' => count(self::duplicateGroups()),
|
||||
);
|
||||
if (DatabaseSchema::tableExists($orders)) {
|
||||
@@ -169,10 +169,10 @@
|
||||
{
|
||||
$table = PublicUserTables::table('users'); $groups = array();
|
||||
$queries = array(
|
||||
'email' => "SELECT LOWER(TRIM(email)) duplicate_value,GROUP_CONCAT(Id ORDER BY Id) ids,COUNT(*) amount FROM $table WHERE deleted!='1' AND email!='' GROUP BY LOWER(TRIM(email)) HAVING COUNT(*)>1 LIMIT 30",
|
||||
'email' => "SELECT LOWER(TRIM(email)) duplicate_value,GROUP_CONCAT(Id ORDER BY Id) ids,COUNT(*) amount FROM $table WHERE COALESCE(deleted,0)!='1' AND email!='' GROUP BY LOWER(TRIM(email)) HAVING COUNT(*)>1 LIMIT 30",
|
||||
);
|
||||
if (DatabaseSchema::columnExists($table, 'phone_normalized')) {
|
||||
$queries['phone'] = "SELECT phone_normalized duplicate_value,GROUP_CONCAT(Id ORDER BY Id) ids,COUNT(*) amount FROM $table WHERE deleted!='1' AND phone_normalized IS NOT NULL GROUP BY phone_normalized HAVING COUNT(*)>1 LIMIT 30";
|
||||
$queries['phone'] = "SELECT phone_normalized duplicate_value,GROUP_CONCAT(Id ORDER BY Id) ids,COUNT(*) amount FROM $table WHERE COALESCE(deleted,0)!='1' AND phone_normalized IS NOT NULL GROUP BY phone_normalized HAVING COUNT(*)>1 LIMIT 30";
|
||||
}
|
||||
|
||||
foreach ($queries as $kind => $sql) {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{
|
||||
$query = trim((string) $query); if ($query === '') { return array(); }
|
||||
$sql = 'SELECT Id,email,firstname,lastname,user_name,phone,company,status FROM ' . PublicUserTables::table('users')
|
||||
. ' WHERE deleted!=%s AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR user_name LIKE %ss OR phone LIKE %ss OR company LIKE %ss';
|
||||
. ' WHERE COALESCE(deleted,0)!=%s AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR user_name LIKE %ss OR phone LIKE %ss OR company LIKE %ss';
|
||||
$args = array('1', $query, $query, $query, $query, $query, $query);
|
||||
if (ctype_digit($query)) { $sql .= ' OR Id=%i'; $args[] = (int) $query; }
|
||||
$sql .= ') ORDER BY status DESC,Id DESC LIMIT ' . max(1, min(12, (int) $limit));
|
||||
|
||||
@@ -35,15 +35,15 @@
|
||||
{
|
||||
public static function customers($q='')
|
||||
{
|
||||
$sql='SELECT Id AS id,email,firstname,lastname,user_name,phone,company,status,reg_time,last_visit FROM '.self::table('users').' WHERE deleted!=%s';$args=array('1');$q=trim((string)$q);
|
||||
if($q!==''){$sql.=' AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR phone LIKE %ss OR company LIKE %ss)';for($i=0;$i<5;$i++){$args[]=$q;}}$sql.=' ORDER BY Id DESC LIMIT 500';return call_user_func_array(array('DB','query'),array_merge(array($sql),$args))->getAll()?:array();
|
||||
$sql='SELECT Id AS id,email,firstname,lastname,user_name,phone,company,status,reg_time,last_visit FROM '.self::table('users').' WHERE COALESCE(deleted,0)!=%s';$args=array('1');$q=trim((string)$q);
|
||||
if($q!==''){$sql.=' AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR user_name LIKE %ss OR phone LIKE %ss OR company LIKE %ss';for($i=0;$i<6;$i++){$args[]=$q;}if(ctype_digit($q)){$sql.=' OR Id=%i';$args[]=(int)$q;}$sql.=')';}$sql.=' ORDER BY Id DESC LIMIT 500';return call_user_func_array(array('DB','query'),array_merge(array($sql),$args))->getAll()?:array();
|
||||
}
|
||||
|
||||
public static function exportChunk($q, $beforeId, $limit = 500)
|
||||
{
|
||||
$sql = 'SELECT Id AS id,email,firstname,lastname,user_name,phone,company,status,reg_time,last_visit FROM ' . self::table('users') . ' WHERE deleted!=%s';
|
||||
$sql = 'SELECT Id AS id,email,firstname,lastname,user_name,phone,company,status,reg_time,last_visit FROM ' . self::table('users') . ' WHERE COALESCE(deleted,0)!=%s';
|
||||
$args = array('1'); $q = trim((string) $q);
|
||||
if ($q !== '') { $sql .= ' AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR phone LIKE %ss OR company LIKE %ss)'; for ($i = 0; $i < 5; $i++) { $args[] = $q; } }
|
||||
if ($q !== '') { $sql .= ' AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR user_name LIKE %ss OR phone LIKE %ss OR company LIKE %ss)'; for ($i = 0; $i < 6; $i++) { $args[] = $q; } }
|
||||
if ((int) $beforeId > 0) { $sql .= ' AND Id<%i'; $args[] = (int) $beforeId; }
|
||||
$sql .= ' ORDER BY Id DESC LIMIT ' . max(1, min(1000, (int) $limit));
|
||||
return call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll() ?: array();
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
public static function stats()
|
||||
{
|
||||
$table=self::table('users');return array('total'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE deleted!=%s','1')->getValue(),'active'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE deleted!=%s AND status=%s','1','1')->getValue(),'verified'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE deleted!=%s AND (email_verified_at>0 OR phone_verified_at>0)','1')->getValue(),'fields'=>count(self::fields()));
|
||||
$table=self::table('users');return array('total'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE COALESCE(deleted,0)!=%s','1')->getValue(),'active'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE COALESCE(deleted,0)!=%s AND status=%s','1','1')->getValue(),'verified'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE COALESCE(deleted,0)!=%s AND (email_verified_at>0 OR phone_verified_at>0)','1')->getValue(),'fields'=>count(self::fields()));
|
||||
}
|
||||
|
||||
public static function toggle($id, $currentSystemId = 0)
|
||||
@@ -65,7 +65,7 @@
|
||||
throw new \InvalidArgumentException('Нельзя отключить собственную учётную запись');
|
||||
}
|
||||
|
||||
DB::query('UPDATE '.self::table('users')." SET status=IF(status='1','0','1') WHERE Id=%i AND deleted!=%s",(int)$id,'1');
|
||||
DB::query('UPDATE '.self::table('users')." SET status=IF(status='1','0','1') WHERE Id=%i AND COALESCE(deleted,0)!=%s",(int)$id,'1');
|
||||
$active=(string)DB::query('SELECT status FROM '.self::table('users').' WHERE Id=%i',(int)$id)->getValue()==='1';
|
||||
if(!$active){self::invalidateCustomerSessions((int)$id);}
|
||||
return $active;
|
||||
@@ -73,9 +73,25 @@
|
||||
|
||||
public static function customer($id, $currentSystemId = 0)
|
||||
{
|
||||
$row=DB::query('SELECT Id AS id,email,email_verified_at,firstname,lastname,user_name,phone,phone_normalized,phone_verified_at,company,city,street,street_nr,zipcode,birthday,description,user_group,status,reg_time,last_visit FROM '.self::table('users').' WHERE Id=%i AND deleted!=%s LIMIT 1',(int)$id,'1')->getAssoc();
|
||||
if(!$row){return null;}
|
||||
$row=(array)$row;
|
||||
$source = (new UserRepository())->find((int) $id);
|
||||
if (!$source) { return null; }
|
||||
|
||||
$defaults = array(
|
||||
'email' => null, 'email_verified_at' => 0, 'firstname' => '', 'lastname' => '',
|
||||
'user_name' => '', 'phone' => '', 'phone_normalized' => null, 'phone_verified_at' => 0,
|
||||
'company' => '', 'city' => '', 'street' => '', 'street_nr' => '', 'zipcode' => '',
|
||||
'birthday' => '', 'description' => '', 'user_group' => 0, 'status' => '0',
|
||||
'reg_time' => 0, 'last_visit' => 0,
|
||||
);
|
||||
$row = array('id' => (int) $source['Id']);
|
||||
foreach ($defaults as $key => $default) {
|
||||
$row[$key] = array_key_exists($key, $source) ? $source[$key] : $default;
|
||||
}
|
||||
|
||||
if (empty($row['phone_normalized']) && !empty($row['phone'])) {
|
||||
$row['phone_normalized'] = Phone::normalize($row['phone']);
|
||||
}
|
||||
|
||||
$row['birthday']=(int)$row['birthday']>0?date('Y-m-d',(int)$row['birthday']):'';
|
||||
$values=DB::query('SELECT field_id,value FROM '.self::table('user_profile_values').' WHERE user_id=%i',(int)$id)->getAll()?:array();
|
||||
$extra=array();
|
||||
@@ -137,9 +153,9 @@
|
||||
if($email===''&&$phone===''){throw new \InvalidArgumentException('Укажите email или телефон');}
|
||||
if($adminAccess&&$email===''){throw new \InvalidArgumentException('Для доступа в панель управления укажите email');}
|
||||
if(mb_strlen($email)>100||mb_strlen($userName)>50||mb_strlen($firstName)>50||mb_strlen($lastName)>50){throw new \InvalidArgumentException('Имя, email или логин превышают допустимую длину');}
|
||||
if($email!==''&&DB::query('SELECT Id FROM '.self::table('users').' WHERE LOWER(email)=LOWER(%s) AND Id!=%i AND deleted!=%s LIMIT 1',$email,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот email уже используется');}
|
||||
if($phone!==''&&DB::query('SELECT Id FROM '.self::table('users').' WHERE phone_normalized=%s AND Id!=%i AND deleted!=%s LIMIT 1',$phone,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот телефон уже используется');}
|
||||
if(DB::query('SELECT Id FROM '.self::table('users').' WHERE LOWER(user_name)=LOWER(%s) AND Id!=%i AND deleted!=%s LIMIT 1',$userName,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот логин уже используется');}
|
||||
if($email!==''&&DB::query('SELECT Id FROM '.self::table('users').' WHERE LOWER(email)=LOWER(%s) AND Id!=%i AND COALESCE(deleted,0)!=%s LIMIT 1',$email,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот email уже используется');}
|
||||
if($phone!==''&&DB::query('SELECT Id FROM '.self::table('users').' WHERE phone_normalized=%s AND Id!=%i AND COALESCE(deleted,0)!=%s LIMIT 1',$phone,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот телефон уже используется');}
|
||||
if(DB::query('SELECT Id FROM '.self::table('users').' WHERE LOWER(user_name)=LOWER(%s) AND Id!=%i AND COALESCE(deleted,0)!=%s LIMIT 1',$userName,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот логин уже используется');}
|
||||
$groupIds=array_map(function($item){return (int)$item['id'];},self::groups());
|
||||
if(!in_array($group,$groupIds,true)){throw new \InvalidArgumentException('Выберите активную группу публичных пользователей');}
|
||||
$authSettings=PublicAuthSettings::all();
|
||||
@@ -175,7 +191,10 @@
|
||||
self::saveExtraValues($id,$extra);
|
||||
if($password!==''){(new UserRepository())->setPassword($id,$password);}
|
||||
IdentityLinker::setAdminAccess($id,$adminAccess,$adminRole,(int)$currentSystemId);
|
||||
if($password!==''||$active!=='1'){self::invalidateCustomerSessions($id);}
|
||||
$securityChanged=$password!==''||$active!==(string)$current['user']['status']
|
||||
||$group!==(int)$current['user']['user_group']||$email!==mb_strtolower(trim((string)$current['user']['email']))
|
||||
||$phone!==(string)$current['user']['phone_normalized']||$userName!==(string)$current['user']['user_name'];
|
||||
if($securityChanged){self::invalidateCustomerSessions($id);}
|
||||
DB::commit();
|
||||
}catch(\Throwable $e){DB::rollback();throw $e;}
|
||||
return self::customer($id);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
use App\Adminx\Customers\GlobalSearchProvider;
|
||||
|
||||
return array(
|
||||
'code' => 'customers', 'name' => 'Пользователи сайта', 'version' => '0.10.3',
|
||||
'code' => 'customers', 'name' => 'Пользователи сайта', 'version' => '0.10.4',
|
||||
'permissions' => array('key' => 'customers', 'items' => array(
|
||||
array(
|
||||
'code' => 'view_customers',
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
<div class="drawer-body">
|
||||
<div class="customers-editor-meta"><span><i class="ti ti-id"></i><b data-customer-meta-id>#—</b></span><span><i class="ti ti-user-plus"></i><b data-customer-meta-created>—</b><small>Регистрация</small></span><span><i class="ti ti-clock"></i><b data-customer-meta-visit>—</b><small>Последний вход</small></span></div>
|
||||
<div class="customers-editor-sections">
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--blue-100);--tile-fg:var(--blue-600)"><i class="ti ti-user"></i></span><div><b>Основные данные</b><span>Имя, контакты и данные организации.</span></div></div><div class="form-grid"><label class="field col-6"><span class="field-label">Имя</span><span class="input-wrap customers-editor-input"><i class="ti ti-user"></i><input class="input" name="firstname" maxlength="50"></span></label><label class="field col-6"><span class="field-label">Фамилия</span><span class="input-wrap customers-editor-input"><i class="ti ti-user"></i><input class="input" name="lastname" maxlength="50"></span></label><label class="field col-6"><span class="field-label">Email</span><span class="input-wrap customers-editor-input"><i class="ti ti-mail"></i><input class="input" type="email" name="email" maxlength="100" required></span></label><label class="field col-6"><span class="field-label">Логин</span><span class="input-wrap customers-editor-input"><i class="ti ti-at"></i><input class="input mono" name="user_name" maxlength="50" required></span><span class="field-hint">Можно использовать для входа наравне с email.</span></label><label class="field col-6"><span class="field-label">Телефон</span><span class="input-wrap customers-editor-input"><i class="ti ti-phone"></i><input class="input" type="tel" name="phone" maxlength="50"></span></label><label class="field col-6"><span class="field-label">Компания</span><span class="input-wrap customers-editor-input"><i class="ti ti-building"></i><input class="input" name="company" maxlength="255"></span></label><label class="field col-4"><span class="field-label">Дата рождения</span><span class="input-wrap customers-editor-input"><i class="ti ti-calendar"></i><input class="input" type="date" name="birthday"></span></label><label class="field col-8"><span class="field-label">О пользователе</span><textarea class="textarea" name="description" rows="2"></textarea></label></div></section>
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--blue-100);--tile-fg:var(--blue-600)"><i class="ti ti-user"></i></span><div><b>Основные данные</b><span>Имя, контакты и данные организации.</span></div></div><div class="form-grid"><label class="field col-6"><span class="field-label">Имя</span><span class="input-wrap customers-editor-input"><i class="ti ti-user"></i><input class="input" name="firstname" maxlength="50"></span></label><label class="field col-6"><span class="field-label">Фамилия</span><span class="input-wrap customers-editor-input"><i class="ti ti-user"></i><input class="input" name="lastname" maxlength="50"></span></label><label class="field col-6"><span class="field-label">Email</span><span class="input-wrap customers-editor-input"><i class="ti ti-mail"></i><input class="input" type="email" name="email" maxlength="100"></span><span class="field-hint">Для аккаунта с подтверждённым телефоном необязателен.</span></label><label class="field col-6"><span class="field-label">Логин</span><span class="input-wrap customers-editor-input"><i class="ti ti-at"></i><input class="input mono" name="user_name" maxlength="50" required></span><span class="field-hint">Системный идентификатор аккаунта; при регистрации по телефону заполняется номером.</span></label><label class="field col-6"><span class="field-label">Телефон</span><span class="input-wrap customers-editor-input"><i class="ti ti-phone"></i><input class="input" type="tel" name="phone" maxlength="50"></span></label><label class="field col-6"><span class="field-label">Компания</span><span class="input-wrap customers-editor-input"><i class="ti ti-building"></i><input class="input" name="company" maxlength="255"></span></label><label class="field col-4"><span class="field-label">Дата рождения</span><span class="input-wrap customers-editor-input"><i class="ti ti-calendar"></i><input class="input" type="date" name="birthday"></span></label><label class="field col-8"><span class="field-label">О пользователе</span><textarea class="textarea" name="description" rows="2"></textarea></label></div></section>
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--cyan-100);--tile-fg:var(--cyan-600)"><i class="ti ti-map-pin"></i></span><div><b>Адрес</b><span>Данные для профиля и предзаполнения заказов.</span></div></div><div class="form-grid"><label class="field col-4"><span class="field-label">Город</span><span class="input-wrap customers-editor-input"><i class="ti ti-building-community"></i><input class="input" name="city"></span></label><label class="field col-4"><span class="field-label">Улица</span><span class="input-wrap customers-editor-input"><i class="ti ti-road"></i><input class="input" name="street"></span></label><label class="field col-2"><span class="field-label">Дом</span><span class="input-wrap customers-editor-input"><i class="ti ti-home"></i><input class="input" name="street_nr"></span></label><label class="field col-2"><span class="field-label">Индекс</span><span class="input-wrap customers-editor-input"><i class="ti ti-mailbox"></i><input class="input" name="zipcode"></span></label></div></section>
|
||||
<section class="customers-form-section">
|
||||
<div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--green-100);--tile-fg:var(--green-600)"><i class="ti ti-shield-check"></i></span><div><b>Доступ</b><span>Публичная группа, состояние аккаунта и роль в панели управления.</span></div></div>
|
||||
@@ -189,11 +189,11 @@
|
||||
<label class="customers-switch-card" data-customer-account-card><span><b>Аккаунт включён</b><small data-customer-account-hint>Может входить на сайт и в личный кабинет.</small></span><span class="switch"><input type="checkbox" name="status" value="1"><span></span></span></label>
|
||||
<label class="customers-switch-card" data-customer-admin-card><span><b>Доступ в панель управления</b><small data-customer-admin-hint>{{ can_manage_admin_access ? 'Та же учётная запись сможет работать в административном интерфейсе.' : 'Изменение требует права управления системными пользователями.' }}</small></span><span class="switch"><input type="checkbox" name="admin_access" value="1" data-customer-admin-access{{ can_manage_admin_access ? '' : ' disabled' }}><span></span></span></label>
|
||||
<label class="customers-switch-card"><span><b>Email подтверждён</b><small>Администратор может изменить статус вручную.</small></span><span class="switch"><input type="checkbox" name="email_verified" value="1"><span></span></span></label>
|
||||
<label class="customers-switch-card"><span><b>Телефон подтверждён</b><small>Подготовлено для будущего SMS-гейта.</small></span><span class="switch"><input type="checkbox" name="phone_verified" value="1"><span></span></span></label>
|
||||
<label class="customers-switch-card"><span><b>Телефон подтверждён</b><small>Подтверждается кодом из SMS; администратор может изменить статус вручную.</small></span><span class="switch"><input type="checkbox" name="phone_verified" value="1"><span></span></span></label>
|
||||
</div>
|
||||
</section>
|
||||
{% if fields %}<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--violet-100);--tile-fg:var(--violet-600)"><i class="ti ti-forms"></i></span><div><b>Дополнительные поля</b><span>Активные поля из конструктора публичного профиля.</span></div></div><div class="form-grid">{% for field in fields %}{% if field.is_active %}<label class="field col-6"><span class="field-label">{{ field.name }}{% if field.is_required %} <span class="text-danger">*</span>{% endif %}</span>{% if field.type=='textarea' %}<textarea class="textarea" name="extra[{{ field.id }}]" rows="3"{{ field.is_required?' required':'' }}></textarea>{% elseif field.type=='select' %}<select class="select" name="extra[{{ field.id }}]"{{ field.is_required?' required':'' }}><option value="">Выберите значение</option>{% for choice in field.choices %}<option value="{{ choice|e('html_attr') }}">{{ choice }}</option>{% endfor %}</select>{% elseif field.type=='checkbox' %}<span class="customers-editor-checkbox"><span class="switch"><input type="checkbox" name="extra[{{ field.id }}]" value="1"><span></span></span><span>Да / нет</span></span>{% else %}<input class="input" type="{{ field.type in ['date','number','email','tel'] ? field.type : 'text' }}" name="extra[{{ field.id }}]"{{ field.is_required?' required':'' }}>{% endif %}<span class="field-hint mono">{{ field.code }}</span></label>{% endif %}{% endfor %}</div></section>{% endif %}
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti ti-key"></i></span><div><b>Безопасность</b><span>Пароль изменится, только если заполнить поле.</span></div></div><label class="field customers-editor-password"><span class="field-label">Новый пароль</span><span class="input-wrap customers-editor-input"><i class="ti ti-lock"></i><input class="input" type="password" name="password" autocomplete="new-password" placeholder="Оставьте пустым, чтобы не менять"></span><span class="field-hint">При смене пароля будут отозваны remember-сессии и ссылки восстановления.</span></label></section>
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti ti-key"></i></span><div><b>Безопасность</b><span>Пароль изменится, только если заполнить поле.</span></div></div><label class="field customers-editor-password"><span class="field-label">Новый пароль</span><span class="input-wrap customers-editor-input"><i class="ti ti-lock"></i><input class="input" type="password" name="password" autocomplete="new-password" placeholder="Оставьте пустым, чтобы не менять"></span><span class="field-hint">Используется только при разрешённом входе по паролю; активные ссылки и сессии будут отозваны.</span></label></section>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="drawer-footer"><button class="btn btn-secondary" type="button" data-close-drawer>Закрыть</button><button class="btn btn-primary" type="submit"><i class="ti ti-device-floppy"></i>Сохранить</button></footer>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
*/
|
||||
class BulkEditor
|
||||
{
|
||||
const VERSION = 1;
|
||||
const VERSION = 2;
|
||||
const MAX_DOCUMENTS = 5000;
|
||||
const CHUNK_SIZE = 20;
|
||||
const EXPIRES_AFTER = 7200;
|
||||
@@ -52,6 +52,19 @@
|
||||
'document_property' => array('label' => 'Свойство / артикул', 'payload' => 'property'),
|
||||
);
|
||||
|
||||
protected static $stateOperations = array(
|
||||
'search_include' => array('column'=>'document_in_search','payload'=>'in_search','value'=>'1','before'=>array('0'=>'Вне поиска','1'=>'В поиске')),
|
||||
'search_exclude' => array('column'=>'document_in_search','payload'=>'in_search','value'=>'0','before'=>array('0'=>'Вне поиска','1'=>'В поиске')),
|
||||
'robots_index' => array('column'=>'document_meta_robots','payload'=>'meta_robots','value'=>'index,follow','before'=>array()),
|
||||
'robots_noindex' => array('column'=>'document_meta_robots','payload'=>'meta_robots','value'=>'noindex,nofollow','before'=>array()),
|
||||
'sitemap_include' => array('column'=>'document_in_sitemap','payload'=>'in_sitemap','value'=>'1','before'=>array('0'=>'Не добавляется','1'=>'Добавляется')),
|
||||
'sitemap_exclude' => array('column'=>'document_in_sitemap','payload'=>'in_sitemap','value'=>'0','before'=>array('0'=>'Не добавляется','1'=>'Добавляется')),
|
||||
'technical' => array('column'=>'document_is_technical','payload'=>'is_technical','value'=>'1','before'=>array('0'=>'Обычный','1'=>'Служебный')),
|
||||
'public' => array('column'=>'document_is_technical','payload'=>'is_technical','value'=>'0','before'=>array('0'=>'Обычный','1'=>'Служебный')),
|
||||
'sitemap_frequency' => array('column'=>'document_sitemap_freq','payload'=>'sitemap_frequency','value_input'=>'sitemap_frequency','before'=>array('0'=>'always','1'=>'hourly','2'=>'daily','3'=>'weekly','4'=>'monthly','5'=>'yearly','6'=>'never')),
|
||||
'sitemap_priority' => array('column'=>'document_sitemap_pr','payload'=>'sitemap_priority','value_input'=>'sitemap_priority','before'=>array()),
|
||||
);
|
||||
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
@@ -198,6 +211,14 @@
|
||||
if (!$row) { return false; }
|
||||
|
||||
$type = $operation['type'];
|
||||
if (isset($operation['target_type']) && $operation['target_type'] === 'state') {
|
||||
if ($type === 'technical' && Model::isProtectedDocument($documentId)) { return false; }
|
||||
$current = (string) $row[$operation['target']];
|
||||
if ($current === (string) $operation['value']) { return false; }
|
||||
$service->save($documentId, array($operation['payload'] => $operation['value']), $actorId, 'bulk_editor');
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($type === 'publish' || $type === 'unpublish') {
|
||||
if (Model::isProtectedDocument($documentId)) { return false; }
|
||||
$status = $type === 'publish' ? 1 : 0;
|
||||
@@ -250,12 +271,32 @@
|
||||
protected static function operation(array $input, array $filters)
|
||||
{
|
||||
$type = isset($input['operation']) ? (string) $input['operation'] : '';
|
||||
$allowed = array('fill', 'set', 'clear', 'replace', 'move', 'publish', 'unpublish', 'recalculate');
|
||||
$allowed = array_merge(
|
||||
array('fill', 'set', 'clear', 'replace', 'move', 'publish', 'unpublish', 'recalculate'),
|
||||
array_keys(self::$stateOperations)
|
||||
);
|
||||
if (!in_array($type, $allowed, true)) {
|
||||
throw new \InvalidArgumentException('Выберите действие');
|
||||
}
|
||||
|
||||
$operation = array('type' => $type, 'label' => self::operationLabel($type));
|
||||
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']] : '');
|
||||
if ($type === 'sitemap_frequency' && !array_key_exists($value, $config['before'])) {
|
||||
throw new \InvalidArgumentException('Выберите частоту обновления sitemap');
|
||||
}
|
||||
|
||||
if ($type === 'sitemap_priority' && (!is_numeric($value) || (float) $value < 0 || (float) $value > 1)) {
|
||||
throw new \InvalidArgumentException('Приоритет sitemap должен быть от 0 до 1');
|
||||
}
|
||||
|
||||
return array_merge($operation, $config, array(
|
||||
'target_type' => 'state', 'target' => $config['column'], 'value' => $value,
|
||||
'target_label' => self::operationTargetLabel($type),
|
||||
));
|
||||
}
|
||||
|
||||
if (in_array($type, array('move', 'publish', 'unpublish', 'recalculate'), true)) {
|
||||
if ($type === 'move') {
|
||||
$targetRubricId = isset($input['target_rubric_id']) ? (int) $input['target_rubric_id'] : 0;
|
||||
@@ -346,9 +387,9 @@
|
||||
$currentValues = array();
|
||||
$ids = array();
|
||||
foreach ($rows as $row) { $ids[] = (int) $row['Id']; }
|
||||
if (isset($operation['target_type']) && $operation['target_type'] === 'document') {
|
||||
if (isset($operation['target_type']) && in_array($operation['target_type'], array('document', 'state'), true)) {
|
||||
$column = (string) $operation['target'];
|
||||
if (!isset(self::$documentFields[$column])) {
|
||||
if ($operation['target_type'] === 'document' && !isset(self::$documentFields[$column])) {
|
||||
throw new \InvalidArgumentException('Поле документа недоступно для массового изменения');
|
||||
}
|
||||
|
||||
@@ -406,6 +447,17 @@
|
||||
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') {
|
||||
if ($operation['type'] === 'technical' && Model::isProtectedDocument((int) $row['Id'])) {
|
||||
return array('before'=>'Системный документ','after'=>'Без изменений','changed'=>false,'note'=>'Главную и страницу 404 нельзя сделать служебными');
|
||||
}
|
||||
|
||||
$current = self::currentValue($row, $operation);
|
||||
$before = isset($operation['before'][$current]) ? $operation['before'][$current] : $current;
|
||||
$after = isset($operation['before'][(string) $operation['value']]) ? $operation['before'][(string) $operation['value']] : (string) $operation['value'];
|
||||
return array('before'=>$before,'after'=>$after,'changed'=>$current !== (string) $operation['value'],'note'=>'');
|
||||
}
|
||||
|
||||
$current = self::currentValue($row, $operation);
|
||||
$next = self::changedValue($current, $operation);
|
||||
return array('before' => $current, 'after' => $next['value'], 'changed' => $next['changed'], 'note' => $next['changed'] ? '' : 'Значение уже соответствует действию');
|
||||
@@ -417,7 +469,7 @@
|
||||
return (string) $row['__bulk_current'];
|
||||
}
|
||||
|
||||
if ($operation['target_type'] === 'document') {
|
||||
if ($operation['target_type'] === 'document' || $operation['target_type'] === 'state') {
|
||||
if (!array_key_exists($operation['target'], $row)) {
|
||||
$value = DB::query(
|
||||
'SELECT `' . $operation['target'] . '` FROM ' . ContentTables::table('documents') . ' WHERE Id=%i',
|
||||
@@ -537,12 +589,29 @@
|
||||
$labels = array(
|
||||
'fill' => 'Заполнить пустые', 'set' => 'Установить значение', 'clear' => 'Очистить',
|
||||
'replace' => 'Найти и заменить', 'move' => 'Перенести в рубрику',
|
||||
'publish' => 'Опубликовать', 'unpublish' => 'Снять с публикации',
|
||||
'recalculate' => 'Пересчитать поля и индексы',
|
||||
);
|
||||
'publish' => 'Опубликовать', 'unpublish' => 'Снять с публикации',
|
||||
'recalculate' => 'Пересчитать поля и индексы',
|
||||
'search_include' => 'Включить во внутренний поиск', 'search_exclude' => 'Исключить из внутреннего поиска',
|
||||
'robots_index' => 'Разрешить индексацию поисковиками', 'robots_noindex' => 'Запретить индексацию поисковиками',
|
||||
'sitemap_include' => 'Добавить в sitemap', 'sitemap_exclude' => 'Исключить из sitemap',
|
||||
'technical' => 'Сделать служебными', 'public' => 'Сделать обычными',
|
||||
'sitemap_frequency' => 'Изменить частоту sitemap', 'sitemap_priority' => 'Изменить приоритет sitemap',
|
||||
);
|
||||
return isset($labels[$type]) ? $labels[$type] : $type;
|
||||
}
|
||||
|
||||
protected static function operationTargetLabel($type)
|
||||
{
|
||||
$labels = array(
|
||||
'search_include'=>'Внутренний поиск','search_exclude'=>'Внутренний поиск',
|
||||
'robots_index'=>'Meta robots','robots_noindex'=>'Meta robots',
|
||||
'sitemap_include'=>'Участие в sitemap','sitemap_exclude'=>'Участие в sitemap',
|
||||
'technical'=>'Публичный доступ','public'=>'Публичный доступ',
|
||||
'sitemap_frequency'=>'Частота sitemap','sitemap_priority'=>'Приоритет sitemap',
|
||||
);
|
||||
return isset($labels[$type]) ? $labels[$type] : '';
|
||||
}
|
||||
|
||||
protected static function shortValue($value)
|
||||
{
|
||||
$value = trim(preg_replace('/\s+/u', ' ', strip_tags((string) $value)));
|
||||
|
||||
@@ -226,8 +226,8 @@
|
||||
'document_title', 'document_alias', 'document_alias_header', 'document_alias_history',
|
||||
'document_short_alias', 'document_breadcrumb_title', 'document_excerpt',
|
||||
'document_meta_keywords', 'document_meta_description', 'document_meta_robots',
|
||||
'document_sitemap_freq', 'document_sitemap_pr', 'document_tags', 'document_property',
|
||||
'guid', 'document_status', 'document_in_search', 'document_parent', 'rubric_tmpl_id',
|
||||
'document_sitemap_freq', 'document_sitemap_pr', 'document_in_sitemap', 'document_tags', 'document_property',
|
||||
'guid', 'document_status', 'document_in_search', 'document_is_technical', 'document_parent', 'rubric_tmpl_id',
|
||||
'document_linked_navi_id', 'document_position', 'document_published', 'document_expire',
|
||||
'document_author_id',
|
||||
) as $key) {
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
$countArgs = array_merge(array('SELECT COUNT(*) FROM ' . self::documentsTable() . ' d' . $where), $args);
|
||||
$total = (int) call_user_func_array(array('DB', 'query'), $countArgs)->getValue();
|
||||
|
||||
$sql = 'SELECT d.*, r.rubric_title, r.rubric_alias,(' . $score . ') AS search_relevance'
|
||||
$sql = 'SELECT d.*, r.rubric_title, r.rubric_alias,COALESCE(r.rubric_is_technical,0) rubric_is_technical,(' . $score . ') AS search_relevance'
|
||||
. ' FROM ' . self::documentsTable() . ' d'
|
||||
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id = d.rubric_id'
|
||||
. $where
|
||||
@@ -190,7 +190,7 @@
|
||||
public static function one($id)
|
||||
{
|
||||
$row = DB::query(
|
||||
'SELECT d.*, r.rubric_title, r.rubric_alias FROM ' . self::documentsTable() . ' d'
|
||||
'SELECT d.*, r.rubric_title, r.rubric_alias,COALESCE(r.rubric_is_technical,0) rubric_is_technical FROM ' . self::documentsTable() . ' d'
|
||||
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id = d.rubric_id'
|
||||
. ' WHERE d.Id = %i LIMIT 1',
|
||||
(int) $id
|
||||
@@ -222,6 +222,8 @@
|
||||
'guid' => '',
|
||||
'document_status' => 1,
|
||||
'document_in_search' => 1,
|
||||
'document_is_technical' => $rubric && !empty($rubric['rubric_is_technical']) ? 1 : 0,
|
||||
'document_in_sitemap' => 1,
|
||||
'document_author_id' => 1,
|
||||
'document_parent' => 0,
|
||||
'rubric_tmpl_id' => 0,
|
||||
@@ -624,8 +626,9 @@
|
||||
foreach (array(
|
||||
'document_title', 'document_alias', 'document_alias_header', 'document_alias_history',
|
||||
'document_short_alias', 'document_breadcrumb_title', 'document_excerpt', 'document_meta_keywords',
|
||||
'document_meta_description', 'document_meta_robots', 'document_sitemap_freq', 'document_sitemap_pr',
|
||||
'document_tags', 'document_property', 'guid', 'document_status', 'document_in_search',
|
||||
'document_meta_description', 'document_meta_robots', 'document_sitemap_freq', 'document_sitemap_pr',
|
||||
'document_in_sitemap', 'document_tags', 'document_property', 'guid', 'document_status', 'document_in_search',
|
||||
'document_is_technical',
|
||||
'document_parent', 'rubric_tmpl_id', 'document_linked_navi_id', 'document_position',
|
||||
'document_author_id',
|
||||
) as $key) {
|
||||
@@ -658,6 +661,10 @@
|
||||
$data['document_alias_header'] = 301;
|
||||
}
|
||||
|
||||
if ($id > 0 && self::isProtectedDocument($id)) {
|
||||
$data['document_is_technical'] = 0;
|
||||
}
|
||||
|
||||
$data['rubric_id'] = $rubricId;
|
||||
$data['document_changed'] = $now;
|
||||
$data['document_version'] = $existing ? max(1, (int) $existing['document_version']) + 1 : 1;
|
||||
@@ -944,6 +951,7 @@
|
||||
$source['document_deleted'] = '0';
|
||||
$source['document_count_print'] = 0;
|
||||
$source['document_count_view'] = 0;
|
||||
$source['module_catalog'] = '';
|
||||
$source['document_author_id'] = self::legacyAuthorId($authorId);
|
||||
$source['document_changed'] = time();
|
||||
$source['document_version'] = 1;
|
||||
@@ -1458,13 +1466,15 @@
|
||||
'document_meta_keywords' => trim($get('document_meta_keywords')),
|
||||
'document_meta_description' => trim($get('document_meta_description')),
|
||||
'document_meta_robots' => self::robotsValue($get('document_meta_robots')),
|
||||
'document_sitemap_freq' => self::sitemapFreqValue($get('document_sitemap_freq')),
|
||||
'document_sitemap_pr' => self::sitemapPrValue($get('document_sitemap_pr')),
|
||||
'document_sitemap_freq' => self::sitemapFreqValue($get('document_sitemap_freq')),
|
||||
'document_sitemap_pr' => self::sitemapPrValue($get('document_sitemap_pr')),
|
||||
'document_in_sitemap' => (int) $get('document_in_sitemap') === 1 ? 1 : 0,
|
||||
'document_tags' => trim($get('document_tags')),
|
||||
'document_property' => trim($get('document_property')),
|
||||
'guid' => substr(trim($get('guid')), 0, 100),
|
||||
'document_status' => (int) $get('document_status') === 1 ? '1' : '0',
|
||||
'document_in_search' => (int) $get('document_in_search') === 1 ? '1' : '0',
|
||||
'document_status' => (int) $get('document_status') === 1 ? '1' : '0',
|
||||
'document_in_search' => (int) $get('document_in_search') === 1 ? '1' : '0',
|
||||
'document_is_technical' => (int) $get('document_is_technical') === 1 ? 1 : 0,
|
||||
'document_parent' => (int) $get('document_parent'),
|
||||
'rubric_tmpl_id' => (int) $get('rubric_tmpl_id'),
|
||||
'document_linked_navi_id' => (int) $get('document_linked_navi_id'),
|
||||
@@ -1662,6 +1672,10 @@
|
||||
$row['document_status'] = (int) $row['document_status'];
|
||||
$row['document_deleted'] = (int) $row['document_deleted'];
|
||||
$row['document_in_search'] = (int) $row['document_in_search'];
|
||||
$row['document_is_technical'] = isset($row['document_is_technical']) ? (int) $row['document_is_technical'] : 0;
|
||||
$row['document_in_sitemap'] = isset($row['document_in_sitemap']) ? (int) $row['document_in_sitemap'] : 1;
|
||||
$row['rubric_is_technical'] = isset($row['rubric_is_technical']) ? (int) $row['rubric_is_technical'] : 0;
|
||||
$row['is_technical_effective'] = $row['document_is_technical'] || $row['rubric_is_technical'];
|
||||
$row['document_published'] = (int) $row['document_published'];
|
||||
$row['document_expire'] = (int) $row['document_expire'];
|
||||
$row['document_changed'] = (int) $row['document_changed'];
|
||||
@@ -2711,11 +2725,16 @@
|
||||
|
||||
protected static function clearDocumentCache($documentId)
|
||||
{
|
||||
ContentCacheInvalidator::document($documentId, false);
|
||||
DB::afterCommit(function () use ($documentId) { ContentCacheInvalidator::document($documentId, false); });
|
||||
}
|
||||
|
||||
protected static function buildDocumentSnapshot($documentId)
|
||||
{
|
||||
if (DB::$transaction_in_progress) {
|
||||
DB::afterCommit(function () use ($documentId) { ContentCacheInvalidator::document($documentId, true); });
|
||||
return null;
|
||||
}
|
||||
|
||||
return ContentCacheInvalidator::document($documentId, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,7 @@
|
||||
'document_parent' => 'Родитель', 'rubric_tmpl_id' => 'Шаблон', 'document_linked_navi_id' => 'Навигация',
|
||||
'document_position' => 'Позиция', 'document_published' => 'Дата публикации', 'document_expire' => 'Дата окончания',
|
||||
'document_author_id' => 'Автор', 'document_sitemap_freq' => 'Sitemap: частота', 'document_sitemap_pr' => 'Sitemap: приоритет',
|
||||
'document_in_sitemap' => 'Sitemap: участие', 'document_is_technical' => 'Служебный документ',
|
||||
);
|
||||
$out = array();
|
||||
foreach ($document as $key => $value) {
|
||||
@@ -238,8 +239,12 @@
|
||||
$value = (int) $value > 0 ? date('d.m.Y H:i:s', (int) $value) : 'не задано';
|
||||
} elseif ($key === 'document_status') {
|
||||
$value = (int) $value === 1 ? 'Опубликован' : 'Черновик';
|
||||
} elseif ($key === 'document_in_search') {
|
||||
$value = (int) $value === 1 ? 'В поиске' : 'Скрыт';
|
||||
} elseif ($key === 'document_in_search') {
|
||||
$value = (int) $value === 1 ? 'В поиске' : 'Скрыт';
|
||||
} elseif ($key === 'document_in_sitemap') {
|
||||
$value = (int) $value === 1 ? 'Добавляется' : 'Не добавляется';
|
||||
} elseif ($key === 'document_is_technical') {
|
||||
$value = (int) $value === 1 ? 'Служебный' : 'Обычный';
|
||||
}
|
||||
|
||||
$out[] = array('key' => $key, 'title' => $labels[$key], 'value' => (string) $value, 'value_preview' => self::shorten((string) $value, 420));
|
||||
|
||||
@@ -1243,6 +1243,10 @@
|
||||
.documents-bulk-controls .field {
|
||||
min-width: 0;
|
||||
}
|
||||
.documents-bulk-controls .field[hidden],
|
||||
[data-bulk-technical-help][hidden] {
|
||||
display: none;
|
||||
}
|
||||
.documents-bulk-search,
|
||||
.documents-bulk-value {
|
||||
grid-column: span 2;
|
||||
@@ -1270,6 +1274,23 @@
|
||||
margin-top: 1px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.documents-bulk-technical-help {
|
||||
margin: 0 16px 16px;
|
||||
}
|
||||
.documents-bulk-technical-help b,
|
||||
.documents-bulk-technical-help span {
|
||||
display: block;
|
||||
}
|
||||
.documents-bulk-technical-help span {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.documents-technical-alert b,
|
||||
.documents-technical-alert span {
|
||||
display: block;
|
||||
}
|
||||
.documents-technical-alert span {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.documents-bulk-step-footer,
|
||||
.documents-bulk-run {
|
||||
display: flex;
|
||||
@@ -2654,22 +2675,25 @@
|
||||
}
|
||||
.documents-value-row {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr) 34px;
|
||||
grid-template-columns: 106px minmax(0, 1fr) 34px;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-soft);
|
||||
box-shadow: inset 0 0 0 1px var(--border-default);
|
||||
}
|
||||
.documents-value-row-tools,
|
||||
.documents-value-row-tools {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 34px);
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
.documents-value-row-fields {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.documents-value-row-fields {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.documents-value-row-list_single .documents-value-row-fields {
|
||||
@@ -2687,6 +2711,22 @@
|
||||
}
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.documents-value-row {
|
||||
grid-template-columns: minmax(0, 1fr) 34px;
|
||||
}
|
||||
.documents-value-row-tools {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
justify-content: start;
|
||||
}
|
||||
.documents-value-row-fields {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
}
|
||||
.documents-value-remove {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
.documents-value-row-packages .documents-value-row-fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
@@ -3669,7 +3669,8 @@
|
||||
var help = root.querySelector('[data-bulk-target-help]');
|
||||
if (!help || !target) { return; }
|
||||
var selected = target.options[target.selectedIndex];
|
||||
var message = selected ? String(selected.getAttribute('data-help') || '') : '';
|
||||
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') {
|
||||
message = 'Это заголовок документа в панели. Название товара на сайте обычно берётся из поля рубрики, помеченного «витрина: название товара на сайте».';
|
||||
}
|
||||
@@ -3704,8 +3705,11 @@
|
||||
var editsField = ['fill', 'set', 'clear', 'replace'].indexOf(value) !== -1;
|
||||
setVisibility('[data-bulk-target-wrap]', editsField);
|
||||
setVisibility('[data-bulk-search-wrap]', value === 'replace');
|
||||
setVisibility('[data-bulk-value-wrap]', value === 'fill' || value === 'set' || value === 'replace');
|
||||
setVisibility('[data-bulk-rubric-target-wrap]', value === 'move');
|
||||
setVisibility('[data-bulk-value-wrap]', value === 'fill' || value === 'set' || value === 'replace');
|
||||
setVisibility('[data-bulk-rubric-target-wrap]', value === 'move');
|
||||
setVisibility('[data-bulk-sitemap-frequency-wrap]', value === 'sitemap_frequency');
|
||||
setVisibility('[data-bulk-sitemap-priority-wrap]', value === 'sitemap_priority');
|
||||
setVisibility('[data-bulk-technical-help]', value === 'technical' || value === 'public');
|
||||
updateTargetHelp();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,302 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="auto_003cc12783448df3">x</phrase>
|
||||
<phrase data="auto_0156bf1b8aac8565">Add more...</phrase>
|
||||
<phrase data="auto_02b4ebb5dc50bca5">The address of the document will be written into the link field.</phrase>
|
||||
<phrase data="auto_02fdd4b8923613d6">Up to 10 characters, optional.</phrase>
|
||||
<phrase data="auto_04c0389bc0db2353">Token copied</phrase>
|
||||
<phrase data="auto_080887ebc7bb5402"><button class="btn btn-ghost btn-icon btn-sm documents-media-remove" type="button" data-document-media-remove data-tooltip="Delete" aria-label="Delete"><i class="ti ti-trash"></i></button></phrase>
|
||||
<phrase data="auto_0957de938411c15b">field</phrase>
|
||||
<phrase data="auto_0af969e08c220502">Clear all field lines?</phrase>
|
||||
<phrase data="auto_0db49b55557f2bde">Parameter</phrase>
|
||||
<phrase data="auto_0e7fbe3404b67fe3">Rebuild JSON snapshots?</phrase>
|
||||
<phrase data="auto_0f9ccee053fe3b6b">New</phrase>
|
||||
<phrase data="auto_0ff730d95f9c5bc3">click to select</phrase>
|
||||
<phrase data="auto_122f4893d3d0d802">Failed to check alias</phrase>
|
||||
<phrase data="auto_1291e1971ba28ee6">API token created</phrase>
|
||||
<phrase data="auto_12bfabff3d7dc01c">Add a keyword</phrase>
|
||||
<phrase data="auto_18337ef195f7d88d"><div class="modal-body"><div class="input-wrap documents-relation-search"><i class="ti ti-search"></i><input class="input" type="search" placeholder="ID, title or alias" data-relation-search></div><div class="documents-picker-status" data-relation-status>Loading...</div><div class="documents-relation-list" data-relation-list></div></div></phrase>
|
||||
<phrase data="auto_1894f01f2cfa4702">JSON snapshot</phrase>
|
||||
<phrase data="auto_1a7725e697463fd8">to</phrase>
|
||||
<phrase data="auto_1af72c14888c4574">Creation preset</phrase>
|
||||
<phrase data="auto_1bc16af35fa47103">uh</phrase>
|
||||
<phrase data="auto_1d00a5603a94c17d">Teaser ID/link</phrase>
|
||||
<phrase data="auto_1d402bc8ffda29cd">Internal server error. Please refresh the page and try again.</phrase>
|
||||
<phrase data="auto_1f864100c5c97f02"><button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-down data-tooltip="Below" aria-label="Below"><i class="ti ti-arrow-down"></i></button></phrase>
|
||||
<phrase data="auto_22dc89ab84f07324">"><button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Select file" aria-label="Select file"><i class="ti ti-</phrase>
|
||||
<phrase data="auto_230b5877294e9726">ts</phrase>
|
||||
<phrase data="auto_23289c89f8c038a0">The document ID will be written in the field.</phrase>
|
||||
<phrase data="auto_242f64ddfe84899b">after saving</phrase>
|
||||
<phrase data="auto_25a9845975119103">Creation preset saved</phrase>
|
||||
<phrase data="auto_25dc101484dbd55c">Failed to load list</phrase>
|
||||
<phrase data="auto_285cc4001478d568">h</phrase>
|
||||
<phrase data="auto_28c993e4935524d8">h</phrase>
|
||||
<phrase data="auto_28fb6167ec0a79fe"><section class="documents-revision-system"><div class="documents-revision-subhead"><i class="ti ti-settings"></i><b>Basic settings</b><span></phrase>
|
||||
<phrase data="auto_292688e9915e161b">[link]" data-media-key="link" value="" placeholder="Link or document" data-document-media-url data-document-picker-type="all"></phrase>
|
||||
<phrase data="auto_29f3b29df0d0e54f">Restore</phrase>
|
||||
<phrase data="auto_2a9790fd6fab2922"><button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-doc-pick data-tooltip="Select document" aria-label="Select document"><i class="ti ti-file-search"></i></button></div></phrase>
|
||||
<phrase data="auto_2ba58b00b4c52021"></span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="field" checked><span>All</span></label></div></phrase>
|
||||
<phrase data="auto_2c501dc73177148a">ID, name or alias</phrase>
|
||||
<phrase data="auto_2e31dae95f35e3b8">Select file</phrase>
|
||||
<phrase data="auto_2ec20105a2757791">Column 3</phrase>
|
||||
<phrase data="auto_2f1256a9610f7d64">Redirect saved</phrase>
|
||||
<phrase data="auto_2ff1fe42207df255">The secret is ready to be copied.</phrase>
|
||||
<phrase data="auto_301361625f901825">The server returned an incorrect response.</phrase>
|
||||
<phrase data="auto_30278b05221dd957">Refresh page</phrase>
|
||||
<phrase data="auto_32b74a3c47908c1e">Untitled</phrase>
|
||||
<phrase data="auto_33392e5848b4a8eb">JSON snapshots reassembled:</phrase>
|
||||
<phrase data="auto_336d12a4f96485a3"><button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-up data-tooltip="Above" aria-label="Above"><i class="ti ti-arrow-up"></i></button></phrase>
|
||||
<phrase data="auto_359282f9ef75e654">Document ID</phrase>
|
||||
<phrase data="auto_35de32df9d942d9a">Note added</phrase>
|
||||
<phrase data="auto_3697bef0b3fc93f5">new</phrase>
|
||||
<phrase data="auto_3812a04b189ec1fd">new</phrase>
|
||||
<phrase data="auto_39704f311a86cd23">Draft</phrase>
|
||||
<phrase data="auto_3de49828e86a4016">Title</phrase>
|
||||
<phrase data="auto_3e096d0063299173">Revision deleted</phrase>
|
||||
<phrase data="auto_3e25fbc59741a10a">Document title</phrase>
|
||||
<phrase data="auto_3e670a1ec591883c">Files uploaded</phrase>
|
||||
<phrase data="auto_3eb77d3c13c51f92">Select revision</phrase>
|
||||
<phrase data="auto_3f039532d65bb5e3">Rebuild JSON</phrase>
|
||||
<phrase data="auto_4044fbc87708b9ad">Clear daily statistics?</phrase>
|
||||
<phrase data="auto_40785832c27c3a51">Column 1</phrase>
|
||||
<phrase data="auto_42ba924b3732dc07">fields</phrase>
|
||||
<phrase data="auto_433e4b40a403cdb0">th</phrase>
|
||||
<phrase data="auto_447d642f14fb2c8c">There are no matching files in the folder</phrase>
|
||||
<phrase data="auto_46551a5e6e43e854">Statistics cleared</phrase>
|
||||
<phrase data="auto_48be85b47e187f3c">not created yet</phrase>
|
||||
<phrase data="auto_4a05d8619dedc462">Copy</phrase>
|
||||
<phrase data="auto_4b4c88ed13b23b15">Redirect removed</phrase>
|
||||
<phrase data="auto_4d60d98f499d1580">Audits</phrase>
|
||||
<phrase data="auto_4f566fb5e3df4276">I</phrase>
|
||||
<phrase data="auto_516cbe2af24391af">l</phrase>
|
||||
<phrase data="auto_51b4eac98af84251">a</phrase>
|
||||
<phrase data="auto_51f784cde1916288">b</phrase>
|
||||
<phrase data="auto_522e27d7402089bc">Document saved</phrase>
|
||||
<phrase data="auto_53e908b396e3a6b3"><span class="badge badge-gray">not created</span></phrase>
|
||||
<phrase data="auto_54a8375d14dfdab5">Failed to read folder</phrase>
|
||||
<phrase data="auto_56ac0cff0f4a358f">Note deleted</phrase>
|
||||
<phrase data="auto_576895a59897535b">File not selected</phrase>
|
||||
<phrase data="auto_577021b5ac1fd915">f</phrase>
|
||||
<phrase data="auto_57afad1302f9645e">o</phrase>
|
||||
<phrase data="auto_588c8feeb973dcb1"><div><dt>File</dt><dd class="mono"></phrase>
|
||||
<phrase data="auto_59ddef8a55503c83">kg</phrase>
|
||||
<phrase data="auto_5a3b4bcf55400711">r</phrase>
|
||||
<phrase data="auto_5bd0d9fe0580526f">. The window must be left open until completion.</phrase>
|
||||
<phrase data="auto_5c019fa5d03ca5cf">in the section</phrase>
|
||||
<phrase data="auto_5e6c96299a2f11e9">s</phrase>
|
||||
<phrase data="auto_5e6d6b07b27b7a7c">All view_count lines will be removed. General document counters will remain unchanged.</phrase>
|
||||
<phrase data="auto_5f9eaa37c4e2ccff"><button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Select file" aria-label="Select file"><i class="ti ti-paperclip"></i></button></phrase>
|
||||
<phrase data="auto_600363a73de5f3a5">Rebuild</phrase>
|
||||
<phrase data="auto_60512114c18fd5ed"><span class="badge badge-amber">needs reassembly</span></phrase>
|
||||
<phrase data="auto_619742ac4a834c45">doc.</phrase>
|
||||
<phrase data="auto_63b1fa390e91f75c">0 B</phrase>
|
||||
<phrase data="auto_65b7f1673713a00c">d</phrase>
|
||||
<phrase data="auto_66d207b9ebe9e4e3"><div class="documents-term-status is-error"><i class="ti ti-alert-circle"></i><span>Failed to load options</span></div></phrase>
|
||||
<phrase data="auto_6702cc12ca25aff1"><div class="empty-state">There are no revisions yet. The first snapshot will appear after saving the document.</div></phrase>
|
||||
<phrase data="auto_691245657a52bdce"><div><label class="documents-revision-check" aria-label="Restore</phrase>
|
||||
<phrase data="auto_6972375800d3e590">Clear the field?</phrase>
|
||||
<phrase data="auto_698ace159126e252">Column 2</phrase>
|
||||
<phrase data="auto_6b595786e068b5bc">Alias can be left empty</phrase>
|
||||
<phrase data="auto_6baaabf049b42cca">e</phrase>
|
||||
<phrase data="auto_6bee957a1f3ef55d">Length</phrase>
|
||||
<phrase data="auto_6c190c23240564f5">f</phrase>
|
||||
<phrase data="auto_6e408d3c9c3156bf">Delete "</phrase>
|
||||
<phrase data="auto_6e52ded051aa402d">Withdrawn</phrase>
|
||||
<phrase data="auto_6f658622a374b8d6"></p></div><button class="modal-close" type="button" data-relation-close aria-label="Close"><i class="ti ti-x"></i></button></div></phrase>
|
||||
<phrase data="auto_6fe63eee472a5a93"><span class="badge badge-green">relevant</span></phrase>
|
||||
<phrase data="auto_707a724dce1fa0d4">URL copied</phrase>
|
||||
<phrase data="auto_72c618d46424c93a">There are no saved values yet</phrase>
|
||||
<phrase data="auto_736a75710565c2f2">[description]" data-media-key="description" rows="2" placeholder="Description"></textarea></phrase>
|
||||
<phrase data="auto_75285ce97dc1dc97"><button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-down data-tooltip="Below" aria-label="Below"><i class="ti ti-arrow-down"></i></button></phrase>
|
||||
<phrase data="auto_781e545c1063d5db">The integration will immediately lose access. It will be impossible to return this token.</phrase>
|
||||
<phrase data="auto_7845e83aad6bffff">Errors found:</phrase>
|
||||
<phrase data="auto_7a4f40b5797415a7">Category template: /</phrase>
|
||||
<phrase data="auto_7a9b21737266b2d1">, errors:</phrase>
|
||||
<phrase data="auto_7bd54e89985213b9">Template</phrase>
|
||||
<phrase data="auto_7c3bb087be98e4e9">Failed to load statistics</phrase>
|
||||
<phrase data="auto_7dcc70f455f9f122">API token revoked</phrase>
|
||||
<phrase data="auto_7f09659cde925652">Find or add a tag</phrase>
|
||||
<phrase data="auto_7f7328fc7d7316b8">n</phrase>
|
||||
<phrase data="auto_80a2c74df6ffb08d">Open the desired folder and confirm your selection. All suitable files from it will be added to the field.</phrase>
|
||||
<phrase data="auto_80c97478d2179d1c"><div><dt>State</dt><dd></phrase>
|
||||
<phrase data="auto_83c8e08533872aff"><div class="empty-state">Loading...</div></phrase>
|
||||
<phrase data="auto_84b96b359e9c61d3">The contents of the snapshot will appear after selecting a revision.</phrase>
|
||||
<phrase data="auto_86ea33aef5e95d4f">Delete</phrase>
|
||||
<phrase data="auto_8922542f90c16aee">Copied</phrase>
|
||||
<phrase data="auto_8bf2ee2944fdd73f">optional</phrase>
|
||||
<phrase data="auto_8c34e8bce7d519b3">The description will appear after filling out the meta description.</phrase>
|
||||
<phrase data="auto_8d333d8b91196c4b">Documents</phrase>
|
||||
<phrase data="auto_8d5d38d878604c11">The document has been restored</phrase>
|
||||
<phrase data="auto_8e8ca8428150664e">g</phrase>
|
||||
<phrase data="auto_8ff9429d66687894">No matches</phrase>
|
||||
<phrase data="auto_919be7c1e27ba64c"><button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-up data-tooltip="Above" aria-label="Above"><i class="ti ti-arrow-up"></i></button></phrase>
|
||||
<phrase data="auto_9541be7605dbe971">Permissions for one external integration.</phrase>
|
||||
<phrase data="auto_96664ee8380d446c">The document has changed</phrase>
|
||||
<phrase data="auto_9729bca62f6c558e">Request Error</phrase>
|
||||
<phrase data="auto_97c557fb133f1f95">fields</phrase>
|
||||
<phrase data="auto_98b2073ed1815f28">Clear</phrase>
|
||||
<phrase data="auto_9b16ca539e602189">Revoke</phrase>
|
||||
<phrase data="auto_9bf756bd3124a21d">Changes saved</phrase>
|
||||
<phrase data="auto_9c4c0897f8ec6cf2">[title]" data-media-key="title" value="" placeholder="Image title"></phrase>
|
||||
<phrase data="auto_9c7449bfd16b63a0">Remove</phrase>
|
||||
<phrase data="auto_9caf4a95f812be6a">n</phrase>
|
||||
<phrase data="auto_9d1e4bc060f5da40">Added from folder:</phrase>
|
||||
<phrase data="auto_9d809f884b63cdec">Edit</phrase>
|
||||
<phrase data="auto_9da304d709611b05">Failed to load revisions</phrase>
|
||||
<phrase data="auto_9f0b990967c8a2ad">Meaning</phrase>
|
||||
<phrase data="auto_9f3b3c3b78133faa">Select document</phrase>
|
||||
<phrase data="auto_a15870f87e67e329"><div><dt>Formed by</dt><dd></phrase>
|
||||
<phrase data="auto_a18928c7baf57b4e">Fields only</phrase>
|
||||
<phrase data="auto_a2e9f606da125a72">b</phrase>
|
||||
<phrase data="auto_a37cb7b62a6f1b65">The category does not have a path template: the document alias is used from the root of the site.</phrase>
|
||||
<phrase data="auto_a45e66bc42d77b08">Failed to generate alias</phrase>
|
||||
<phrase data="auto_a59e2c2429fe7a88">and</phrase>
|
||||
<phrase data="auto_a5ecb551ebd318c2">Height,</phrase>
|
||||
<phrase data="auto_a63e1ce2e55b8d0a">No image selected</phrase>
|
||||
<phrase data="auto_a690b9dead4d8d8d"><div><dt>Size</dt><dd></phrase>
|
||||
<phrase data="auto_a7774650638b586d">JSON snapshot rebuilt</phrase>
|
||||
<phrase data="auto_a78e4bb6eb8c9ced">Document revisions</phrase>
|
||||
<phrase data="auto_a8504d513adfb6f5">Heading</phrase>
|
||||
<phrase data="auto_a90ed3357338f605">Loading...</phrase>
|
||||
<phrase data="auto_ab30701c85ebee86">Remove document</phrase>
|
||||
<phrase data="auto_ac3008315dc50b25">Add from this folder</phrase>
|
||||
<phrase data="auto_ae008e22ec3a8627">All lines in this field will be removed from the document after saving.</phrase>
|
||||
<phrase data="auto_af962c31c15d28d8"><button class="btn btn-ghost btn-icon btn-sm documents-drag-handle" type="button" data-doc-drag draggable="true" data-tooltip="Drag" aria-label="Drag"><i class="ti ti-grip-vertical"></i></button></phrase>
|
||||
<phrase data="auto_b0a419827426861b">Preset deleted</phrase>
|
||||
<phrase data="auto_b0d827d33fc8eb3f">Pictures</phrase>
|
||||
<phrase data="auto_b1c30d9335b4ffb7">/ + document alias. The date is taken from the publication.</phrase>
|
||||
<phrase data="auto_b279170b217c1e48"><div><dt>Fields</dt><dd class="mono"></phrase>
|
||||
<phrase data="auto_b2ac54889c1e667e">Batch rebuild stopped</phrase>
|
||||
<phrase data="auto_b36631e2d2874351"><article class="documents-revision-field documents-revision-system-field"><div><label class="documents-revision-check" aria-label="Restore</phrase>
|
||||
<phrase data="auto_b383b6f9be5e7ca2">Failed to get snapshot status</phrase>
|
||||
<phrase data="auto_b40bbca36d2d172e">with</phrase>
|
||||
<phrase data="auto_b5251271237d718b">These files are already in the field</phrase>
|
||||
<phrase data="auto_b92a4704544f84aa">The pictures will be sequentially reassembled to</phrase>
|
||||
<phrase data="auto_b9917824e609b5b3">pictures</phrase>
|
||||
<phrase data="auto_baa96cc9f67a7eb8">yu</phrase>
|
||||
<phrase data="auto_be1aff65661f854c">[name]" data-media-key="name" value="" placeholder="File name"></phrase>
|
||||
<phrase data="auto_be4a59e132257571">Revoke an API token?</phrase>
|
||||
<phrase data="auto_be7ec45710267c5c"><section class="documents-revision-content"><div class="documents-revision-subhead"><i class="ti ti-forms"></i><b>Category fields</b><span></phrase>
|
||||
<phrase data="auto_bea15c7bbd9e167c">e</phrase>
|
||||
<phrase data="auto_beed168817322eeb">from</phrase>
|
||||
<phrase data="auto_bf6379458cca6658">m</phrase>
|
||||
<phrase data="auto_c13875d6c76e5c2e">w</phrase>
|
||||
<phrase data="auto_c2b70c3bb20678a7">Failed to apply filters</phrase>
|
||||
<phrase data="auto_c52083faa73d3829">Field</phrase>
|
||||
<phrase data="auto_c5eb26618a7ede98">Read only</phrase>
|
||||
<phrase data="auto_c7fca87d8dd9b0e6">Click to find document</phrase>
|
||||
<phrase data="auto_c9d8ea6476aa730d">Add "</phrase>
|
||||
<phrase data="auto_cccd51a8c6aa88c1"><div class="documents-term-status"><i class="ti ti-loader-2"></i><span>Looking for matches...</span></div></phrase>
|
||||
<phrase data="auto_ccfe048a5551ed0f">preset</phrase>
|
||||
<phrase data="auto_cdd3b42f2a48df2f">Delete preset</phrase>
|
||||
<phrase data="auto_ceebfda5155965db">Failed to generate short alias</phrase>
|
||||
<phrase data="auto_cf0fc49836b59906">can be left blank</phrase>
|
||||
<phrase data="auto_cf412de9896e972b">Select at least one resolution</phrase>
|
||||
<phrase data="auto_d0888e0521387c25">Width,</phrase>
|
||||
<phrase data="auto_d164037dc0f1bd06">Delete revision</phrase>
|
||||
<phrase data="auto_d2264bee0979ddf6"><div><dt>Status</dt><dd>Loading...</dd></div></phrase>
|
||||
<phrase data="auto_d6951b2c8c477f98">All elements of this field will be removed from the document after saving.</phrase>
|
||||
<phrase data="auto_d6b08d92a7694cd4">Weight,</phrase>
|
||||
<phrase data="auto_d77df87cd6098c73">History of field values</phrase>
|
||||
<phrase data="auto_d7907f4d435df378">Additionally</phrase>
|
||||
<phrase data="auto_d944cde8b15c4d8b">at</phrase>
|
||||
<phrase data="auto_d9c83b31f5cb2376">Failed to check short alias</phrase>
|
||||
<phrase data="auto_da97cf5fe3fc4c42">Checking...</phrase>
|
||||
<phrase data="auto_db4d819570e77e56">draft</phrase>
|
||||
<phrase data="auto_dcedec8acc4af7ca">Assignment document</phrase>
|
||||
<phrase data="auto_dd0ec9a1c7140562">Add files from a folder</phrase>
|
||||
<phrase data="auto_defc150c8003aa92">Revisions deleted</phrase>
|
||||
<phrase data="auto_df5763ef792cf2f4">cm</phrase>
|
||||
<phrase data="auto_e01aed78c667a5aa"><button class="btn btn-ghost btn-icon btn-sm documents-value-remove" type="button" data-document-value-remove data-tooltip="Delete" aria-label="Delete"><i class="ti ti-trash"></i></button></phrase>
|
||||
<phrase data="auto_e4cedbc81a3c0d4e">ъ</phrase>
|
||||
<phrase data="auto_e576260a6a03f359">t</phrase>
|
||||
<phrase data="auto_e76f1ce35ab97dfb"></span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="document" checked><span>All</span></label></div></phrase>
|
||||
<phrase data="auto_e83e7fbf2312ab9e">The document has already been changed. Refresh the page.</phrase>
|
||||
<phrase data="auto_e94209605c9c0a4e"><button class="documents-media-thumb" type="button" data-document-media-pick aria-label="Select file"><span><i class="ti ti-</phrase>
|
||||
<phrase data="auto_ec6be72560e6078a">The story is still empty</phrase>
|
||||
<phrase data="auto_eeb60e75b96656e7">Incorrect answer</phrase>
|
||||
<phrase data="auto_f0b532724126bbe8">Clear all field elements?</phrase>
|
||||
<phrase data="auto_f26e5c0c72082089">Find or add a keyword</phrase>
|
||||
<phrase data="auto_f3d123e46297531d">in</phrase>
|
||||
<phrase data="auto_f42c85cd57ba4aa0">Stay</phrase>
|
||||
<phrase data="auto_f6069600bcb07eea"><div class="modal-footer"><div class="mf-left documents-picker-count" data-relation-count></div><button class="btn btn-ghost" type="button" data-relation-close>Close</button></div></phrase>
|
||||
<phrase data="auto_f61de80bf0ca15a4">No document selected</phrase>
|
||||
<phrase data="auto_f7286439b109e428">New redirect</phrase>
|
||||
<phrase data="auto_f7806fce80e51b40">Old URL</phrase>
|
||||
<phrase data="auto_f914c65278294e4a"><div class="modal-header"><span class="dialog-icon info"><i class="ti ti-file-search"></i></span><div style="flex:1"><h3>Select document</h3><p class="text-secondary" style="margin-top:4px"></phrase>
|
||||
<phrase data="auto_fa9f0524f3a0531e">Add a tag</phrase>
|
||||
<phrase data="auto_fc8b3939cc9dc467">The path will be written in the document field.</phrase>
|
||||
<phrase data="auto_fda74d41e3311f87">A newer version is already saved on the server. Refresh the page, check the changes, and save the document again.</phrase>
|
||||
<phrase data="auto_ffec67789fb1fcdc">sch</phrase>
|
||||
<phrase data="documents_bulk_action">Action</phrase>
|
||||
<phrase data="documents_bulk_action_fallback">Action</phrase>
|
||||
<phrase data="documents_bulk_documents_suffix"> documents.</phrase>
|
||||
<phrase data="documents_bulk_errors">errors</phrase>
|
||||
<phrase data="documents_bulk_processed">processed</phrase>
|
||||
<phrase data="documents_bulk_unchanged">unchanged</phrase>
|
||||
<phrase data="runtime_05d4ae78b6f1bfee">n</phrase>
|
||||
<phrase data="runtime_0918b4ba92684eaa">Title</phrase>
|
||||
<phrase data="runtime_0940401102b8a3fe">t</phrase>
|
||||
<phrase data="runtime_11cef8141437f8a6">ъ</phrase>
|
||||
<phrase data="runtime_15f2ccabeb425c29">th</phrase>
|
||||
<phrase data="runtime_1cc10a3947f7c245">d</phrase>
|
||||
<phrase data="runtime_1d51ebe0fc45e38e">Fields only</phrase>
|
||||
<phrase data="runtime_259f56cb715ba3a3">e</phrase>
|
||||
<phrase data="runtime_3051067c6324c63f">h</phrase>
|
||||
<phrase data="runtime_30fbc377ae9122ed">e</phrase>
|
||||
<phrase data="runtime_3164696756227147">Note added</phrase>
|
||||
<phrase data="runtime_34c6fbb62da8ce5c">n</phrase>
|
||||
<phrase data="runtime_37d43a57a463add5">yu</phrase>
|
||||
<phrase data="runtime_40831c5d98a57dfc">h</phrase>
|
||||
<phrase data="runtime_446cdc8e3c0d7efc">s</phrase>
|
||||
<phrase data="runtime_46b0d3855548054a">JSON snapshot rebuilt</phrase>
|
||||
<phrase data="runtime_4f60bd4e24e7dc7f">w</phrase>
|
||||
<phrase data="runtime_50338c005c41fb4e">o</phrase>
|
||||
<phrase data="runtime_5225a95b111d47bf">to</phrase>
|
||||
<phrase data="runtime_53b10ee4405f56d6">not created yet</phrase>
|
||||
<phrase data="runtime_557f4cd2c6d3c150">cm</phrase>
|
||||
<phrase data="runtime_61095f9d62dfe291">The document has been restored</phrase>
|
||||
<phrase data="runtime_6228f1454b196ffd">API token revoked</phrase>
|
||||
<phrase data="runtime_65fd24f1cb37fec4">Draft</phrase>
|
||||
<phrase data="runtime_72a81659de042887">Revisions deleted</phrase>
|
||||
<phrase data="runtime_734dbc2b54ab6c95">r</phrase>
|
||||
<phrase data="runtime_754309a41e0df98a">f</phrase>
|
||||
<phrase data="runtime_7592b7ec4eba966a">ts</phrase>
|
||||
<phrase data="runtime_7b0ff8a2e5fdd66d">m</phrase>
|
||||
<phrase data="runtime_823c4eb3e895adc9">a</phrase>
|
||||
<phrase data="runtime_85011fce4419ee63">Field #</phrase>
|
||||
<phrase data="runtime_894f97c52af9e5ac">Note deleted</phrase>
|
||||
<phrase data="runtime_9aa032336a8e122c">b</phrase>
|
||||
<phrase data="runtime_9b21f5cf014c9efb">I</phrase>
|
||||
<phrase data="runtime_9b6276c9a4ac5507">Document saved</phrase>
|
||||
<phrase data="runtime_a1f5d39a12e8f6be">with</phrase>
|
||||
<phrase data="runtime_ab1f4be7e5d6fd8b">g</phrase>
|
||||
<phrase data="runtime_ae51ade91d2b85b7">Revision deleted</phrase>
|
||||
<phrase data="runtime_b8012cb642c887a0">in</phrase>
|
||||
<phrase data="runtime_b95d78565f154f82">b</phrase>
|
||||
<phrase data="runtime_bb69a96efca3303e">x</phrase>
|
||||
<phrase data="runtime_bbb369f210d2969f">sch</phrase>
|
||||
<phrase data="runtime_beee80c256b458da">at</phrase>
|
||||
<phrase data="runtime_bf0d18ef68a8a34e">can be left blank</phrase>
|
||||
<phrase data="runtime_c08fb476eca49399">kg</phrase>
|
||||
<phrase data="runtime_c5412ba2cc2fc45a">Creation preset saved</phrase>
|
||||
<phrase data="runtime_c78364c5d0f27706">B</phrase>
|
||||
<phrase data="runtime_cdd02bd8b5684812">Redirect removed</phrase>
|
||||
<phrase data="runtime_d92fff560eb5495e">l</phrase>
|
||||
<phrase data="runtime_dd48804aa38b03e1">Preset deleted</phrase>
|
||||
<phrase data="runtime_dd591284d827e17f">f</phrase>
|
||||
<phrase data="runtime_e1c88af54b9fced2">Document #</phrase>
|
||||
<phrase data="runtime_e35c8bb151d3530d">Redirect saved</phrase>
|
||||
<phrase data="runtime_e93c3d6287d39cc3">API token created</phrase>
|
||||
<phrase data="runtime_ee71f8405f87d786">uh</phrase>
|
||||
<phrase data="runtime_eead37caf818d0fa">and</phrase>
|
||||
<phrase data="runtime_f4019994d4e75a2a">after saving</phrase>
|
||||
<phrase data="runtime_f9a506077fa0d961">New</phrase>
|
||||
<phrase data="runtime_fff8b34d92dab340">Template</phrase>
|
||||
<phrase data="auto_f8af664b7ab2ef75">Remove all items?</phrase>
|
||||
<phrase data="auto_10403ca9b11a0b49">After saving, the items will be removed from the document. Unused files can be restored from the media trash.</phrase>
|
||||
<phrase data="auto_5ede041b60096d58">Remove the image?</phrase>
|
||||
<phrase data="auto_c5b3cd090f16f479">After saving, the image will be removed from the document. The unused file can be restored from the media trash.</phrase>
|
||||
<phrase data="auto_e26c8664b60bfa9a">Remove the image from the document?</phrase>
|
||||
<phrase data="auto_02e536f7b2d5cec8">The document was saved, but some old files were not moved to the trash.</phrase>
|
||||
</language>
|
||||
|
||||
@@ -1,302 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="auto_003cc12783448df3">х</phrase>
|
||||
<phrase data="auto_0156bf1b8aac8565">Добавить ещё…</phrase>
|
||||
<phrase data="auto_02b4ebb5dc50bca5">В поле-ссылку будет записан адрес документа.</phrase>
|
||||
<phrase data="auto_02fdd4b8923613d6">До 10 символов, необязательно.</phrase>
|
||||
<phrase data="auto_04c0389bc0db2353">Токен скопирован</phrase>
|
||||
<phrase data="auto_080887ebc7bb5402"><button class="btn btn-ghost btn-icon btn-sm documents-media-remove" type="button" data-document-media-remove data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button></phrase>
|
||||
<phrase data="auto_0957de938411c15b">поле</phrase>
|
||||
<phrase data="auto_0af969e08c220502">Очистить все строки поля?</phrase>
|
||||
<phrase data="auto_0db49b55557f2bde">Параметр</phrase>
|
||||
<phrase data="auto_0e7fbe3404b67fe3">Пересобрать JSON-снимки?</phrase>
|
||||
<phrase data="auto_0f9ccee053fe3b6b">Новый</phrase>
|
||||
<phrase data="auto_0ff730d95f9c5bc3">нажмите, чтобы выбрать</phrase>
|
||||
<phrase data="auto_122f4893d3d0d802">Не удалось проверить alias</phrase>
|
||||
<phrase data="auto_1291e1971ba28ee6">API-токен создан</phrase>
|
||||
<phrase data="auto_12bfabff3d7dc01c">Добавить ключевое слово</phrase>
|
||||
<phrase data="auto_18337ef195f7d88d"><div class="modal-body"><div class="input-wrap documents-relation-search"><i class="ti ti-search"></i><input class="input" type="search" placeholder="ID, название или alias" data-relation-search></div><div class="documents-picker-status" data-relation-status>Загрузка...</div><div class="documents-relation-list" data-relation-list></div></div></phrase>
|
||||
<phrase data="auto_1894f01f2cfa4702">JSON-снимок</phrase>
|
||||
<phrase data="auto_1a7725e697463fd8">к</phrase>
|
||||
<phrase data="auto_1af72c14888c4574">Пресет создания</phrase>
|
||||
<phrase data="auto_1bc16af35fa47103">э</phrase>
|
||||
<phrase data="auto_1d00a5603a94c17d">ID тизера / ссылка</phrase>
|
||||
<phrase data="auto_1d402bc8ffda29cd">Внутренняя ошибка сервера. Обновите страницу и повторите попытку.</phrase>
|
||||
<phrase data="auto_1f864100c5c97f02"><button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-down data-tooltip="Ниже" aria-label="Ниже"><i class="ti ti-arrow-down"></i></button></phrase>
|
||||
<phrase data="auto_22dc89ab84f07324">"><button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Выбрать файл" aria-label="Выбрать файл"><i class="ti ti-</phrase>
|
||||
<phrase data="auto_230b5877294e9726">ц</phrase>
|
||||
<phrase data="auto_23289c89f8c038a0">В поле будет записан ID документа.</phrase>
|
||||
<phrase data="auto_242f64ddfe84899b">после сохранения</phrase>
|
||||
<phrase data="auto_25a9845975119103">Пресет создания сохранён</phrase>
|
||||
<phrase data="auto_25dc101484dbd55c">Не удалось загрузить список</phrase>
|
||||
<phrase data="auto_285cc4001478d568">ч</phrase>
|
||||
<phrase data="auto_28c993e4935524d8">з</phrase>
|
||||
<phrase data="auto_28fb6167ec0a79fe"><section class="documents-revision-system"><div class="documents-revision-subhead"><i class="ti ti-settings"></i><b>Основные настройки</b><span></phrase>
|
||||
<phrase data="auto_292688e9915e161b">[link]" data-media-key="link" value="" placeholder="Ссылка или документ" data-document-media-url data-document-picker-type="all"></phrase>
|
||||
<phrase data="auto_29f3b29df0d0e54f">Восстановить</phrase>
|
||||
<phrase data="auto_2a9790fd6fab2922"><button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-doc-pick data-tooltip="Выбрать документ" aria-label="Выбрать документ"><i class="ti ti-file-search"></i></button></div></phrase>
|
||||
<phrase data="auto_2ba58b00b4c52021"></span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="field" checked><span>Все</span></label></div></phrase>
|
||||
<phrase data="auto_2c501dc73177148a">ID, название или alias</phrase>
|
||||
<phrase data="auto_2e31dae95f35e3b8">Выбрать файл</phrase>
|
||||
<phrase data="auto_2ec20105a2757791">Колонка 3</phrase>
|
||||
<phrase data="auto_2f1256a9610f7d64">Редирект сохранён</phrase>
|
||||
<phrase data="auto_2ff1fe42207df255">Секрет готов к копированию.</phrase>
|
||||
<phrase data="auto_301361625f901825">Сервер вернул некорректный ответ.</phrase>
|
||||
<phrase data="auto_30278b05221dd957">Обновить страницу</phrase>
|
||||
<phrase data="auto_32b74a3c47908c1e">Без названия</phrase>
|
||||
<phrase data="auto_33392e5848b4a8eb">JSON-снимки пересобраны:</phrase>
|
||||
<phrase data="auto_336d12a4f96485a3"><button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-up data-tooltip="Выше" aria-label="Выше"><i class="ti ti-arrow-up"></i></button></phrase>
|
||||
<phrase data="auto_359282f9ef75e654">ID документа</phrase>
|
||||
<phrase data="auto_35de32df9d942d9a">Заметка добавлена</phrase>
|
||||
<phrase data="auto_3697bef0b3fc93f5">новый</phrase>
|
||||
<phrase data="auto_3812a04b189ec1fd">новое</phrase>
|
||||
<phrase data="auto_39704f311a86cd23">Черновик</phrase>
|
||||
<phrase data="auto_3de49828e86a4016">Название</phrase>
|
||||
<phrase data="auto_3e096d0063299173">Ревизия удалена</phrase>
|
||||
<phrase data="auto_3e25fbc59741a10a">Заголовок документа</phrase>
|
||||
<phrase data="auto_3e670a1ec591883c">Файлы загружены</phrase>
|
||||
<phrase data="auto_3eb77d3c13c51f92">Выберите ревизию</phrase>
|
||||
<phrase data="auto_3f039532d65bb5e3">Пересобрать JSON</phrase>
|
||||
<phrase data="auto_4044fbc87708b9ad">Очистить подневную статистику?</phrase>
|
||||
<phrase data="auto_40785832c27c3a51">Колонка 1</phrase>
|
||||
<phrase data="auto_42ba924b3732dc07">поля</phrase>
|
||||
<phrase data="auto_433e4b40a403cdb0">й</phrase>
|
||||
<phrase data="auto_447d642f14fb2c8c">В папке нет подходящих файлов</phrase>
|
||||
<phrase data="auto_46551a5e6e43e854">Статистика очищена</phrase>
|
||||
<phrase data="auto_48be85b47e187f3c">ещё не создан</phrase>
|
||||
<phrase data="auto_4a05d8619dedc462">Копировать</phrase>
|
||||
<phrase data="auto_4b4c88ed13b23b15">Редирект удалён</phrase>
|
||||
<phrase data="auto_4d60d98f499d1580">Ревизии</phrase>
|
||||
<phrase data="auto_4f566fb5e3df4276">я</phrase>
|
||||
<phrase data="auto_516cbe2af24391af">л</phrase>
|
||||
<phrase data="auto_51b4eac98af84251">а</phrase>
|
||||
<phrase data="auto_51f784cde1916288">ь</phrase>
|
||||
<phrase data="auto_522e27d7402089bc">Документ сохранён</phrase>
|
||||
<phrase data="auto_53e908b396e3a6b3"><span class="badge badge-gray">не создан</span></phrase>
|
||||
<phrase data="auto_54a8375d14dfdab5">Не удалось прочитать папку</phrase>
|
||||
<phrase data="auto_56ac0cff0f4a358f">Заметка удалена</phrase>
|
||||
<phrase data="auto_576895a59897535b">Файл не выбран</phrase>
|
||||
<phrase data="auto_577021b5ac1fd915">ф</phrase>
|
||||
<phrase data="auto_57afad1302f9645e">о</phrase>
|
||||
<phrase data="auto_588c8feeb973dcb1"><div><dt>Файл</dt><dd class="mono"></phrase>
|
||||
<phrase data="auto_59ddef8a55503c83">кг</phrase>
|
||||
<phrase data="auto_5a3b4bcf55400711">р</phrase>
|
||||
<phrase data="auto_5bd0d9fe0580526f">. Окно нужно оставить открытым до завершения.</phrase>
|
||||
<phrase data="auto_5c019fa5d03ca5cf">в рубрике</phrase>
|
||||
<phrase data="auto_5e6c96299a2f11e9">ы</phrase>
|
||||
<phrase data="auto_5e6d6b07b27b7a7c">Все строки view_count будут удалены. Общие счётчики документов останутся без изменений.</phrase>
|
||||
<phrase data="auto_5f9eaa37c4e2ccff"><button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Выбрать файл" aria-label="Выбрать файл"><i class="ti ti-paperclip"></i></button></phrase>
|
||||
<phrase data="auto_600363a73de5f3a5">Пересобрать</phrase>
|
||||
<phrase data="auto_60512114c18fd5ed"><span class="badge badge-amber">нужна пересборка</span></phrase>
|
||||
<phrase data="auto_619742ac4a834c45">док.</phrase>
|
||||
<phrase data="auto_63b1fa390e91f75c">0 Б</phrase>
|
||||
<phrase data="auto_65b7f1673713a00c">д</phrase>
|
||||
<phrase data="auto_66d207b9ebe9e4e3"><div class="documents-term-status is-error"><i class="ti ti-alert-circle"></i><span>Не удалось загрузить варианты</span></div></phrase>
|
||||
<phrase data="auto_6702cc12ca25aff1"><div class="empty-state">Ревизий пока нет. Первый снимок появится после сохранения документа.</div></phrase>
|
||||
<phrase data="auto_691245657a52bdce"><div><label class="documents-revision-check" aria-label="Восстановить</phrase>
|
||||
<phrase data="auto_6972375800d3e590">Очистить поле?</phrase>
|
||||
<phrase data="auto_698ace159126e252">Колонка 2</phrase>
|
||||
<phrase data="auto_6b595786e068b5bc">Alias можно оставить пустым</phrase>
|
||||
<phrase data="auto_6baaabf049b42cca">ё</phrase>
|
||||
<phrase data="auto_6bee957a1f3ef55d">Длина,</phrase>
|
||||
<phrase data="auto_6c190c23240564f5">ж</phrase>
|
||||
<phrase data="auto_6e408d3c9c3156bf">Удалить «</phrase>
|
||||
<phrase data="auto_6e52ded051aa402d">Отозван</phrase>
|
||||
<phrase data="auto_6f658622a374b8d6"></p></div><button class="modal-close" type="button" data-relation-close aria-label="Закрыть"><i class="ti ti-x"></i></button></div></phrase>
|
||||
<phrase data="auto_6fe63eee472a5a93"><span class="badge badge-green">актуален</span></phrase>
|
||||
<phrase data="auto_707a724dce1fa0d4">URL скопирован</phrase>
|
||||
<phrase data="auto_72c618d46424c93a">Сохранённых значений пока нет</phrase>
|
||||
<phrase data="auto_736a75710565c2f2">[description]" data-media-key="description" rows="2" placeholder="Описание"></textarea></phrase>
|
||||
<phrase data="auto_75285ce97dc1dc97"><button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-down data-tooltip="Ниже" aria-label="Ниже"><i class="ti ti-arrow-down"></i></button></phrase>
|
||||
<phrase data="auto_781e545c1063d5db">Интеграция сразу потеряет доступ. Вернуть этот токен будет невозможно.</phrase>
|
||||
<phrase data="auto_7845e83aad6bffff">Найдено ошибок:</phrase>
|
||||
<phrase data="auto_7a4f40b5797415a7">Шаблон рубрики: /</phrase>
|
||||
<phrase data="auto_7a9b21737266b2d1">, ошибок:</phrase>
|
||||
<phrase data="auto_7bd54e89985213b9">Шаблон</phrase>
|
||||
<phrase data="auto_7c3bb087be98e4e9">Не удалось загрузить статистику</phrase>
|
||||
<phrase data="auto_7dcc70f455f9f122">API-токен отозван</phrase>
|
||||
<phrase data="auto_7f09659cde925652">Найти или добавить тег</phrase>
|
||||
<phrase data="auto_7f7328fc7d7316b8">н</phrase>
|
||||
<phrase data="auto_80a2c74df6ffb08d">Откройте нужную папку и подтвердите выбор. В поле добавятся все подходящие файлы из неё.</phrase>
|
||||
<phrase data="auto_80c97478d2179d1c"><div><dt>Состояние</dt><dd></phrase>
|
||||
<phrase data="auto_83c8e08533872aff"><div class="empty-state">Загрузка...</div></phrase>
|
||||
<phrase data="auto_84b96b359e9c61d3">Содержимое снимка появится после выбора ревизии.</phrase>
|
||||
<phrase data="auto_86ea33aef5e95d4f">Удалить</phrase>
|
||||
<phrase data="auto_8922542f90c16aee">Скопировано</phrase>
|
||||
<phrase data="auto_8bf2ee2944fdd73f">необязательно</phrase>
|
||||
<phrase data="auto_8c34e8bce7d519b3">Описание появится после заполнения meta description.</phrase>
|
||||
<phrase data="auto_8d333d8b91196c4b">Документов</phrase>
|
||||
<phrase data="auto_8d5d38d878604c11">Документ восстановлен</phrase>
|
||||
<phrase data="auto_8e8ca8428150664e">г</phrase>
|
||||
<phrase data="auto_8ff9429d66687894">Совпадений нет</phrase>
|
||||
<phrase data="auto_919be7c1e27ba64c"><button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-up data-tooltip="Выше" aria-label="Выше"><i class="ti ti-arrow-up"></i></button></phrase>
|
||||
<phrase data="auto_9541be7605dbe971">Права доступа для одной внешней интеграции.</phrase>
|
||||
<phrase data="auto_96664ee8380d446c">Документ изменился</phrase>
|
||||
<phrase data="auto_9729bca62f6c558e">Ошибка запроса</phrase>
|
||||
<phrase data="auto_97c557fb133f1f95">полей</phrase>
|
||||
<phrase data="auto_98b2073ed1815f28">Очистить</phrase>
|
||||
<phrase data="auto_9b16ca539e602189">Отозвать</phrase>
|
||||
<phrase data="auto_9bf756bd3124a21d">Изменения сохранены</phrase>
|
||||
<phrase data="auto_9c4c0897f8ec6cf2">[title]" data-media-key="title" value="" placeholder="Заголовок изображения"></phrase>
|
||||
<phrase data="auto_9c7449bfd16b63a0">Убрать</phrase>
|
||||
<phrase data="auto_9caf4a95f812be6a">п</phrase>
|
||||
<phrase data="auto_9d1e4bc060f5da40">Добавлено из папки:</phrase>
|
||||
<phrase data="auto_9d809f884b63cdec">Изменить</phrase>
|
||||
<phrase data="auto_9da304d709611b05">Не удалось загрузить ревизии</phrase>
|
||||
<phrase data="auto_9f0b990967c8a2ad">Значение</phrase>
|
||||
<phrase data="auto_9f3b3c3b78133faa">Выбрать документ</phrase>
|
||||
<phrase data="auto_a15870f87e67e329"><div><dt>Сформирован</dt><dd></phrase>
|
||||
<phrase data="auto_a18928c7baf57b4e">Только поля</phrase>
|
||||
<phrase data="auto_a2e9f606da125a72">б</phrase>
|
||||
<phrase data="auto_a37cb7b62a6f1b65">У рубрики нет шаблона пути: alias документа используется от корня сайта.</phrase>
|
||||
<phrase data="auto_a45e66bc42d77b08">Не удалось сгенерировать alias</phrase>
|
||||
<phrase data="auto_a59e2c2429fe7a88">и</phrase>
|
||||
<phrase data="auto_a5ecb551ebd318c2">Высота,</phrase>
|
||||
<phrase data="auto_a63e1ce2e55b8d0a">Изображение не выбрано</phrase>
|
||||
<phrase data="auto_a690b9dead4d8d8d"><div><dt>Размер</dt><dd></phrase>
|
||||
<phrase data="auto_a7774650638b586d">JSON-снимок пересобран</phrase>
|
||||
<phrase data="auto_a78e4bb6eb8c9ced">Ревизии документа</phrase>
|
||||
<phrase data="auto_a8504d513adfb6f5">Заголовок</phrase>
|
||||
<phrase data="auto_a90ed3357338f605">Загрузка...</phrase>
|
||||
<phrase data="auto_ab30701c85ebee86">Убрать документ</phrase>
|
||||
<phrase data="auto_ac3008315dc50b25">Добавить из этой папки</phrase>
|
||||
<phrase data="auto_ae008e22ec3a8627">Все строки этого поля будут удалены из документа после сохранения.</phrase>
|
||||
<phrase data="auto_af962c31c15d28d8"><button class="btn btn-ghost btn-icon btn-sm documents-drag-handle" type="button" data-doc-drag draggable="true" data-tooltip="Перетащить" aria-label="Перетащить"><i class="ti ti-grip-vertical"></i></button></phrase>
|
||||
<phrase data="auto_b0a419827426861b">Пресет удалён</phrase>
|
||||
<phrase data="auto_b0d827d33fc8eb3f">Снимки</phrase>
|
||||
<phrase data="auto_b1c30d9335b4ffb7">/ + alias документа. Дата берётся из публикации.</phrase>
|
||||
<phrase data="auto_b279170b217c1e48"><div><dt>Поля</dt><dd class="mono"></phrase>
|
||||
<phrase data="auto_b2ac54889c1e667e">Пакетная пересборка остановлена</phrase>
|
||||
<phrase data="auto_b36631e2d2874351"><article class="documents-revision-field documents-revision-system-field"><div><label class="documents-revision-check" aria-label="Восстановить</phrase>
|
||||
<phrase data="auto_b383b6f9be5e7ca2">Не удалось получить статус снимка</phrase>
|
||||
<phrase data="auto_b40bbca36d2d172e">с</phrase>
|
||||
<phrase data="auto_b5251271237d718b">Эти файлы уже есть в поле</phrase>
|
||||
<phrase data="auto_b92a4704544f84aa">Снимки будут последовательно пересобраны для</phrase>
|
||||
<phrase data="auto_b9917824e609b5b3">снимков</phrase>
|
||||
<phrase data="auto_baa96cc9f67a7eb8">ю</phrase>
|
||||
<phrase data="auto_be1aff65661f854c">[name]" data-media-key="name" value="" placeholder="Название файла"></phrase>
|
||||
<phrase data="auto_be4a59e132257571">Отозвать API-токен?</phrase>
|
||||
<phrase data="auto_be7ec45710267c5c"><section class="documents-revision-content"><div class="documents-revision-subhead"><i class="ti ti-forms"></i><b>Поля рубрики</b><span></phrase>
|
||||
<phrase data="auto_bea15c7bbd9e167c">е</phrase>
|
||||
<phrase data="auto_beed168817322eeb">из</phrase>
|
||||
<phrase data="auto_bf6379458cca6658">м</phrase>
|
||||
<phrase data="auto_c13875d6c76e5c2e">ш</phrase>
|
||||
<phrase data="auto_c2b70c3bb20678a7">Не удалось применить фильтры</phrase>
|
||||
<phrase data="auto_c52083faa73d3829">Поле</phrase>
|
||||
<phrase data="auto_c5eb26618a7ede98">Только чтение</phrase>
|
||||
<phrase data="auto_c7fca87d8dd9b0e6">Нажмите, чтобы найти документ</phrase>
|
||||
<phrase data="auto_c9d8ea6476aa730d">Добавить «</phrase>
|
||||
<phrase data="auto_cccd51a8c6aa88c1"><div class="documents-term-status"><i class="ti ti-loader-2"></i><span>Ищем совпадения…</span></div></phrase>
|
||||
<phrase data="auto_ccfe048a5551ed0f">пресет</phrase>
|
||||
<phrase data="auto_cdd3b42f2a48df2f">Удалить пресет</phrase>
|
||||
<phrase data="auto_ceebfda5155965db">Не удалось сгенерировать короткий алиас</phrase>
|
||||
<phrase data="auto_cf0fc49836b59906">можно оставить пустым</phrase>
|
||||
<phrase data="auto_cf412de9896e972b">Выберите хотя бы одно разрешение</phrase>
|
||||
<phrase data="auto_d0888e0521387c25">Ширина,</phrase>
|
||||
<phrase data="auto_d164037dc0f1bd06">Удалить ревизию</phrase>
|
||||
<phrase data="auto_d2264bee0979ddf6"><div><dt>Состояние</dt><dd>Загрузка...</dd></div></phrase>
|
||||
<phrase data="auto_d6951b2c8c477f98">Все элементы этого поля будут удалены из документа после сохранения.</phrase>
|
||||
<phrase data="auto_d6b08d92a7694cd4">Вес,</phrase>
|
||||
<phrase data="auto_d77df87cd6098c73">История значений полей</phrase>
|
||||
<phrase data="auto_d7907f4d435df378">Дополнительно</phrase>
|
||||
<phrase data="auto_d944cde8b15c4d8b">у</phrase>
|
||||
<phrase data="auto_d9c83b31f5cb2376">Не удалось проверить короткий alias</phrase>
|
||||
<phrase data="auto_da97cf5fe3fc4c42">Проверка...</phrase>
|
||||
<phrase data="auto_db4d819570e77e56">черновик</phrase>
|
||||
<phrase data="auto_dcedec8acc4af7ca">Документ назначения</phrase>
|
||||
<phrase data="auto_dd0ec9a1c7140562">Добавить файлы из папки</phrase>
|
||||
<phrase data="auto_defc150c8003aa92">Ревизии удалены</phrase>
|
||||
<phrase data="auto_df5763ef792cf2f4">см</phrase>
|
||||
<phrase data="auto_e01aed78c667a5aa"><button class="btn btn-ghost btn-icon btn-sm documents-value-remove" type="button" data-document-value-remove data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button></phrase>
|
||||
<phrase data="auto_e4cedbc81a3c0d4e">ъ</phrase>
|
||||
<phrase data="auto_e576260a6a03f359">т</phrase>
|
||||
<phrase data="auto_e76f1ce35ab97dfb"></span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="document" checked><span>Все</span></label></div></phrase>
|
||||
<phrase data="auto_e83e7fbf2312ab9e">Документ уже изменён. Обновите страницу.</phrase>
|
||||
<phrase data="auto_e94209605c9c0a4e"><button class="documents-media-thumb" type="button" data-document-media-pick aria-label="Выбрать файл"><span><i class="ti ti-</phrase>
|
||||
<phrase data="auto_ec6be72560e6078a">История пока пустая</phrase>
|
||||
<phrase data="auto_eeb60e75b96656e7">Некорректный ответ</phrase>
|
||||
<phrase data="auto_f0b532724126bbe8">Очистить все элементы поля?</phrase>
|
||||
<phrase data="auto_f26e5c0c72082089">Найти или добавить ключевое слово</phrase>
|
||||
<phrase data="auto_f3d123e46297531d">в</phrase>
|
||||
<phrase data="auto_f42c85cd57ba4aa0">Остаться</phrase>
|
||||
<phrase data="auto_f6069600bcb07eea"><div class="modal-footer"><div class="mf-left documents-picker-count" data-relation-count></div><button class="btn btn-ghost" type="button" data-relation-close>Закрыть</button></div></phrase>
|
||||
<phrase data="auto_f61de80bf0ca15a4">Документ не выбран</phrase>
|
||||
<phrase data="auto_f7286439b109e428">Новый редирект</phrase>
|
||||
<phrase data="auto_f7806fce80e51b40">Старый URL</phrase>
|
||||
<phrase data="auto_f914c65278294e4a"><div class="modal-header"><span class="dialog-icon info"><i class="ti ti-file-search"></i></span><div style="flex:1"><h3>Выбрать документ</h3><p class="text-secondary" style="margin-top:4px"></phrase>
|
||||
<phrase data="auto_fa9f0524f3a0531e">Добавить тег</phrase>
|
||||
<phrase data="auto_fc8b3939cc9dc467">Путь будет записан в поле документа.</phrase>
|
||||
<phrase data="auto_fda74d41e3311f87">На сервере уже сохранена более новая версия. Обновите страницу, проверьте изменения и сохраните документ повторно.</phrase>
|
||||
<phrase data="auto_ffec67789fb1fcdc">щ</phrase>
|
||||
<phrase data="documents_bulk_action">Действие</phrase>
|
||||
<phrase data="documents_bulk_action_fallback">Действие</phrase>
|
||||
<phrase data="documents_bulk_documents_suffix"> документов.</phrase>
|
||||
<phrase data="documents_bulk_errors">ошибок</phrase>
|
||||
<phrase data="documents_bulk_processed">обработано</phrase>
|
||||
<phrase data="documents_bulk_unchanged">без изменений</phrase>
|
||||
<phrase data="runtime_05d4ae78b6f1bfee">н</phrase>
|
||||
<phrase data="runtime_0918b4ba92684eaa">Название</phrase>
|
||||
<phrase data="runtime_0940401102b8a3fe">т</phrase>
|
||||
<phrase data="runtime_11cef8141437f8a6">ъ</phrase>
|
||||
<phrase data="runtime_15f2ccabeb425c29">й</phrase>
|
||||
<phrase data="runtime_1cc10a3947f7c245">д</phrase>
|
||||
<phrase data="runtime_1d51ebe0fc45e38e">Только поля</phrase>
|
||||
<phrase data="runtime_259f56cb715ba3a3">е</phrase>
|
||||
<phrase data="runtime_3051067c6324c63f">з</phrase>
|
||||
<phrase data="runtime_30fbc377ae9122ed">ё</phrase>
|
||||
<phrase data="runtime_3164696756227147">Заметка добавлена</phrase>
|
||||
<phrase data="runtime_34c6fbb62da8ce5c">п</phrase>
|
||||
<phrase data="runtime_37d43a57a463add5">ю</phrase>
|
||||
<phrase data="runtime_40831c5d98a57dfc">ч</phrase>
|
||||
<phrase data="runtime_446cdc8e3c0d7efc">ы</phrase>
|
||||
<phrase data="runtime_46b0d3855548054a">JSON-снимок пересобран</phrase>
|
||||
<phrase data="runtime_4f60bd4e24e7dc7f">ш</phrase>
|
||||
<phrase data="runtime_50338c005c41fb4e">о</phrase>
|
||||
<phrase data="runtime_5225a95b111d47bf">к</phrase>
|
||||
<phrase data="runtime_53b10ee4405f56d6">ещё не создан</phrase>
|
||||
<phrase data="runtime_557f4cd2c6d3c150">см</phrase>
|
||||
<phrase data="runtime_61095f9d62dfe291">Документ восстановлен</phrase>
|
||||
<phrase data="runtime_6228f1454b196ffd">API-токен отозван</phrase>
|
||||
<phrase data="runtime_65fd24f1cb37fec4">Черновик</phrase>
|
||||
<phrase data="runtime_72a81659de042887">Ревизии удалены</phrase>
|
||||
<phrase data="runtime_734dbc2b54ab6c95">р</phrase>
|
||||
<phrase data="runtime_754309a41e0df98a">ф</phrase>
|
||||
<phrase data="runtime_7592b7ec4eba966a">ц</phrase>
|
||||
<phrase data="runtime_7b0ff8a2e5fdd66d">м</phrase>
|
||||
<phrase data="runtime_823c4eb3e895adc9">а</phrase>
|
||||
<phrase data="runtime_85011fce4419ee63">Поле #</phrase>
|
||||
<phrase data="runtime_894f97c52af9e5ac">Заметка удалена</phrase>
|
||||
<phrase data="runtime_9aa032336a8e122c">б</phrase>
|
||||
<phrase data="runtime_9b21f5cf014c9efb">я</phrase>
|
||||
<phrase data="runtime_9b6276c9a4ac5507">Документ сохранён</phrase>
|
||||
<phrase data="runtime_a1f5d39a12e8f6be">с</phrase>
|
||||
<phrase data="runtime_ab1f4be7e5d6fd8b">г</phrase>
|
||||
<phrase data="runtime_ae51ade91d2b85b7">Ревизия удалена</phrase>
|
||||
<phrase data="runtime_b8012cb642c887a0">в</phrase>
|
||||
<phrase data="runtime_b95d78565f154f82">ь</phrase>
|
||||
<phrase data="runtime_bb69a96efca3303e">х</phrase>
|
||||
<phrase data="runtime_bbb369f210d2969f">щ</phrase>
|
||||
<phrase data="runtime_beee80c256b458da">у</phrase>
|
||||
<phrase data="runtime_bf0d18ef68a8a34e">можно оставить пустым</phrase>
|
||||
<phrase data="runtime_c08fb476eca49399">кг</phrase>
|
||||
<phrase data="runtime_c5412ba2cc2fc45a">Пресет создания сохранён</phrase>
|
||||
<phrase data="runtime_c78364c5d0f27706">Б</phrase>
|
||||
<phrase data="runtime_cdd02bd8b5684812">Редирект удалён</phrase>
|
||||
<phrase data="runtime_d92fff560eb5495e">л</phrase>
|
||||
<phrase data="runtime_dd48804aa38b03e1">Пресет удалён</phrase>
|
||||
<phrase data="runtime_dd591284d827e17f">ж</phrase>
|
||||
<phrase data="runtime_e1c88af54b9fced2">Документ #</phrase>
|
||||
<phrase data="runtime_e35c8bb151d3530d">Редирект сохранён</phrase>
|
||||
<phrase data="runtime_e93c3d6287d39cc3">API-токен создан</phrase>
|
||||
<phrase data="runtime_ee71f8405f87d786">э</phrase>
|
||||
<phrase data="runtime_eead37caf818d0fa">и</phrase>
|
||||
<phrase data="runtime_f4019994d4e75a2a">после сохранения</phrase>
|
||||
<phrase data="runtime_f9a506077fa0d961">Новый</phrase>
|
||||
<phrase data="runtime_fff8b34d92dab340">Шаблон</phrase>
|
||||
<phrase data="auto_f8af664b7ab2ef75">Удалить все элементы?</phrase>
|
||||
<phrase data="auto_10403ca9b11a0b49">После сохранения элементы исчезнут из документа. Неиспользуемые файлы можно будет восстановить из корзины медиа.</phrase>
|
||||
<phrase data="auto_5ede041b60096d58">Удалить изображение?</phrase>
|
||||
<phrase data="auto_c5b3cd090f16f479">После сохранения изображение исчезнет из документа. Неиспользуемый файл можно будет восстановить из корзины медиа.</phrase>
|
||||
<phrase data="auto_e26c8664b60bfa9a">Удалить изображение из документа?</phrase>
|
||||
<phrase data="auto_02e536f7b2d5cec8">Документ сохранён, но часть старых файлов не перемещена в корзину.</phrase>
|
||||
</language>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `{{prefix}}_documents`
|
||||
ADD COLUMN IF NOT EXISTS `document_is_technical` TINYINT UNSIGNED NOT NULL DEFAULT 0 AFTER `document_in_search`,
|
||||
ADD COLUMN IF NOT EXISTS `document_in_sitemap` TINYINT UNSIGNED NOT NULL DEFAULT 1 AFTER `document_sitemap_pr`,
|
||||
ADD KEY IF NOT EXISTS `idx_public_visibility` (`document_is_technical`,`document_in_sitemap`);
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'documents',
|
||||
'name' => 'Документы',
|
||||
'version' => '0.3.9',
|
||||
'version' => '0.4.2',
|
||||
|
||||
'permissions' => array(
|
||||
'key' => 'documents',
|
||||
@@ -131,6 +131,7 @@
|
||||
array('id' => '014_document_relation_edges', 'file' => 'migrations/014_document_relation_edges.sql'),
|
||||
array('id' => '015_document_creation_presets', 'file' => 'migrations/015_document_creation_presets.sql'),
|
||||
array('id' => '016_reconcile_document_content_columns', 'file' => 'migrations/016_reconcile_document_content_columns.php'),
|
||||
array('id' => '017_document_public_visibility', 'file' => 'migrations/017_document_public_visibility.sql'),
|
||||
),
|
||||
|
||||
'view_globals' => array(
|
||||
|
||||
@@ -79,9 +79,19 @@
|
||||
<option value="clear">Очистить значение</option>
|
||||
<option value="replace">Найти и заменить</option>
|
||||
<option value="move">Перенести в другую рубрику</option>
|
||||
<option value="publish">Опубликовать</option>
|
||||
<option value="unpublish">Снять с публикации</option>
|
||||
<option value="recalculate">Пересчитать поля и индексы</option>
|
||||
<option value="publish">Опубликовать</option>
|
||||
<option value="unpublish">Снять с публикации</option>
|
||||
<option value="technical">Сделать служебными</option>
|
||||
<option value="public">Сделать обычными</option>
|
||||
<option value="search_include">Включить во внутренний поиск</option>
|
||||
<option value="search_exclude">Исключить из внутреннего поиска</option>
|
||||
<option value="robots_index">Разрешить индексацию поисковиками</option>
|
||||
<option value="robots_noindex">Запретить индексацию поисковиками</option>
|
||||
<option value="sitemap_include">Добавить в sitemap</option>
|
||||
<option value="sitemap_exclude">Исключить из sitemap</option>
|
||||
<option value="sitemap_frequency">Изменить частоту sitemap</option>
|
||||
<option value="sitemap_priority">Изменить приоритет sitemap</option>
|
||||
<option value="recalculate">Пересчитать поля и индексы</option>
|
||||
</select></label>
|
||||
|
||||
<label class="field" data-bulk-target-wrap hidden><span>Поле</span><select class="select" name="target" data-bulk-target>
|
||||
@@ -96,16 +106,25 @@
|
||||
<label class="field" data-bulk-search-wrap hidden><span>Что найти</span><input class="input" type="text" name="search" placeholder="Точное вхождение"></label>
|
||||
<label class="field documents-bulk-value" data-bulk-value-wrap hidden><span>Новое значение</span><textarea class="textarea" name="value" rows="3" placeholder="Введите значение"></textarea></label>
|
||||
|
||||
<label class="field" data-bulk-rubric-target-wrap hidden><span>Новая рубрика</span><select class="select" name="target_rubric_id">
|
||||
<label class="field" data-bulk-rubric-target-wrap hidden><span>Новая рубрика</span><select class="select" name="target_rubric_id">
|
||||
<option value="0">Выберите рубрику</option>
|
||||
{% for rubric in bulk_options.rubrics %}
|
||||
<option value="{{ rubric.Id }}">{{ rubric.rubric_title }}</option>
|
||||
{% endfor %}
|
||||
</select></label>
|
||||
</select></label>
|
||||
|
||||
<label class="field" data-bulk-sitemap-frequency-wrap hidden><span>Частота обновления</span><select class="select" name="sitemap_frequency">
|
||||
<option value="0">always</option><option value="1">hourly</option><option value="2">daily</option>
|
||||
<option value="3" selected>weekly</option><option value="4">monthly</option><option value="5">yearly</option><option value="6">never</option>
|
||||
</select></label>
|
||||
<label class="field" data-bulk-sitemap-priority-wrap hidden><span>Приоритет</span><select class="select" name="sitemap_priority">
|
||||
{% for pr in [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1] %}<option value="{{ pr }}"{{ pr == 0.5 ? ' selected' : '' }}>{{ pr }}</option>{% endfor %}
|
||||
</select></label>
|
||||
</div>
|
||||
<div class="documents-bulk-target-help" data-bulk-target-help hidden>
|
||||
<div class="documents-bulk-target-help" data-bulk-target-help hidden>
|
||||
<i class="ti ti-info-circle"></i><span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info documents-bulk-technical-help" data-bulk-technical-help hidden><i class="ti ti-tool alert-ic"></i><div><b class="alert-title">Служебный документ не удаляется</b><span>Он остаётся доступен запросам, полям и связям, но не открывается отдельной страницей и автоматически исключается из поиска, sitemap и SEO-проверок.</span></div></div>
|
||||
<div class="documents-bulk-step-footer">
|
||||
<span class="text-secondary"><i class="ti ti-info-circle"></i>Ничего не сохранится до подтверждения результата проверки.</span>
|
||||
<button class="btn btn-primary" type="submit" data-bulk-preview><i class="ti ti-eye"></i>Проверить изменения</button>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
</div>
|
||||
|
||||
<div class="documents-editor-workspace" data-document-workspace data-active-panel="main" data-catalog-mode="{{ catalog_mode ? '1' : '0' }}" data-is-new="{{ is_new ? '1' : '0' }}">
|
||||
{% if catalog_mode and product_context_links %}{% include '@catalog/_context-links.twig' with {context_links: product_context_links} %}{% endif %}
|
||||
<div class="documents-workspace-navigation">
|
||||
<div class="tabs documents-workspace-tabs" role="tablist" aria-label="{{ catalog_mode ? 'Разделы товара' : 'Разделы документа' }}">
|
||||
<button class="tab is-active" type="button" role="tab" aria-selected="true" data-document-workspace-tab="main"><i class="ti ti-file-description"></i>Основное</button>
|
||||
@@ -301,7 +302,10 @@
|
||||
<div class="documents-card-body">
|
||||
<div class="form-grid">
|
||||
<label class="field col-6"><span class="field-label">Статус</span><select class="select" name="document_status"><option value="1"{{ document.document_status == 1 ? ' selected' : '' }}>Опубликован</option><option value="0"{{ document.document_status == 0 ? ' selected' : '' }}>Черновик</option></select></label>
|
||||
<label class="field col-6"><span class="field-label">Поиск</span><select class="select" name="document_in_search"><option value="1"{{ document.document_in_search == 1 ? ' selected' : '' }}>В поиске</option><option value="0"{{ document.document_in_search == 0 ? ' selected' : '' }}>Скрыт</option></select></label>
|
||||
<label class="field col-6"><span class="field-label">Поиск</span><select class="select" name="document_in_search"><option value="1"{{ document.document_in_search == 1 ? ' selected' : '' }}>В поиске</option><option value="0"{{ document.document_in_search == 0 ? ' selected' : '' }}>Скрыт</option></select></label>
|
||||
<label class="field col-6"><span class="field-label">Публичный доступ</span><select class="select" name="document_is_technical"><option value="0"{{ not document.document_is_technical ? ' selected' : '' }}>Обычный документ</option><option value="1"{{ document.document_is_technical ? ' selected' : '' }}>Служебный документ</option></select><span class="field-hint">Служебный документ доступен полям, связям и запросам, но не открывается отдельной страницей.</span></label>
|
||||
<label class="field col-6"><span class="field-label">Sitemap</span><select class="select" name="document_in_sitemap"><option value="1"{{ document.document_in_sitemap ? ' selected' : '' }}>Добавлять</option><option value="0"{{ not document.document_in_sitemap ? ' selected' : '' }}>Не добавлять</option></select></label>
|
||||
{% if document.rubric_is_technical %}<div class="alert alert-info documents-technical-alert col-12"><i class="ti ti-tool alert-ic"></i><div><b class="alert-title">Рубрика служебная</b><span>Публичный URL этого документа скрыт настройкой рубрики независимо от переключателя выше.</span></div></div>{% endif %}
|
||||
<label class="field col-12"><span class="field-label">Автор</span><select class="select" name="document_author_id">{% for author in authors %}<option value="{{ author.id }}"{{ document.document_author_id == author.id ? ' selected' : '' }}>{{ author.name }}{% if not author.active %} · отключён{% endif %}</option>{% endfor %}</select></label>
|
||||
<div class="field col-12">
|
||||
<span class="field-label">Родительский документ</span>
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
<header>
|
||||
{% if can_manage %}<input type="checkbox" value="{{ item.Id }}" data-document-check aria-label="Выбрать документ #{{ item.Id }}">{% endif %}
|
||||
<div class="documents-admin-card-title"><a href="{{ ADMINX_BASE }}/documents/{{ item.Id }}/edit">{{ item.document_title }}</a><small>#{{ item.Id }}</small>{{ document_ui.public_link(item) }}</div>
|
||||
<span class="badge badge-{{ item.state_badge }}">{{ item.state_label }}</span>
|
||||
<span class="badge badge-{{ item.state_badge }}">{{ item.state_label }}</span>{% if item.is_technical_effective %}<span class="badge badge-gray"><i class="ti ti-tool"></i>Служебный</span>{% endif %}
|
||||
</header>
|
||||
<div class="documents-admin-card-body">
|
||||
{% if admin_view.mode == 'custom' %}
|
||||
@@ -237,7 +237,8 @@
|
||||
</td>
|
||||
<td class="documents-cell-state">
|
||||
<div class="documents-badges">
|
||||
<span class="badge badge-{{ item.state_badge }}">{{ item.state_label }}</span>
|
||||
<span class="badge badge-{{ item.state_badge }}">{{ item.state_label }}</span>
|
||||
{% if item.is_technical_effective %}<span class="badge badge-gray"><i class="ti ti-tool"></i>Служебный</span>{% endif %}
|
||||
{% if item.document_in_search %}<span class="badge badge-blue documents-search-flag"><i class="ti ti-search"></i>В поиске</span>{% endif %}
|
||||
{% if item.Id == 1 %}<span class="badge badge-amber">system</span>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -233,6 +233,9 @@
|
||||
border: none;
|
||||
min-height: 260px;
|
||||
}
|
||||
.events-mobile-label {
|
||||
display: none;
|
||||
}
|
||||
.events-table-day td {
|
||||
padding: 9px 14px;
|
||||
background: var(--background-muted);
|
||||
@@ -247,18 +250,147 @@
|
||||
gap: 8px;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.events-summary {
|
||||
display: none;
|
||||
}
|
||||
.events-tabs {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.events-table-wrap {
|
||||
max-height: none;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: visible;
|
||||
}
|
||||
.events-table,
|
||||
.events-table tbody,
|
||||
.events-table tr,
|
||||
.events-table th,
|
||||
.events-table td {
|
||||
.events-table-wrap .table.table-compact.events-table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
table-layout: auto;
|
||||
}
|
||||
.events-table colgroup,
|
||||
.events-table thead {
|
||||
display: none;
|
||||
}
|
||||
.events-table tbody {
|
||||
display: block;
|
||||
}
|
||||
.events-table .events-row {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px 12px;
|
||||
padding: 12px 14px 12px 17px;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
.events-table .events-row:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.events-table .events-row::before {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
bottom: 12px;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
border-radius: 3px;
|
||||
background: var(--event-state-color, var(--gray-300));
|
||||
content: "";
|
||||
}
|
||||
.events-table .events-row td {
|
||||
display: block;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.events-table .events-row.has-open-details {
|
||||
background: var(--background-muted);
|
||||
}
|
||||
.events-row td:first-child::before {
|
||||
display: none;
|
||||
}
|
||||
.events-cell-state {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
.events-cell-time {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
align-self: center;
|
||||
font-size: 11px !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.events-cell-actor,
|
||||
.events-cell-message {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.events-cell-actor {
|
||||
display: flex !important;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
grid-row: 2;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.events-cell-message {
|
||||
grid-row: 3;
|
||||
padding-top: 2px !important;
|
||||
}
|
||||
.events-cell-message .events-message {
|
||||
line-height: 1.45;
|
||||
}
|
||||
.events-cell-message .text-xs {
|
||||
margin-top: 3px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.events-cell-ip {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px !important;
|
||||
}
|
||||
.events-cell-actions {
|
||||
grid-column: 2;
|
||||
grid-row: 4;
|
||||
justify-self: end;
|
||||
}
|
||||
.events-mobile-label {
|
||||
display: inline;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-family);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.events-details-row,
|
||||
.events-details-row td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.events-details-row[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.events-details-row td {
|
||||
padding: 0 14px 12px 17px !important;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
.events-details {
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
.events-table-day,
|
||||
.events-table-day td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.events-table-day td {
|
||||
padding: 8px 14px;
|
||||
}
|
||||
.events-filter,
|
||||
.events-filter .input-wrap,
|
||||
|
||||
@@ -35,9 +35,11 @@
|
||||
if (hidden) {
|
||||
details.removeAttribute('hidden');
|
||||
button.classList.add('active');
|
||||
row.classList.add('has-open-details');
|
||||
} else {
|
||||
details.setAttribute('hidden', 'hidden');
|
||||
button.classList.remove('active');
|
||||
row.classList.remove('has-open-details');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'events',
|
||||
'name' => 'Системные события',
|
||||
'version' => '0.1.1',
|
||||
'version' => '0.1.3',
|
||||
|
||||
'permissions' => array(
|
||||
'key' => 'events',
|
||||
@@ -49,7 +49,7 @@
|
||||
'icon' => 'ti ti-history',
|
||||
'permission' => 'view_events',
|
||||
'group' => 'Система',
|
||||
'sort_order' => 35,
|
||||
'sort_order' => 99,
|
||||
'match' => array(
|
||||
'/events',
|
||||
),
|
||||
|
||||
@@ -110,15 +110,15 @@
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr class="events-row {{ row.state_class }}">
|
||||
<td class="mono text-muted">{{ row.time_display }}</td>
|
||||
<td><span class="badge {{ row.source_type_badge }} events-source-kind">{{ row.source_type_label }}</span></td>
|
||||
<td><div class="events-referrer-source">{{ row.source_name }}</div></td>
|
||||
<td>
|
||||
<td class="mono text-muted events-cell-time">{{ row.time_display }}</td>
|
||||
<td class="events-cell-state"><span class="badge {{ row.source_type_badge }} events-source-kind">{{ row.source_type_label }}</span></td>
|
||||
<td class="events-cell-actor"><span class="events-mobile-label">Источник</span><div class="events-referrer-source">{{ row.source_name }}</div></td>
|
||||
<td class="events-cell-message">
|
||||
<div class="events-message mono">{{ row.message }}</div>
|
||||
{% if row.url %}<div class="text-muted text-xs events-referrer-url">{{ row.url }}</div>{% endif %}
|
||||
</td>
|
||||
<td><span class="badge badge-gray events-hit-count">{{ row.hits }}</span></td>
|
||||
<td>
|
||||
<td class="events-cell-ip"><span class="events-mobile-label">Переходы</span><span class="badge badge-gray events-hit-count">{{ row.hits }}</span></td>
|
||||
<td class="events-cell-actions">
|
||||
{% if row.details_text %}
|
||||
<button class="btn btn-ghost btn-icon btn-sm" type="button" data-events-details data-tooltip="Детали" aria-label="Детали"><i class="ti ti-chevron-down"></i></button>
|
||||
{% endif %}
|
||||
@@ -170,15 +170,15 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="events-row {{ row.state_class }}">
|
||||
<td class="mono text-muted">{{ row.time_display }}</td>
|
||||
<td><span class="badge {{ row.state_badge }} events-state"><i class="{{ row.state_icon }}"></i>{{ row.state_label }}</span></td>
|
||||
<td>{{ row.actor }}</td>
|
||||
<td>
|
||||
<td class="mono text-muted events-cell-time">{{ row.time_display }}</td>
|
||||
<td class="events-cell-state"><span class="badge {{ row.state_badge }} events-state"><i class="{{ row.state_icon }}"></i>{{ row.state_label }}</span></td>
|
||||
<td class="events-cell-actor"><span class="events-mobile-label">Пользователь</span>{{ row.actor }}</td>
|
||||
<td class="events-cell-message">
|
||||
<div class="events-message">{{ row.message }}</div>
|
||||
{% if row.url %}<div class="mono text-muted text-xs">{{ row.url }}</div>{% endif %}
|
||||
</td>
|
||||
<td class="mono text-muted">{{ row.ip }}</td>
|
||||
<td>
|
||||
<td class="mono text-muted events-cell-ip">{% if row.ip %}<span class="events-mobile-label">IP</span>{{ row.ip }}{% endif %}</td>
|
||||
<td class="events-cell-actions">
|
||||
{% if row.details_text %}
|
||||
<button class="btn btn-ghost btn-icon btn-sm" type="button" data-events-details data-tooltip="Детали" aria-label="Детали"><i class="ti ti-chevron-down"></i></button>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="auto_0f0b8f55edcc806d">Users</phrase>
|
||||
<phrase data="auto_1240ef2fb91174ca">" will be deleted along with the assignment of rights.</phrase>
|
||||
<phrase data="auto_5ea67f9e32546482">Role "</phrase>
|
||||
<phrase data="auto_721da5d070ce0d16">Failed to delete</phrase>
|
||||
@@ -8,4 +9,5 @@
|
||||
<phrase data="auto_8b064896cc26e067">Delete role?</phrase>
|
||||
<phrase data="auto_c00aca49199296d7">Network error</phrase>
|
||||
<phrase data="auto_e02d4b658f2984f4">New role</phrase>
|
||||
<phrase data="runtime_845595d6e899ecb0">Users</phrase>
|
||||
</language>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="auto_0f0b8f55edcc806d">Пользователи</phrase>
|
||||
<phrase data="auto_1240ef2fb91174ca">» будет удалена вместе с назначением прав.</phrase>
|
||||
<phrase data="auto_5ea67f9e32546482">Роль «</phrase>
|
||||
<phrase data="auto_721da5d070ce0d16">Не удалось удалить</phrase>
|
||||
@@ -8,4 +9,5 @@
|
||||
<phrase data="auto_8b064896cc26e067">Удалить роль?</phrase>
|
||||
<phrase data="auto_c00aca49199296d7">Ошибка сети</phrase>
|
||||
<phrase data="auto_e02d4b658f2984f4">Новая роль</phrase>
|
||||
<phrase data="runtime_845595d6e899ecb0">Пользователи</phrase>
|
||||
</language>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Adminx\Support\ModuleExtensions;
|
||||
use App\Adminx\Support\InterfaceSettings;
|
||||
use App\Common\AdminLocation;
|
||||
use App\Common\ModuleManager;
|
||||
use App\Common\ModuleMigrator;
|
||||
@@ -115,6 +116,11 @@
|
||||
|
||||
if (!empty($item['has_dashboard'])) {
|
||||
ModuleSettings::set('adminx_dashboard_enabled', (bool) $dashboardEnabled, $code, 'bool');
|
||||
InterfaceSettings::setModuleDashboardVisibility(
|
||||
$code,
|
||||
(bool) $dashboardEnabled,
|
||||
ModuleExtensions::dashboardDefinitions()
|
||||
);
|
||||
}
|
||||
|
||||
return self::one($code);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
return array(
|
||||
'code' => 'modules',
|
||||
'name' => 'Модули',
|
||||
'version' => '0.2.0',
|
||||
'version' => '0.2.1',
|
||||
|
||||
'settings' => array(
|
||||
'repository_enabled' => array('label' => 'Удалённый каталог модулей', 'type' => 'bool', 'default' => false, 'group' => 'modules_repository'),
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
<phrase data="auto_11609834a799e96c">editing</phrase>
|
||||
<phrase data="auto_141924f7e6e6cff8">Menu item</phrase>
|
||||
<phrase data="auto_1a4a85e95ad35531">Menu structure</phrase>
|
||||
<phrase data="auto_1e9b9882dbfae49a">Access groups</phrase>
|
||||
<phrase data="auto_22b85f14d5be7427">Drag item</phrase>
|
||||
<phrase data="auto_29d2f0e6febdd8f5">Check item fields</phrase>
|
||||
<phrase data="auto_2b7bc3187b5e647a">Unfolding</phrase>
|
||||
<phrase data="auto_32b74a3c47908c1e">Untitled</phrase>
|
||||
<phrase data="auto_3697bef0b3fc93f5">new</phrase>
|
||||
<phrase data="auto_3757f7f85265ba50">Menu root</phrase>
|
||||
<phrase data="auto_42039dbdd60cceb2">Check the form fields</phrase>
|
||||
<phrase data="auto_4297c5c52ad0e7c1">Alias saved</phrase>
|
||||
<phrase data="auto_50fa71be94dbeb13">Level 1</phrase>
|
||||
<phrase data="auto_542e162a598ab71f"><button class="btn btn-ghost btn-icon btn-sm navigation-action-level" type="button" data-navigation-item-indent="1" data-tooltip="Increase level" aria-label="Increase level"><i class="ti ti-indent-increase"></i></button></phrase>
|
||||
<phrase data="auto_650078bec041bcf9">Failed to load item</phrase>
|
||||
<phrase data="auto_68ee41202091a86a">(copy)</phrase>
|
||||
<phrase data="auto_6f6792553955fa39">New menu item</phrase>
|
||||
<phrase data="auto_710b4d61663b2ab5">Drag</phrase>
|
||||
<phrase data="auto_71a2dbf76a91220d">Level 3</phrase>
|
||||
<phrase data="auto_7287413ae60a4181"><button class="btn btn-ghost btn-icon btn-sm navigation-action-level" type="button" data-navigation-item-indent="-1" data-tooltip="Decrease level" aria-label="Decrease level"><i class="ti ti-indent-decrease"></i></button></phrase>
|
||||
<phrase data="auto_73dfa7ea85e8eb75">Alias</phrase>
|
||||
<phrase data="auto_7e8692a57c2aa378">groups</phrase>
|
||||
@@ -35,6 +39,7 @@
|
||||
<phrase data="auto_c2b70c3bb20678a7">Failed to apply filters</phrase>
|
||||
<phrase data="auto_c610cbf449544098">not selected</phrase>
|
||||
<phrase data="auto_c6a433696290fb08">Select at least one group</phrase>
|
||||
<phrase data="auto_cd0b995b4ac664c4">Level 2</phrase>
|
||||
<phrase data="auto_d09bf6d4c36412be">" data-tooltip="On/Off" aria-label="On/Off"><i class="ti ti-power"></i></button></phrase>
|
||||
<phrase data="auto_d32b8258ca6f9d3e">" data-tooltip="Change" aria-label="Change"><i class="ti ti-pencil"></i></button></phrase>
|
||||
<phrase data="auto_d7d9310af67c647b">Select image</phrase>
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
<phrase data="auto_11609834a799e96c">редактирование</phrase>
|
||||
<phrase data="auto_141924f7e6e6cff8">Пункт меню</phrase>
|
||||
<phrase data="auto_1a4a85e95ad35531">Структура меню</phrase>
|
||||
<phrase data="auto_1e9b9882dbfae49a">Группы доступа</phrase>
|
||||
<phrase data="auto_22b85f14d5be7427">Перетащить пункт</phrase>
|
||||
<phrase data="auto_29d2f0e6febdd8f5">Проверьте поля пункта</phrase>
|
||||
<phrase data="auto_2b7bc3187b5e647a">Разворачивание</phrase>
|
||||
<phrase data="auto_32b74a3c47908c1e">Без названия</phrase>
|
||||
<phrase data="auto_3697bef0b3fc93f5">новый</phrase>
|
||||
<phrase data="auto_3757f7f85265ba50">Корень меню</phrase>
|
||||
<phrase data="auto_42039dbdd60cceb2">Проверьте поля формы</phrase>
|
||||
<phrase data="auto_4297c5c52ad0e7c1">Алиас сохранён</phrase>
|
||||
<phrase data="auto_50fa71be94dbeb13">Уровень 1</phrase>
|
||||
<phrase data="auto_542e162a598ab71f"><button class="btn btn-ghost btn-icon btn-sm navigation-action-level" type="button" data-navigation-item-indent="1" data-tooltip="Увеличить уровень" aria-label="Увеличить уровень"><i class="ti ti-indent-increase"></i></button></phrase>
|
||||
<phrase data="auto_650078bec041bcf9">Не удалось загрузить пункт</phrase>
|
||||
<phrase data="auto_68ee41202091a86a">(копия)</phrase>
|
||||
<phrase data="auto_6f6792553955fa39">Новый пункт меню</phrase>
|
||||
<phrase data="auto_710b4d61663b2ab5">Перетащить</phrase>
|
||||
<phrase data="auto_71a2dbf76a91220d">Уровень 3</phrase>
|
||||
<phrase data="auto_7287413ae60a4181"><button class="btn btn-ghost btn-icon btn-sm navigation-action-level" type="button" data-navigation-item-indent="-1" data-tooltip="Уменьшить уровень" aria-label="Уменьшить уровень"><i class="ti ti-indent-decrease"></i></button></phrase>
|
||||
<phrase data="auto_73dfa7ea85e8eb75">Алиас</phrase>
|
||||
<phrase data="auto_7e8692a57c2aa378">группы</phrase>
|
||||
@@ -35,6 +39,7 @@
|
||||
<phrase data="auto_c2b70c3bb20678a7">Не удалось применить фильтры</phrase>
|
||||
<phrase data="auto_c610cbf449544098">не выбран</phrase>
|
||||
<phrase data="auto_c6a433696290fb08">Выберите хотя бы одну группу</phrase>
|
||||
<phrase data="auto_cd0b995b4ac664c4">Уровень 2</phrase>
|
||||
<phrase data="auto_d09bf6d4c36412be">" data-tooltip="Вкл/выкл" aria-label="Вкл/выкл"><i class="ti ti-power"></i></button></phrase>
|
||||
<phrase data="auto_d32b8258ca6f9d3e">" data-tooltip="Изменить" aria-label="Изменить"><i class="ti ti-pencil"></i></button></phrase>
|
||||
<phrase data="auto_d7d9310af67c647b">Выбрать изображение</phrase>
|
||||
|
||||
@@ -92,6 +92,12 @@
|
||||
self.save().catch(function () {});
|
||||
});
|
||||
setTimeout(function () { self.bindEditors(); }, 120);
|
||||
this.openFromLocation();
|
||||
},
|
||||
|
||||
openFromLocation: function () {
|
||||
var id = Number(new URLSearchParams(window.location.search).get('edit'));
|
||||
if (Number.isInteger(id) && id > 0) { this.open(id); }
|
||||
},
|
||||
|
||||
base: function () { return this.form.getAttribute('data-base') || ''; },
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'public_site',
|
||||
'name' => 'Публичный сайт',
|
||||
'version' => '0.5.2',
|
||||
'version' => '0.5.3',
|
||||
'permissions' => array(
|
||||
'key' => 'public_site',
|
||||
'items' => array(
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="auto_032ed2dfb1db0f14">meaning "</phrase>
|
||||
<phrase data="auto_05e7bc6e97af552c">found</phrase>
|
||||
<phrase data="auto_07577dba507e4ec0">For each request, the complete Legacy sequence will be checked against the ID in ascending and descending order. Native will only be enabled if there is an exact match; any discrepancy will automatically cancel the change.</phrase>
|
||||
<phrase data="auto_08cb0aef9082fa66">specify a constant</phrase>
|
||||
<phrase data="auto_0b90dcb0d77c2aeb">Order verified</phrase>
|
||||
<phrase data="auto_0c0d58a11ba9633a">contains</phrase>
|
||||
<phrase data="auto_0e18a76b455f27f5">Random order is used without other levels</phrase>
|
||||
<phrase data="auto_0fd261036ee38c89">Document</phrase>
|
||||
<phrase data="auto_104e36078b0fb18a">one of the parameter values "</phrase>
|
||||
<phrase data="auto_1199e46110c703d0">Checking requests</phrase>
|
||||
<phrase data="auto_15782150a0e84091"><div class="request-explain-options-state"><i class="ti ti-loader-2"></i>Search...</div></phrase>
|
||||
<phrase data="auto_189d15bc7eec18dd">Comparison</phrase>
|
||||
<phrase data="auto_19114f713f604d3e">Material</phrase>
|
||||
<phrase data="auto_1c4bc45438abb81c">Field "</phrase>
|
||||
<phrase data="auto_1e1b70b17c42674c">Nothing found</phrase>
|
||||
<phrase data="auto_1f1437928ef434ed">Delete condition?</phrase>
|
||||
<phrase data="auto_27cb45d3018b58de"><div class="request-explain-tree-empty">There are no active conditions in the group</div></phrase>
|
||||
<phrase data="auto_27d21beea42d7951">less</phrase>
|
||||
<phrase data="auto_28eebd6e05bfd532">requests in the list.</phrase>
|
||||
<phrase data="auto_2c6cbba7ca20d02c">And</phrase>
|
||||
<phrase data="auto_2f8f4ee43113f5e0">greater than or equal to</phrase>
|
||||
<phrase data="auto_31f574d9b034389c">Random order</phrase>
|
||||
<phrase data="auto_32b74a3c47908c1e">Untitled</phrase>
|
||||
<phrase data="auto_33424a86f54e6100">parameter value "</phrase>
|
||||
<phrase data="auto_351bdca36f04479b">» filled</phrase>
|
||||
<phrase data="auto_359282f9ef75e654">Document ID</phrase>
|
||||
<phrase data="auto_378361e0be144aac">starts with</phrase>
|
||||
<phrase data="auto_389de083564f342a"><div class="request-explain-options-state"><i class="ti ti-file-off"></i>Documents not found</div></phrase>
|
||||
<phrase data="auto_3bc36e9ea4a0955a">· deleted</phrase>
|
||||
<phrase data="auto_4ae50d30739d5840">Close</phrase>
|
||||
<phrase data="auto_4b6f3241a7b5a788">. An empty parameter does not participate in filtering.</phrase>
|
||||
<phrase data="auto_4c2932b20bd66231">Boolean value</phrase>
|
||||
<phrase data="auto_4c386bc752f0e6df">[structured data]</phrase>
|
||||
<phrase data="auto_4e68100dbb3f79b4">Cache, integrations, compatibility, result contract and Native executor.</phrase>
|
||||
<phrase data="auto_4e8f0c3a1fc7e9ad">Explain the result</phrase>
|
||||
<phrase data="auto_537aca486e1ad692">The order is not fixed</phrase>
|
||||
<phrase data="auto_5a5b2e3257454e31">» in the format |value|</phrase>
|
||||
<phrase data="auto_5b1c65c008232b00">Terms</phrase>
|
||||
<phrase data="auto_5b9a18a54f30b067">Reinforce the order without changing the output?</phrase>
|
||||
<phrase data="auto_5c196ee8576211db">Request</phrase>
|
||||
<phrase data="auto_5c879d95515a61f9">less than or equal to</phrase>
|
||||
<phrase data="auto_5e09daddaaae2f28">active conditions</phrase>
|
||||
<phrase data="auto_5e5ccbc2d58c978f">No documents found</phrase>
|
||||
<phrase data="auto_5e6ded00f244387d">Verification failed</phrase>
|
||||
<phrase data="auto_5e70abc6833fb7dc">included in the list</phrase>
|
||||
<phrase data="auto_5e8acb5405d7c6b6">Reinforce order</phrase>
|
||||
<phrase data="auto_5f8e0e29375c5a82">Requests</phrase>
|
||||
<phrase data="auto_60b8ce4b9f94a2f2">checked by free condition</phrase>
|
||||
<phrase data="auto_60ea15b1d4c0f5dc"><span class="request-explain-condition-state"><i class="ti"></i></span><div class="request-explain-condition-copy"><div><b></b><code></code></div><dl><div><dt>Actually</dt><dd data-actual></dd></div><div><dt>Condition</dt><dd data-expected></dd></div></dl><p hidden></p></div></phrase>
|
||||
<phrase data="auto_61f6bda106d823f8">matches one of the values</phrase>
|
||||
<phrase data="auto_61f764b6b199e9de">Remains Legacy</phrase>
|
||||
<phrase data="auto_62fc87d293033233">· extra IDs:</phrase>
|
||||
<phrase data="auto_63aff46d2fcad83f">is compared with the value</phrase>
|
||||
<phrase data="auto_65706e55e18054d0">Any element inside is enough</phrase>
|
||||
<phrase data="auto_66809f536a0e52de">equals</phrase>
|
||||
<phrase data="auto_686b2c5053cc58b8">Query conditions and actual result of executor.</phrase>
|
||||
<phrase data="auto_6f4a8d79ac8c9786">Validation error</phrase>
|
||||
<phrase data="auto_714f3223440dbb5f">range min/max from the parameter "</phrase>
|
||||
<phrase data="auto_72aecd9ad85642d8">Error</phrase>
|
||||
<phrase data="auto_78142342bc327ad4">Basic parameters, conditions and presentation of the result.</phrase>
|
||||
<phrase data="auto_7a46f37072ba9ad5">not included in the list</phrase>
|
||||
<phrase data="auto_7aa95369282ead6c">groups</phrase>
|
||||
<phrase data="auto_7c3834203266fab3">· withdrawn from publication</phrase>
|
||||
<phrase data="auto_7edf68dad8afcc29">The order is different</phrase>
|
||||
<phrase data="auto_7f0369a76d750f0a">Native confirmed</phrase>
|
||||
<phrase data="auto_804cf21f365ef47b"><i class="ti ti-loader-2"></i><span>Checking the request...</span></phrase>
|
||||
<phrase data="auto_851f9c331eaf483e">One coincidence is enough</phrase>
|
||||
<phrase data="auto_86ea33aef5e95d4f">Delete</phrase>
|
||||
<phrase data="auto_87451e618697c593">", when the parameter "</phrase>
|
||||
<phrase data="auto_8d2fab2d123f89ab">Yes</phrase>
|
||||
<phrase data="auto_92d3ae9f63c77ac5">· no ID:</phrase>
|
||||
<phrase data="auto_947023ee6188249c">Preparation</phrase>
|
||||
<phrase data="auto_9540ae80e12ace33">Shuffle cannot be combined with other levels</phrase>
|
||||
<phrase data="auto_96d88e8e31201094">All available fields have already been added</phrase>
|
||||
<phrase data="auto_97ab80304c3f0564">Enable Confirmed</phrase>
|
||||
<phrase data="auto_97b8648c721d2183">Ready</phrase>
|
||||
<phrase data="auto_9b52dbd7737373bd">Post verification</phrase>
|
||||
<phrase data="auto_9c8243db16c84bef">fixed value</phrase>
|
||||
<phrase data="auto_a4588dfa770d10c3">Test results</phrase>
|
||||
<phrase data="auto_a480e18f45a4e204">Saved PHP code</phrase>
|
||||
<phrase data="auto_a4d765aa97521f40">does not contain</phrase>
|
||||
<phrase data="auto_ad0995eca44142c4">the result of the saved PHP code.</phrase>
|
||||
<phrase data="auto_ae8e1a46739261fa">parameter value "</phrase>
|
||||
<phrase data="auto_b1a5a8acb3ed7df1">Request verified</phrase>
|
||||
<phrase data="auto_b22f4c76befc0be0">parameter</phrase>
|
||||
<phrase data="auto_b58e8d5d7248d75f">decimal number from parameter "</phrase>
|
||||
<phrase data="auto_b5d52a68fc7c1aca">Requires Legacy</phrase>
|
||||
<phrase data="auto_beed168817322eeb">from</phrase>
|
||||
<phrase data="auto_c00aca49199296d7">Network error</phrase>
|
||||
<phrase data="auto_c139a7f87025424d">Position</phrase>
|
||||
<phrase data="auto_c34624da0ccdeeec">no changes:</phrase>
|
||||
<phrase data="auto_c5439a1ef48ca614">Scan stopped</phrase>
|
||||
<phrase data="auto_c8de745114264843">Explain</phrase>
|
||||
<phrase data="auto_cc5462f639836db2">Final order by ID has already been added</phrase>
|
||||
<phrase data="auto_d20c1341aa2d75f5">Results vary</phrase>
|
||||
<phrase data="auto_d2881051aa8668f1">doesn't start with</phrase>
|
||||
<phrase data="auto_d39886d5753cc067">· checked states:</phrase>
|
||||
<phrase data="auto_d52c691335394be0">an integer from the parameter "</phrase>
|
||||
<phrase data="auto_dbfb65b02e39f8f7">not equal</phrase>
|
||||
<phrase data="auto_dd02bc21dabc217c">There are no queries with loose order</phrase>
|
||||
<phrase data="auto_de0428ac4f69cb1d">All elements must match</phrase>
|
||||
<phrase data="auto_df119811d4b0283b">Found:</phrase>
|
||||
<phrase data="auto_e0b0e0fb72dfdc42">The order is fixed:</phrase>
|
||||
<phrase data="auto_e0b8fe4f27c488e1">Failed to upload documents</phrase>
|
||||
<phrase data="auto_e0f1201ba77617e7">No explicit sorting</phrase>
|
||||
<phrase data="auto_e2ec03f1e750f62b">This field is already being sorted</phrase>
|
||||
<phrase data="auto_e2fbce9d8a1f491f">Validation failed</phrase>
|
||||
<phrase data="auto_e3c6d93632cf386b">Find document</phrase>
|
||||
<phrase data="auto_e43ee32b856aa90a">We are checking the request...</phrase>
|
||||
<phrase data="auto_e939dbf27072b380">more</phrase>
|
||||
<phrase data="auto_e9b76330e794cf78">Matches</phrase>
|
||||
<phrase data="auto_ea84a51a0b3a4cdc">Document verification</phrase>
|
||||
<phrase data="auto_edf1164e6de24189">Return Legacy</phrase>
|
||||
<phrase data="auto_ee1a08bdb4da0f24">Network error when checking document</phrase>
|
||||
<phrase data="auto_eec268a8aa03da99">OR</phrase>
|
||||
<phrase data="auto_efb923a146075185">Need manual verification</phrase>
|
||||
<phrase data="auto_eff79c40e2100ae5">In progress</phrase>
|
||||
<phrase data="auto_f0dff5ab4a669f7a">Saved</phrase>
|
||||
<phrase data="auto_f285f61cdca7dc54"><div class="request-explain-options-state is-error"><i class="ti ti-alert-circle"></i>Search unavailable</div></phrase>
|
||||
<phrase data="auto_f49d62eb4ff9605b">selected field</phrase>
|
||||
<phrase data="auto_f4f8afbe303728f1">You can add up to eight levels</phrase>
|
||||
<phrase data="auto_f554d87de9a7370d">All elements inside must be executed</phrase>
|
||||
<phrase data="auto_f5edf19efa035287">Check and secure</phrase>
|
||||
<phrase data="auto_f6f3a1a1ff2cd5b6"><i class="ti ti-filter-off"></i><b>Documents not found</b><span>Check the conditions, publication status and selected category.</span></phrase>
|
||||
<phrase data="auto_f82a8219414c7c8e">No</phrase>
|
||||
<phrase data="auto_fd2566032f8228a3">Comparison value</phrase>
|
||||
<phrase data="runtime_04d278fefcfa9d3c">Check the fields</phrase>
|
||||
<phrase data="runtime_2fe8f2fd72439d26">Saved</phrase>
|
||||
<phrase data="runtime_6087165c7ca7a986">No explicit sorting</phrase>
|
||||
<phrase data="runtime_6e42b31a32562d11">Preview updated</phrase>
|
||||
<phrase data="runtime_85011fce4419ee63">Field #</phrase>
|
||||
<phrase data="runtime_8f88b95463e29667">Random order</phrase>
|
||||
<phrase data="runtime_cef09ed8cdc5809d">Request verified</phrase>
|
||||
</language>
|
||||
@@ -1,136 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<language>
|
||||
<phrase data="auto_032ed2dfb1db0f14">значением «</phrase>
|
||||
<phrase data="auto_05e7bc6e97af552c">найдено</phrase>
|
||||
<phrase data="auto_07577dba507e4ec0">Для каждого запроса полная Legacy-последовательность будет сверена с ID по возрастанию и убыванию. Native включится только при точном совпадении; любое расхождение автоматически отменит изменение.</phrase>
|
||||
<phrase data="auto_08cb0aef9082fa66">укажите константу</phrase>
|
||||
<phrase data="auto_0b90dcb0d77c2aeb">Порядок проверен</phrase>
|
||||
<phrase data="auto_0c0d58a11ba9633a">содержит</phrase>
|
||||
<phrase data="auto_0e18a76b455f27f5">Случайный порядок используется без других уровней</phrase>
|
||||
<phrase data="auto_0fd261036ee38c89">Документ</phrase>
|
||||
<phrase data="auto_104e36078b0fb18a">одним из значений параметра «</phrase>
|
||||
<phrase data="auto_1199e46110c703d0">Проверка запросов</phrase>
|
||||
<phrase data="auto_15782150a0e84091"><div class="request-explain-options-state"><i class="ti ti-loader-2"></i>Поиск…</div></phrase>
|
||||
<phrase data="auto_189d15bc7eec18dd">Сравнение</phrase>
|
||||
<phrase data="auto_19114f713f604d3e">Материал</phrase>
|
||||
<phrase data="auto_1c4bc45438abb81c">Поле «</phrase>
|
||||
<phrase data="auto_1e1b70b17c42674c">Ничего не найдено</phrase>
|
||||
<phrase data="auto_1f1437928ef434ed">Удалить условие?</phrase>
|
||||
<phrase data="auto_27cb45d3018b58de"><div class="request-explain-tree-empty">В группе нет активных условий</div></phrase>
|
||||
<phrase data="auto_27d21beea42d7951">меньше</phrase>
|
||||
<phrase data="auto_28eebd6e05bfd532">запросов в списке.</phrase>
|
||||
<phrase data="auto_2c6cbba7ca20d02c">И</phrase>
|
||||
<phrase data="auto_2f8f4ee43113f5e0">больше или равно</phrase>
|
||||
<phrase data="auto_31f574d9b034389c">Случайный порядок</phrase>
|
||||
<phrase data="auto_32b74a3c47908c1e">Без названия</phrase>
|
||||
<phrase data="auto_33424a86f54e6100">значением параметра «</phrase>
|
||||
<phrase data="auto_351bdca36f04479b">» заполнен</phrase>
|
||||
<phrase data="auto_359282f9ef75e654">ID документа</phrase>
|
||||
<phrase data="auto_378361e0be144aac">начинается с</phrase>
|
||||
<phrase data="auto_389de083564f342a"><div class="request-explain-options-state"><i class="ti ti-file-off"></i>Документы не найдены</div></phrase>
|
||||
<phrase data="auto_3bc36e9ea4a0955a">· удалён</phrase>
|
||||
<phrase data="auto_4ae50d30739d5840">Закрыть</phrase>
|
||||
<phrase data="auto_4b6f3241a7b5a788">. Пустой параметр не участвует в фильтрации.</phrase>
|
||||
<phrase data="auto_4c2932b20bd66231">Логическое значение</phrase>
|
||||
<phrase data="auto_4c386bc752f0e6df">[структурированные данные]</phrase>
|
||||
<phrase data="auto_4e68100dbb3f79b4">Кеш, интеграции, совместимость, контракт результата и Native executor.</phrase>
|
||||
<phrase data="auto_4e8f0c3a1fc7e9ad">Объяснить результат</phrase>
|
||||
<phrase data="auto_537aca486e1ad692">Порядок не закреплён</phrase>
|
||||
<phrase data="auto_5a5b2e3257454e31">» в формате |значение|</phrase>
|
||||
<phrase data="auto_5b1c65c008232b00">Условия</phrase>
|
||||
<phrase data="auto_5b9a18a54f30b067">Закрепить порядок без изменения выдачи?</phrase>
|
||||
<phrase data="auto_5c196ee8576211db">Запрос</phrase>
|
||||
<phrase data="auto_5c879d95515a61f9">меньше или равно</phrase>
|
||||
<phrase data="auto_5e09daddaaae2f28">активных условий</phrase>
|
||||
<phrase data="auto_5e5ccbc2d58c978f">Документы не найдены</phrase>
|
||||
<phrase data="auto_5e6ded00f244387d">Проверка не выполнена</phrase>
|
||||
<phrase data="auto_5e70abc6833fb7dc">входит в список</phrase>
|
||||
<phrase data="auto_5e8acb5405d7c6b6">Закрепить порядок</phrase>
|
||||
<phrase data="auto_5f8e0e29375c5a82">Запросов</phrase>
|
||||
<phrase data="auto_60b8ce4b9f94a2f2">проверяется свободным условием</phrase>
|
||||
<phrase data="auto_60ea15b1d4c0f5dc"><span class="request-explain-condition-state"><i class="ti"></i></span><div class="request-explain-condition-copy"><div><b></b><code></code></div><dl><div><dt>Фактически</dt><dd data-actual></dd></div><div><dt>Условие</dt><dd data-expected></dd></div></dl><p hidden></p></div></phrase>
|
||||
<phrase data="auto_61f6bda106d823f8">совпадает с одним из значений</phrase>
|
||||
<phrase data="auto_61f764b6b199e9de">Остаётся Legacy</phrase>
|
||||
<phrase data="auto_62fc87d293033233">· лишние ID:</phrase>
|
||||
<phrase data="auto_63aff46d2fcad83f">сравнивается со значением</phrase>
|
||||
<phrase data="auto_65706e55e18054d0">Достаточно любого элемента внутри</phrase>
|
||||
<phrase data="auto_66809f536a0e52de">равно</phrase>
|
||||
<phrase data="auto_686b2c5053cc58b8">Условия запроса и фактический итог executor.</phrase>
|
||||
<phrase data="auto_6f4a8d79ac8c9786">Ошибка проверки</phrase>
|
||||
<phrase data="auto_714f3223440dbb5f">диапазоном min/max из параметра «</phrase>
|
||||
<phrase data="auto_72aecd9ad85642d8">Ошибка</phrase>
|
||||
<phrase data="auto_78142342bc327ad4">Основные параметры, условия и оформление результата.</phrase>
|
||||
<phrase data="auto_7a46f37072ba9ad5">не входит в список</phrase>
|
||||
<phrase data="auto_7aa95369282ead6c">групп</phrase>
|
||||
<phrase data="auto_7c3834203266fab3">· снят с публикации</phrase>
|
||||
<phrase data="auto_7edf68dad8afcc29">Отличается порядок</phrase>
|
||||
<phrase data="auto_7f0369a76d750f0a">Native подтверждён</phrase>
|
||||
<phrase data="auto_804cf21f365ef47b"><i class="ti ti-loader-2"></i><span>Проверяем запрос…</span></phrase>
|
||||
<phrase data="auto_851f9c331eaf483e">Достаточно одного совпадения</phrase>
|
||||
<phrase data="auto_86ea33aef5e95d4f">Удалить</phrase>
|
||||
<phrase data="auto_87451e618697c593">», когда параметр «</phrase>
|
||||
<phrase data="auto_8d2fab2d123f89ab">Да</phrase>
|
||||
<phrase data="auto_92d3ae9f63c77ac5">· нет ID:</phrase>
|
||||
<phrase data="auto_947023ee6188249c">Подготовка</phrase>
|
||||
<phrase data="auto_9540ae80e12ace33">Случайный порядок нельзя сочетать с другими уровнями</phrase>
|
||||
<phrase data="auto_96d88e8e31201094">Все доступные поля уже добавлены</phrase>
|
||||
<phrase data="auto_97ab80304c3f0564">Включить подтверждённые</phrase>
|
||||
<phrase data="auto_97b8648c721d2183">Готов</phrase>
|
||||
<phrase data="auto_9b52dbd7737373bd">Проверка публикации</phrase>
|
||||
<phrase data="auto_9c8243db16c84bef">фиксированное значение</phrase>
|
||||
<phrase data="auto_a4588dfa770d10c3">Результаты проверки</phrase>
|
||||
<phrase data="auto_a480e18f45a4e204">Сохранённый PHP-код</phrase>
|
||||
<phrase data="auto_a4d765aa97521f40">не содержит</phrase>
|
||||
<phrase data="auto_ad0995eca44142c4">результатом сохранённого PHP-кода.</phrase>
|
||||
<phrase data="auto_ae8e1a46739261fa">значение параметра «</phrase>
|
||||
<phrase data="auto_b1a5a8acb3ed7df1">Запрос проверен</phrase>
|
||||
<phrase data="auto_b22f4c76befc0be0">параметр</phrase>
|
||||
<phrase data="auto_b58e8d5d7248d75f">десятичным числом из параметра «</phrase>
|
||||
<phrase data="auto_b5d52a68fc7c1aca">Требуется Legacy</phrase>
|
||||
<phrase data="auto_beed168817322eeb">из</phrase>
|
||||
<phrase data="auto_c00aca49199296d7">Ошибка сети</phrase>
|
||||
<phrase data="auto_c139a7f87025424d">Позиция</phrase>
|
||||
<phrase data="auto_c34624da0ccdeeec">· без изменений:</phrase>
|
||||
<phrase data="auto_c5439a1ef48ca614">Проверка остановлена</phrase>
|
||||
<phrase data="auto_c8de745114264843">Объяснить</phrase>
|
||||
<phrase data="auto_cc5462f639836db2">Финальный порядок по ID уже добавлен</phrase>
|
||||
<phrase data="auto_d20c1341aa2d75f5">Результаты различаются</phrase>
|
||||
<phrase data="auto_d2881051aa8668f1">не начинается с</phrase>
|
||||
<phrase data="auto_d39886d5753cc067">· проверено состояний:</phrase>
|
||||
<phrase data="auto_d52c691335394be0">целым числом из параметра «</phrase>
|
||||
<phrase data="auto_dbfb65b02e39f8f7">не равно</phrase>
|
||||
<phrase data="auto_dd02bc21dabc217c">Запросов с незакреплённым порядком нет</phrase>
|
||||
<phrase data="auto_de0428ac4f69cb1d">Должны совпасть все элементы</phrase>
|
||||
<phrase data="auto_df119811d4b0283b">Найдено:</phrase>
|
||||
<phrase data="auto_e0b0e0fb72dfdc42">Порядок закреплён:</phrase>
|
||||
<phrase data="auto_e0b8fe4f27c488e1">Не удалось загрузить документы</phrase>
|
||||
<phrase data="auto_e0f1201ba77617e7">Без явной сортировки</phrase>
|
||||
<phrase data="auto_e2ec03f1e750f62b">Это поле уже участвует в сортировке</phrase>
|
||||
<phrase data="auto_e2fbce9d8a1f491f">Не удалось выполнить проверку</phrase>
|
||||
<phrase data="auto_e3c6d93632cf386b">Найти документ</phrase>
|
||||
<phrase data="auto_e43ee32b856aa90a">Проверяем запрос…</phrase>
|
||||
<phrase data="auto_e939dbf27072b380">больше</phrase>
|
||||
<phrase data="auto_e9b76330e794cf78">Совпадает</phrase>
|
||||
<phrase data="auto_ea84a51a0b3a4cdc">Проверка документа</phrase>
|
||||
<phrase data="auto_edf1164e6de24189">Вернуть Legacy</phrase>
|
||||
<phrase data="auto_ee1a08bdb4da0f24">Ошибка сети при проверке документа</phrase>
|
||||
<phrase data="auto_eec268a8aa03da99">ИЛИ</phrase>
|
||||
<phrase data="auto_efb923a146075185">Нужна ручная проверка</phrase>
|
||||
<phrase data="auto_eff79c40e2100ae5">Выполняется</phrase>
|
||||
<phrase data="auto_f0dff5ab4a669f7a">Сохранено</phrase>
|
||||
<phrase data="auto_f285f61cdca7dc54"><div class="request-explain-options-state is-error"><i class="ti ti-alert-circle"></i>Поиск недоступен</div></phrase>
|
||||
<phrase data="auto_f49d62eb4ff9605b">выбранное поле</phrase>
|
||||
<phrase data="auto_f4f8afbe303728f1">Можно добавить не больше восьми уровней</phrase>
|
||||
<phrase data="auto_f554d87de9a7370d">Должны выполняться все элементы внутри</phrase>
|
||||
<phrase data="auto_f5edf19efa035287">Проверить и закрепить</phrase>
|
||||
<phrase data="auto_f6f3a1a1ff2cd5b6"><i class="ti ti-filter-off"></i><b>Документы не найдены</b><span>Проверьте условия, статус публикации и выбранную рубрику.</span></phrase>
|
||||
<phrase data="auto_f82a8219414c7c8e">Нет</phrase>
|
||||
<phrase data="auto_fd2566032f8228a3">Значение для сравнения</phrase>
|
||||
<phrase data="runtime_04d278fefcfa9d3c">Проверьте поля</phrase>
|
||||
<phrase data="runtime_2fe8f2fd72439d26">Сохранено</phrase>
|
||||
<phrase data="runtime_6087165c7ca7a986">Без явной сортировки</phrase>
|
||||
<phrase data="runtime_6e42b31a32562d11">Предпросмотр обновлён</phrase>
|
||||
<phrase data="runtime_85011fce4419ee63">Поле #</phrase>
|
||||
<phrase data="runtime_8f88b95463e29667">Случайный порядок</phrase>
|
||||
<phrase data="runtime_cef09ed8cdc5809d">Запрос проверен</phrase>
|
||||
</language>
|
||||
@@ -21,6 +21,8 @@
|
||||
use App\Content\ContentTables;
|
||||
use App\Content\PublicUserTables;
|
||||
use App\Common\AuditLog;
|
||||
use App\Common\Cache;
|
||||
use App\Common\CacheKey;
|
||||
use App\Common\FileCacheInvalidator;
|
||||
use App\Content\Fields\FieldConditionEvaluator;
|
||||
use App\Content\Fields\FieldRegistry;
|
||||
@@ -970,7 +972,8 @@
|
||||
'rubric_title' => trim(isset($input['rubric_title']) ? (string) $input['rubric_title'] : ''),
|
||||
'rubric_alias' => trim(isset($input['rubric_alias']) ? (string) $input['rubric_alias'] : ''),
|
||||
'rubric_template_id' => max(1, (int) (isset($input['rubric_template_id']) ? $input['rubric_template_id'] : 1)),
|
||||
'rubric_docs_active' => !empty($input['rubric_docs_active']) ? 1 : 0,
|
||||
'rubric_docs_active' => !empty($input['rubric_docs_active']) ? 1 : 0,
|
||||
'rubric_is_technical' => !empty($input['rubric_is_technical']) ? 1 : 0,
|
||||
'rubric_meta_gen' => !empty($input['rubric_meta_gen']) ? '1' : '0',
|
||||
'rubric_alias_history' => !empty($input['rubric_alias_history']) ? '1' : '0',
|
||||
'rubric_description' => isset($input['rubric_description']) ? (string) $input['rubric_description'] : '',
|
||||
@@ -1383,6 +1386,7 @@
|
||||
$row['templates_count'] = isset($row['templates_count']) ? (int) $row['templates_count'] : 0;
|
||||
$row['site_template_title'] = isset($row['site_template_title']) ? trim((string) $row['site_template_title']) : '';
|
||||
$row['form_conditions_enabled'] = !empty($row['rubric_form_conditions']);
|
||||
$row['rubric_is_technical'] = !empty($row['rubric_is_technical']) ? 1 : 0;
|
||||
$row['rubric_purpose'] = isset($row['rubric_purpose']) && (string) $row['rubric_purpose'] === 'directory'
|
||||
? 'directory'
|
||||
: 'content';
|
||||
@@ -1524,6 +1528,8 @@
|
||||
protected static function clearRubricCache($rubricId)
|
||||
{
|
||||
FileCacheInvalidator::rubric($rubricId);
|
||||
Cache::forgetTag(CacheKey::tag('search'));
|
||||
Cache::forgetTag(CacheKey::tag('catalog-admin-stats'));
|
||||
}
|
||||
|
||||
protected static function nextPosition($table, $field)
|
||||
|
||||
@@ -72,8 +72,16 @@
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var rubricId = parseInt(params.get('templates'), 10) || 0;
|
||||
var editId = parseInt(params.get('edit'), 10) || 0;
|
||||
var fieldsId = parseInt(params.get('fields'), 10) || 0;
|
||||
var create = params.get('create') === '1';
|
||||
var row;
|
||||
if (fieldsId) {
|
||||
row = document.querySelector('[data-rubric-row][data-id="' + fieldsId + '"]');
|
||||
if (!row) { return; }
|
||||
this.pendingFieldId = parseInt(params.get('field'), 10) || 0;
|
||||
this.openFields(row);
|
||||
return;
|
||||
}
|
||||
if (rubricId) {
|
||||
row = document.querySelector('[data-rubric-row][data-id="' + rubricId + '"]');
|
||||
if (!row) { return; }
|
||||
@@ -636,6 +644,7 @@
|
||||
this.form.reset();
|
||||
this.form.elements.id.value = '';
|
||||
this.form.elements.rubric_template_id.value = '1';
|
||||
if (this.form.elements.rubric_is_technical) { this.form.elements.rubric_is_technical.value = '0'; }
|
||||
var isDirectory = purpose === 'directory';
|
||||
if (this.form.elements.rubric_purpose) {
|
||||
this.form.elements.rubric_purpose.value = isDirectory ? 'directory' : 'content';
|
||||
@@ -681,6 +690,7 @@
|
||||
self.form.elements.rubric_template_id.value = item.rubric_template_id || 1;
|
||||
if (self.form.elements.rubric_template_id.selectedIndex < 0) { self.form.elements.rubric_template_id.selectedIndex = 0; }
|
||||
self.form.elements.rubric_docs_active.value = String(item.rubric_docs_active || 0);
|
||||
if (self.form.elements.rubric_is_technical) { self.form.elements.rubric_is_technical.value = String(item.rubric_is_technical || 0); }
|
||||
self.form.elements.rubric_meta_gen.value = String(item.rubric_meta_gen || 0);
|
||||
self.form.elements.rubric_alias_history.value = String(item.rubric_alias_history || 0);
|
||||
self.form.elements.rubric_description.value = item.rubric_description || '';
|
||||
@@ -1605,6 +1615,7 @@
|
||||
self.renderLinkedFieldSets();
|
||||
self.fillGroupSelect();
|
||||
if (Adminx.Drawer) { Adminx.Drawer.open('rubricFieldsDrawer'); }
|
||||
if (self.pendingFieldId) { var fieldId = self.pendingFieldId; self.pendingFieldId = 0; self.selectBuilderField(fieldId); }
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `{{prefix}}_rubrics`
|
||||
ADD COLUMN IF NOT EXISTS `rubric_is_technical` TINYINT UNSIGNED NOT NULL DEFAULT 0 AFTER `rubric_docs_active`;
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'rubrics',
|
||||
'name' => 'Рубрики и поля',
|
||||
'version' => '0.3.6',
|
||||
'version' => '0.3.10',
|
||||
'field_sets' => \App\Adminx\Rubrics\RubricFieldPresets::definitions(),
|
||||
'hook_definitions' => array(
|
||||
array(
|
||||
@@ -84,6 +84,7 @@
|
||||
array('id' => '014_rubric_purpose', 'file' => 'migrations/014_rubric_purpose.php'),
|
||||
array('id' => '015_merge_directory_permissions', 'file' => 'migrations/015_merge_directory_permissions.php'),
|
||||
array('id' => '016_rubric_trash', 'file' => 'migrations/016_rubric_trash.sql'),
|
||||
array('id' => '017_rubric_public_visibility', 'file' => 'migrations/017_rubric_public_visibility.sql'),
|
||||
),
|
||||
|
||||
'routes' => array(
|
||||
|
||||
@@ -159,7 +159,8 @@
|
||||
{% if rubric.rubric_docs_active == 1 %}<span class="badge badge-green">активна</span>{% else %}<span class="badge badge-gray">выкл</span>{% endif %}
|
||||
{% if rubric.rubric_meta_gen == '1' %}<span class="badge badge-cyan">meta</span>{% endif %}
|
||||
{% if rubric.rubric_alias_history == '1' %}<span class="badge badge-violet">history</span>{% endif %}
|
||||
{% if rubric.rubric_purpose == 'directory' %}<span class="badge badge-cyan">справочник</span>{% endif %}
|
||||
{% if rubric.rubric_purpose == 'directory' %}<span class="badge badge-cyan">справочник</span>{% endif %}
|
||||
{% if rubric.rubric_is_technical %}<span class="badge badge-gray"><i class="ti ti-tool"></i> служебная</span>{% endif %}
|
||||
{% if rubric.Id == 1 %}<span class="badge badge-amber">system</span>{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@@ -390,7 +391,8 @@
|
||||
<div class="rubrics-field-section-body">
|
||||
<div class="form-grid">
|
||||
<label class="field col-6"><span class="field-label">Шаблон сайта</span><select class="select" name="rubric_template_id">{% for t in templates %}<option value="{{ t.Id }}">{{ t.template_title }}</option>{% else %}<option value="1">Основной шаблон</option>{% endfor %}</select></label>
|
||||
<label class="field col-6"><span class="field-label">Документы</span><select class="select" name="rubric_docs_active"><option value="1">Активны</option><option value="0">Скрыты</option></select></label>
|
||||
<label class="field col-6"><span class="field-label">Документы</span><select class="select" name="rubric_docs_active"><option value="1">Активны</option><option value="0">Скрыты</option></select></label>
|
||||
<label class="field col-6"><span class="field-label">Публичный доступ</span><select class="select" name="rubric_is_technical"><option value="0">Обычная рубрика</option><option value="1">Служебная рубрика</option></select><span class="field-hint">Служебные документы работают внутри системы, но их URL возвращают 404; они не попадают в поиск, sitemap и SEO-проверки.</span></label>
|
||||
<label class="field col-6"><span class="field-label">Meta</span><select class="select" name="rubric_meta_gen"><option value="0">Вручную</option><option value="1">Автоматически</option></select></label>
|
||||
<label class="field col-6"><span class="field-label">История алиасов</span><select class="select" name="rubric_alias_history"><option value="0">Выключена</option><option value="1">Включена</option></select></label>
|
||||
</div>
|
||||
@@ -732,7 +734,7 @@
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-rubric-og-default><i class="ti ti-wand"></i>Базовая разметка</button>
|
||||
</div>
|
||||
<div class="rubrics-template-fields">
|
||||
<div class="alert alert-info rubrics-og-help"><span class="icon-tile" style="--tile-bg:var(--cyan-100);--tile-fg:var(--cyan-600)"><i class="ti ti-photo-share"></i></span><div><b>Изображение задаётся полем рубрики</b><p>Откройте палитру тегов, вставьте медиаполе и добавьте формат <code>[img]</code>: <code><meta property="og:image" content="[tag:rfld:photo][img]"></code>.</p></div></div>
|
||||
<div class="alert alert-info rubrics-og-help"><span class="icon-tile" style="--tile-bg:var(--cyan-100);--tile-fg:var(--cyan-600)"><i class="ti ti-photo-share"></i></span><div><b>Изображение задаётся полем рубрики</b><p>Выберите медиаполе в палитре и добавьте после него <code>[img]</code>. Исходник: <code><meta property="og:image" content="[tag:domain][tag:fld:ALIAS][img]"></code>. Версия, вписанная в 800 × 800 пикселей: <code><meta property="og:image" content="[tag:domain][tag:f800x800:[tag:fld:ALIAS][img]]"></code>. Для галереи берётся первое изображение.</p></div></div>
|
||||
{{ ui.code_field('OG-разметка', 'rubric_og_template', 420, '', true) }}
|
||||
<div class="rubrics-og-tags"><span><code>[tag:og:title]</code><small>Заголовок</small></span><span><code>[tag:og:description]</code><small>Meta description</small></span><span><code>[tag:og:url]</code><small>Канонический URL</small></span><span><code>[tag:og:sitename]</code><small>Название сайта</small></span></div>
|
||||
</div>
|
||||
|
||||
@@ -23,16 +23,25 @@
|
||||
use App\Common\IpBlocker;
|
||||
use App\Common\Permission;
|
||||
use App\Helpers\Request;
|
||||
use App\Helpers\Response;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
public function index(array $params = array())
|
||||
{
|
||||
if (!Permission::check('view_ip_blocks')) {
|
||||
Response::forbidden();
|
||||
return '';
|
||||
}
|
||||
|
||||
$type = Request::getStr('type', 'ip') === 'user_agent' ? 'user_agent' : 'ip';
|
||||
$query = Request::getStr('q', '');
|
||||
AdminAssets::addStyle($this->base() . '/modules/Security/assets/security.css', 50);
|
||||
AdminAssets::addScript($this->base() . '/modules/Security/assets/security.js', 50);
|
||||
return $this->render('@security/index.twig', array(
|
||||
'rows' => IpBlocker::all(Request::getStr('q', '')),
|
||||
'q' => Request::getStr('q', ''),
|
||||
'rows' => $type === 'user_agent' ? IpBlocker::allUserAgents($query) : IpBlocker::all($query),
|
||||
'q' => $query,
|
||||
'active_type' => $type,
|
||||
'current_ip' => Request::ip(),
|
||||
'can_manage' => Permission::check('manage_ip_blocks'),
|
||||
));
|
||||
@@ -63,6 +72,41 @@
|
||||
return $this->success('IP-адрес разблокирован', array('redirect' => $this->base() . '/security/ip-blocks'));
|
||||
}
|
||||
|
||||
public function blockUserAgent(array $params = array())
|
||||
{
|
||||
if (($guard = $this->guard()) !== null) { return $guard; }
|
||||
try {
|
||||
$pattern = Request::postStr('pattern', '');
|
||||
$matchType = Request::postStr('match_type', 'contains');
|
||||
$reason = Request::postStr('reason', '');
|
||||
$hours = Request::postInt('hours', 0);
|
||||
$expires = $hours > 0 ? time() + min($hours, 87600) * 3600 : null;
|
||||
$row = IpBlocker::blockUserAgent($pattern, $reason, $expires, Auth::id(), $matchType);
|
||||
AuditLog::record('security.user_agent_blocked', array(
|
||||
'actor_id' => Auth::id(), 'target_type' => 'user_agent', 'target_id' => $pattern,
|
||||
'meta' => array('reason' => $reason, 'expires_at' => $expires, 'match_type' => $matchType),
|
||||
));
|
||||
return $this->success('User-Agent заблокирован', array(
|
||||
'redirect' => $this->base() . '/security/ip-blocks?type=user_agent', 'data' => array('block' => $row),
|
||||
));
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage(), array(), 422);
|
||||
}
|
||||
}
|
||||
|
||||
public function unblockUserAgent(array $params = array())
|
||||
{
|
||||
if (($guard = $this->guard()) !== null) { return $guard; }
|
||||
$id = isset($params['id']) ? (int) $params['id'] : 0;
|
||||
IpBlocker::unblockUserAgent($id);
|
||||
AuditLog::record('security.user_agent_unblocked', array(
|
||||
'actor_id' => Auth::id(), 'target_type' => 'user_agent_block', 'target_id' => $id,
|
||||
));
|
||||
return $this->success('User-Agent разблокирован', array(
|
||||
'redirect' => $this->base() . '/security/ip-blocks?type=user_agent',
|
||||
));
|
||||
}
|
||||
|
||||
protected function guard()
|
||||
{
|
||||
return $this->guardPermission('manage_ip_blocks');
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
.security-tabs {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.security-form-card {
|
||||
padding: 18px;
|
||||
}
|
||||
@@ -17,6 +20,12 @@
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.security-form-card label small {
|
||||
color: var(--text-muted);
|
||||
font-size: 11.5px;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.security-filter {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -29,6 +38,7 @@
|
||||
}
|
||||
.security-table-card .table {
|
||||
width: 100%;
|
||||
min-width: 720px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.security-table-card th:nth-child(1) {
|
||||
@@ -46,6 +56,18 @@
|
||||
.security-table-card td:nth-child(2) {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.security-agent-pattern {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.security-agent-pattern + small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.security-table-card .table-actions {
|
||||
padding-right: 16px;
|
||||
text-align: right;
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
return Adminx.Ajax.post(url, data).then(function (payload) { Adminx.Loader.hide(); Adminx.Ajax.handle(payload); }).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
|
||||
}
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target.closest('[data-ip-block-form]');
|
||||
var form = event.target.closest('[data-ip-block-form], [data-user-agent-block-form]');
|
||||
if (!form) { return; }
|
||||
event.preventDefault(); post(form.action, new FormData(form));
|
||||
});
|
||||
document.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-ip-unblock]');
|
||||
if (!button) { return; }
|
||||
Adminx.Confirm.open({ kind: 'error', title: 'Разблокировать IP?', message: 'Доступ с этого адреса будет восстановлен сразу.', confirmLabel: 'Разблокировать', onConfirm: function () { post(Adminx.base() + '/security/ip-blocks/' + button.dataset.ipUnblock + '/delete'); } });
|
||||
var button = event.target.closest('[data-ip-unblock], [data-user-agent-unblock]');
|
||||
if (!button) { return; }
|
||||
var isAgent = button.hasAttribute('data-user-agent-unblock');
|
||||
var id = isAgent ? button.dataset.userAgentUnblock : button.dataset.ipUnblock;
|
||||
Adminx.Confirm.open({
|
||||
kind: 'error',
|
||||
title: isAgent ? 'Разблокировать User-Agent?' : 'Разблокировать IP?',
|
||||
message: 'Доступ для этого правила будет восстановлен сразу.',
|
||||
confirmLabel: 'Разблокировать',
|
||||
onConfirm: function () { post(Adminx.base() + (isAgent ? '/security/user-agent-blocks/' : '/security/ip-blocks/') + id + '/delete'); }
|
||||
});
|
||||
});
|
||||
})(window, document);
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
<language>
|
||||
<phrase data="auto_c00aca49199296d7">Network error</phrase>
|
||||
<phrase data="auto_d4ab263ae425a030">Unblock</phrase>
|
||||
<phrase data="auto_e5599f37c0609107">Access from this address will be restored immediately.</phrase>
|
||||
<phrase data="auto_ec00504a76e2a497">Unblock IP?</phrase>
|
||||
</language>
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
<language>
|
||||
<phrase data="auto_c00aca49199296d7">Ошибка сети</phrase>
|
||||
<phrase data="auto_d4ab263ae425a030">Разблокировать</phrase>
|
||||
<phrase data="auto_e5599f37c0609107">Доступ с этого адреса будет восстановлен сразу.</phrase>
|
||||
<phrase data="auto_ec00504a76e2a497">Разблокировать IP?</phrase>
|
||||
</language>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file adminx/modules/Security/migrations/002_create_user_agent_blocks.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\Common\DatabaseSchema;
|
||||
use App\Common\SystemTables;
|
||||
|
||||
return function () {
|
||||
$table = SystemTables::table('user_agent_blocks');
|
||||
if (DatabaseSchema::tableExists($table)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
DB::query(
|
||||
'CREATE TABLE `' . $table . '` ('
|
||||
. '`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,'
|
||||
. '`pattern_hash` CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,'
|
||||
. '`pattern` VARCHAR(500) NOT NULL,'
|
||||
. "`match_type` VARCHAR(16) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT 'contains',"
|
||||
. '`reason` VARCHAR(500) NOT NULL DEFAULT \'\','
|
||||
. '`expires_at` INT UNSIGNED NULL,`actor_id` INT UNSIGNED NOT NULL DEFAULT 0,'
|
||||
. '`created_at` INT UNSIGNED NOT NULL,PRIMARY KEY (`id`),'
|
||||
. 'UNIQUE KEY `uq_user_agent_pattern` (`pattern_hash`),KEY `idx_user_agent_expires` (`expires_at`)'
|
||||
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
return 1;
|
||||
};
|
||||
@@ -17,27 +17,27 @@
|
||||
return array(
|
||||
'code' => 'security',
|
||||
'name' => 'Безопасность',
|
||||
'version' => '0.1.0',
|
||||
'version' => '0.2.0',
|
||||
'permissions' => array('key' => 'security', 'items' => array(
|
||||
array(
|
||||
'code' => 'view_ip_blocks',
|
||||
'group_code' => 'navigation',
|
||||
'name' => 'IP-блокировки: просмотр',
|
||||
'description' => 'Просмотр заблокированных IP-адресов.',
|
||||
'name' => 'Блокировки запросов: просмотр',
|
||||
'description' => 'Просмотр заблокированных IP-адресов и User-Agent.',
|
||||
'sort_order' => 10,
|
||||
),
|
||||
array(
|
||||
'code' => 'manage_ip_blocks',
|
||||
'group_code' => 'admin',
|
||||
'name' => 'IP-блокировки: управление',
|
||||
'description' => 'Блокировка и разблокировка IP-адресов.',
|
||||
'name' => 'Блокировки запросов: управление',
|
||||
'description' => 'Блокировка и разблокировка IP-адресов и User-Agent.',
|
||||
'sort_order' => 20,
|
||||
),
|
||||
), 'icon' => 'ti ti-shield-lock', 'priority' => 42),
|
||||
'navigation' => array(
|
||||
array(
|
||||
'code' => 'security_ip_blocks',
|
||||
'label' => 'IP-блокировки',
|
||||
'label' => 'Блокировки запросов',
|
||||
'url' => '/security/ip-blocks',
|
||||
'icon' => 'ti ti-shield-lock',
|
||||
'permission' => 'view_ip_blocks',
|
||||
@@ -51,11 +51,14 @@
|
||||
),
|
||||
'migrations' => array(
|
||||
array('id' => '001_create_ip_blocks', 'file' => 'migrations/001_create_ip_blocks.sql'),
|
||||
array('id' => '002_create_user_agent_blocks', 'file' => 'migrations/002_create_user_agent_blocks.php'),
|
||||
),
|
||||
'routes' => array(
|
||||
array('GET', '/security/ip-blocks', array(\App\Adminx\Security\Controller::class, 'index')),
|
||||
array('POST', '/security/ip-blocks', array(\App\Adminx\Security\Controller::class, 'block')),
|
||||
array('POST', '/security/ip-blocks/{id}/delete', array(\App\Adminx\Security\Controller::class, 'unblock')),
|
||||
array('GET', '/security/ip-blocks', array(\App\Adminx\Security\Controller::class, 'index'), array('permission' => 'view_ip_blocks')),
|
||||
array('POST', '/security/ip-blocks', array(\App\Adminx\Security\Controller::class, 'block'), array('permission' => 'manage_ip_blocks')),
|
||||
array('POST', '/security/ip-blocks/{id}/delete', array(\App\Adminx\Security\Controller::class, 'unblock'), array('permission' => 'manage_ip_blocks')),
|
||||
array('POST', '/security/user-agent-blocks', array(\App\Adminx\Security\Controller::class, 'blockUserAgent'), array('permission' => 'manage_ip_blocks')),
|
||||
array('POST', '/security/user-agent-blocks/{id}/delete', array(\App\Adminx\Security\Controller::class, 'unblockUserAgent'), array('permission' => 'manage_ip_blocks')),
|
||||
),
|
||||
'view_globals' => array('module_code' => 'security'),
|
||||
);
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
{% extends '@adminx/main.twig' %}
|
||||
{% block title %}IP-блокировки{% endblock %}
|
||||
{% block title %}Блокировки запросов{% endblock %}
|
||||
{% block content %}
|
||||
<nav class="breadcrumbs" aria-label="Хлебные крошки"><a href="{{ ADMINX_BASE }}/">Главная</a><i class="ti ti-chevron-right"></i><span>IP-блокировки</span></nav>
|
||||
<div class="page-header"><div><h1>IP-блокировки</h1><p class="text-secondary">Ручные блокировки доступа и автоматическая защита частоты запросов</p></div></div>
|
||||
<nav class="breadcrumbs" aria-label="Хлебные крошки"><a href="{{ ADMINX_BASE }}/">Главная</a><i class="ti ti-chevron-right"></i><span>Блокировки запросов</span></nav>
|
||||
<div class="page-header"><div><h1>Блокировки запросов</h1><p class="text-secondary">Ограничение доступа по IP-адресу или признаку в User-Agent</p></div></div>
|
||||
|
||||
<nav class="tabs security-tabs" aria-label="Тип блокировки">
|
||||
<a class="tab{{ active_type == 'ip' ? ' is-active' : '' }}" href="{{ ADMINX_BASE }}/security/ip-blocks?type=ip"><i class="ti ti-world"></i>IP-адреса</a>
|
||||
<a class="tab{{ active_type == 'user_agent' ? ' is-active' : '' }}" href="{{ ADMINX_BASE }}/security/ip-blocks?type=user_agent"><i class="ti ti-robot-off"></i>User-Agent</a>
|
||||
</nav>
|
||||
|
||||
<div class="security-layout">
|
||||
{% if can_manage %}<section class="card security-form-card">
|
||||
<div class="settings-section-head"><span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti ti-shield-plus"></i></span><div><h2>Заблокировать адрес</h2><p>Текущий адрес: <span class="mono">{{ current_ip }}</span></p></div></div>
|
||||
<form data-ip-block-form action="{{ ADMINX_BASE }}/security/ip-blocks" method="post">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<label><span>IP-адрес</span><input class="input mono" name="ip" required placeholder="203.0.113.10"></label>
|
||||
<label><span>Причина</span><input class="input" name="reason" maxlength="500" placeholder="Причина блокировки"></label>
|
||||
<label><span>Срок</span><select class="select" name="hours"><option value="0">Бессрочно</option><option value="1">1 час</option><option value="24">24 часа</option><option value="168">7 дней</option><option value="720">30 дней</option></select></label>
|
||||
<button class="btn btn-danger" type="submit"><i class="ti ti-ban"></i>Заблокировать</button>
|
||||
</form>
|
||||
{% if active_type == 'user_agent' %}
|
||||
<div class="settings-section-head"><span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti ti-robot-off"></i></span><div><h2>Заблокировать агента</h2><p>Правило действует на публичном сайте.</p></div></div>
|
||||
<form data-user-agent-block-form action="{{ ADMINX_BASE }}/security/user-agent-blocks" method="post">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<label><span>Режим проверки</span><select class="select" name="match_type"><option value="contains">Содержит фрагмент</option><option value="exact">Полное совпадение</option></select></label>
|
||||
<label><span>User-Agent или характерный фрагмент</span><textarea class="textarea mono" name="pattern" rows="4" maxlength="500" required placeholder="meta-externalagent/1.1"></textarea><small>Для робота обычно достаточно его уникального имени. Не используйте общие слова вроде Mozilla или Chrome.</small></label>
|
||||
<label><span>Причина</span><input class="input" name="reason" maxlength="500" placeholder="Нежелательный робот"></label>
|
||||
<label><span>Срок</span><select class="select" name="hours"><option value="0">Бессрочно</option><option value="1">1 час</option><option value="24">24 часа</option><option value="168">7 дней</option><option value="720">30 дней</option></select></label>
|
||||
<button class="btn btn-danger" type="submit"><i class="ti ti-ban"></i>Заблокировать</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="settings-section-head"><span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti ti-shield-plus"></i></span><div><h2>Заблокировать адрес</h2><p>Ваш текущий адрес: <span class="mono">{{ current_ip }}</span></p></div></div>
|
||||
<form data-ip-block-form action="{{ ADMINX_BASE }}/security/ip-blocks" method="post">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<label><span>IP-адрес</span><input class="input mono" name="ip" required placeholder="203.0.113.10"></label>
|
||||
<label><span>Причина</span><input class="input" name="reason" maxlength="500" placeholder="Причина блокировки"></label>
|
||||
<label><span>Срок</span><select class="select" name="hours"><option value="0">Бессрочно</option><option value="1">1 час</option><option value="24">24 часа</option><option value="168">7 дней</option><option value="720">30 дней</option></select></label>
|
||||
<button class="btn btn-danger" type="submit"><i class="ti ti-ban"></i>Заблокировать</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>{% endif %}
|
||||
|
||||
<section class="security-list-section">
|
||||
<div class="section-header"><div class="section-icon"><i class="ti ti-shield-lock"></i></div><div><div class="section-eyebrow">Безопасность</div><h2>Заблокированные IP</h2><p class="section-desc">Постоянные и ограниченные по времени блокировки.</p></div><div class="section-header-right"><form method="get" class="security-filter"><div class="input-wrap"><i class="ti ti-search"></i><input class="input" name="q" value="{{ q }}" placeholder="IP или причина"></div><button class="btn btn-secondary" type="submit"><i class="ti ti-filter"></i>Найти</button></form></div></div>
|
||||
<div class="card security-table-card ax-list-card"><div class="ax-list-head"><div class="ax-list-title"><span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti ti-shield-lock"></i></span><div><h2>Активные блокировки</h2><p class="text-secondary">Адреса, для которых ограничен доступ к сайту.</p></div></div><span class="badge badge-red">{{ rows|length }}</span></div><div class="table-responsive"><table class="table"><thead><tr><th>IP-адрес</th><th>Причина</th><th>Создано</th><th>Срок</th><th class="table-actions">Действия</th></tr></thead><tbody>
|
||||
{% for row in rows %}<tr><td class="mono"><strong>{{ row.ip }}</strong></td><td>{{ row.reason ?: 'Без комментария' }}</td><td>{{ row.created_at|date('d.m.Y H:i') }}</td><td>{% if row.expires_at %}<span class="badge badge-amber">до {{ row.expires_at|date('d.m.Y H:i') }}</span>{% else %}<span class="badge badge-red">Бессрочно</span>{% endif %}</td><td class="table-actions">{% if can_manage %}<button class="btn btn-icon btn-danger-ghost" type="button" data-ip-unblock="{{ row.id }}" title="Разблокировать"><i class="ti ti-lock-open"></i></button>{% endif %}</td></tr>
|
||||
{% else %}<tr><td colspan="5"><div class="empty-state"><i class="ti ti-shield-check"></i><p>Заблокированных адресов нет</p></div></td></tr>{% endfor %}
|
||||
<div class="section-header"><div class="section-icon"><i class="ti {{ active_type == 'user_agent' ? 'ti-robot-off' : 'ti-shield-lock' }}"></i></div><div><div class="section-eyebrow">Безопасность</div><h2>{{ active_type == 'user_agent' ? 'Заблокированные агенты' : 'Заблокированные IP' }}</h2><p class="section-desc">{{ active_type == 'user_agent' ? 'Точные агенты и характерные фрагменты строк браузеров и роботов.' : 'Постоянные и ограниченные по времени блокировки адресов.' }}</p></div><div class="section-header-right"><form method="get" class="security-filter"><input type="hidden" name="type" value="{{ active_type }}"><div class="input-wrap"><i class="ti ti-search"></i><input class="input" name="q" value="{{ q }}" placeholder="{{ active_type == 'user_agent' ? 'Агент или причина' : 'IP или причина' }}"></div><button class="btn btn-secondary" type="submit"><i class="ti ti-filter"></i>Найти</button></form></div></div>
|
||||
<div class="card security-table-card ax-list-card"><div class="ax-list-head"><div class="ax-list-title"><span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti {{ active_type == 'user_agent' ? 'ti-robot-off' : 'ti-shield-lock' }}"></i></span><div><h2>Активные блокировки</h2><p class="text-secondary">Истёкшие правила удаляются автоматически.</p></div></div><span class="badge badge-red">{{ rows|length }}</span></div><div class="table-responsive"><table class="table"><thead>
|
||||
{% if active_type == 'user_agent' %}<tr><th>Правило</th><th>Причина</th><th>Создано</th><th>Срок</th><th class="table-actions">Действия</th></tr>{% else %}<tr><th>IP-адрес</th><th>Причина</th><th>Создано</th><th>Срок</th><th class="table-actions">Действия</th></tr>{% endif %}
|
||||
</thead><tbody>
|
||||
{% for row in rows %}<tr>
|
||||
{% if active_type == 'user_agent' %}<td><strong class="mono security-agent-pattern">{{ row.pattern }}</strong><small>{{ row.match_type == 'exact' ? 'Полное совпадение' : 'Содержит фрагмент' }}</small></td>{% else %}<td class="mono"><strong>{{ row.ip }}</strong></td>{% endif %}
|
||||
<td>{{ row.reason ?: 'Без комментария' }}</td><td>{{ row.created_at|date('d.m.Y H:i') }}</td><td>{% if row.expires_at %}<span class="badge badge-amber">до {{ row.expires_at|date('d.m.Y H:i') }}</span>{% else %}<span class="badge badge-red">Бессрочно</span>{% endif %}</td><td class="table-actions">{% if can_manage %}<button class="btn btn-icon btn-danger-ghost" type="button" {% if active_type == 'user_agent' %}data-user-agent-unblock="{{ row.id }}" data-tooltip="Разблокировать User-Agent"{% else %}data-ip-unblock="{{ row.id }}" data-tooltip="Разблокировать IP"{% endif %} aria-label="Разблокировать"><i class="ti ti-lock-open"></i></button>{% endif %}</td></tr>
|
||||
{% else %}<tr><td colspan="5"><div class="empty-state"><i class="ti ti-shield-check"></i><p>Активных блокировок нет</p></div></td></tr>{% endfor %}
|
||||
</tbody></table></div></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
use App\Common\Permission;
|
||||
use App\Common\Navigation;
|
||||
use App\Common\AuditLog;
|
||||
use App\Common\Cache;
|
||||
use App\Common\Settings;
|
||||
use App\Adminx\Support\CodeEditor;
|
||||
use App\Adminx\Support\AdminLocale;
|
||||
@@ -154,6 +155,35 @@
|
||||
return $this->success('Настройки безопасности сохранены', array('reload' => true));
|
||||
}
|
||||
|
||||
public function saveNotificationPreference(array $params = array())
|
||||
{
|
||||
if (($err = $this->guard()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
|
||||
try {
|
||||
$preference = ModuleExtensions::setNotificationPreference(
|
||||
Request::postStr('module_code'),
|
||||
Request::postStr('key'),
|
||||
Request::postBool('enabled')
|
||||
);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage(), array(), 404);
|
||||
}
|
||||
|
||||
Cache::forget('adminx.notifications.' . (int) Auth::id() . '.' . md5((string) ADMINX_BASE));
|
||||
AuditLog::record('settings.notification_preference_updated', array(
|
||||
'actor_id' => Auth::id(),
|
||||
'target_type' => 'settings',
|
||||
'target_id' => $preference['module_code'] . ':' . $preference['key'],
|
||||
'meta' => array('enabled' => !empty($preference['enabled'])),
|
||||
));
|
||||
|
||||
return $this->success('Настройка уведомлений сохранена', array(
|
||||
'data' => array('preference' => $preference),
|
||||
));
|
||||
}
|
||||
|
||||
public function pagination(array $params = array())
|
||||
{
|
||||
$item = Model::pagination(isset($params['id']) ? (int) $params['id'] : 0);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'settings',
|
||||
'name' => 'Настройки',
|
||||
'version' => '0.1.2',
|
||||
'version' => '0.1.4',
|
||||
|
||||
'permissions' => array(
|
||||
'key' => 'settings',
|
||||
@@ -54,6 +54,7 @@
|
||||
'/settings',
|
||||
'/security/ip-blocks',
|
||||
'/system/console',
|
||||
'/system/updates',
|
||||
),
|
||||
),
|
||||
array(
|
||||
@@ -185,6 +186,7 @@
|
||||
array('POST', '/settings', array(\App\Adminx\Settings\Controller::class, 'save')),
|
||||
array('POST', '/settings/interface', array(\App\Adminx\Settings\Controller::class, 'saveInterface')),
|
||||
array('POST', '/settings/security', array(\App\Adminx\Settings\Controller::class, 'saveSecurity')),
|
||||
array('POST', '/settings/notifications', array(\App\Adminx\Settings\Controller::class, 'saveNotificationPreference')),
|
||||
array('GET', '/settings/paginations/{id}', array(\App\Adminx\Settings\Controller::class, 'pagination')),
|
||||
array('POST', '/settings/paginations', array(\App\Adminx\Settings\Controller::class, 'savePagination')),
|
||||
array('POST', '/settings/paginations/{id}', array(\App\Adminx\Settings\Controller::class, 'savePagination')),
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
return array(
|
||||
'code' => 'updates',
|
||||
'name' => 'Обновления',
|
||||
'version' => '0.2.0',
|
||||
'version' => '0.2.1',
|
||||
'notifications' => array('provider' => array(\App\Adminx\Updates\NotificationProvider::class, 'items'), 'permission' => 'view_core_updates', 'sort_order' => 45),
|
||||
'permissions' => array(
|
||||
'key' => 'updates',
|
||||
@@ -32,7 +32,7 @@
|
||||
'priority' => 42,
|
||||
),
|
||||
'navigation' => array(
|
||||
array('code' => 'core_updates', 'label' => 'Обновления', 'url' => '/system/updates', 'icon' => 'ti ti-refresh', 'permission' => 'view_core_updates', 'group' => 'Система', 'sort_order' => 38, 'match' => array('/system/updates')),
|
||||
array('code' => 'core_updates', 'label' => 'Обновления', 'url' => '/system/updates', 'icon' => 'ti ti-refresh', 'permission' => 'view_core_updates', 'group' => 'Система', 'parent' => 'settings', 'sort_order' => 90, 'match' => array('/system/updates')),
|
||||
),
|
||||
'routes' => array(
|
||||
array('GET', '/system/updates', array(\App\Adminx\Updates\Controller::class, 'index')),
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<phrase data="auto_f2d6a66eee4f2dba">average</phrase>
|
||||
<phrase data="auto_f2f391cf89444519">excellent</phrase>
|
||||
<phrase data="auto_f89814d87e446cdd">Change:</phrase>
|
||||
<phrase data="users_account_tab">Account</phrase>
|
||||
<phrase data="users_browser">Browser</phrase>
|
||||
<phrase data="users_device">Device</phrase>
|
||||
<phrase data="users_never">Never</phrase>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<phrase data="auto_f2d6a66eee4f2dba">средний</phrase>
|
||||
<phrase data="auto_f2f391cf89444519">отличный</phrase>
|
||||
<phrase data="auto_f89814d87e446cdd">Изменение:</phrase>
|
||||
<phrase data="users_account_tab">Учётная запись</phrase>
|
||||
<phrase data="users_browser">Браузер</phrase>
|
||||
<phrase data="users_device">Устройство</phrase>
|
||||
<phrase data="users_never">Не было</phrase>
|
||||
|
||||
+13
-2
@@ -53,14 +53,25 @@
|
||||
{% if notifications.total > 0 %}<span class="topbar-badge">{{ notifications.total > 99 ? '99+' : notifications.total }}</span>{% endif %}
|
||||
</button>
|
||||
<div class="dropdown-menu right notif-menu" role="menu">
|
||||
<div class="notif-head"><b>{{ lang.topbar_notifications|default('Уведомления') }}</b>{% if notifications.total > 0 %}<span class="text-muted text-xs">{{ notifications.total > 99 ? '99+' : notifications.total }}</span>{% endif %}</div>
|
||||
<div class="notif-head"><b>{{ lang.topbar_notifications|default('Уведомления') }}</b><span class="notif-head-actions">{% if notifications.total > 0 %}<span class="text-muted text-xs">{{ notifications.total > 99 ? '99+' : notifications.total }}</span>{% endif %}{% if can_manage_notification_preferences and notification_preferences %}<button class="btn btn-ghost btn-icon btn-sm" type="button" data-notification-settings-toggle aria-expanded="false" aria-label="Настроить уведомления" data-tooltip="Настроить уведомления"><i class="ti ti-adjustments"></i></button>{% endif %}</span></div>
|
||||
<div class="notif-list">
|
||||
{% for item in notifications.items %}
|
||||
<a class="notif-item" href="{{ item.url }}"><span class="icon-tile" style="--tile-bg:{{ item.bg }};--tile-fg:{{ item.fg }}"><i class="{{ item.icon }}"></i></span><span class="notif-body"><b>{{ item.title }}</b><small>{{ item.text }}</small></span><span class="notif-count">{{ item.count > 99 ? '99+' : item.count }}</span></a>
|
||||
<a class="notif-item" href="{{ item.url }}"{% if item.key %} data-notification-key="{{ item.key|e('html_attr') }}"{% endif %}><span class="icon-tile" style="--tile-bg:{{ item.bg }};--tile-fg:{{ item.fg }}"><i class="{{ item.icon }}"></i></span><span class="notif-body"><b>{{ item.title }}</b><small>{{ item.text }}</small></span><span class="notif-count">{{ item.count > 99 ? '99+' : item.count }}</span></a>
|
||||
{% else %}
|
||||
<div class="notif-empty"><i class="ti ti-checks"></i>{{ lang.topbar_no_notifications|default('Нет новых уведомлений') }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if can_manage_notification_preferences and notification_preferences %}
|
||||
<div class="notif-settings" data-notification-settings hidden>
|
||||
<div class="notif-settings-copy"><b>Что показывать</b><small>Проверки продолжат работать, меняется только список в колокольчике.</small></div>
|
||||
{% for preference in notification_preferences %}
|
||||
<label class="notif-setting-row">
|
||||
<span><b>{{ preference.label }}</b>{% if preference.description %}<small>{{ preference.description }}</small>{% endif %}</span>
|
||||
<span class="switch"><input type="checkbox" data-notification-preference data-module-code="{{ preference.module_code|e('html_attr') }}" data-key="{{ preference.key|e('html_attr') }}"{{ preference.enabled ? ' checked' : '' }}><span></span></span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown" id="userMenu">
|
||||
|
||||
@@ -19,6 +19,54 @@
|
||||
Если товарный модуль не установлен, интерфейс не должен предлагать товарные
|
||||
сущности или пустой список «товарных каталогов».
|
||||
|
||||
## Назначение полей товара
|
||||
|
||||
Откройте товарный каталог и вкладку **Настройки → Назначение полей товара**.
|
||||
Для каждой роли выберите поле этой рубрики: название, артикул, цену, остаток,
|
||||
изображения, описание, гарантию, текст и номер регистрационного документа и
|
||||
другие значения. **Автоматически** сохраняет совместимость со
|
||||
старым alias; **Не назначено** отключает роль без переименования полей или
|
||||
изменения документов. Дополнительные поля проекта необязательны. Колонки
|
||||
программы оплаты остаются в стандартном быстром редакторе, если соответствующие
|
||||
роли назначены; в обычной рубрике они не добавляются.
|
||||
|
||||
В блоке **Сохранённое сопоставление** укажите номер документа этой рубрики и
|
||||
нажмите **Проверить**. Preview читает сохранённые значения и показывает поля,
|
||||
цену, остаток и причины недоступности. Выбор образца не сохраняет настройки.
|
||||
Дата публикации учитывается только при включённой общей настройке сайта.
|
||||
Отсутствие дополнительного регистрационного поля не блокирует покупку;
|
||||
исключение из прайса не означает запрет покупки.
|
||||
|
||||
Для конкретной выборки используйте **Проверка выборки**: выберите каталог и раздел,
|
||||
область поиска и строку, фид либо исходный товар и лимит рекомендаций. Нажмите
|
||||
**Проверить выборку**. Смена параметров и автосохранение ролей такую проверку не
|
||||
запускают; прежний результат при изменении параметров скрывается.
|
||||
|
||||
Результат строится штатными репозиториями по сохранённым настройкам. Каталог
|
||||
учитывает источники раздела и условия запроса; проверяется как сам вариант, так
|
||||
и его группа. Отсутствие варианта среди отдельных карточек не означает отсутствие
|
||||
группы. Скрытие пункта меню не исключает опубликованную SEO-страницу раздела
|
||||
из проверки. Каталог и поиск проверяются до пагинации; посетительские фильтры
|
||||
каталога не применяются. Поиск использует штатный pipeline с синонимами и
|
||||
исправлением опечаток и показывает фактическую поисковую строку. При неоднозначном
|
||||
исправлении замена не выбирается, как и на сайте. Статистика поиска не изменяется.
|
||||
Рекомендации показывают
|
||||
фактическую выдачу с её ранжированием, лимитом и кешем. Фид проверяет разрешённые
|
||||
категории, публикацию, цену и исключение из прайса без генерации XML. Пустой набор
|
||||
категорий не расширяется до всего каталога. Ошибка индекса означает незавершённую
|
||||
проверку, а не отсутствие товара в выборке.
|
||||
|
||||
Просмотр значений требует прав на товары, документы и чтение рубрики; для
|
||||
ограниченной роли учитывается группа связанной публичной учётной записи.
|
||||
Дополнительно нужны права просмотра поиска, фидов или условий запроса для
|
||||
соответствующей проверки. Ссылки из preview открывают штатные
|
||||
редакторы полей, запроса, шаблонов, фильтров раздела и набора характеристик.
|
||||
Шаблон карточки учитывает опубликованное представление и резервный вывод темы.
|
||||
Сохранение изменённых ролей пересчитывает коммерческие значения индекса;
|
||||
смена назначения на обычный каталог очищает только проекции, сохраняя исходные
|
||||
характеристики, варианты и данные доставки. Включение товарного назначения
|
||||
может занять время из-за полной индексации рубрики.
|
||||
|
||||
## Обзор товарного каталога
|
||||
|
||||
Страница **Каталог → Характеристики → Обзор** — начальная точка товарного
|
||||
@@ -744,6 +792,33 @@ CommerceML: интеграции не должны писать прямо в т
|
||||
|
||||
## Безопасное изменение
|
||||
|
||||
### Связанные настройки товара и раздела
|
||||
|
||||
В штатном редакторе товара и боковой панели раздела блок **Связанные настройки**
|
||||
ведёт к фильтрам раздела, набору характеристик, шаблону карточки в каталоге и
|
||||
шаблону страницы. Ссылки открываются в отдельной вкладке, не закрывая редактор.
|
||||
Показываются только доступные по вашим правам настройки. Для drawer требуется
|
||||
право управления соответствующим разделом панели.
|
||||
|
||||
У товара наборы берутся из реально загруженных групп характеристик, без повторов.
|
||||
Фильтры и карточки отмечены названием раздела, поскольку товар может находиться
|
||||
в нескольких разделах. Для native-фильтров открывается редактор фильтров
|
||||
характеристик; для прежних фильтров открывается вкладка «Фильтры» раздела.
|
||||
У раздела шаблон страницы относится к **связанному документу**, который может
|
||||
находиться в другой рубрике. Без связанного документа этой ссылки нет.
|
||||
|
||||
Опубликованное native-представление списка имеет приоритет над товарным шаблоном.
|
||||
Без него ссылка ведёт к опубликованному шаблону карточки или штатному шаблону
|
||||
активной темы. Пустой или отсутствующий альтернативный шаблон страницы означает
|
||||
возврат к основному шаблону рубрики. Назначения отдельных подборок и запросов
|
||||
могут переопределять вид карточки вне этого раздела.
|
||||
|
||||
Блок отражает **сохранённое** состояние: после смены шаблона или характеристик
|
||||
сохраните товар и откройте редактор заново. У раздела ссылки обновляются после
|
||||
сохранения; у нового раздела появляются после первого сохранения.
|
||||
|
||||
### Проверка изменений
|
||||
|
||||
1. Сначала меняйте структуру и поля в тестовом разделе.
|
||||
2. Проверьте сохранение документа и повторное открытие редактора.
|
||||
3. Пересоберите индекс и сравните товары, отдельные значения и диапазоны.
|
||||
|
||||
@@ -19,6 +19,51 @@ with tree sections.
|
||||
If the product module is not installed, the interface should not offer products
|
||||
entities or an empty list of “product catalogs”.
|
||||
|
||||
## Product Field Roles
|
||||
|
||||
Open a product catalog and select **Settings → Product Field Roles**. Assign
|
||||
fields from that rubric to the name, SKU, price, stock, images and other roles.
|
||||
**Automatic** preserves legacy alias matching; **Not Assigned** disables a role
|
||||
without renaming fields or modifying documents. Optional project fields are not
|
||||
required. Payment-program columns remain in the default quick editor when those
|
||||
roles are mapped; an ordinary rubric does not add them. Description, warranty,
|
||||
registration document text and registration number also have dedicated roles.
|
||||
|
||||
Under **Saved Mapping**, enter a document ID from this rubric and select
|
||||
**Check**. The preview reads saved values and shows mapped fields, price, stock
|
||||
and availability reasons. Selecting a sample does not save settings. Publication
|
||||
dates apply only when the site's publication-date setting is enabled. A missing
|
||||
optional registration field does not block purchase; price-list exclusion is
|
||||
separate from purchase availability.
|
||||
|
||||
Use **Selection Check** for a particular catalog section, search scope and query,
|
||||
feed, or recommendation source product and limit. Select **Check Selection** to
|
||||
run it. Changing parameters or automatically saving roles does not run this
|
||||
check; changing parameters hides an outdated result.
|
||||
|
||||
Native repositories evaluate saved settings. Catalog checks include category
|
||||
sources, request conditions, the individual variant and its group. A variant
|
||||
missing as a separate card does not imply its group is missing. Hiding a menu
|
||||
item does not exclude its published SEO category page. Catalog and search checks
|
||||
run before pagination, without visitor catalog filters. Search uses the native
|
||||
pipeline with synonyms and typo correction and shows the effective query.
|
||||
Ambiguous corrections are not selected, just as on the site. Search statistics
|
||||
are not changed. Recommendations use the actual ranked and cached result with the
|
||||
selected limit. Feed checks cover allowed categories, publication, price and
|
||||
price-list exclusion without generating XML. An empty allowed category set never
|
||||
expands to the whole catalog. An index error means an incomplete check, not a
|
||||
negative selection result.
|
||||
|
||||
Reading values requires product and document permissions plus rubric read access;
|
||||
restricted roles use the linked public account's group. Search, feed and request
|
||||
condition checks additionally require the corresponding module permission.
|
||||
Preview links open the
|
||||
native field, request, template, section-filter and attribute-set editors. The
|
||||
card link respects the published template and the theme fallback. Saving changed
|
||||
roles refreshes commercial index values. Switching to a regular catalog removes
|
||||
only derived projections, preserving native attributes, variants and shipping
|
||||
data. Enabling product mode may take time because it rebuilds the rubric index.
|
||||
|
||||
## Product catalog overview
|
||||
|
||||
The scheduler refreshes the product quality summary hourly. Its latest snapshot
|
||||
@@ -701,6 +746,33 @@ unfinished appeal. The full API and state descriptions are in
|
||||
|
||||
## Safe change
|
||||
|
||||
### Related product and section settings
|
||||
|
||||
The native product editor and section drawer contain **Related Settings** links
|
||||
to section filters, attribute sets, catalog card templates and page templates.
|
||||
Links open in a separate tab without closing the editor. Only destinations
|
||||
allowed by your permissions are shown. Drawer editors require the corresponding
|
||||
management permission.
|
||||
|
||||
Product attribute sets come from the actual loaded attribute groups and are
|
||||
deduplicated. Filters and cards identify their section because one product can
|
||||
belong to several sections. Native filters open the attribute filter editor;
|
||||
legacy filters open the section's Filters tab. A section's page template belongs
|
||||
to its **linked document**, which may use a different rubric. There is no page
|
||||
template link when no document is linked.
|
||||
|
||||
A published native list presentation takes precedence over a product card
|
||||
template. Otherwise the link opens the published card template or the active
|
||||
theme's standard template. Missing or empty alternate page templates fall back
|
||||
to the rubric's main template. Individual collections and requests may override
|
||||
the card appearance outside this section.
|
||||
|
||||
Links reflect **saved** settings. After changing a product's template or
|
||||
attributes, save and reopen the editor. Section links refresh after saving;
|
||||
new sections receive links after their first save.
|
||||
|
||||
### Checking changes
|
||||
|
||||
1. First change the structure and fields in the test section.
|
||||
2. Check to save the document and reopen the editor.
|
||||
3. Rebuild the index and compare products, individual values, and ranges.
|
||||
|
||||
@@ -396,6 +396,52 @@ To associate a service with a payment option:
|
||||
If the service is not selected, the method is considered payment without online transfer: for example,
|
||||
in cash, by card upon receipt or by bank transfer.
|
||||
|
||||
## Abandoned carts and archive
|
||||
|
||||
Open `Store -> Orders -> Abandoned carts` and select **Archive with contacts**
|
||||
in the state filter above the table. The view button opens the saved customer
|
||||
details, cart contents and traffic attribution, including any captured UTM tags.
|
||||
|
||||
After three days without activity, carts with saved customer data move to the
|
||||
archive during cleanup. **Process overdue carts** runs this operation manually.
|
||||
Archived records are exempt from the regular cart retention period.
|
||||
|
||||
**Path to order** covers sessions created in the last 30 days, including those
|
||||
that resulted in an order. The table filters do not restrict these counters.
|
||||
The contact counter records entry of a first name, last name, email or phone.
|
||||
Choosing delivery or payment does not imply that contacts were entered, and an
|
||||
empty phone mask does not count. Partial input counts, so this is not the number
|
||||
of completed forms or customers with a callable phone number.
|
||||
|
||||
## Payment Attempts And Order Statuses
|
||||
|
||||
Starting payment again first checks the existing attempt. A pending payment
|
||||
reuses its confirmation link; an ambiguous network failure reuses its original
|
||||
idempotency key. A new attempt starts after a confirmed cancellation. Previous
|
||||
attempts retain their order association. A late pending response cannot remove
|
||||
a confirmed paid flag. Cancelled orders cannot start payment.
|
||||
|
||||
An order with an ongoing payment cannot have its amount or contents changed
|
||||
until that payment is checked. Each gateway declares additional required
|
||||
customer fields, such as email. Checkout and online payments currently use RUB;
|
||||
the feed currency neither changes the order currency nor converts its prices.
|
||||
|
||||
Under `Shop -> Settings -> Statuses`, assign each status a stable purpose:
|
||||
new, processing, shipped, completed, cancelled, or other. Renaming a status does
|
||||
not change its behavior. Review the purpose of custom statuses after upgrading.
|
||||
|
||||
## Checkout And Archived Carts
|
||||
|
||||
Checkout checks current product publication and prices. Changed prices require
|
||||
the customer to confirm the recalculated cart; unavailable items must be removed.
|
||||
Submitting the same checkout again returns the existing order instead of
|
||||
creating a duplicate.
|
||||
|
||||
The abandoned-cart table provides pagination while retaining filters and sort
|
||||
direction. Retention applies to the cart as a whole, so older lines do not expire
|
||||
individually in an active cart. Adding a product after checkout or archiving
|
||||
starts a new cart without overwriting the previous contact and traffic snapshot.
|
||||
|
||||
## Public pages
|
||||
|
||||
Addresses for cart, checkout, favorites, viewed items and personal orders
|
||||
|
||||
@@ -423,6 +423,58 @@ Commerce также дополняет общий view model товарной к
|
||||
Если сервис не выбран, способ считается оплатой без онлайн-перехода: например,
|
||||
наличными, картой при получении или банковским переводом.
|
||||
|
||||
Повторное нажатие оплаты сначала проверяет уже начатый платёж. Если он ещё
|
||||
ожидает подтверждения, покупатель получает прежнюю ссылку. При сетевой ошибке
|
||||
повторный запрос использует тот же ключ, чтобы не создавать второй платёж.
|
||||
Новая попытка создаётся после подтверждённой отмены предыдущей. Старые попытки
|
||||
остаются связанными с заказом. Запоздавший ответ «ожидание» не снимает оплату.
|
||||
|
||||
Отменённый заказ оплатить нельзя. Изменять состав и сумму заказа с начатой
|
||||
оплатой нельзя до проверки её результата. Сервис может требовать дополнительные
|
||||
данные покупателя, например email: требования объявляет сам платёжный модуль.
|
||||
В текущем оформлении и онлайн-оплате используется RUB; валюта фида не меняет
|
||||
валюту заказа и не конвертирует цены.
|
||||
|
||||
## Назначение статусов
|
||||
|
||||
В `Магазин -> Настройки -> Статусы` у статуса есть название, цвет и назначение:
|
||||
новый, в обработке, отправлен, выполнен, отменён или другой. Название можно
|
||||
менять и переводить: поведение заказа определяется назначением, а не текстом.
|
||||
Для собственного промежуточного статуса используйте «Другой». После обновления
|
||||
проверьте назначения нестандартных статусов, особенно отмены и обработки.
|
||||
|
||||
## Проверка корзины перед заказом
|
||||
|
||||
Перед оформлением магазин заново проверяет публикацию и цену каждого товара.
|
||||
Если цена изменилась, покупателю показывается пересчитанная корзина и требуется
|
||||
повторное подтверждение. Недоступный товар нужно удалить из корзины. Повторная
|
||||
отправка одного оформления возвращает созданный заказ, а не создаёт дубликат.
|
||||
|
||||
## Брошенные корзины и архив
|
||||
|
||||
Откройте `Магазин -> Заказы -> Брошенные корзины`. В фильтре состояния над
|
||||
таблицей выберите **Архив с контактами**. Кнопка просмотра в строке открывает
|
||||
сохранённые данные покупателя, состав корзины и сведения об источнике перехода,
|
||||
включая UTM-метки, если они были переданы.
|
||||
|
||||
Через трое суток без активности корзины с сохранёнными данными переходят в архив
|
||||
при очередной очистке. Кнопка **Обработать просроченные** запускает эту обработку
|
||||
вручную. Архивные записи не удаляются по обычному сроку хранения корзин.
|
||||
|
||||
Блок **Путь до заказа** показывает сессии за последние 30 дней, включая
|
||||
завершившиеся заказом. Он не ограничивается текущим фильтром таблицы.
|
||||
«Заполняли контакты» учитывает отдельный факт ввода имени, фамилии, email или
|
||||
телефона. Выбор доставки или оплаты не означает, что контакты уже заполнены;
|
||||
пустая маска телефона также не считается контактом. Частично введённые данные
|
||||
учитываются, поэтому этот показатель не означает число полностью заполненных
|
||||
форм или доступных для звонка покупателей.
|
||||
|
||||
Страницы внизу таблицы позволяют просмотреть весь архив, сохраняя выбранные
|
||||
фильтры и направление сортировки. Срок хранения определяется по активности
|
||||
корзины целиком: старые позиции не исчезают по отдельности из активной корзины.
|
||||
Если покупатель после заказа или архивирования снова добавляет товар, создаётся
|
||||
новая корзина. Предыдущий снимок контактов и источников перехода не меняется.
|
||||
|
||||
## Публичные страницы
|
||||
|
||||
Адреса корзины, оформления, избранного, просмотренных товаров и личных заказов
|
||||
|
||||
@@ -282,6 +282,20 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_ip_blocks` (
|
||||
KEY `idx_expires` (`expires_at`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `{{prefix}}_user_agent_blocks` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`pattern_hash` CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`pattern` VARCHAR(500) NOT NULL,
|
||||
`match_type` VARCHAR(16) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT 'contains',
|
||||
`reason` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`expires_at` INT UNSIGNED DEFAULT NULL,
|
||||
`actor_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`created_at` INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_user_agent_pattern` (`pattern_hash`),
|
||||
KEY `idx_user_agent_expires` (`expires_at`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `{{prefix}}_referrer_log` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`log_date` DATE NOT NULL,
|
||||
@@ -316,6 +330,7 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_not_found_log` (
|
||||
`query_string` VARCHAR(512) NOT NULL DEFAULT '',
|
||||
`referer` VARCHAR(512) NOT NULL DEFAULT '',
|
||||
`user_agent` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`request_ip` VARCHAR(45) NOT NULL DEFAULT '',
|
||||
`hits` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`resolved` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`first_seen_at` INT UNSIGNED NOT NULL,
|
||||
@@ -613,6 +628,7 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_rubrics` (
|
||||
`rubric_author_id` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`rubric_created` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`rubric_docs_active` TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`rubric_is_technical` TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`rubric_start_code` TEXT NOT NULL,
|
||||
`rubric_code_start` TEXT NOT NULL,
|
||||
`rubric_code_end` TEXT NOT NULL,
|
||||
@@ -807,11 +823,13 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_documents` (
|
||||
`document_version` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`document_author_id` MEDIUMINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`document_in_search` ENUM('1','0') NOT NULL DEFAULT '1',
|
||||
`document_is_technical` TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`document_meta_keywords` TEXT NOT NULL,
|
||||
`document_meta_description` TEXT NOT NULL,
|
||||
`document_meta_robots` ENUM('index,follow','index,nofollow','noindex,nofollow') NOT NULL DEFAULT 'index,follow',
|
||||
`document_sitemap_freq` TINYINT NOT NULL DEFAULT 3,
|
||||
`document_sitemap_pr` FLOAT DEFAULT 0.5,
|
||||
`document_in_sitemap` TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`document_status` ENUM('1','0') NOT NULL DEFAULT '1',
|
||||
`document_deleted` ENUM('0','1') NOT NULL DEFAULT '0',
|
||||
`document_count_print` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
@@ -830,6 +848,7 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_documents` (
|
||||
KEY `idx_status` (`document_status`),
|
||||
KEY `idx_published` (`document_published`),
|
||||
KEY `idx_expire` (`document_expire`),
|
||||
KEY `idx_public_visibility` (`document_is_technical`,`document_in_sitemap`),
|
||||
KEY `idx_request` (`Id`, `rubric_id`, `document_status`, `document_deleted`),
|
||||
KEY `idx_public_listing` (`rubric_id`, `document_status`, `document_deleted`, `document_published`, `Id`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4;
|
||||
@@ -1232,6 +1251,7 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_module_catalog_items` (
|
||||
`fields_use` TEXT DEFAULT NULL,
|
||||
`filters_use` TEXT DEFAULT NULL,
|
||||
`filters_settings` TEXT DEFAULT NULL,
|
||||
`source_item_ids` TEXT DEFAULT NULL,
|
||||
`attribute_set_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`attributes_runtime` VARCHAR(20) NOT NULL DEFAULT 'legacy',
|
||||
`filter_runtime` VARCHAR(20) NOT NULL DEFAULT 'legacy',
|
||||
|
||||
@@ -108,6 +108,14 @@
|
||||
'media.restored' => 'Медиа восстановлено из корзины',
|
||||
'media.purged' => 'Медиа удалено окончательно',
|
||||
'customers.exported' => 'Пользователи сайта выгружены в CSV',
|
||||
'customers.updated' => 'Профиль пользователя сайта изменён',
|
||||
'customers.deleted' => 'Пользователь сайта удалён',
|
||||
'customers.status_changed' => 'Доступ пользователя сайта изменён',
|
||||
'customers.auth_settings_updated' => 'Настройки публичной регистрации изменены',
|
||||
'customers.auth_pages_updated' => 'Страницы публичной авторизации изменены',
|
||||
'customers.auth_form_updated' => 'Шаблон публичной авторизации изменён',
|
||||
'customers.auth_form_reset' => 'Шаблон публичной авторизации сброшен',
|
||||
'auth.public_login_failed' => 'Неудачная попытка публичного входа',
|
||||
'commerce.orders_exported' => 'Заказы выгружены в CSV',
|
||||
'popups.leads_exported' => 'Заявки из поп-апов выгружены в CSV',
|
||||
'quiz.created' => 'Подбор товара создан',
|
||||
@@ -175,6 +183,7 @@
|
||||
'catalog.variant_selector_settings_saved' => 'Вид выбора вариантов изменён',
|
||||
'catalog.bed_attributes_imported' => 'Характеристики кроватей заполнены из прайс-листа',
|
||||
'commerce.order_created' => 'Заказ создан менеджером',
|
||||
'commerce.order_claimed' => 'Заказ принят менеджером в работу',
|
||||
'commerce.order_composition_updated' => 'Состав заказа изменён',
|
||||
'console.executed' => 'Код в системной консоли выполнен',
|
||||
'console.execution_failed' => 'Ошибка системной консоли',
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
protected static function publicById($id)
|
||||
{
|
||||
$row = DB::query(
|
||||
'SELECT * FROM ' . PublicUserTables::table('users') . ' WHERE Id = %i AND deleted != %s LIMIT 1',
|
||||
'SELECT * FROM ' . PublicUserTables::table('users') . ' WHERE Id = %i AND COALESCE(deleted,0) != %s LIMIT 1',
|
||||
(int) $id,
|
||||
'1'
|
||||
)->getAssoc();
|
||||
@@ -217,7 +217,7 @@
|
||||
protected static function publicByEmail($email)
|
||||
{
|
||||
$row = DB::query(
|
||||
'SELECT * FROM ' . PublicUserTables::table('users') . ' WHERE LOWER(email) = LOWER(%s) AND deleted != %s LIMIT 1',
|
||||
'SELECT * FROM ' . PublicUserTables::table('users') . ' WHERE LOWER(email) = LOWER(%s) AND COALESCE(deleted,0) != %s LIMIT 1',
|
||||
trim((string) $email),
|
||||
'1'
|
||||
)->getAssoc();
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
|
||||
public static function bootstrap()
|
||||
{
|
||||
if (self::check()) {
|
||||
self::refreshAuthenticatedUser();
|
||||
}
|
||||
|
||||
if (!self::check()) {
|
||||
self::restoreRememberedUser();
|
||||
}
|
||||
@@ -152,7 +156,7 @@
|
||||
. ' usr.status, usr.deleted, grp.user_group_permission'
|
||||
. ' FROM `' . PublicUserTables::table('users') . '` usr'
|
||||
. ' LEFT JOIN `' . PublicUserTables::table('user_groups') . '` grp ON grp.user_group = usr.user_group'
|
||||
. ' WHERE usr.deleted != %s'
|
||||
. ' WHERE COALESCE(usr.deleted,0) != %s'
|
||||
. ' AND ((usr.email = %s1 AND (usr.email_verified_at>0 OR usr.user_name=%s1 OR usr.phone_verified_at=0))'
|
||||
. ' OR usr.user_name = %s1 OR usr.phone_normalized = %s2) LIMIT 1',
|
||||
'1',
|
||||
@@ -168,7 +172,7 @@
|
||||
. ' usr.status, usr.deleted, grp.user_group_permission'
|
||||
. ' FROM `' . PublicUserTables::table('users') . '` usr'
|
||||
. ' LEFT JOIN `' . PublicUserTables::table('user_groups') . '` grp ON grp.user_group = usr.user_group'
|
||||
. ' WHERE usr.deleted != %s AND (usr.email = %s1 OR usr.user_name = %s1) LIMIT 1',
|
||||
. ' WHERE COALESCE(usr.deleted,0) != %s AND (usr.email = %s1 OR usr.user_name = %s1) LIMIT 1',
|
||||
'1',
|
||||
(string) $identifier
|
||||
)->getAssoc();
|
||||
@@ -271,6 +275,35 @@
|
||||
return password_needs_rehash((string) $hash, PASSWORD_BCRYPT, array('cost' => 12));
|
||||
}
|
||||
|
||||
/** Keep an open public session in sync with account status and permissions. */
|
||||
protected static function refreshAuthenticatedUser()
|
||||
{
|
||||
$user = self::findUserById((int) Session::get('user_id'));
|
||||
$systemIdentity = !empty($_SESSION['user_system_identity']);
|
||||
$passwordChanged = $user && !$systemIdentity
|
||||
&& !hash_equals((string) $user['password'], (string) Session::get('user_pass'));
|
||||
if (!$user || (string) $user['status'] !== '1' || (string) $user['deleted'] === '1' || $passwordChanged) {
|
||||
self::forgetRememberCookie();
|
||||
Session::regenerateId(true);
|
||||
self::clearIdentity();
|
||||
return false;
|
||||
}
|
||||
|
||||
Session::del(self::permissionKeys());
|
||||
Session::set('user_name', PublicUserNames::format($user['user_name'], $user['firstname'], $user['lastname']));
|
||||
Session::set('user_firstname', (string) $user['firstname']);
|
||||
Session::set('user_lastname', (string) $user['lastname']);
|
||||
Session::set('user_pass', (string) $user['password']);
|
||||
Session::set('user_group', (int) $user['user_group']);
|
||||
Session::set('user_email', (string) $user['email']);
|
||||
Session::set('user_country', strtoupper((string) $user['country']));
|
||||
foreach (self::permissions((string) $user['user_group_permission']) as $permission) {
|
||||
Session::set($permission, 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function upgradePassword($userId, $password)
|
||||
{
|
||||
$hash = password_hash((string) $password, PASSWORD_BCRYPT, array('cost' => 12));
|
||||
@@ -340,7 +373,7 @@
|
||||
. ' LEFT JOIN `' . PublicUserTables::table('user_groups') . '` grp ON grp.user_group = usr.user_group'
|
||||
. ' WHERE ses.hash = %s AND ses.token_version=2 AND ses.agent = %s'
|
||||
. ' AND ses.expires_at >= %i AND ses.last_active >= %i'
|
||||
. ' AND usr.status = %s AND usr.deleted != %s LIMIT 1',
|
||||
. ' AND usr.status = %s AND COALESCE(usr.deleted,0) != %s LIMIT 1',
|
||||
$tokenHash,
|
||||
mb_substr(Request::userAgent(), 0, 255),
|
||||
$now,
|
||||
|
||||
@@ -22,19 +22,28 @@
|
||||
|
||||
class IpBlocker
|
||||
{
|
||||
protected static $userAgentTableAvailable;
|
||||
protected static $activeUserAgentRules;
|
||||
|
||||
public static function table()
|
||||
{
|
||||
return SystemTables::table('ip_blocks');
|
||||
}
|
||||
|
||||
public static function userAgentTable()
|
||||
{
|
||||
return SystemTables::table('user_agent_blocks');
|
||||
}
|
||||
|
||||
public static function enforce($maxAttempts = 120, $decaySeconds = 60)
|
||||
{
|
||||
$ip = Request::ip();
|
||||
$block = self::findActive($ip, true);
|
||||
if ($block) {
|
||||
$agentBlock = self::findActiveUserAgent(Request::userAgent(), true);
|
||||
if ($block || $agentBlock) {
|
||||
Response::setStatus(403);
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo 'Доступ с этого IP-адреса заблокирован.';
|
||||
echo 'Доступ заблокирован.';
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -106,6 +115,93 @@
|
||||
return (bool) DB::query('DELETE FROM `' . self::table() . '` WHERE id = %i', (int) $id);
|
||||
}
|
||||
|
||||
public static function allUserAgents($query = '')
|
||||
{
|
||||
if (!self::hasUserAgentTable()) { return array(); }
|
||||
|
||||
$query = trim((string) $query);
|
||||
$sql = 'SELECT * FROM `' . self::userAgentTable() . '`';
|
||||
if ($query !== '') {
|
||||
$sql .= ' WHERE pattern LIKE %ss OR reason LIKE %ss';
|
||||
return DB::query($sql . ' ORDER BY created_at DESC, id DESC', $query, $query)->getAll() ?: array();
|
||||
}
|
||||
|
||||
return DB::query($sql . ' ORDER BY created_at DESC, id DESC')->getAll() ?: array();
|
||||
}
|
||||
|
||||
public static function blockUserAgent($pattern, $reason, $expiresAt = null, $actorId = 0, $matchType = 'contains')
|
||||
{
|
||||
if (!self::hasUserAgentTable()) {
|
||||
throw new \RuntimeException('Примените миграции в разделе «База данных → Миграции»');
|
||||
}
|
||||
|
||||
$pattern = self::validateUserAgentPattern($pattern, $matchType);
|
||||
$matchType = $matchType === 'exact' ? 'exact' : 'contains';
|
||||
$expiresAt = $expiresAt === null ? null : (int) $expiresAt;
|
||||
if ($expiresAt !== null && $expiresAt <= time()) {
|
||||
throw new \InvalidArgumentException('Срок блокировки должен быть в будущем');
|
||||
}
|
||||
|
||||
$hash = hash('sha256', $matchType . '|' . mb_strtolower($pattern, 'UTF-8'));
|
||||
if ($expiresAt === null) {
|
||||
DB::query(
|
||||
'INSERT INTO `' . self::userAgentTable() . '` (pattern_hash, pattern, match_type, reason, expires_at, actor_id, created_at)'
|
||||
. ' VALUES (%s, %s, %s, %s, NULL, %i, %i) ON DUPLICATE KEY UPDATE reason=VALUES(reason),'
|
||||
. ' expires_at=NULL, actor_id=VALUES(actor_id), created_at=VALUES(created_at)',
|
||||
$hash, $pattern, $matchType, trim((string) $reason), (int) $actorId, time()
|
||||
);
|
||||
} else {
|
||||
DB::query(
|
||||
'INSERT INTO `' . self::userAgentTable() . '` (pattern_hash, pattern, match_type, reason, expires_at, actor_id, created_at)'
|
||||
. ' VALUES (%s, %s, %s, %s, %i, %i, %i) ON DUPLICATE KEY UPDATE reason=VALUES(reason),'
|
||||
. ' expires_at=VALUES(expires_at), actor_id=VALUES(actor_id), created_at=VALUES(created_at)',
|
||||
$hash, $pattern, $matchType, trim((string) $reason), $expiresAt, (int) $actorId, time()
|
||||
);
|
||||
}
|
||||
|
||||
self::forgetActiveUserAgentRules();
|
||||
return self::findUserAgentRule($hash);
|
||||
}
|
||||
|
||||
public static function unblockUserAgent($id)
|
||||
{
|
||||
if (!self::hasUserAgentTable()) { return false; }
|
||||
|
||||
$result = (bool) DB::query('DELETE FROM `' . self::userAgentTable() . '` WHERE id = %i', (int) $id);
|
||||
self::forgetActiveUserAgentRules();
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function findActiveUserAgent($userAgent, $failSilently = false)
|
||||
{
|
||||
$userAgent = trim((string) $userAgent);
|
||||
if ($userAgent === '') { return null; }
|
||||
|
||||
foreach (self::activeUserAgentRules() as $row) {
|
||||
$pattern = isset($row['pattern']) ? trim((string) $row['pattern']) : '';
|
||||
if ($pattern === '') { continue; }
|
||||
$exact = isset($row['match_type']) && $row['match_type'] === 'exact';
|
||||
if (($exact && strcasecmp($userAgent, $pattern) === 0)
|
||||
|| (!$exact && mb_stripos($userAgent, $pattern, 0, 'UTF-8') !== false)) {
|
||||
return (array) $row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function suggestUserAgentPattern($userAgent)
|
||||
{
|
||||
$userAgent = trim((string) $userAgent);
|
||||
if ($userAgent === '') { return array('pattern' => '', 'match_type' => 'exact'); }
|
||||
|
||||
if (preg_match('/\b([a-z][a-z0-9._-]*(?:bot|crawler|spider|slurp|preview|fetcher|agent|monitor)[a-z0-9._-]*(?:\/[a-z0-9._-]+)?)/i', $userAgent, $match)) {
|
||||
return array('pattern' => $match[1], 'match_type' => 'contains');
|
||||
}
|
||||
|
||||
return array('pattern' => mb_substr($userAgent, 0, 500), 'match_type' => 'exact');
|
||||
}
|
||||
|
||||
public static function findActive($ip, $failSilently = false)
|
||||
{
|
||||
$ip = self::validateIp($ip);
|
||||
@@ -126,7 +222,96 @@
|
||||
{
|
||||
$limit = max(1, min(1000, (int) $limit));
|
||||
DB::query('DELETE FROM `' . self::table() . '` WHERE expires_at IS NOT NULL AND expires_at <= %i LIMIT ' . $limit, time());
|
||||
return (int) DB::affectedRows();
|
||||
$count = (int) DB::affectedRows();
|
||||
if (self::hasUserAgentTable()) {
|
||||
DB::query('DELETE FROM `' . self::userAgentTable() . '` WHERE expires_at IS NOT NULL AND expires_at <= %i LIMIT ' . $limit, time());
|
||||
$userAgentCount = (int) DB::affectedRows();
|
||||
$count += $userAgentCount;
|
||||
if ($userAgentCount > 0) { self::forgetActiveUserAgentRules(); }
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
protected static function findUserAgentRule($hash)
|
||||
{
|
||||
if (!self::hasUserAgentTable()) { return null; }
|
||||
|
||||
$row = DB::query('SELECT * FROM `' . self::userAgentTable() . '` WHERE pattern_hash = %s LIMIT 1', $hash)->getAssoc();
|
||||
return $row ? (array) $row : null;
|
||||
}
|
||||
|
||||
protected static function hasUserAgentTable()
|
||||
{
|
||||
if (self::$userAgentTableAvailable !== null) {
|
||||
return self::$userAgentTableAvailable;
|
||||
}
|
||||
|
||||
try {
|
||||
self::$userAgentTableAvailable = DatabaseSchema::tableExists(self::userAgentTable());
|
||||
} catch (\Throwable $e) {
|
||||
self::$userAgentTableAvailable = false;
|
||||
}
|
||||
|
||||
return self::$userAgentTableAvailable;
|
||||
}
|
||||
|
||||
protected static function activeUserAgentRules()
|
||||
{
|
||||
if (self::$activeUserAgentRules !== null) {
|
||||
return self::$activeUserAgentRules;
|
||||
}
|
||||
|
||||
$cacheKey = self::activeUserAgentRulesCacheKey();
|
||||
$miss = new \stdClass();
|
||||
$rows = Cache::get($cacheKey, $miss);
|
||||
if ($rows !== $miss && is_array($rows)) {
|
||||
self::$activeUserAgentRules = $rows;
|
||||
return $rows;
|
||||
}
|
||||
|
||||
if (!self::hasUserAgentTable()) {
|
||||
self::$activeUserAgentRules = array();
|
||||
Cache::set($cacheKey, array(), 30);
|
||||
return array();
|
||||
}
|
||||
|
||||
self::$activeUserAgentRules = DB::query(
|
||||
'SELECT * FROM `' . self::userAgentTable() . '`'
|
||||
. ' WHERE expires_at IS NULL OR expires_at > %i ORDER BY id ASC',
|
||||
time()
|
||||
)->getAll() ?: array();
|
||||
Cache::set($cacheKey, self::$activeUserAgentRules, 60);
|
||||
return self::$activeUserAgentRules;
|
||||
}
|
||||
|
||||
protected static function forgetActiveUserAgentRules()
|
||||
{
|
||||
self::$activeUserAgentRules = null;
|
||||
Cache::forget(self::activeUserAgentRulesCacheKey());
|
||||
}
|
||||
|
||||
protected static function activeUserAgentRulesCacheKey()
|
||||
{
|
||||
return 'ip-blocker:user-agent-rules:' . self::userAgentTable();
|
||||
}
|
||||
|
||||
protected static function validateUserAgentPattern($pattern, $matchType)
|
||||
{
|
||||
$pattern = trim((string) $pattern);
|
||||
if ($pattern === '') {
|
||||
throw new \InvalidArgumentException('Укажите User-Agent или его характерный фрагмент');
|
||||
}
|
||||
|
||||
if (mb_strlen($pattern, 'UTF-8') > 500) {
|
||||
throw new \InvalidArgumentException('User-Agent не должен быть длиннее 500 символов');
|
||||
}
|
||||
|
||||
if ($matchType !== 'exact' && mb_strlen($pattern, 'UTF-8') < 5) {
|
||||
throw new \InvalidArgumentException('Фрагмент User-Agent должен содержать не меньше 5 символов');
|
||||
}
|
||||
|
||||
return $pattern;
|
||||
}
|
||||
|
||||
protected static function validateIp($ip)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Helpers\Request;
|
||||
use DB;
|
||||
|
||||
/**
|
||||
@@ -24,6 +25,8 @@
|
||||
*/
|
||||
class NotFoundLog
|
||||
{
|
||||
protected static $requestIpColumn;
|
||||
|
||||
public static function table()
|
||||
{
|
||||
return SystemTables::table('not_found_log');
|
||||
@@ -43,22 +46,32 @@
|
||||
$query = (string) parse_url($uri, PHP_URL_QUERY);
|
||||
$referer = isset($_SERVER['HTTP_REFERER']) ? (string) $_SERVER['HTTP_REFERER'] : '';
|
||||
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
|
||||
$ip = Request::ip();
|
||||
$now = time();
|
||||
|
||||
DB::query(
|
||||
'INSERT INTO `' . self::table() . '`'
|
||||
. ' (path_hash, path, query_string, referer, user_agent, hits, first_seen_at, last_seen_at)'
|
||||
. ' VALUES (%s, %s, %s, %s, %s, 1, %i, %i)'
|
||||
. ' ON DUPLICATE KEY UPDATE hits = hits + 1, last_seen_at = VALUES(last_seen_at),'
|
||||
. ' referer = VALUES(referer), query_string = VALUES(query_string)',
|
||||
hash('sha256', $path),
|
||||
mb_substr($path, 0, 512),
|
||||
mb_substr($query, 0, 512),
|
||||
mb_substr($referer, 0, 512),
|
||||
mb_substr($ua, 0, 255),
|
||||
$now,
|
||||
$now
|
||||
);
|
||||
if (self::hasRequestIpColumn()) {
|
||||
DB::query(
|
||||
'INSERT INTO `' . self::table() . '`'
|
||||
. ' (path_hash, path, query_string, referer, user_agent, request_ip, hits, first_seen_at, last_seen_at)'
|
||||
. ' VALUES (%s, %s, %s, %s, %s, %s, 1, %i, %i)'
|
||||
. ' ON DUPLICATE KEY UPDATE hits = hits + 1, last_seen_at = VALUES(last_seen_at),'
|
||||
. ' referer = VALUES(referer), query_string = VALUES(query_string),'
|
||||
. ' user_agent = VALUES(user_agent), request_ip = VALUES(request_ip)',
|
||||
hash('sha256', $path), mb_substr($path, 0, 512), mb_substr($query, 0, 512),
|
||||
mb_substr($referer, 0, 512), mb_substr($ua, 0, 255), mb_substr($ip, 0, 45), $now, $now
|
||||
);
|
||||
} else {
|
||||
DB::query(
|
||||
'INSERT INTO `' . self::table() . '`'
|
||||
. ' (path_hash, path, query_string, referer, user_agent, hits, first_seen_at, last_seen_at)'
|
||||
. ' VALUES (%s, %s, %s, %s, %s, 1, %i, %i)'
|
||||
. ' ON DUPLICATE KEY UPDATE hits = hits + 1, last_seen_at = VALUES(last_seen_at),'
|
||||
. ' referer = VALUES(referer), query_string = VALUES(query_string), user_agent = VALUES(user_agent)',
|
||||
hash('sha256', $path), mb_substr($path, 0, 512), mb_substr($query, 0, 512),
|
||||
mb_substr($referer, 0, 512), mb_substr($ua, 0, 255), $now, $now
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('NotFound log write failed: ' . $e->getMessage());
|
||||
@@ -66,6 +79,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
protected static function hasRequestIpColumn()
|
||||
{
|
||||
if (self::$requestIpColumn !== null) { return self::$requestIpColumn; }
|
||||
try {
|
||||
self::$requestIpColumn = DatabaseSchema::columnExists(self::table(), 'request_ip');
|
||||
} catch (\Throwable $e) {
|
||||
self::$requestIpColumn = false;
|
||||
}
|
||||
|
||||
return self::$requestIpColumn;
|
||||
}
|
||||
|
||||
protected static function isNoise($path)
|
||||
{
|
||||
$noise = array('/favicon.ico', '/robots.txt', '/sitemap.xml', '/ads.txt');
|
||||
@@ -87,9 +112,11 @@
|
||||
$where = array();
|
||||
$args = array();
|
||||
if ($query !== '') {
|
||||
$where[] = '(path LIKE %ss OR referer LIKE %ss)';
|
||||
$args[] = $query;
|
||||
$args[] = $query;
|
||||
$withIp = self::hasRequestIpColumn();
|
||||
$where[] = $withIp
|
||||
? '(path LIKE %ss OR referer LIKE %ss OR user_agent LIKE %ss OR request_ip LIKE %ss)'
|
||||
: '(path LIKE %ss OR referer LIKE %ss OR user_agent LIKE %ss)';
|
||||
$args = array_fill(0, $withIp ? 4 : 3, $query);
|
||||
}
|
||||
|
||||
if ($onlyUnresolved) {
|
||||
@@ -106,6 +133,32 @@
|
||||
return $rows ?: array();
|
||||
}
|
||||
|
||||
public static function exportChunk($query = '', $onlyUnresolved = false, $beforeId = 0, $limit = 500)
|
||||
{
|
||||
$query = trim((string) $query);
|
||||
$limit = max(1, min(1000, (int) $limit));
|
||||
$where = array();
|
||||
$args = array();
|
||||
if ($query !== '') {
|
||||
$withIp = self::hasRequestIpColumn();
|
||||
$where[] = $withIp
|
||||
? '(path LIKE %ss OR referer LIKE %ss OR user_agent LIKE %ss OR request_ip LIKE %ss)'
|
||||
: '(path LIKE %ss OR referer LIKE %ss OR user_agent LIKE %ss)';
|
||||
$args = array_fill(0, $withIp ? 4 : 3, $query);
|
||||
}
|
||||
|
||||
if ($onlyUnresolved) { $where[] = 'resolved = 0'; }
|
||||
if ((int) $beforeId > 0) {
|
||||
$where[] = 'id < %i';
|
||||
$args[] = (int) $beforeId;
|
||||
}
|
||||
|
||||
$sql = 'SELECT * FROM `' . self::table() . '`';
|
||||
if ($where) { $sql .= ' WHERE ' . implode(' AND ', $where); }
|
||||
$sql .= ' ORDER BY id DESC LIMIT ' . $limit;
|
||||
return call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll() ?: array();
|
||||
}
|
||||
|
||||
public static function stats()
|
||||
{
|
||||
$row = DB::query(
|
||||
@@ -142,4 +195,10 @@
|
||||
DB::query('DELETE FROM `' . self::table() . '` WHERE resolved = 1');
|
||||
return (int) DB::affectedRows();
|
||||
}
|
||||
|
||||
public static function clearAll()
|
||||
{
|
||||
DB::query('DELETE FROM `' . self::table() . '`');
|
||||
return (int) DB::affectedRows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,28 @@
|
||||
) !== false;
|
||||
}
|
||||
|
||||
/** Private ranges allowed only for an explicitly trusted local endpoint. */
|
||||
public static function isPrivateNetworkIp($ip)
|
||||
{
|
||||
$ip = (string) $ip;
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$long = ip2long($ip);
|
||||
if ($long === false) { return false; }
|
||||
$long = (float) sprintf('%u', $long);
|
||||
return ($long >= 167772160 && $long <= 184549375)
|
||||
|| ($long >= 2886729728 && $long <= 2887778303)
|
||||
|| ($long >= 3232235520 && $long <= 3232301055);
|
||||
}
|
||||
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$packed = @inet_pton($ip);
|
||||
return is_string($packed) && strlen($packed) === 16
|
||||
&& (ord($packed[0]) & 0xfe) === 0xfc;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function request($method, $url, $body, array $options)
|
||||
{
|
||||
if (!function_exists('curl_init')) {
|
||||
@@ -275,8 +297,9 @@
|
||||
throw new \RuntimeException('Удалённый хост не имеет доступного адреса');
|
||||
}
|
||||
|
||||
$trustedPrivateHost = in_array($host, $options['allowed_private_hosts'], true);
|
||||
foreach ($ips as $ip) {
|
||||
if (!self::isPublicIp($ip)) {
|
||||
if (!self::isPublicIp($ip) && (!$trustedPrivateHost || !self::isPrivateNetworkIp($ip))) {
|
||||
throw new \RuntimeException('Удалённый хост указывает на private или reserved сеть');
|
||||
}
|
||||
}
|
||||
@@ -420,6 +443,7 @@
|
||||
'source' => 'runtime',
|
||||
'download_path' => '',
|
||||
'allow_http_errors' => false,
|
||||
'allowed_private_hosts' => array(),
|
||||
), $options);
|
||||
$options['allowed_schemes'] = array_values(array_intersect(array('http', 'https'), array_map('strtolower', (array) $options['allowed_schemes'])));
|
||||
$options['allowed_ports'] = array_values(array_unique(array_map('intval', (array) $options['allowed_ports'])));
|
||||
@@ -429,6 +453,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['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']))));
|
||||
$options['headers'] = self::headers((array) $options['headers']);
|
||||
$options['source'] = substr(
|
||||
preg_replace('/[^a-z0-9_.-]+/', '_', strtolower(trim((string) $options['source']))),
|
||||
|
||||
@@ -21,18 +21,26 @@
|
||||
/** Shared defensive headers for public and administrative responses. */
|
||||
class ResponseSecurityHeaders
|
||||
{
|
||||
public static function all()
|
||||
public static function all(array $extraConnectSources = array())
|
||||
{
|
||||
$connectSources = "'self' https: wss:";
|
||||
foreach ($extraConnectSources as $source) {
|
||||
$source = trim((string) $source);
|
||||
if ($source !== '' && preg_match('#^https?://(?:localhost|127\.0\.0\.1)(?::\*)?$#i', $source)) {
|
||||
$connectSources .= ' ' . $source;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'X-Content-Type-Options: nosniff',
|
||||
'X-Frame-Options: SAMEORIGIN',
|
||||
'Referrer-Policy: strict-origin-when-cross-origin',
|
||||
'Permissions-Policy: camera=(), microphone=(), geolocation=()',
|
||||
"Content-Security-Policy: default-src 'self' data: blob: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: blob: http: https:; font-src 'self' data: https:; connect-src 'self' https: wss:; frame-src 'self' https:",
|
||||
"Content-Security-Policy: default-src 'self' data: blob: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: blob: http: https:; font-src 'self' data: https:; connect-src " . $connectSources . "; frame-src 'self' https:",
|
||||
);
|
||||
}
|
||||
|
||||
public static function apply()
|
||||
public static function apply(array $extraConnectSources = array())
|
||||
{
|
||||
if (function_exists('header_remove')) {
|
||||
@header_remove('X-Powered-By');
|
||||
@@ -43,6 +51,6 @@
|
||||
@header_remove('X-Engine-Site');
|
||||
}
|
||||
|
||||
Request::setHeaders(self::all());
|
||||
Request::setHeaders(self::all($extraConnectSources));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user