alchemy_cms 8.3.11 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +58 -10
  4. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  5. data/app/assets/builds/alchemy/admin/print.css +1 -1
  6. data/app/assets/builds/alchemy/admin/remote-select.css +1 -0
  7. data/app/assets/builds/alchemy/admin/tags-autocomplete.css +1 -0
  8. data/app/assets/builds/alchemy/admin.css +4 -4
  9. data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
  10. data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
  11. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  12. data/app/assets/builds/alchemy/light-theme.css +1 -1
  13. data/app/assets/builds/alchemy/preview.min.js +1 -1
  14. data/app/assets/builds/alchemy/theme.css +1 -1
  15. data/app/assets/builds/alchemy/welcome.css +1 -1
  16. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  17. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
  18. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  19. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
  20. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  21. data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +1 -1
  22. data/app/components/alchemy/admin/dashboard/widgets/news_reader.html.erb +38 -0
  23. data/app/components/alchemy/admin/dashboard/widgets/news_reader.rb +54 -0
  24. data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +1 -1
  25. data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +1 -1
  26. data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +11 -3
  27. data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +6 -1
  28. data/app/components/alchemy/admin/element_editor.html.erb +1 -1
  29. data/app/components/alchemy/admin/element_select.rb +30 -20
  30. data/app/components/alchemy/admin/element_status_indicators.html.erb +16 -0
  31. data/app/components/alchemy/admin/element_status_indicators.rb +31 -0
  32. data/app/components/alchemy/admin/link_dialog/external_tab.rb +2 -1
  33. data/app/components/alchemy/admin/main_navigation/entry.rb +61 -0
  34. data/app/components/alchemy/admin/main_navigation/menu.rb +21 -0
  35. data/app/components/alchemy/admin/main_navigation/sub_navigation.rb +31 -0
  36. data/app/components/alchemy/admin/page_node.html.erb +1 -1
  37. data/app/components/alchemy/admin/page_permitted_roles_select.html.erb +11 -0
  38. data/app/components/alchemy/admin/page_permitted_roles_select.rb +35 -0
  39. data/app/components/alchemy/admin/page_select.rb +18 -8
  40. data/app/components/alchemy/admin/preview_time_select.rb +1 -1
  41. data/app/components/alchemy/admin/toolbar_button.rb +43 -20
  42. data/app/components/alchemy/admin/uploader_button.rb +66 -0
  43. data/app/components/alchemy/ingredients/base_editor.rb +10 -1
  44. data/app/components/alchemy/ingredients/boolean_editor.rb +1 -0
  45. data/app/components/alchemy/ingredients/color_editor.rb +6 -3
  46. data/app/components/alchemy/ingredients/datetime_view.rb +1 -1
  47. data/app/components/alchemy/ingredients/picture_view.rb +2 -4
  48. data/app/components/alchemy/menubar.html.erb +157 -0
  49. data/app/components/alchemy/menubar.rb +15 -0
  50. data/app/controllers/alchemy/admin/attachments_controller.rb +12 -9
  51. data/app/controllers/alchemy/admin/base_controller.rb +34 -21
  52. data/app/controllers/alchemy/admin/dashboard_controller.rb +4 -0
  53. data/app/controllers/alchemy/admin/elements_controller.rb +4 -23
  54. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  55. data/app/controllers/alchemy/admin/nodes_controller.rb +1 -1
  56. data/app/controllers/alchemy/admin/pages_controller.rb +1 -1
  57. data/app/controllers/alchemy/admin/pictures_controller.rb +6 -3
  58. data/app/controllers/alchemy/admin/resources_controller.rb +5 -5
  59. data/app/controllers/alchemy/admin/sites_controller.rb +1 -1
  60. data/app/controllers/alchemy/base_controller.rb +24 -16
  61. data/app/controllers/alchemy/pages_controller.rb +29 -8
  62. data/app/controllers/alchemy/sitemap_controller.rb +58 -0
  63. data/app/controllers/concerns/alchemy/admin/csp_protection.rb +43 -0
  64. data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -1
  65. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +3 -1
  66. data/{lib → app/controllers/concerns}/alchemy/controller_actions.rb +13 -0
  67. data/app/controllers/concerns/alchemy/legacy_page_redirects.rb +4 -4
  68. data/app/helpers/alchemy/admin/base_helper.rb +4 -1
  69. data/app/helpers/alchemy/admin/form_helper.rb +0 -2
  70. data/app/helpers/alchemy/admin/navigation_helper.rb +10 -181
  71. data/app/helpers/alchemy/elements_block_helper.rb +14 -0
  72. data/app/helpers/alchemy/resources_helper.rb +15 -1
  73. data/app/javascript/alchemy_admin/components/attachment_select.js +11 -20
  74. data/app/javascript/alchemy_admin/components/auto_submit.js +2 -4
  75. data/app/javascript/alchemy_admin/components/button.js +0 -6
  76. data/app/javascript/alchemy_admin/components/clear_schedule_button.js +21 -0
  77. data/app/javascript/alchemy_admin/components/color_select.js +50 -45
  78. data/app/javascript/alchemy_admin/components/conditional_field.js +50 -0
  79. data/app/javascript/alchemy_admin/components/dirty_guard.js +77 -0
  80. data/app/javascript/alchemy_admin/components/dom_id_select.js +17 -10
  81. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -2
  82. data/app/javascript/alchemy_admin/components/element_editor.js +29 -56
  83. data/app/javascript/alchemy_admin/components/element_select.js +26 -66
  84. data/app/javascript/alchemy_admin/components/icon.js +9 -4
  85. data/app/javascript/alchemy_admin/{image_cropper.js → components/image_cropper.js} +39 -53
  86. data/app/javascript/alchemy_admin/components/image_overlay_link.js +14 -0
  87. data/app/javascript/alchemy_admin/components/index.js +12 -1
  88. data/app/javascript/alchemy_admin/components/main_navi_entry.js +75 -0
  89. data/app/javascript/alchemy_admin/components/menu_node.js +73 -0
  90. data/app/javascript/alchemy_admin/components/news_reader.js +109 -0
  91. data/app/javascript/alchemy_admin/components/node_folder.js +36 -0
  92. data/app/javascript/alchemy_admin/components/node_form.js +34 -0
  93. data/app/javascript/alchemy_admin/components/node_select.js +25 -30
  94. data/app/javascript/alchemy_admin/components/node_tree.js +56 -0
  95. data/app/javascript/alchemy_admin/components/page_node.js +2 -1
  96. data/app/javascript/alchemy_admin/components/page_select.js +16 -34
  97. data/app/javascript/alchemy_admin/components/picture_archive.js +92 -0
  98. data/app/javascript/alchemy_admin/components/picture_editor.js +2 -1
  99. data/app/javascript/alchemy_admin/components/preview_window.js +11 -3
  100. data/app/javascript/alchemy_admin/components/remote_select.js +461 -81
  101. data/app/javascript/alchemy_admin/components/select.js +26 -88
  102. data/app/javascript/alchemy_admin/components/sitemap.js +2 -1
  103. data/app/javascript/alchemy_admin/components/sortable_elements.js +2 -1
  104. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +63 -48
  105. data/app/javascript/alchemy_admin/components/tinymce.js +2 -1
  106. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +3 -2
  107. data/app/javascript/alchemy_admin/components/uploader.js +19 -3
  108. data/app/javascript/alchemy_admin/confirm_dialog.js +7 -1
  109. data/app/javascript/alchemy_admin/dialog.js +182 -110
  110. data/app/javascript/alchemy_admin/hotkeys.js +0 -5
  111. data/app/javascript/alchemy_admin/i18n.js +3 -9
  112. data/app/javascript/alchemy_admin/image_overlay.js +58 -42
  113. data/app/javascript/alchemy_admin/initializer.js +2 -2
  114. data/app/javascript/alchemy_admin/link_dialog.js +15 -2
  115. data/app/javascript/alchemy_admin/routes.js +18 -0
  116. data/app/javascript/alchemy_admin/turbo_stream_actions.js +88 -0
  117. data/app/javascript/alchemy_admin/utils/ajax.js +14 -3
  118. data/app/javascript/alchemy_admin/utils/events.js +5 -0
  119. data/app/javascript/alchemy_admin/utils/json_script.js +12 -0
  120. data/app/javascript/alchemy_admin/utils/tom_select.js +171 -0
  121. data/app/javascript/alchemy_admin.js +2 -19
  122. data/app/javascript/preview.js +131 -113
  123. data/app/jobs/alchemy/invalidate_elements_cache_job.rb +1 -1
  124. data/app/models/alchemy/admin/content_security_policy.rb +143 -0
  125. data/app/models/alchemy/admin/filters/base.rb +7 -0
  126. data/app/models/alchemy/admin/filters/datepicker.rb +1 -1
  127. data/app/models/alchemy/admin/filters/select.rb +20 -7
  128. data/app/models/alchemy/attachment.rb +14 -7
  129. data/app/models/alchemy/cache_control.rb +104 -0
  130. data/app/models/alchemy/current.rb +1 -1
  131. data/app/models/alchemy/eager_loading.rb +0 -2
  132. data/app/models/alchemy/element.rb +23 -8
  133. data/app/models/alchemy/element_definition.rb +1 -0
  134. data/app/models/alchemy/elements_repository.rb +11 -0
  135. data/app/models/alchemy/folded_page.rb +5 -1
  136. data/app/models/alchemy/ingredient.rb +26 -0
  137. data/app/models/alchemy/ingredients/datetime.rb +1 -1
  138. data/app/models/alchemy/ingredients/link.rb +2 -0
  139. data/app/models/alchemy/ingredients/picture.rb +3 -0
  140. data/app/models/alchemy/ingredients/text.rb +2 -0
  141. data/app/models/alchemy/language.rb +19 -7
  142. data/app/models/alchemy/legacy_page_url.rb +6 -1
  143. data/app/models/alchemy/node.rb +51 -0
  144. data/app/models/alchemy/page/page_natures.rb +41 -8
  145. data/app/models/alchemy/page/page_scopes.rb +23 -0
  146. data/app/models/alchemy/page/sitemap_cache_key.rb +56 -0
  147. data/app/models/alchemy/page.rb +88 -32
  148. data/app/models/alchemy/page_definition.rb +1 -1
  149. data/app/models/alchemy/page_mutex.rb +16 -0
  150. data/app/models/alchemy/page_version.rb +24 -1
  151. data/app/models/alchemy/permissions.rb +6 -5
  152. data/app/models/alchemy/picture.rb +15 -7
  153. data/app/models/alchemy/picture_description.rb +22 -0
  154. data/app/models/alchemy/picture_thumb.rb +18 -0
  155. data/app/models/alchemy/publishable/timestamp_resolver.rb +70 -0
  156. data/app/models/alchemy/resource.rb +1 -1
  157. data/app/models/alchemy/safe_url_validator.rb +34 -0
  158. data/app/models/alchemy/site.rb +8 -3
  159. data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +7 -3
  160. data/app/models/alchemy/storage_adapter/active_storage.rb +4 -1
  161. data/app/models/alchemy/tag.rb +10 -4
  162. data/{lib → app/models/concerns}/alchemy/filetypes.rb +5 -0
  163. data/app/models/concerns/alchemy/picture_thumbnails.rb +61 -6
  164. data/app/models/concerns/alchemy/publishable.rb +14 -27
  165. data/app/models/concerns/alchemy/taggable.rb +92 -0
  166. data/app/models/concerns/alchemy/user_methods.rb +95 -0
  167. data/app/services/alchemy/duplicate_element.rb +5 -2
  168. data/app/services/alchemy/element_preloader.rb +1 -2
  169. data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
  170. data/app/stylesheets/alchemy/_extends.scss +0 -8
  171. data/app/stylesheets/alchemy/_themes.scss +10 -9
  172. data/app/stylesheets/alchemy/admin/_empty-state.scss +29 -0
  173. data/app/stylesheets/alchemy/admin/_help.scss +10 -0
  174. data/app/stylesheets/alchemy/admin/_tom-select.scss +104 -15
  175. data/app/stylesheets/alchemy/admin/archive.scss +56 -11
  176. data/app/stylesheets/alchemy/admin/base.scss +4 -5
  177. data/app/stylesheets/alchemy/admin/buttons.scss +12 -1
  178. data/app/stylesheets/alchemy/admin/dashboard.scss +150 -20
  179. data/app/stylesheets/alchemy/admin/dialogs.scss +27 -36
  180. data/app/stylesheets/alchemy/admin/element-select.scss +1 -1
  181. data/app/stylesheets/alchemy/admin/elements.scss +8 -24
  182. data/app/stylesheets/alchemy/admin/flash.scss +2 -1
  183. data/app/stylesheets/alchemy/admin/forms.scss +60 -24
  184. data/app/stylesheets/alchemy/admin/frame.scss +40 -4
  185. data/app/stylesheets/alchemy/admin/image_library.scss +9 -12
  186. data/app/stylesheets/alchemy/admin/navigation.scss +17 -14
  187. data/app/stylesheets/alchemy/admin/node-select.scss +13 -37
  188. data/app/stylesheets/alchemy/admin/nodes.scss +1 -1
  189. data/app/stylesheets/alchemy/admin/page-select.scss +6 -97
  190. data/app/stylesheets/alchemy/admin/print.scss +0 -4
  191. data/app/stylesheets/alchemy/admin/remote-select.scss +170 -0
  192. data/app/stylesheets/alchemy/admin/search.scss +2 -3
  193. data/app/stylesheets/alchemy/admin/selects.scss +1 -294
  194. data/app/stylesheets/alchemy/admin/sitemap.scss +2 -1
  195. data/app/stylesheets/alchemy/admin/tables.scss +7 -1
  196. data/app/stylesheets/alchemy/admin/tags-autocomplete.scss +17 -0
  197. data/app/stylesheets/alchemy/admin/tags.scss +0 -13
  198. data/app/stylesheets/alchemy/admin/toolbar.scss +9 -6
  199. data/app/stylesheets/alchemy/admin/upload.scss +61 -1
  200. data/app/stylesheets/alchemy/admin.scss +4 -1
  201. data/app/types/alchemy/cache_control_type.rb +84 -0
  202. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  203. data/app/views/alchemy/_menubar.html.erb +2 -154
  204. data/app/views/alchemy/_preview_mode_code.html.erb +0 -3
  205. data/app/views/alchemy/admin/_left_menu.html.erb +18 -5
  206. data/app/views/alchemy/admin/_top_menu.html.erb +1 -1
  207. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +5 -4
  208. data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
  209. data/app/views/alchemy/admin/attachments/_files_list.html.erb +71 -83
  210. data/app/views/alchemy/admin/attachments/assign.turbo_stream.erb +8 -0
  211. data/app/views/alchemy/admin/attachments/index.html.erb +32 -7
  212. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  213. data/app/views/alchemy/admin/crop.html.erb +20 -20
  214. data/app/views/alchemy/admin/dashboard/_footer.html.erb +0 -3
  215. data/app/views/alchemy/admin/dashboard/_top.html.erb +1 -0
  216. data/app/views/alchemy/admin/dashboard/help.html.erb +84 -57
  217. data/app/views/alchemy/admin/dashboard/license.html.erb +6 -0
  218. data/app/views/alchemy/admin/elements/_footer.html.erb +0 -1
  219. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  220. data/app/views/alchemy/admin/elements/_header.html.erb +2 -19
  221. data/app/views/alchemy/admin/elements/_preview_text_quote.html.erb +3 -0
  222. data/app/views/alchemy/admin/elements/_schedule.html.erb +7 -7
  223. data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -1
  224. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +1 -1
  225. data/app/views/alchemy/admin/elements/index.html.erb +1 -1
  226. data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +2 -2
  227. data/app/views/alchemy/admin/elements/update.turbo_stream.erb +31 -0
  228. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +6 -3
  229. data/app/views/alchemy/admin/languages/_table.html.erb +0 -2
  230. data/app/views/alchemy/admin/languages/index.html.erb +25 -29
  231. data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +1 -3
  232. data/app/views/alchemy/admin/layoutpages/edit.html.erb +1 -1
  233. data/app/views/alchemy/admin/layoutpages/index.html.erb +26 -7
  234. data/app/views/alchemy/admin/nodes/_form.html.erb +3 -21
  235. data/app/views/alchemy/admin/nodes/_node.html.erb +93 -88
  236. data/app/views/alchemy/admin/nodes/index.html.erb +15 -23
  237. data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -6
  238. data/app/views/alchemy/admin/pages/_form.html.erb +2 -1
  239. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +1 -1
  240. data/app/views/alchemy/admin/pages/_table.html.erb +2 -4
  241. data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -1
  242. data/app/views/alchemy/admin/pages/edit.html.erb +10 -21
  243. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +1 -1
  244. data/app/views/alchemy/admin/pages/index.html.erb +26 -31
  245. data/app/views/alchemy/admin/pages/info.html.erb +2 -2
  246. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +2 -2
  247. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
  248. data/app/views/alchemy/admin/partials/_routes.html.erb +24 -48
  249. data/app/views/alchemy/admin/pictures/_archive.html.erb +3 -17
  250. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  251. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -3
  252. data/app/views/alchemy/admin/pictures/_form.html.erb +0 -1
  253. data/app/views/alchemy/admin/pictures/_infos.html.erb +5 -1
  254. data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
  255. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -4
  256. data/app/views/alchemy/admin/pictures/assign.turbo_stream.erb +7 -0
  257. data/app/views/alchemy/admin/pictures/index.html.erb +38 -30
  258. data/app/views/alchemy/admin/pictures/show.html.erb +41 -41
  259. data/app/views/alchemy/admin/resources/_empty_state.html.erb +27 -0
  260. data/app/views/alchemy/admin/resources/_pagination.html.erb +1 -1
  261. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
  262. data/app/views/alchemy/admin/resources/_resource_table.html.erb +0 -2
  263. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +3 -2
  264. data/app/views/alchemy/admin/resources/index.html.erb +32 -13
  265. data/app/views/alchemy/admin/sites/index.html.erb +24 -22
  266. data/app/views/alchemy/admin/styleguide/index.html.erb +35 -12
  267. data/app/views/alchemy/admin/tags/index.html.erb +27 -25
  268. data/app/views/alchemy/admin/tinymce/_setup.html.erb +6 -8
  269. data/app/views/alchemy/admin/translations/_en.js +3 -0
  270. data/app/views/alchemy/admin/uploader/_button.html.erb +11 -17
  271. data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -3
  272. data/app/views/alchemy/pages/sitemap.xml.erb +2 -0
  273. data/app/views/kaminari/alchemy/_first_page.html.erb +1 -2
  274. data/app/views/kaminari/alchemy/_gap.html.erb +0 -1
  275. data/app/views/kaminari/alchemy/_last_page.html.erb +1 -2
  276. data/app/views/kaminari/alchemy/_next_page.html.erb +1 -2
  277. data/app/views/kaminari/alchemy/_page.html.erb +1 -2
  278. data/app/views/kaminari/alchemy/_paginator.html.erb +0 -1
  279. data/app/views/kaminari/alchemy/_prev_page.html.erb +1 -2
  280. data/app/views/layouts/alchemy/admin/dialog.html.erb +3 -0
  281. data/app/views/layouts/alchemy/admin.html.erb +5 -6
  282. data/config/brakeman.ignore +23 -1
  283. data/config/importmap.rb +10 -8
  284. data/config/initializers/dragonfly.rb +21 -17
  285. data/config/locales/alchemy.en.yml +52 -11
  286. data/config/routes.rb +3 -3
  287. data/db/migrate/20260727120000_add_permitted_roles_to_alchemy_pages.rb +23 -0
  288. data/db/migrate/20260820120000_add_cached_tag_list_to_taggables.rb +57 -0
  289. data/lib/alchemy/configuration/number_option.rb +13 -0
  290. data/lib/alchemy/configuration/proc_option.rb +13 -0
  291. data/lib/alchemy/configuration.rb +2 -0
  292. data/lib/alchemy/configurations/admin_components.rb +30 -0
  293. data/lib/alchemy/configurations/format_matchers.rb +7 -1
  294. data/lib/alchemy/configurations/main.rb +103 -4
  295. data/lib/alchemy/configurations/menu.rb +91 -0
  296. data/lib/alchemy/configurations/menu_item.rb +123 -0
  297. data/lib/alchemy/configurations/sitemap.rb +1 -0
  298. data/lib/alchemy/configurations/uploader.rb +15 -1
  299. data/lib/alchemy/engine.rb +0 -6
  300. data/lib/alchemy/i18n.rb +26 -0
  301. data/lib/alchemy/modules.rb +52 -50
  302. data/lib/alchemy/tasks/generate_thumbnails.rb +99 -0
  303. data/lib/alchemy/tasks/tidy.rb +1 -1
  304. data/lib/alchemy/test_support/capybara_helpers.rb +48 -37
  305. data/lib/alchemy/test_support/current_language_shared_examples.rb +1 -1
  306. data/lib/alchemy/test_support/factories/dummy_user_factory.rb +4 -1
  307. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +132 -0
  308. data/lib/alchemy/test_support/shared_ingredient_examples.rb +52 -0
  309. data/lib/alchemy/test_support/shared_publishable_examples.rb +29 -4
  310. data/lib/alchemy/upgrader/eight_four.rb +237 -0
  311. data/lib/alchemy/upgrader/register_module_rewriter.rb +177 -0
  312. data/lib/alchemy/upgrader.rb +2 -1
  313. data/lib/alchemy/version.rb +1 -1
  314. data/lib/alchemy.rb +0 -1
  315. data/lib/alchemy_cms.rb +0 -13
  316. data/lib/generators/alchemy/elements/templates/view.html.erb +1 -1
  317. data/lib/generators/alchemy/elements/templates/view.html.haml +1 -1
  318. data/lib/generators/alchemy/elements/templates/view.html.slim +1 -1
  319. data/lib/generators/alchemy/install/install_generator.rb +2 -13
  320. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +24 -9
  321. data/lib/generators/alchemy/module/module_generator.rb +1 -0
  322. data/lib/generators/alchemy/module/templates/ability.rb.tt +1 -1
  323. data/lib/generators/alchemy/module/templates/module_config.rb.tt +16 -18
  324. data/lib/generators/alchemy/user_columns_migration/templates/migration.rb.tt +19 -0
  325. data/lib/generators/alchemy/user_columns_migration/user_columns_migration_generator.rb +30 -0
  326. data/lib/tasks/alchemy/thumbnails.rake +16 -29
  327. data/lib/tasks/alchemy/upgrade.rake +45 -3
  328. data/vendor/javascript/cropperjs.min.js +3 -3
  329. data/vendor/javascript/floating-ui.min.js +1 -1
  330. data/vendor/javascript/tinymce.min.js +5 -5
  331. data/vendor/javascript/tom-select.min.js +1 -1
  332. metadata +85 -65
  333. data/app/javascript/alchemy_admin/components/action.js +0 -50
  334. data/app/javascript/alchemy_admin/dirty.js +0 -55
  335. data/app/javascript/alchemy_admin/node_tree.js +0 -82
  336. data/app/javascript/alchemy_admin/picture_selector.js +0 -67
  337. data/app/javascript/alchemy_admin/templates/compiled.js +0 -1
  338. data/app/javascript/alchemy_admin/templates/node_folder.hbs +0 -3
  339. data/app/stylesheets/alchemy/admin/attachment-select.scss +0 -19
  340. data/app/views/alchemy/admin/_main_navi.html.erb +0 -6
  341. data/app/views/alchemy/admin/attachments/assign.js.erb +0 -15
  342. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +0 -30
  343. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +0 -20
  344. data/app/views/alchemy/admin/partials/_sub_navigation.html.erb +0 -11
  345. data/app/views/alchemy/admin/pictures/assign.js.erb +0 -13
  346. data/app/views/alchemy/base/error_notice.js.erb +0 -1
  347. data/app/views/alchemy/base/permission_denied.js.erb +0 -3
  348. data/app/views/alchemy/base/redirect.js.erb +0 -11
  349. data/config/alchemy/config.yml +0 -217
  350. data/config/alchemy/modules.yml +0 -82
  351. data/config/initializers/rails_live_reload.rb +0 -13
  352. data/lib/alchemy/dev_support/live_reload_watcher.rb +0 -5
  353. data/lib/alchemy/taggable.rb +0 -49
  354. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -34
  355. data/vendor/assets/stylesheets/alchemy_admin/select2.css +0 -1
  356. data/vendor/javascript/handlebars.min.js +0 -29
  357. data/vendor/javascript/jquery.min.js +0 -2
  358. data/vendor/javascript/rails-ujs.min.js +0 -1
  359. data/vendor/javascript/select2.min.js +0 -23
  360. /data/{lib → app/controllers/concerns}/alchemy/ability_helper.rb +0 -0
  361. /data/{lib → app/controllers/concerns}/alchemy/admin/locale.rb +0 -0
  362. /data/{lib → app/controllers/concerns}/alchemy/admin/preview_time.rb +0 -0
  363. /data/{lib → app/controllers/concerns}/alchemy/admin/timezone.rb +0 -0
  364. /data/{lib → app/controllers/concerns}/alchemy/configuration_methods.rb +0 -0
  365. /data/{lib → app/models}/alchemy/admin/preview_url.rb +0 -0
  366. /data/{lib → app/models/concerns}/alchemy/hints.rb +0 -0
  367. /data/{lib → app/models/concerns}/alchemy/name_conversions.rb +0 -0
  368. /data/{lib → app/services}/alchemy/elements_finder.rb +0 -0
