| @@ -8,13 +8,18 @@ | ||
| 8 | 8 | use ABlocks\Classes\FileUpload; |
| 9 | 9 | use ABlocks\Classes\AssetsGenerator; |
| 10 | 10 | use ABlocks\Classes\RegisterScripts; |
| 11 | 11 | use ABlocks\Classes\GlobalCssGenerator; |
| 12 | +use ABlocks\Classes\GlobalClasses; | |
| 12 | 13 | use ABlocks\Classes\FontLoadLocally; |
| 13 | 14 | use ABlocks\Admin\Menu; |
| 14 | 15 | use ABlocks\Helper; |
| 15 | 16 | |
| 16 | 17 | class Assets { |
| 18 | + | |
| 19 | + /** Records which plugin version last generated the page stylesheets. */ | |
| 20 | + const BUILD_OPTION = 'ablocks_assets_build'; | |
| 21 | + | |
| 17 | 22 | public $current_page_template_part = []; |
| 18 | 23 | public $current_page_blocks = []; |
| 19 | 24 | private $FileUpload; |
| 20 | 25 | private $current_page_slug = ''; |
| @@ -106,18 +111,32 @@ | ||
| 106 | 111 | 'ajax_url' => esc_url( admin_url( 'admin-ajax.php' ) ), |
| 107 | 112 | 'is_pro' => (bool) Helper::is_active_ablocks_pro(), |
| 108 | 113 | 'is_archive' => (bool) is_archive(), |
| 109 | 114 | 'archive_post_type' => $this->get_current_archive_post_type(), |
| 115 | + // Responsive breakpoint widths (px) shared with the JS CSS generators | |
| 116 | + // and the block editor so media queries match the frontend. | |
| 117 | + 'breakpoints' => Helper::get_breakpoints(), | |
| 118 | + // Ordered device list (Desktop + built-ins + custom breakpoints) for | |
| 119 | + // the atomic block's responsive device switcher. Widest-first, so | |
| 120 | + // the editor and the frontend agree on breakpoint precedence. | |
| 121 | + 'responsive_devices' => Helper::get_responsive_devices(), | |
| 122 | + // Whether breakpoint queries overlap (cascade) or are exclusive | |
| 123 | + // bands (strict). The editor preview builds matching queries. | |
| 124 | + 'breakpoint_mode' => Helper::get_breakpoint_mode(), | |
| 110 | 125 | ]; |
| 111 | 126 | } |
| 112 | 127 | |
| 113 | 128 | public function get_localize_dashboard_data() { |
| 114 | - if ( ! current_user_can( 'manage_options' ) ) { | |
| 129 | + // Anyone permitted to reach an aBlocks screen needs the nonce, or the | |
| 130 | + // dashboard loads and then 403s on every request it makes. | |
| 131 | + if ( ! current_user_can( Permissions::ACCESS ) ) { | |
| 115 | 132 | return $this->get_localize_script_data(); |
| 116 | 133 | } |
| 117 | 134 | return array_merge($this->get_localize_script_data(), [ |
| 118 | 135 | 'nonce' => wp_create_nonce( 'wp_rest' ), |
| 119 | 136 | 'ablocks_nonce' => wp_create_nonce( 'ablocks_nonce' ), |
| 137 | + 'permissions' => Permissions::for_user(), | |
| 138 | + 'is_admin_user' => Permissions::is_real_admin(), | |
| 120 | 139 | ]); |
| 121 | 140 | } |
| 122 | 141 | public function get_localize_editor_data() { |
| 123 | 142 | if ( ! current_user_can( 'edit_posts' ) ) { |
| @@ -196,8 +215,11 @@ | ||
| 196 | 215 | 'academy_lms' => Helper::is_active_academy(), |
| 197 | 216 | 'storeengine' => Helper::is_active_storeengine(), |
| 198 | 217 | 'wp_map_block' => Helper::is_active_wp_map_block(), |
| 199 | 218 | 'easy_content_manager' => Helper::is_active_easy_content_manager(), |
| 219 | + 'zencommunity' => Helper::is_active_zencommunity(), | |
| 220 | + 'gemboards' => Helper::is_active_gemboards(), | |
| 221 | + 'quizpress' => Helper::is_active_quizpress(), | |
| 200 | 222 | ] |
| 201 | 223 | ); |
| 202 | 224 | return apply_filters( |
| 203 | 225 | 'ablocks/assets/dashboard_scripts_data', |
| @@ -229,8 +251,11 @@ | ||
| 229 | 251 | 'global_color' => (array) Helper::get_settings( 'global_color', [] ), |
| 230 | 252 | 'global_typography' => (array) Helper::get_settings( 'global_typography', [] ), |
| 231 | 253 | 'global_typography_list' => wp_list_pluck( Helper::get_settings( 'global_typography', [] ), 'value', 'id' ), |
| 232 | 254 | 'global_font_family_fallback' => (string) Helper::get_settings( 'global_font_family_fallback', 'Sans-serif' ), |
| 255 | + // Editor paste — see includes/api/paste-controller.php. | |
| 256 | + 'paste_google_docs' => (bool) Helper::get_settings( 'paste_google_docs', true ), | |
| 257 | + 'paste_convert_webp' => (bool) Helper::get_settings( 'paste_convert_webp', true ), | |
| 233 | 258 | // Theme.json + Font Library families, so uploaded/theme fonts are |
| 234 | 259 | // selectable next to the Google catalog. |
| 235 | 260 | 'theme_fonts' => \ABlocks\Classes\FontStack::get_theme_font_families(), |
| 236 | 261 | ], |
| @@ -243,9 +268,13 @@ | ||
| 243 | 268 | 'quizpress' => Helper::is_active_quizpress(), |
| 244 | 269 | 'easy_content_manager' => Helper::is_active_easy_content_manager(), |
| 245 | 270 | ], |
| 246 | 271 | 'blocks_status' => $ablocks_blocks, |
| 247 | - 'post_types' => $post_types | |
| 272 | + 'post_types' => $post_types, | |
| 273 | + // What this user may do inside the editor. Read by | |
| 274 | + // src/utils/permissions.js to hide controls they cannot use. | |
| 275 | + 'permissions' => Permissions::for_user(), | |
| 276 | + 'is_admin_user' => Permissions::is_real_admin(), | |
| 248 | 277 | ); |
| 249 | 278 | return apply_filters( |
| 250 | 279 | 'ablocks/assets/editor_scripts_data', |
| 251 | 280 | array_merge( |
| @@ -807,23 +836,108 @@ | ||
| 807 | 836 | |
| 808 | 837 | public function is_assets_generated() { |
| 809 | 838 | $file_name = $this->current_page_slug; |
| 810 | 839 | $css_file_path = $this->FileUpload->get_file_path( $file_name . '.min.css' ); |
| 811 | - return file_exists( $css_file_path ); | |
| 840 | + | |
| 841 | + if ( ! file_exists( $css_file_path ) ) { | |
| 842 | + return false; | |
| 843 | + } | |
| 844 | + | |
| 845 | + // The file embeds the global-class CSS this page needs, so editing a | |
| 846 | + // class has to make every generated stylesheet stale. Comparing the | |
| 847 | + // file's mtime against the library's change stamp rebuilds them lazily, | |
| 848 | + // one page at a time on next visit, without stamping a revision into | |
| 849 | + // every file name (which the per-post delete in Blocks relies on). | |
| 850 | + // | |
| 851 | + // Strictly greater, not >=: mtime has one-second resolution, so a file | |
| 852 | + // written in the same second as a class edit is indistinguishable from | |
| 853 | + // one written just after it. Treating that tie as stale costs one extra | |
| 854 | + // regeneration; treating it as fresh would serve the old CSS until the | |
| 855 | + // next edit. | |
| 856 | + return filemtime( $css_file_path ) > max( | |
| 857 | + GlobalClasses::get_revision(), | |
| 858 | + self::build_revision() | |
| 859 | + ); | |
| 812 | 860 | } |
| 813 | 861 | |
| 862 | + /** | |
| 863 | + * When this plugin's own CSS last changed, as a timestamp. | |
| 864 | + * | |
| 865 | + * The generated file bakes in each block's static stylesheet (see | |
| 866 | + * AssetsGenerator, which concatenates get_static_css() into it), so a CSS | |
| 867 | + * fix shipped in an update reached nobody whose pages were generated before | |
| 868 | + * it: the file still existed, still parsed, and nothing about it looked | |
| 869 | + * stale, so it was served unchanged forever. Verified by planting a | |
| 870 | + * stylesheet carrying the previous release's rules — it survived every | |
| 871 | + * subsequent page load untouched. That is why a fix could land in the | |
| 872 | + * editor, which loads each block's style.css directly, and never appear on | |
| 873 | + * the front end. | |
| 874 | + * | |
| 875 | + * Keyed on the version rather than a file scan: it is one option read on | |
| 876 | + * the common path, and every shipped CSS change comes with a version bump. | |
| 877 | + * Each page then rebuilds once, on its next visit, exactly the way a | |
| 878 | + * global-class edit already makes them rebuild. | |
| 879 | + * | |
| 880 | + * @return int Timestamp of the running version's first request. | |
| 881 | + */ | |
| 882 | + public static function build_revision() { | |
| 883 | + $stamp = get_option( self::BUILD_OPTION ); | |
| 884 | + | |
| 885 | + if ( | |
| 886 | + is_array( $stamp ) && | |
| 887 | + isset( $stamp['version'], $stamp['time'] ) && | |
| 888 | + ABLOCKS_VERSION === $stamp['version'] | |
| 889 | + ) { | |
| 890 | + return (int) $stamp['time']; | |
| 891 | + } | |
| 892 | + | |
| 893 | + $now = time(); | |
| 894 | + update_option( | |
| 895 | + self::BUILD_OPTION, | |
| 896 | + [ | |
| 897 | + 'version' => ABLOCKS_VERSION, | |
| 898 | + 'time' => $now, | |
| 899 | + ], | |
| 900 | + true | |
| 901 | + ); | |
| 902 | + | |
| 903 | + return $now; | |
| 904 | + } | |
| 905 | + | |
| 814 | 906 | public function set_current_page_template_part( $content, $block ) { |
| 815 | 907 | if ( ! isset( $block['blockName'] ) && is_array( $block ) ) { |
| 816 | 908 | foreach ( $block as $block_item ) { |
| 817 | - if ( ! empty( $block_item['blockName'] ) && strpos( $block_item['blockName'], 'ablocks/' ) !== false ) { | |
| 909 | + if ( $this->is_page_asset_block( $block_item ) ) { | |
| 818 | 910 | $this->current_page_blocks[] = $block_item; |
| 819 | 911 | } |
| 820 | 912 | } |
| 821 | 913 | } |
| 822 | - if ( ! empty( $block['blockName'] ) && strpos( $block['blockName'], 'ablocks/' ) !== false ) { | |
| 914 | + if ( $this->is_page_asset_block( $block ) ) { | |
| 823 | 915 | $this->current_page_blocks[] = $block; |
| 824 | 916 | } |
| 825 | 917 | return $content; |
| 918 | + } | |
| 919 | + | |
| 920 | + /** | |
| 921 | + * Whether a top-level block contributes to the page's generated assets. | |
| 922 | + * | |
| 923 | + * A synced pattern counts too: AssetsGenerator::recursive_block_parser() | |
| 924 | + * already expands its reference. Collecting only `ablocks/*` names left a | |
| 925 | + * page whose content is just a pattern with no combined CSS/JS at all — so a | |
| 926 | + * Loop Filter placed from a pattern rendered unstyled and did nothing when | |
| 927 | + * clicked. | |
| 928 | + * | |
| 929 | + * @param mixed $block Parsed block. | |
| 930 | + * @return bool | |
| 931 | + */ | |
| 932 | + private function is_page_asset_block( $block ) { | |
| 933 | + if ( ! is_array( $block ) || empty( $block['blockName'] ) ) { | |
| 934 | + return false; | |
| 935 | + } | |
| 936 | + if ( 'core/block' === $block['blockName'] ) { | |
| 937 | + return ! empty( $block['attrs']['ref'] ); | |
| 938 | + } | |
| 939 | + return strpos( $block['blockName'], 'ablocks/' ) !== false; | |
| 826 | 940 | } |
| 827 | 941 | |
| 828 | 942 | public function set_theme_builder_locations( $args ) { |
| 829 | 943 | $this->theme_builder_locations = $args; |