release: AVE.cms 3.3 build 0.41

This commit is contained in:
2026-09-07 07:11:28 +03:00
parent 95c92e7142
commit 5dad4a1cf1
136 changed files with 3784 additions and 1241 deletions
+39 -1
View File
@@ -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
View File
@@ -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",
+40
View File
@@ -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();
+97 -2
View File
@@ -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
View File
@@ -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 : ''),
);
}
}
+3 -2
View File
@@ -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'),
+1
View File
@@ -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'),
+81 -2
View File
@@ -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);
+48
View File
@@ -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
View File
@@ -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()) {
+2
View File
@@ -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>
+2
View File
@@ -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>
+47 -4
View File
@@ -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,
);
}
}
+130 -22
View File
@@ -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);
}
}
+109 -3
View File
@@ -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">&lt;div class=&quot;alert alert-error&quot;&gt;&lt;i class=&quot;ti ti-alert-triangle&quot;&gt;&lt;/i&gt;&lt;div&gt;Errors:</phrase>
<phrase data="auto_a456fd20d54a422c">&lt;/p&gt;&lt;/div&gt;&lt;button class=&quot;modal-close&quot; type=&quot;button&quot; data-catalog-recompile-close aria-label=&quot;Close&quot; disabled&gt;&lt;i class=&quot;ti ti-x&quot;&gt;&lt;/i&gt;&lt;/button&gt;&lt;/div&gt;&lt;div class=&quot;modal-body&quot;&gt;&lt;div class=&quot;catalog-recompile-status&quot; data-catalog-recompile-status&gt;Preparing...&lt;/div&gt;&lt;div class=&quot;progress&quot;&gt;&lt;div class=&quot;progress-bar&quot; data-catalog-recompile-progress style=&quot;width:0%;--progress-color:var(--blue-500)&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;catalog-recompile-errors&quot; data-catalog-recompile-errors&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;modal-footer&quot;&gt;&lt;button class=&quot;btn btn-ghost&quot; type=&quot;button&quot; data-catalog-recompile-stop&gt;Stop&lt;/button&gt;&lt;button class=&quot;btn btn-primary&quot; type=&quot;button&quot; data-catalog-recompile-close disabled&gt;Close&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;</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">&lt;div class=&quot;alert alert-error&quot;&gt;&lt;i class=&quot;ti ti-alert-triangle&quot;&gt;&lt;/i&gt;&lt;div&gt;Ошибки:</phrase>
<phrase data="auto_a456fd20d54a422c">&lt;/p&gt;&lt;/div&gt;&lt;button class=&quot;modal-close&quot; type=&quot;button&quot; data-catalog-recompile-close aria-label=&quot;Закрыть&quot; disabled&gt;&lt;i class=&quot;ti ti-x&quot;&gt;&lt;/i&gt;&lt;/button&gt;&lt;/div&gt;&lt;div class=&quot;modal-body&quot;&gt;&lt;div class=&quot;catalog-recompile-status&quot; data-catalog-recompile-status&gt;Подготовка...&lt;/div&gt;&lt;div class=&quot;progress&quot;&gt;&lt;div class=&quot;progress-bar&quot; data-catalog-recompile-progress style=&quot;width:0%;--progress-color:var(--blue-500)&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;catalog-recompile-errors&quot; data-catalog-recompile-errors&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;modal-footer&quot;&gt;&lt;button class=&quot;btn btn-ghost&quot; type=&quot;button&quot; data-catalog-recompile-stop&gt;Остановить&lt;/button&gt;&lt;button class=&quot;btn btn-primary&quot; type=&quot;button&quot; data-catalog-recompile-close disabled&gt;Закрыть&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;</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;
};
+3 -1
View File
@@ -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 }}&amp;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 %}&amp;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 }}&amp;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>
+4 -3
View File
@@ -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>
+8 -1
View File
@@ -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';
}
+2 -2
View File
@@ -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',
+1 -1
View File
@@ -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>
+12 -6
View File
@@ -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');}
}
+4 -4
View File
@@ -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));
+32 -13
View File
@@ -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);
+1 -1
View File
@@ -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',
+3 -3
View File
@@ -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>
+77 -8
View File
@@ -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) {
+28 -9
View File
@@ -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);
}
+7 -2
View File
@@ -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));
+45 -5
View File
@@ -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);
}
+7 -3
View File
@@ -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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm documents-media-remove&quot; type=&quot;button&quot; data-document-media-remove data-tooltip=&quot;Delete&quot; aria-label=&quot;Delete&quot;&gt;&lt;i class=&quot;ti ti-trash&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;div class=&quot;modal-body&quot;&gt;&lt;div class=&quot;input-wrap documents-relation-search&quot;&gt;&lt;i class=&quot;ti ti-search&quot;&gt;&lt;/i&gt;&lt;input class=&quot;input&quot; type=&quot;search&quot; placeholder=&quot;ID, title or alias&quot; data-relation-search&gt;&lt;/div&gt;&lt;div class=&quot;documents-picker-status&quot; data-relation-status&gt;Loading...&lt;/div&gt;&lt;div class=&quot;documents-relation-list&quot; data-relation-list&gt;&lt;/div&gt;&lt;/div&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-down data-tooltip=&quot;Below&quot; aria-label=&quot;Below&quot;&gt;&lt;i class=&quot;ti ti-arrow-down&quot;&gt;&lt;/i&gt;&lt;/button&gt;</phrase>
<phrase data="auto_22dc89ab84f07324">&quot;&gt;&lt;button class=&quot;btn btn-secondary btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-pick data-tooltip=&quot;Select file&quot; aria-label=&quot;Select file&quot;&gt;&lt;i class=&quot;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">&lt;section class=&quot;documents-revision-system&quot;&gt;&lt;div class=&quot;documents-revision-subhead&quot;&gt;&lt;i class=&quot;ti ti-settings&quot;&gt;&lt;/i&gt;&lt;b&gt;Basic settings&lt;/b&gt;&lt;span&gt;</phrase>
<phrase data="auto_292688e9915e161b">[link]&quot; data-media-key=&quot;link&quot; value=&quot;&quot; placeholder=&quot;Link or document&quot; data-document-media-url data-document-picker-type=&quot;all&quot;&gt;</phrase>
<phrase data="auto_29f3b29df0d0e54f">Restore</phrase>
<phrase data="auto_2a9790fd6fab2922">&lt;button class=&quot;btn btn-secondary btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-doc-pick data-tooltip=&quot;Select document&quot; aria-label=&quot;Select document&quot;&gt;&lt;i class=&quot;ti ti-file-search&quot;&gt;&lt;/i&gt;&lt;/button&gt;&lt;/div&gt;</phrase>
<phrase data="auto_2ba58b00b4c52021">&lt;/span&gt;&lt;label class=&quot;documents-revision-group-check&quot;&gt;&lt;input type=&quot;checkbox&quot; data-document-revision-group=&quot;field&quot; checked&gt;&lt;span&gt;All&lt;/span&gt;&lt;/label&gt;&lt;/div&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-up data-tooltip=&quot;Above&quot; aria-label=&quot;Above&quot;&gt;&lt;i class=&quot;ti ti-arrow-up&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;span class=&quot;badge badge-gray&quot;&gt;not created&lt;/span&gt;</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">&lt;div&gt;&lt;dt&gt;File&lt;/dt&gt;&lt;dd class=&quot;mono&quot;&gt;</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">&lt;button class=&quot;btn btn-secondary btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-pick data-tooltip=&quot;Select file&quot; aria-label=&quot;Select file&quot;&gt;&lt;i class=&quot;ti ti-paperclip&quot;&gt;&lt;/i&gt;&lt;/button&gt;</phrase>
<phrase data="auto_600363a73de5f3a5">Rebuild</phrase>
<phrase data="auto_60512114c18fd5ed">&lt;span class=&quot;badge badge-amber&quot;&gt;needs reassembly&lt;/span&gt;</phrase>
<phrase data="auto_619742ac4a834c45">doc.</phrase>
<phrase data="auto_63b1fa390e91f75c">0 B</phrase>
<phrase data="auto_65b7f1673713a00c">d</phrase>
<phrase data="auto_66d207b9ebe9e4e3">&lt;div class=&quot;documents-term-status is-error&quot;&gt;&lt;i class=&quot;ti ti-alert-circle&quot;&gt;&lt;/i&gt;&lt;span&gt;Failed to load options&lt;/span&gt;&lt;/div&gt;</phrase>
<phrase data="auto_6702cc12ca25aff1">&lt;div class=&quot;empty-state&quot;&gt;There are no revisions yet. The first snapshot will appear after saving the document.&lt;/div&gt;</phrase>
<phrase data="auto_691245657a52bdce">&lt;div&gt;&lt;label class=&quot;documents-revision-check&quot; aria-label=&quot;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 &quot;</phrase>
<phrase data="auto_6e52ded051aa402d">Withdrawn</phrase>
<phrase data="auto_6f658622a374b8d6">&lt;/p&gt;&lt;/div&gt;&lt;button class=&quot;modal-close&quot; type=&quot;button&quot; data-relation-close aria-label=&quot;Close&quot;&gt;&lt;i class=&quot;ti ti-x&quot;&gt;&lt;/i&gt;&lt;/button&gt;&lt;/div&gt;</phrase>
<phrase data="auto_6fe63eee472a5a93">&lt;span class=&quot;badge badge-green&quot;&gt;relevant&lt;/span&gt;</phrase>
<phrase data="auto_707a724dce1fa0d4">URL copied</phrase>
<phrase data="auto_72c618d46424c93a">There are no saved values yet</phrase>
<phrase data="auto_736a75710565c2f2">[description]&quot; data-media-key=&quot;description&quot; rows=&quot;2&quot; placeholder=&quot;Description&quot;&gt;&lt;/textarea&gt;</phrase>
<phrase data="auto_75285ce97dc1dc97">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm&quot; type=&quot;button&quot; data-document-value-down data-tooltip=&quot;Below&quot; aria-label=&quot;Below&quot;&gt;&lt;i class=&quot;ti ti-arrow-down&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;div&gt;&lt;dt&gt;State&lt;/dt&gt;&lt;dd&gt;</phrase>
<phrase data="auto_83c8e08533872aff">&lt;div class=&quot;empty-state&quot;&gt;Loading...&lt;/div&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm&quot; type=&quot;button&quot; data-document-value-up data-tooltip=&quot;Above&quot; aria-label=&quot;Above&quot;&gt;&lt;i class=&quot;ti ti-arrow-up&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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]&quot; data-media-key=&quot;title&quot; value=&quot;&quot; placeholder=&quot;Image title&quot;&gt;</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">&lt;div&gt;&lt;dt&gt;Formed by&lt;/dt&gt;&lt;dd&gt;</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">&lt;div&gt;&lt;dt&gt;Size&lt;/dt&gt;&lt;dd&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm documents-drag-handle&quot; type=&quot;button&quot; data-doc-drag draggable=&quot;true&quot; data-tooltip=&quot;Drag&quot; aria-label=&quot;Drag&quot;&gt;&lt;i class=&quot;ti ti-grip-vertical&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;div&gt;&lt;dt&gt;Fields&lt;/dt&gt;&lt;dd class=&quot;mono&quot;&gt;</phrase>
<phrase data="auto_b2ac54889c1e667e">Batch rebuild stopped</phrase>
<phrase data="auto_b36631e2d2874351">&lt;article class=&quot;documents-revision-field documents-revision-system-field&quot;&gt;&lt;div&gt;&lt;label class=&quot;documents-revision-check&quot; aria-label=&quot;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]&quot; data-media-key=&quot;name&quot; value=&quot;&quot; placeholder=&quot;File name&quot;&gt;</phrase>
<phrase data="auto_be4a59e132257571">Revoke an API token?</phrase>
<phrase data="auto_be7ec45710267c5c">&lt;section class=&quot;documents-revision-content&quot;&gt;&lt;div class=&quot;documents-revision-subhead&quot;&gt;&lt;i class=&quot;ti ti-forms&quot;&gt;&lt;/i&gt;&lt;b&gt;Category fields&lt;/b&gt;&lt;span&gt;</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 &quot;</phrase>
<phrase data="auto_cccd51a8c6aa88c1">&lt;div class=&quot;documents-term-status&quot;&gt;&lt;i class=&quot;ti ti-loader-2&quot;&gt;&lt;/i&gt;&lt;span&gt;Looking for matches...&lt;/span&gt;&lt;/div&gt;</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">&lt;div&gt;&lt;dt&gt;Status&lt;/dt&gt;&lt;dd&gt;Loading...&lt;/dd&gt;&lt;/div&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm documents-value-remove&quot; type=&quot;button&quot; data-document-value-remove data-tooltip=&quot;Delete&quot; aria-label=&quot;Delete&quot;&gt;&lt;i class=&quot;ti ti-trash&quot;&gt;&lt;/i&gt;&lt;/button&gt;</phrase>
<phrase data="auto_e4cedbc81a3c0d4e">ъ</phrase>
<phrase data="auto_e576260a6a03f359">t</phrase>
<phrase data="auto_e76f1ce35ab97dfb">&lt;/span&gt;&lt;label class=&quot;documents-revision-group-check&quot;&gt;&lt;input type=&quot;checkbox&quot; data-document-revision-group=&quot;document&quot; checked&gt;&lt;span&gt;All&lt;/span&gt;&lt;/label&gt;&lt;/div&gt;</phrase>
<phrase data="auto_e83e7fbf2312ab9e">The document has already been changed. Refresh the page.</phrase>
<phrase data="auto_e94209605c9c0a4e">&lt;button class=&quot;documents-media-thumb&quot; type=&quot;button&quot; data-document-media-pick aria-label=&quot;Select file&quot;&gt;&lt;span&gt;&lt;i class=&quot;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">&lt;div class=&quot;modal-footer&quot;&gt;&lt;div class=&quot;mf-left documents-picker-count&quot; data-relation-count&gt;&lt;/div&gt;&lt;button class=&quot;btn btn-ghost&quot; type=&quot;button&quot; data-relation-close&gt;Close&lt;/button&gt;&lt;/div&gt;</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">&lt;div class=&quot;modal-header&quot;&gt;&lt;span class=&quot;dialog-icon info&quot;&gt;&lt;i class=&quot;ti ti-file-search&quot;&gt;&lt;/i&gt;&lt;/span&gt;&lt;div style=&quot;flex:1&quot;&gt;&lt;h3&gt;Select document&lt;/h3&gt;&lt;p class=&quot;text-secondary&quot; style=&quot;margin-top:4px&quot;&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm documents-media-remove&quot; type=&quot;button&quot; data-document-media-remove data-tooltip=&quot;Удалить&quot; aria-label=&quot;Удалить&quot;&gt;&lt;i class=&quot;ti ti-trash&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;div class=&quot;modal-body&quot;&gt;&lt;div class=&quot;input-wrap documents-relation-search&quot;&gt;&lt;i class=&quot;ti ti-search&quot;&gt;&lt;/i&gt;&lt;input class=&quot;input&quot; type=&quot;search&quot; placeholder=&quot;ID, название или alias&quot; data-relation-search&gt;&lt;/div&gt;&lt;div class=&quot;documents-picker-status&quot; data-relation-status&gt;Загрузка...&lt;/div&gt;&lt;div class=&quot;documents-relation-list&quot; data-relation-list&gt;&lt;/div&gt;&lt;/div&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-down data-tooltip=&quot;Ниже&quot; aria-label=&quot;Ниже&quot;&gt;&lt;i class=&quot;ti ti-arrow-down&quot;&gt;&lt;/i&gt;&lt;/button&gt;</phrase>
<phrase data="auto_22dc89ab84f07324">&quot;&gt;&lt;button class=&quot;btn btn-secondary btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-pick data-tooltip=&quot;Выбрать файл&quot; aria-label=&quot;Выбрать файл&quot;&gt;&lt;i class=&quot;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">&lt;section class=&quot;documents-revision-system&quot;&gt;&lt;div class=&quot;documents-revision-subhead&quot;&gt;&lt;i class=&quot;ti ti-settings&quot;&gt;&lt;/i&gt;&lt;b&gt;Основные настройки&lt;/b&gt;&lt;span&gt;</phrase>
<phrase data="auto_292688e9915e161b">[link]&quot; data-media-key=&quot;link&quot; value=&quot;&quot; placeholder=&quot;Ссылка или документ&quot; data-document-media-url data-document-picker-type=&quot;all&quot;&gt;</phrase>
<phrase data="auto_29f3b29df0d0e54f">Восстановить</phrase>
<phrase data="auto_2a9790fd6fab2922">&lt;button class=&quot;btn btn-secondary btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-doc-pick data-tooltip=&quot;Выбрать документ&quot; aria-label=&quot;Выбрать документ&quot;&gt;&lt;i class=&quot;ti ti-file-search&quot;&gt;&lt;/i&gt;&lt;/button&gt;&lt;/div&gt;</phrase>
<phrase data="auto_2ba58b00b4c52021">&lt;/span&gt;&lt;label class=&quot;documents-revision-group-check&quot;&gt;&lt;input type=&quot;checkbox&quot; data-document-revision-group=&quot;field&quot; checked&gt;&lt;span&gt;Все&lt;/span&gt;&lt;/label&gt;&lt;/div&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-up data-tooltip=&quot;Выше&quot; aria-label=&quot;Выше&quot;&gt;&lt;i class=&quot;ti ti-arrow-up&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;span class=&quot;badge badge-gray&quot;&gt;не создан&lt;/span&gt;</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">&lt;div&gt;&lt;dt&gt;Файл&lt;/dt&gt;&lt;dd class=&quot;mono&quot;&gt;</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">&lt;button class=&quot;btn btn-secondary btn-icon btn-sm&quot; type=&quot;button&quot; data-document-media-pick data-tooltip=&quot;Выбрать файл&quot; aria-label=&quot;Выбрать файл&quot;&gt;&lt;i class=&quot;ti ti-paperclip&quot;&gt;&lt;/i&gt;&lt;/button&gt;</phrase>
<phrase data="auto_600363a73de5f3a5">Пересобрать</phrase>
<phrase data="auto_60512114c18fd5ed">&lt;span class=&quot;badge badge-amber&quot;&gt;нужна пересборка&lt;/span&gt;</phrase>
<phrase data="auto_619742ac4a834c45">док.</phrase>
<phrase data="auto_63b1fa390e91f75c">0 Б</phrase>
<phrase data="auto_65b7f1673713a00c">д</phrase>
<phrase data="auto_66d207b9ebe9e4e3">&lt;div class=&quot;documents-term-status is-error&quot;&gt;&lt;i class=&quot;ti ti-alert-circle&quot;&gt;&lt;/i&gt;&lt;span&gt;Не удалось загрузить варианты&lt;/span&gt;&lt;/div&gt;</phrase>
<phrase data="auto_6702cc12ca25aff1">&lt;div class=&quot;empty-state&quot;&gt;Ревизий пока нет. Первый снимок появится после сохранения документа.&lt;/div&gt;</phrase>
<phrase data="auto_691245657a52bdce">&lt;div&gt;&lt;label class=&quot;documents-revision-check&quot; aria-label=&quot;Восстановить</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">&lt;/p&gt;&lt;/div&gt;&lt;button class=&quot;modal-close&quot; type=&quot;button&quot; data-relation-close aria-label=&quot;Закрыть&quot;&gt;&lt;i class=&quot;ti ti-x&quot;&gt;&lt;/i&gt;&lt;/button&gt;&lt;/div&gt;</phrase>
<phrase data="auto_6fe63eee472a5a93">&lt;span class=&quot;badge badge-green&quot;&gt;актуален&lt;/span&gt;</phrase>
<phrase data="auto_707a724dce1fa0d4">URL скопирован</phrase>
<phrase data="auto_72c618d46424c93a">Сохранённых значений пока нет</phrase>
<phrase data="auto_736a75710565c2f2">[description]&quot; data-media-key=&quot;description&quot; rows=&quot;2&quot; placeholder=&quot;Описание&quot;&gt;&lt;/textarea&gt;</phrase>
<phrase data="auto_75285ce97dc1dc97">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm&quot; type=&quot;button&quot; data-document-value-down data-tooltip=&quot;Ниже&quot; aria-label=&quot;Ниже&quot;&gt;&lt;i class=&quot;ti ti-arrow-down&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;div&gt;&lt;dt&gt;Состояние&lt;/dt&gt;&lt;dd&gt;</phrase>
<phrase data="auto_83c8e08533872aff">&lt;div class=&quot;empty-state&quot;&gt;Загрузка...&lt;/div&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm&quot; type=&quot;button&quot; data-document-value-up data-tooltip=&quot;Выше&quot; aria-label=&quot;Выше&quot;&gt;&lt;i class=&quot;ti ti-arrow-up&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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]&quot; data-media-key=&quot;title&quot; value=&quot;&quot; placeholder=&quot;Заголовок изображения&quot;&gt;</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">&lt;div&gt;&lt;dt&gt;Сформирован&lt;/dt&gt;&lt;dd&gt;</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">&lt;div&gt;&lt;dt&gt;Размер&lt;/dt&gt;&lt;dd&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm documents-drag-handle&quot; type=&quot;button&quot; data-doc-drag draggable=&quot;true&quot; data-tooltip=&quot;Перетащить&quot; aria-label=&quot;Перетащить&quot;&gt;&lt;i class=&quot;ti ti-grip-vertical&quot;&gt;&lt;/i&gt;&lt;/button&gt;</phrase>
<phrase data="auto_b0a419827426861b">Пресет удалён</phrase>
<phrase data="auto_b0d827d33fc8eb3f">Снимки</phrase>
<phrase data="auto_b1c30d9335b4ffb7">/ + alias документа. Дата берётся из публикации.</phrase>
<phrase data="auto_b279170b217c1e48">&lt;div&gt;&lt;dt&gt;Поля&lt;/dt&gt;&lt;dd class=&quot;mono&quot;&gt;</phrase>
<phrase data="auto_b2ac54889c1e667e">Пакетная пересборка остановлена</phrase>
<phrase data="auto_b36631e2d2874351">&lt;article class=&quot;documents-revision-field documents-revision-system-field&quot;&gt;&lt;div&gt;&lt;label class=&quot;documents-revision-check&quot; aria-label=&quot;Восстановить</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]&quot; data-media-key=&quot;name&quot; value=&quot;&quot; placeholder=&quot;Название файла&quot;&gt;</phrase>
<phrase data="auto_be4a59e132257571">Отозвать API-токен?</phrase>
<phrase data="auto_be7ec45710267c5c">&lt;section class=&quot;documents-revision-content&quot;&gt;&lt;div class=&quot;documents-revision-subhead&quot;&gt;&lt;i class=&quot;ti ti-forms&quot;&gt;&lt;/i&gt;&lt;b&gt;Поля рубрики&lt;/b&gt;&lt;span&gt;</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">&lt;div class=&quot;documents-term-status&quot;&gt;&lt;i class=&quot;ti ti-loader-2&quot;&gt;&lt;/i&gt;&lt;span&gt;Ищем совпадения…&lt;/span&gt;&lt;/div&gt;</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">&lt;div&gt;&lt;dt&gt;Состояние&lt;/dt&gt;&lt;dd&gt;Загрузка...&lt;/dd&gt;&lt;/div&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm documents-value-remove&quot; type=&quot;button&quot; data-document-value-remove data-tooltip=&quot;Удалить&quot; aria-label=&quot;Удалить&quot;&gt;&lt;i class=&quot;ti ti-trash&quot;&gt;&lt;/i&gt;&lt;/button&gt;</phrase>
<phrase data="auto_e4cedbc81a3c0d4e">ъ</phrase>
<phrase data="auto_e576260a6a03f359">т</phrase>
<phrase data="auto_e76f1ce35ab97dfb">&lt;/span&gt;&lt;label class=&quot;documents-revision-group-check&quot;&gt;&lt;input type=&quot;checkbox&quot; data-document-revision-group=&quot;document&quot; checked&gt;&lt;span&gt;Все&lt;/span&gt;&lt;/label&gt;&lt;/div&gt;</phrase>
<phrase data="auto_e83e7fbf2312ab9e">Документ уже изменён. Обновите страницу.</phrase>
<phrase data="auto_e94209605c9c0a4e">&lt;button class=&quot;documents-media-thumb&quot; type=&quot;button&quot; data-document-media-pick aria-label=&quot;Выбрать файл&quot;&gt;&lt;span&gt;&lt;i class=&quot;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">&lt;div class=&quot;modal-footer&quot;&gt;&lt;div class=&quot;mf-left documents-picker-count&quot; data-relation-count&gt;&lt;/div&gt;&lt;button class=&quot;btn btn-ghost&quot; type=&quot;button&quot; data-relation-close&gt;Закрыть&lt;/button&gt;&lt;/div&gt;</phrase>
<phrase data="auto_f61de80bf0ca15a4">Документ не выбран</phrase>
<phrase data="auto_f7286439b109e428">Новый редирект</phrase>
<phrase data="auto_f7806fce80e51b40">Старый URL</phrase>
<phrase data="auto_f914c65278294e4a">&lt;div class=&quot;modal-header&quot;&gt;&lt;span class=&quot;dialog-icon info&quot;&gt;&lt;i class=&quot;ti ti-file-search&quot;&gt;&lt;/i&gt;&lt;/span&gt;&lt;div style=&quot;flex:1&quot;&gt;&lt;h3&gt;Выбрать документ&lt;/h3&gt;&lt;p class=&quot;text-secondary&quot; style=&quot;margin-top:4px&quot;&gt;</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`);
+2 -1
View File
@@ -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(
+26 -7
View File
@@ -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>
+5 -1
View File
@@ -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>
+3 -2
View File
@@ -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>
+140 -8
View File
@@ -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,
+2
View File
@@ -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');
}
},
+2 -2
View File
@@ -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',
),
+12 -12
View File
@@ -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">&quot; will be deleted along with the assignment of rights.</phrase>
<phrase data="auto_5ea67f9e32546482">Role &quot;</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);
+1 -1
View File
@@ -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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm navigation-action-level&quot; type=&quot;button&quot; data-navigation-item-indent=&quot;1&quot; data-tooltip=&quot;Increase level&quot; aria-label=&quot;Increase level&quot;&gt;&lt;i class=&quot;ti ti-indent-increase&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm navigation-action-level&quot; type=&quot;button&quot; data-navigation-item-indent=&quot;-1&quot; data-tooltip=&quot;Decrease level&quot; aria-label=&quot;Decrease level&quot;&gt;&lt;i class=&quot;ti ti-indent-decrease&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&quot; data-tooltip=&quot;On/Off&quot; aria-label=&quot;On/Off&quot;&gt;&lt;i class=&quot;ti ti-power&quot;&gt;&lt;/i&gt;&lt;/button&gt;</phrase>
<phrase data="auto_d32b8258ca6f9d3e">&quot; data-tooltip=&quot;Change&quot; aria-label=&quot;Change&quot;&gt;&lt;i class=&quot;ti ti-pencil&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm navigation-action-level&quot; type=&quot;button&quot; data-navigation-item-indent=&quot;1&quot; data-tooltip=&quot;Увеличить уровень&quot; aria-label=&quot;Увеличить уровень&quot;&gt;&lt;i class=&quot;ti ti-indent-increase&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&lt;button class=&quot;btn btn-ghost btn-icon btn-sm navigation-action-level&quot; type=&quot;button&quot; data-navigation-item-indent=&quot;-1&quot; data-tooltip=&quot;Уменьшить уровень&quot; aria-label=&quot;Уменьшить уровень&quot;&gt;&lt;i class=&quot;ti ti-indent-decrease&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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">&quot; data-tooltip=&quot;Вкл/выкл&quot; aria-label=&quot;Вкл/выкл&quot;&gt;&lt;i class=&quot;ti ti-power&quot;&gt;&lt;/i&gt;&lt;/button&gt;</phrase>
<phrase data="auto_d32b8258ca6f9d3e">&quot; data-tooltip=&quot;Изменить&quot; aria-label=&quot;Изменить&quot;&gt;&lt;i class=&quot;ti ti-pencil&quot;&gt;&lt;/i&gt;&lt;/button&gt;</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') || ''; },
+1 -1
View File
@@ -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 &quot;</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 &quot;</phrase>
<phrase data="auto_1199e46110c703d0">Checking requests</phrase>
<phrase data="auto_15782150a0e84091">&lt;div class=&quot;request-explain-options-state&quot;&gt;&lt;i class=&quot;ti ti-loader-2&quot;&gt;&lt;/i&gt;Search...&lt;/div&gt;</phrase>
<phrase data="auto_189d15bc7eec18dd">Comparison</phrase>
<phrase data="auto_19114f713f604d3e">Material</phrase>
<phrase data="auto_1c4bc45438abb81c">Field &quot;</phrase>
<phrase data="auto_1e1b70b17c42674c">Nothing found</phrase>
<phrase data="auto_1f1437928ef434ed">Delete condition?</phrase>
<phrase data="auto_27cb45d3018b58de">&lt;div class=&quot;request-explain-tree-empty&quot;&gt;There are no active conditions in the group&lt;/div&gt;</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 &quot;</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">&lt;div class=&quot;request-explain-options-state&quot;&gt;&lt;i class=&quot;ti ti-file-off&quot;&gt;&lt;/i&gt;Documents not found&lt;/div&gt;</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">&lt;span class=&quot;request-explain-condition-state&quot;&gt;&lt;i class=&quot;ti&quot;&gt;&lt;/i&gt;&lt;/span&gt;&lt;div class=&quot;request-explain-condition-copy&quot;&gt;&lt;div&gt;&lt;b&gt;&lt;/b&gt;&lt;code&gt;&lt;/code&gt;&lt;/div&gt;&lt;dl&gt;&lt;div&gt;&lt;dt&gt;Actually&lt;/dt&gt;&lt;dd data-actual&gt;&lt;/dd&gt;&lt;/div&gt;&lt;div&gt;&lt;dt&gt;Condition&lt;/dt&gt;&lt;dd data-expected&gt;&lt;/dd&gt;&lt;/div&gt;&lt;/dl&gt;&lt;p hidden&gt;&lt;/p&gt;&lt;/div&gt;</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 &quot;</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">&lt;i class=&quot;ti ti-loader-2&quot;&gt;&lt;/i&gt;&lt;span&gt;Checking the request...&lt;/span&gt;</phrase>
<phrase data="auto_851f9c331eaf483e">One coincidence is enough</phrase>
<phrase data="auto_86ea33aef5e95d4f">Delete</phrase>
<phrase data="auto_87451e618697c593">&quot;, when the parameter &quot;</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 &quot;</phrase>
<phrase data="auto_b1a5a8acb3ed7df1">Request verified</phrase>
<phrase data="auto_b22f4c76befc0be0">parameter</phrase>
<phrase data="auto_b58e8d5d7248d75f">decimal number from parameter &quot;</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&apos;t start with</phrase>
<phrase data="auto_d39886d5753cc067">· checked states:</phrase>
<phrase data="auto_d52c691335394be0">an integer from the parameter &quot;</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">&lt;div class=&quot;request-explain-options-state is-error&quot;&gt;&lt;i class=&quot;ti ti-alert-circle&quot;&gt;&lt;/i&gt;Search unavailable&lt;/div&gt;</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">&lt;i class=&quot;ti ti-filter-off&quot;&gt;&lt;/i&gt;&lt;b&gt;Documents not found&lt;/b&gt;&lt;span&gt;Check the conditions, publication status and selected category.&lt;/span&gt;</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">&lt;div class=&quot;request-explain-options-state&quot;&gt;&lt;i class=&quot;ti ti-loader-2&quot;&gt;&lt;/i&gt;Поиск…&lt;/div&gt;</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">&lt;div class=&quot;request-explain-tree-empty&quot;&gt;В группе нет активных условий&lt;/div&gt;</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">&lt;div class=&quot;request-explain-options-state&quot;&gt;&lt;i class=&quot;ti ti-file-off&quot;&gt;&lt;/i&gt;Документы не найдены&lt;/div&gt;</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">&lt;span class=&quot;request-explain-condition-state&quot;&gt;&lt;i class=&quot;ti&quot;&gt;&lt;/i&gt;&lt;/span&gt;&lt;div class=&quot;request-explain-condition-copy&quot;&gt;&lt;div&gt;&lt;b&gt;&lt;/b&gt;&lt;code&gt;&lt;/code&gt;&lt;/div&gt;&lt;dl&gt;&lt;div&gt;&lt;dt&gt;Фактически&lt;/dt&gt;&lt;dd data-actual&gt;&lt;/dd&gt;&lt;/div&gt;&lt;div&gt;&lt;dt&gt;Условие&lt;/dt&gt;&lt;dd data-expected&gt;&lt;/dd&gt;&lt;/div&gt;&lt;/dl&gt;&lt;p hidden&gt;&lt;/p&gt;&lt;/div&gt;</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">&lt;i class=&quot;ti ti-loader-2&quot;&gt;&lt;/i&gt;&lt;span&gt;Проверяем запрос…&lt;/span&gt;</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">&lt;div class=&quot;request-explain-options-state is-error&quot;&gt;&lt;i class=&quot;ti ti-alert-circle&quot;&gt;&lt;/i&gt;Поиск недоступен&lt;/div&gt;</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">&lt;i class=&quot;ti ti-filter-off&quot;&gt;&lt;/i&gt;&lt;b&gt;Документы не найдены&lt;/b&gt;&lt;span&gt;Проверьте условия, статус публикации и выбранную рубрику.&lt;/span&gt;</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>
+7 -1
View File
@@ -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)
+11
View File
@@ -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`;
+2 -1
View File
@@ -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(
+5 -3
View File
@@ -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>&lt;meta property="og:image" content="[tag:rfld:photo][img]"&gt;</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>&lt;meta property="og:image" content="[tag:domain][tag:fld:ALIAS][img]"&gt;</code>. Версия, вписанная в 800 × 800 пикселей: <code>&lt;meta property="og:image" content="[tag:domain][tag:f800x800:[tag:fld:ALIAS][img]]"&gt;</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>
+46 -2
View File
@@ -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;
+12 -4
View File
@@ -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;
};
+12 -9
View File
@@ -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'),
);
+38 -15
View File
@@ -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>
+30
View File
@@ -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);
+3 -1
View File
@@ -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')),
+2 -2
View File
@@ -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
View File
@@ -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">
+75
View File
@@ -19,6 +19,54 @@
Если товарный модуль не установлен, интерфейс не должен предлагать товарные
сущности или пустой список «товарных каталогов».
## Назначение полей товара
Откройте товарный каталог и вкладку **Настройки → Назначение полей товара**.
Для каждой роли выберите поле этой рубрики: название, артикул, цену, остаток,
изображения, описание, гарантию, текст и номер регистрационного документа и
другие значения. **Автоматически** сохраняет совместимость со
старым alias; **Не назначено** отключает роль без переименования полей или
изменения документов. Дополнительные поля проекта необязательны. Колонки
программы оплаты остаются в стандартном быстром редакторе, если соответствующие
роли назначены; в обычной рубрике они не добавляются.
В блоке **Сохранённое сопоставление** укажите номер документа этой рубрики и
нажмите **Проверить**. Preview читает сохранённые значения и показывает поля,
цену, остаток и причины недоступности. Выбор образца не сохраняет настройки.
Дата публикации учитывается только при включённой общей настройке сайта.
Отсутствие дополнительного регистрационного поля не блокирует покупку;
исключение из прайса не означает запрет покупки.
Для конкретной выборки используйте **Проверка выборки**: выберите каталог и раздел,
область поиска и строку, фид либо исходный товар и лимит рекомендаций. Нажмите
**Проверить выборку**. Смена параметров и автосохранение ролей такую проверку не
запускают; прежний результат при изменении параметров скрывается.
Результат строится штатными репозиториями по сохранённым настройкам. Каталог
учитывает источники раздела и условия запроса; проверяется как сам вариант, так
и его группа. Отсутствие варианта среди отдельных карточек не означает отсутствие
группы. Скрытие пункта меню не исключает опубликованную SEO-страницу раздела
из проверки. Каталог и поиск проверяются до пагинации; посетительские фильтры
каталога не применяются. Поиск использует штатный pipeline с синонимами и
исправлением опечаток и показывает фактическую поисковую строку. При неоднозначном
исправлении замена не выбирается, как и на сайте. Статистика поиска не изменяется.
Рекомендации показывают
фактическую выдачу с её ранжированием, лимитом и кешем. Фид проверяет разрешённые
категории, публикацию, цену и исключение из прайса без генерации XML. Пустой набор
категорий не расширяется до всего каталога. Ошибка индекса означает незавершённую
проверку, а не отсутствие товара в выборке.
Просмотр значений требует прав на товары, документы и чтение рубрики; для
ограниченной роли учитывается группа связанной публичной учётной записи.
Дополнительно нужны права просмотра поиска, фидов или условий запроса для
соответствующей проверки. Ссылки из preview открывают штатные
редакторы полей, запроса, шаблонов, фильтров раздела и набора характеристик.
Шаблон карточки учитывает опубликованное представление и резервный вывод темы.
Сохранение изменённых ролей пересчитывает коммерческие значения индекса;
смена назначения на обычный каталог очищает только проекции, сохраняя исходные
характеристики, варианты и данные доставки. Включение товарного назначения
может занять время из-за полной индексации рубрики.
## Обзор товарного каталога
Страница **Каталог → Характеристики → Обзор** — начальная точка товарного
@@ -744,6 +792,33 @@ CommerceML: интеграции не должны писать прямо в т
## Безопасное изменение
### Связанные настройки товара и раздела
В штатном редакторе товара и боковой панели раздела блок **Связанные настройки**
ведёт к фильтрам раздела, набору характеристик, шаблону карточки в каталоге и
шаблону страницы. Ссылки открываются в отдельной вкладке, не закрывая редактор.
Показываются только доступные по вашим правам настройки. Для drawer требуется
право управления соответствующим разделом панели.
У товара наборы берутся из реально загруженных групп характеристик, без повторов.
Фильтры и карточки отмечены названием раздела, поскольку товар может находиться
в нескольких разделах. Для native-фильтров открывается редактор фильтров
характеристик; для прежних фильтров открывается вкладка «Фильтры» раздела.
У раздела шаблон страницы относится к **связанному документу**, который может
находиться в другой рубрике. Без связанного документа этой ссылки нет.
Опубликованное native-представление списка имеет приоритет над товарным шаблоном.
Без него ссылка ведёт к опубликованному шаблону карточки или штатному шаблону
активной темы. Пустой или отсутствующий альтернативный шаблон страницы означает
возврат к основному шаблону рубрики. Назначения отдельных подборок и запросов
могут переопределять вид карточки вне этого раздела.
Блок отражает **сохранённое** состояние: после смены шаблона или характеристик
сохраните товар и откройте редактор заново. У раздела ссылки обновляются после
сохранения; у нового раздела появляются после первого сохранения.
### Проверка изменений
1. Сначала меняйте структуру и поля в тестовом разделе.
2. Проверьте сохранение документа и повторное открытие редактора.
3. Пересоберите индекс и сравните товары, отдельные значения и диапазоны.
+72
View File
@@ -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.
+46
View File
@@ -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
+52
View File
@@ -423,6 +423,58 @@ Commerce также дополняет общий view model товарной к
Если сервис не выбран, способ считается оплатой без онлайн-перехода: например,
наличными, картой при получении или банковским переводом.
Повторное нажатие оплаты сначала проверяет уже начатый платёж. Если он ещё
ожидает подтверждения, покупатель получает прежнюю ссылку. При сетевой ошибке
повторный запрос использует тот же ключ, чтобы не создавать второй платёж.
Новая попытка создаётся после подтверждённой отмены предыдущей. Старые попытки
остаются связанными с заказом. Запоздавший ответ «ожидание» не снимает оплату.
Отменённый заказ оплатить нельзя. Изменять состав и сумму заказа с начатой
оплатой нельзя до проверки её результата. Сервис может требовать дополнительные
данные покупателя, например email: требования объявляет сам платёжный модуль.
В текущем оформлении и онлайн-оплате используется RUB; валюта фида не меняет
валюту заказа и не конвертирует цены.
## Назначение статусов
В `Магазин -> Настройки -> Статусы` у статуса есть название, цвет и назначение:
новый, в обработке, отправлен, выполнен, отменён или другой. Название можно
менять и переводить: поведение заказа определяется назначением, а не текстом.
Для собственного промежуточного статуса используйте «Другой». После обновления
проверьте назначения нестандартных статусов, особенно отмены и обработки.
## Проверка корзины перед заказом
Перед оформлением магазин заново проверяет публикацию и цену каждого товара.
Если цена изменилась, покупателю показывается пересчитанная корзина и требуется
повторное подтверждение. Недоступный товар нужно удалить из корзины. Повторная
отправка одного оформления возвращает созданный заказ, а не создаёт дубликат.
## Брошенные корзины и архив
Откройте `Магазин -> Заказы -> Брошенные корзины`. В фильтре состояния над
таблицей выберите **Архив с контактами**. Кнопка просмотра в строке открывает
сохранённые данные покупателя, состав корзины и сведения об источнике перехода,
включая UTM-метки, если они были переданы.
Через трое суток без активности корзины с сохранёнными данными переходят в архив
при очередной очистке. Кнопка **Обработать просроченные** запускает эту обработку
вручную. Архивные записи не удаляются по обычному сроку хранения корзин.
Блок **Путь до заказа** показывает сессии за последние 30 дней, включая
завершившиеся заказом. Он не ограничивается текущим фильтром таблицы.
«Заполняли контакты» учитывает отдельный факт ввода имени, фамилии, email или
телефона. Выбор доставки или оплаты не означает, что контакты уже заполнены;
пустая маска телефона также не считается контактом. Частично введённые данные
учитываются, поэтому этот показатель не означает число полностью заполненных
форм или доступных для звонка покупателей.
Страницы внизу таблицы позволяют просмотреть весь архив, сохраняя выбранные
фильтры и направление сортировки. Срок хранения определяется по активности
корзины целиком: старые позиции не исчезают по отдельности из активной корзины.
Если покупатель после заказа или архивирования снова добавляет товар, создаётся
новая корзина. Предыдущий снимок контактов и источников перехода не меняется.
## Публичные страницы
Адреса корзины, оформления, избранного, просмотренных товаров и личных заказов
+20
View File
@@ -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',
+9
View File
@@ -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' => 'Ошибка системной консоли',
+2 -2
View File
@@ -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();
+36 -3
View File
@@ -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,
+188 -3
View File
@@ -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)
+76 -17
View File
@@ -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();
}
}
+28 -1
View File
@@ -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']))),
+12 -4
View File
@@ -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