@@ -4,7 +4,6 @@
4
4
  current_page: a page object for the currently displayed page
5
5
  num_pages: total number of pages
6
6
  per_page: number of items to fetch per page
7
- remote: data-remote
8
7
  -%>
9
8
  <% if current_page.first? %>
10
9
  <span class="first-page disabled">
@@ -12,7 +11,7 @@
12
11
  </span>
13
12
  <% else %>
14
13
  <sl-tooltip content="<%= Alchemy.t(:first, scope: 'pagination') %>" placement="top-start">
15
- <%= link_to url, remote: remote, class: 'first-page' do %>
14
+ <%= link_to url, class: 'first-page' do %>
16
15
  <%= render_icon("skip-left") %>
17
16
  <% end %>
18
17
  </sl-tooltip>
@@ -3,6 +3,5 @@
3
3
  current_page: a page object for the currently displayed page
4
4
  num_pages: total number of pages
5
5
  per_page: number of items to fetch per page
6
- remote: data-remote
7
6
  -%>
8
7
  <span class="page gap"><%= render_icon(:more) %></span>
@@ -4,7 +4,6 @@
4
4
  current_page: a page object for the currently displayed page
5
5
  num_pages: total number of pages
6
6
  per_page: number of items to fetch per page
7
- remote: data-remote
8
7
  -%>
9
8
  <% if current_page.last? %>
10
9
  <span class="last-page disabled">
@@ -12,7 +11,7 @@
12
11
  </span>
13
12
  <% else %>
14
13
  <sl-tooltip content="<%= Alchemy.t(:last, scope: 'pagination') %>" placement="top-end">
15
- <%= link_to url, remote: remote, class: 'last-page' do %>
14
+ <%= link_to url, class: 'last-page' do %>
16
15
  <%= render_icon("skip-right") %>
17
16
  <% end %>
18
17
  </sl-tooltip>
@@ -4,7 +4,6 @@
4
4
  current_page: a page object for the currently displayed page
5
5
  num_pages: total number of pages
6
6
  per_page: number of items to fetch per page
7
- remote: data-remote
8
7
  -%>
9
8
  <% if current_page.last? -%>
10
9
  <span class="next_page disabled">
@@ -12,7 +11,7 @@
12
11
  </span>
13
12
  <% else -%>
14
13
  <sl-tooltip content="<%= Alchemy.t(:next_page, scope: 'pagination') %>" placement="top-end">
15
- <%= link_to url, rel: 'next', remote: remote, class: 'next_page' do %>
14
+ <%= link_to url, rel: 'next', class: 'next_page' do %>
16
15
  <%= render_icon("arrow-right-s") %>
17
16
  <% end %>
18
17
  </sl-tooltip>
@@ -5,10 +5,9 @@
5
5
  current_page: a page object for the currently displayed page
6
6
  num_pages: total number of pages
7
7
  per_page: number of items to fetch per page
8
- remote: data-remote
9
8
  -%>
10
9
  <% if page.current? -%>
11
10
  <span class="page current"><%= page -%></span>
12
11
  <% else -%>
13
- <%= link_to page, url, opts = {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page'} -%>
12
+ <%= link_to page, url, opts = {rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page'} -%>
14
13
  <% end -%>
@@ -3,7 +3,6 @@
3
3
  current_page: a page object for the currently displayed page
4
4
  total_pages: total number of pages
5
5
  per_page: number of items to fetch per page
6
- remote: data-remote
7
6
  paginator: the paginator that renders the pagination tags inside
8
7
  -%>
9
8
  <div class="pagination">
@@ -4,7 +4,6 @@
4
4
  current_page: a page object for the currently displayed page
5
5
  num_pages: total number of pages
6
6
  per_page: number of items to fetch per page
7
- remote: data-remote
8
7
  -%>
9
8
  <% if current_page.first? -%>
10
9
  <span class="previous_page disabled">
@@ -12,7 +11,7 @@
12
11
  </span>
13
12
  <% else -%>
14
13
  <sl-tooltip class="pagination-label" content="<%= Alchemy.t(:previous_page, scope: 'pagination') %>" placement="top-start">
15
- <%= link_to url, rel: 'prev', remote: remote, class: 'previous_page' do %>
14
+ <%= link_to url, rel: 'prev', class: 'previous_page' do %>
16
15
  <%= render_icon("arrow-left-s") %>
17
16
  <% end %>
18
17
  </sl-tooltip>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag Alchemy::Admin::DIALOG_FRAME_ID do %>
2
+ <%= yield %>
3
+ <% end %>
@@ -8,6 +8,7 @@
8
8
  <link rel="shortcut icon" href="<%= asset_path('alchemy/favicon.ico') %>">
9
9
  <link rel="preload" href="<%= asset_path("alchemy/icons-sprite.svg") %>" as="image" type="<%= Mime::Type.lookup_by_extension(:svg) %>" crossorigin>
10
10
  <%= csrf_meta_tag %>
11
+ <%= csp_meta_tag %>
11
12
  <meta name="robots" content="noindex">
12
13
  <meta name="turbo-prefetch" content="false">
13
14
  <meta name="turbo-cache-control" content="no-cache">
@@ -18,21 +19,19 @@
18
19
  <%= stylesheet_link_tag(stylesheet, 'data-turbo-track' => true) %>
19
20
  <% end %>
20
21
  <%= yield :stylesheets %>
21
- <script>
22
+ <%= tag.script(nonce: content_security_policy_nonce) do %>
22
23
  // Global Alchemy JavaScript object.
23
24
  var Alchemy = {};
24
- // Store regular expression for external link url matching.
25
- Alchemy.link_url_regexp = <%= link_url_regexp.inspect %>;
26
25
  // JS translations
27
26
  <%= alchemy_admin_js_translations %>
28
- </script>
27
+ <% end %>
29
28
  <%= render 'alchemy/admin/tinymce/setup' %>
30
29
  <%= render 'alchemy/admin/partials/routes' %>
31
30
  <%= javascript_importmap_tags("alchemy_admin", importmap: Alchemy.importmap) %>
32
31
  <% Alchemy.config.admin_js_imports.each do |path| %>
33
- <script type="module">
32
+ <%= tag.script(type: "module", nonce: content_security_policy_nonce) do %>
34
33
  import "<%= path %>"
35
- </script>
34
+ <% end %>
36
35
  <% end %>
37
36
  <%= yield :javascript_includes %>
38
37
  </head>
@@ -55,7 +55,29 @@
55
55
  79
56
56
  ],
57
57
  "note": ""
58
+ },
59
+ {
60
+ "warning_type": "Format Validation",
61
+ "warning_code": 30,
62
+ "fingerprint": "c9db7cc83a6210bf4a715ce9f9ffabca1145d51db9fc1ca1e45d965e7151751b",
63
+ "check_name": "ValidationRegex",
64
+ "message": "Insufficient validation for `url` using `/\\A(\\/|\\D[a-z+\\d.-]+:)/`. Use `\\A` and `\\z` as anchors",
65
+ "file": "app/models/alchemy/node.rb",
66
+ "line": 99,
67
+ "link": "https://brakemanscanner.org/docs/warning_types/format_validation/",
68
+ "code": ":url, { :format => ({ :with => /\\A(\\/|\\D[a-z+\\d.-]+:)/ }), :unless => (lambda do\n url.nil?\n end) }",
69
+ "render_path": null,
70
+ "location": {
71
+ "type": "model",
72
+ "model": "Alchemy::Node"
73
+ },
74
+ "user_input": null,
75
+ "confidence": "High",
76
+ "cwe_id": [
77
+ 777
78
+ ],
79
+ "note": "VALID_URL_REGEX deliberately only checks the url prefix (a path or a scheme). It is anchored with \\A, so the multiline bypass does not apply, and executable schemes are rejected by Alchemy::SafeUrlValidator."
58
80
  }
59
81
  ],
60
- "brakeman_version": "7.1.2"
82
+ "brakeman_version": "8.1.0"
61
83
  }
data/config/importmap.rb CHANGED
@@ -2,15 +2,11 @@ pin "@ungap/custom-elements", to: "ungap-custom-elements.min.js", preload: true
2
2
  pin "clipboard", to: "clipboard.min.js", preload: true
3
3
  pin "cropperjs", to: "cropperjs.min.js", preload: true
4
4
  pin "flatpickr", to: "flatpickr.min.js", preload: true # @4.6.13
5
- pin "handlebars", to: "handlebars.min.js", preload: true # @4.7.8
6
- pin "jquery", to: "jquery.min.js", preload: true
7
5
  pin "keymaster", to: "keymaster.min.js", preload: true
8
- pin "select2", to: "select2.min.js", preload: true
9
6
  pin "sortablejs", to: "sortable.min.js", preload: true # @1.15.1
10
7
  pin "@floating-ui/dom", to: "floating-ui.min.js", preload: true
11
8
  pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
12
9
  pin "shoelace", to: "shoelace.min.js", preload: true
13
- pin "@rails/ujs", to: "rails-ujs.min.js", preload: true # @7.1.2
14
10
  pin "tinymce", to: "tinymce.min.js", preload: true
15
11
  pin "tom-select", to: "tom-select.min.js", preload: true
16
12
 
@@ -27,8 +23,14 @@ pin "alchemy_admin/components/tinymce"
27
23
  pin "alchemy_admin/components/remote_select"
28
24
  pin "alchemy_admin/components/select"
29
25
  pin "alchemy_admin/i18n"
30
- pin "alchemy_admin/image_cropper", to: "alchemy/alchemy_admin.min.js"
31
- pin "alchemy_admin/image_overlay", to: "alchemy/alchemy_admin.min.js"
32
- pin "alchemy_admin/picture_selector", to: "alchemy/alchemy_admin.min.js"
33
- pin "alchemy_admin/node_tree", to: "alchemy/alchemy_admin.min.js"
26
+ # Dependencies of the standalone components above. The admin bundle inlines
27
+ # these, but the source files import them by name, so importing a component on
28
+ # its own fails without pins of their own.
29
+ pin "alchemy_admin/components/icon"
30
+ pin "alchemy_admin/growler"
31
+ pin "alchemy_admin/utils/ajax"
32
+ pin "alchemy_admin/utils/dom_helpers"
33
+ pin "alchemy_admin/utils/json_script"
34
+ pin "alchemy_admin/utils/tom_select"
35
+ pin "tom-select/utils", to: "tom-select.min.js"
34
36
  pin "alchemy_admin/utils/events", to: "alchemy/alchemy_admin.min.js"
@@ -1,24 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "alchemy/dragonfly/processors/crop_resize"
4
- require "alchemy/dragonfly/processors/auto_orient"
5
- require "alchemy/dragonfly/processors/thumbnail"
3
+ # Dragonfly is only needed for the legacy dragonfly storage adapter
4
+ # and needs to be added to the app's Gemfile.
5
+ if defined?(::Dragonfly)
6
+ require "alchemy/dragonfly/processors/crop_resize"
7
+ require "alchemy/dragonfly/processors/auto_orient"
8
+ require "alchemy/dragonfly/processors/thumbnail"
6
9
 
7
- # Logger
8
- Dragonfly.logger = Rails.logger
10
+ # Logger
11
+ Dragonfly.logger = Rails.logger
9
12
 
10
- # Add model functionality
11
- if defined?(ActiveRecord::Base)
12
- ActiveRecord::Base.extend Dragonfly::Model
13
- ActiveRecord::Base.extend Dragonfly::Model::Validations
14
- end
13
+ # Add model functionality
14
+ if defined?(ActiveRecord::Base)
15
+ ActiveRecord::Base.extend Dragonfly::Model
16
+ ActiveRecord::Base.extend Dragonfly::Model::Validations
17
+ end
15
18
 
16
- # Dragonfly 1.4.0 only allows `quality` as argument to `encode`
17
- Dragonfly::ImageMagick::Processors::Encode::WHITELISTED_ARGS << "flatten"
18
- Dragonfly::ImageMagick::Processors::Encode::WHITELISTED_ARGS << "background"
19
+ # Dragonfly 1.4.0 only allows `quality` as argument to `encode`
20
+ Dragonfly::ImageMagick::Processors::Encode::WHITELISTED_ARGS << "flatten"
21
+ Dragonfly::ImageMagick::Processors::Encode::WHITELISTED_ARGS << "background"
19
22
 
20
- Rails.application.config.after_initialize do
21
- Dragonfly.app(:alchemy_pictures).add_processor(:crop_resize, Alchemy::Dragonfly::Processors::CropResize.new)
22
- Dragonfly.app(:alchemy_pictures).add_processor(:auto_orient, Alchemy::Dragonfly::Processors::AutoOrient.new)
23
- Dragonfly.app(:alchemy_pictures).add_processor(:thumbnail, Alchemy::Dragonfly::Processors::Thumbnail.new)
23
+ Rails.application.config.after_initialize do
24
+ Dragonfly.app(:alchemy_pictures).add_processor(:crop_resize, Alchemy::Dragonfly::Processors::CropResize.new)
25
+ Dragonfly.app(:alchemy_pictures).add_processor(:auto_orient, Alchemy::Dragonfly::Processors::AutoOrient.new)
26
+ Dragonfly.app(:alchemy_pictures).add_processor(:thumbnail, Alchemy::Dragonfly::Processors::Thumbnail.new)
27
+ end
24
28
  end
@@ -79,9 +79,13 @@ en:
79
79
  application/x-rar: RAR Archive
80
80
  application/x-shockwave-flash: Flash Movie
81
81
  application/zip: ZIP Archive
82
+ audio/aac: AAC Audio
83
+ audio/flac: FLAC Audio
82
84
  audio/mp4: MPEG-4 Audio
83
85
  audio/mpeg: MP3 Audio
86
+ audio/ogg: Ogg Audio
84
87
  audio/wav: WAV Audio
88
+ audio/webm: WebM Audio
85
89
  audio/x-wav: WAV Audio
86
90
  image/avif: AVIF Image
87
91
  image/gif: GIF Image
@@ -95,6 +99,7 @@ en:
95
99
  text/x-vcard: vCard
96
100
  video/mp4: MPEG-4 Video
97
101
  video/mpeg: MPEG Video
102
+ video/ogg: Ogg Video
98
103
  video/quicktime: Quicktime Video
99
104
  video/webm: WebM Video
100
105
  video/x-flv: Flash Video
@@ -236,6 +241,9 @@ en:
236
241
  published: "published"
237
242
  user_counts:
238
243
  online: "online"
244
+ news_reader:
245
+ widget_title: "News"
246
+ no_news: "no news"
239
247
  elements:
240
248
  toolbar:
241
249
  hide: Hide
@@ -364,6 +372,7 @@ en:
364
372
  "Site successfully updated": "Website successfully updated."
365
373
  "Size": "Size"
366
374
  "Successfully deleted element": "Successfully deleted %{element}"
375
+ updated_ingredient: "Successfully updated %{ingredient}"
367
376
  "Tags": "Tags"
368
377
  "This page is locked": "This page is currently locked by %{name}"
369
378
  "Title": "Title"
@@ -491,8 +500,11 @@ en:
491
500
  "Leave the page": "Leave the page"
492
501
  "Open upload form": "Open upload form"
493
502
  "Select all pictures": "Select all pictures"
503
+ resources: Resources
504
+ resource_links:
505
+ guidelines: "Guidelines"
506
+ homepage: "Homepage"
494
507
  hide_element_content: "Hide this elements content."
495
- homepage_does_not_exist: "This language has no homepage yet"
496
508
  dashboard: "Dashboard"
497
509
  image_alt_tag: "Alt-tag"
498
510
  image_caption: "Caption"
@@ -509,6 +521,9 @@ en:
509
521
  language_pages_copied: "Language tree successfully copied."
510
522
  left: "left"
511
523
  legacy_url_info_text: "A link is a redirect from an old URL to the current URL of this page. This redirect happens with a <a href='https://support.google.com/webmasters/answer/93633' target='_blank'>301 status code</a>."
524
+ license: "License"
525
+ licenses:
526
+ BSD-3-Clause: "BSD-3-Clause"
512
527
  link_image: "Link this image."
513
528
  link_overlay_tab_label:
514
529
  anchor: "Anchor"
@@ -550,14 +565,33 @@ en:
550
565
  "no users": "no users"
551
566
  no_default_language_found: "No default language found."
552
567
  no_element_given: "No element given."
553
- no_files_in_archive: "You do not have any files in your archive."
554
- no_images_in_archive: "You don't have any images in your archive."
555
568
  no_more_elements_to_add: "No more elements available."
556
569
  no_resource_found:
557
- default: "No %{resource} found. Please add your first one below."
558
- alchemy/language: "<h2>Your website does not have any languages yet.</h2><p>You need at least one language to work with. Please create one below.</p>"
559
- alchemy/node: "<h2>You do not have any menus yet.</h2><p>With menus your users can navigate your website. You can have multiple menus on your website. How many depends on your website template.</p><p>Please chose which menu you want to create first.</p>"
560
- alchemy/site: "<h2>You do not have any websites yet.</h2><p>In order to serve requests you need to create a website first.</p><p>This form has been filled with sensible defaults that work in most cases.</p>"
570
+ default: |
571
+ <h2>You have no %{resource} yet.</h2>
572
+ alchemy/attachment: |
573
+ <h2>You do not have any files in your archive yet.</h2>
574
+ <p>Use the button below or drag files into this window to upload your first file.</p>
575
+ alchemy/language: |
576
+ <h2>Your website does not have any languages yet.</h2>
577
+ <p>You need at least one language to work with. Please create one below.</p>
578
+ alchemy/node: |
579
+ <h2>You do not have any menus yet.</h2>
580
+ <p>With menus your users can navigate your website. You can have multiple menus on your website. How many depends on your website template.</p>
581
+ <p>Please chose which menu you want to create first.</p>
582
+ alchemy/page: |
583
+ <h2>This language has no homepage yet</h2>
584
+ <p>Please create one below.</p>
585
+ alchemy/picture: |
586
+ <h2>You don't have any images in your archive yet.</h2>
587
+ <p>Use the button below or drag images into this window to upload your first image.</p>
588
+ alchemy/site: |
589
+ <h2>You do not have any websites yet.</h2>
590
+ <p>In order to serve requests you need to create a website first.</p>
591
+ <p>This form has been filled with sensible defaults that work in most cases.</p>
592
+ layoutpages: |
593
+ <h2>This language has no global pages yet</h2>
594
+ <p>Global pages hold content that is shared across all pages, like headers or footers.</p>
561
595
  no_search_results: "Your search did not return any results."
562
596
  no_unpublished_changes: "No unpublished changes"
563
597
  "not a valid image": "This is not an valid image."
@@ -575,7 +609,7 @@ en:
575
609
  "true": "Page is being edited at the moment."
576
610
  "false": ""
577
611
  restricted:
578
- "true": "Page is only accessible by members."
612
+ "true": "Page is only accessible by users with one of its roles."
579
613
  "false": "Page is accessible by all visitors."
580
614
  scheduled:
581
615
  public_on: "Page is visible from %{public_on}"
@@ -685,11 +719,11 @@ en:
685
719
  unlocked_page: "Unlocked page %{name}."
686
720
  uploader:
687
721
  alchemy/attachment:
688
- button_label: Upload file(s)
722
+ button_label: Upload files
689
723
  upload_success: "File %{name} successfully uploaded"
690
724
  upload_failure: "File %{name} upload error: %{error}"
691
725
  alchemy/picture:
692
- button_label: Upload image(s)
726
+ button_label: Upload images
693
727
  upload_success: "Picture %{name} uploaded successfully"
694
728
  upload_failure: "Error while uploading %{name}: %{error}"
695
729
  error: "Could not upload file! %{error}"
@@ -711,7 +745,7 @@ en:
711
745
  "stay logged in": "Stay Logged In"
712
746
  "or to completely": "or completely"
713
747
  "Are you sure?": "Are you sure?"
714
- "Create": "Create"
748
+ "Create": "Create %{resource_name}"
715
749
  "Edit": "Edit"
716
750
  "Delete": "Delete"
717
751
  "Successfully created": "Successfully created"
@@ -764,6 +798,8 @@ en:
764
798
 
765
799
  # Translations for error messages.
766
800
  errors:
801
+ messages:
802
+ invalid_url_scheme: "uses the not allowed scheme %{scheme}:"
767
803
  attributes:
768
804
  public_until:
769
805
  must_be_after_public_on: "must be after visible from date"
@@ -851,6 +887,9 @@ en:
851
887
  conflicting_wildcard_param_key: 'has a conflicting wildcard param "%{param}" already used by the "%{conflicting_layout}" page layout'
852
888
  descendants:
853
889
  still_attached_to_nodes: "The following descendant pages are still attached to menu nodes: %{page_names}. Please remove them first."
890
+ permitted_roles:
891
+ blank: "needs at least one role a restricted page is accessible for"
892
+ inclusion: "contains roles that are not configured: %{roles}"
854
893
  alchemy/element:
855
894
  attributes:
856
895
  page_version_id:
@@ -968,6 +1007,7 @@ en:
968
1007
  public_on: Visible from
969
1008
  public_until: Visible until
970
1009
  restricted: "restricted"
1010
+ permitted_roles: "except for users with one of these roles"
971
1011
  searchable: "show in search"
972
1012
  robot_follow: "robot may follow links"
973
1013
  robot_index: "allow robot to index"
@@ -982,6 +1022,7 @@ en:
982
1022
  image_file_height: "Height"
983
1023
  image_file_width: "Width"
984
1024
  image_file_dimensions: "Dimensions"
1025
+ image_file_format: "File type"
985
1026
  image_file_size: "Filesize"
986
1027
  name: "Name"
987
1028
  tag_list: Tags
data/config/routes.rb CHANGED
@@ -5,12 +5,13 @@ require "alchemy/routing_constraints"
5
5
  Alchemy::Engine.routes.draw do
6
6
  root to: "pages#index"
7
7
 
8
- get "/sitemap.xml", to: "pages#sitemap", format: "xml"
8
+ get "/sitemap.xml", to: "sitemap#show", format: "xml"
9
9
 
10
10
  scope Alchemy.admin_path, constraints: Alchemy.admin_constraints do
11
11
  get "/", to: redirect("#{Alchemy.admin_path}/dashboard"), as: :admin
12
12
  get "/dashboard", to: "admin/dashboard#index", as: :admin_dashboard
13
13
  get "/dashboard/info", to: "admin/dashboard#info", as: :dashboard_info
14
+ get "/dashboard/license", to: "admin/dashboard#license", as: :dashboard_license
14
15
  get "/dashboard/widgets/:id", to: "admin/dashboard/widgets#show", as: :admin_dashboard_widget
15
16
  get "/help", to: "admin/dashboard#help", as: :help
16
17
  get "/update_check" => "admin/update_checks#show", :as => :update_check
@@ -25,7 +26,6 @@ Alchemy::Engine.routes.draw do
25
26
  collection do
26
27
  post :flush
27
28
  post :copy_language_tree
28
- get :create_language
29
29
  get :link
30
30
  get :tree
31
31
  end
@@ -84,7 +84,7 @@ Alchemy::Engine.routes.draw do
84
84
  resources :legacy_page_urls
85
85
  resources :languages do
86
86
  collection do
87
- get :switch
87
+ post :switch
88
88
  end
89
89
  end
90
90
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPermittedRolesToAlchemyPages < ActiveRecord::Migration[7.2]
4
+ # Only SQLite needs this: it has no real ALTER TABLE, so the remove_column in
5
+ # down rebuilds the table via DROP TABLE, whose implicit DELETE cascades into
6
+ # alchemy_page_versions, alchemy_elements and alchemy_ingredients. The
7
+ # adapter's PRAGMA foreign_keys = OFF guard against that is a silent no-op
8
+ # inside a transaction.
9
+ disable_ddl_transaction! if connection.adapter_name.match?(/sqlite/i)
10
+
11
+ def up
12
+ # Adding a nullable column is a plain ALTER TABLE ADD COLUMN, so up can keep
13
+ # the atomicity the declaration above gives up for both directions.
14
+ connection.transaction do
15
+ add_column :alchemy_pages, :permitted_roles, :text, if_not_exists: true
16
+ execute(%(UPDATE #{quote_table_name("alchemy_pages")} SET permitted_roles = '["member"]' WHERE permitted_roles IS NULL))
17
+ end
18
+ end
19
+
20
+ def down
21
+ remove_column :alchemy_pages, :permitted_roles, if_exists: true
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCachedTagListToTaggables < ActiveRecord::Migration[7.2]
4
+ # Only SQLite needs this: it has no real ALTER TABLE, so the remove_column
5
+ # calls in down rebuild the tables via DROP TABLE, whose implicit DELETE
6
+ # cascades into every child row, emptying alchemy_ingredients among others.
7
+ # The adapter's PRAGMA foreign_keys = OFF guard against that is a silent
8
+ # no-op inside a transaction.
9
+ disable_ddl_transaction! if connection.adapter_name.match?(/sqlite/i)
10
+
11
+ TAGGABLE_MODELS = %w[
12
+ Alchemy::Element
13
+ Alchemy::Page
14
+ Alchemy::Picture
15
+ Alchemy::Attachment
16
+ ]
17
+
18
+ def up
19
+ # Adding nullable columns is a plain ALTER TABLE ADD COLUMN, so up can keep
20
+ # the atomicity the declaration above gives up for both directions.
21
+ connection.transaction do
22
+ TAGGABLE_MODELS.each do |model_name|
23
+ add_column model_name.constantize.table_name, :cached_tag_list, :text, if_not_exists: true
24
+ end
25
+
26
+ say_with_time "Backfilling cached_tag_list from existing taggings" do
27
+ backfill
28
+ end
29
+ end
30
+ end
31
+
32
+ def down
33
+ TAGGABLE_MODELS.each do |model_name|
34
+ remove_column model_name.constantize.table_name, :cached_tag_list, if_exists: true
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def backfill
41
+ TAGGABLE_MODELS.each do |model_name|
42
+ table = model_name.constantize.table_name
43
+ # Throwaway model so writing the raw JSON bypasses the real model's
44
+ # serializer and picks up the freshly added column without touching it.
45
+ klass = Class.new(ActiveRecord::Base) { self.table_name = table }
46
+
47
+ Gutentag::Tagging
48
+ .where(taggable_type: model_name)
49
+ .joins(:tag)
50
+ .pluck(:taggable_id, "gutentag_tags.name")
51
+ .group_by(&:first)
52
+ .each do |taggable_id, rows|
53
+ klass.where(id: taggable_id).update_all(cached_tag_list: rows.map(&:last).uniq.to_json)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "alchemy/configuration/base_option"
4
+
5
+ module Alchemy
6
+ class Configuration
7
+ class NumberOption < BaseOption
8
+ def self.value_class
9
+ Numeric
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "alchemy/configuration/base_option"
4
+
5
+ module Alchemy
6
+ class Configuration
7
+ class ProcOption < BaseOption
8
+ def self.value_class
9
+ Proc
10
+ end
11
+ end
12
+ end
13
+ end
@@ -8,7 +8,9 @@ require "alchemy/configuration/collection_option"
8
8
  require "alchemy/configuration/configuration_option"
9
9
  require "alchemy/configuration/class_option"
10
10
  require "alchemy/configuration/integer_option"
11
+ require "alchemy/configuration/number_option"
11
12
  require "alchemy/configuration/pathname_option"
13
+ require "alchemy/configuration/proc_option"
12
14
  require "alchemy/configuration/regexp_option"
13
15
  require "alchemy/configuration/string_option"
14
16
  require "alchemy/configuration/symbol_option"
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Configurations
5
+ # === Admin Component Registry
6
+ #
7
+ # Overridable admin ViewComponents, keyed by role. Extensions can swap a
8
+ # component for their own by assigning a different class name in an
9
+ # initializer:
10
+ #
11
+ # Alchemy.config.admin_components.element_publish_button =
12
+ # "MyEngine::Admin::CustomPublishButton"
13
+ #
14
+ # Core renders these through the registry instead of referencing the class
15
+ # directly, so the swap takes effect everywhere the component is used.
16
+ class AdminComponents < Alchemy::Configuration
17
+ # The per-element publish control rendered in the element toolbar.
18
+ option :element_publish_button, :class, default: "Alchemy::Admin::PublishElementButton"
19
+
20
+ # The page publication/visibility fields rendered in the page settings form.
21
+ option :page_publication_fields, :class, default: "Alchemy::Admin::PagePublicationFields"
22
+
23
+ # The visibility state badges rendered in the element header.
24
+ option :element_status_indicators, :class, default: "Alchemy::Admin::ElementStatusIndicators"
25
+
26
+ # The status badges rendered for a page in the page listing and sitemap.
27
+ option :page_status_indicators, :class, default: "Alchemy::Admin::PageStatusIndicators"
28
+ end
29
+ end
30
+ end
@@ -3,9 +3,15 @@
3
3
  module Alchemy
4
4
  module Configurations
5
5
  class FormatMatchers < Alchemy::Configuration
6
+ # Naming the hierarchical schemes explicitly matters: a shape match like
7
+ # `[a-z]+://` also admits `javascript://%0aalert(1)`, which browsers
8
+ # execute. This is the link dialog's client side check only, the server
9
+ # side boundary is Alchemy.config.allowed_url_schemes.
10
+ LINK_URL = /^((mailto|tel):|(https?|ftp):\/\/|\/)/
11
+
6
12
  option :email, :regexp, default: /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
7
13
  option :url, :regexp, default: /\A[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\z/ix
8
- option :link_url, :regexp, default: /^(tel:|mailto:|\/|[a-z]+:\/\/)/
14
+ option :link_url, :regexp, default: LINK_URL
9
15
  end
10
16
  end
11
17
  end