PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta3
Elementor Website Builder – more than just a page builder v4.3.0-beta3
4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
← All changes | modules/atomic-widgets/styles/css-files-manager.php +123 -21 4.2.04.3.0-beta3 View file →
@@ -1,59 +1,119 @@
1 1 <?php
2 2
3 3 namespace Elementor\Modules\AtomicWidgets\Styles;
4 4
5 +use Elementor\Core\Files\Base as Base_File;
6 +use Elementor\Modules\AtomicWidgets\Styles\CacheValidity\Cache_Validity;
7 +
5 8 class CSS_Files_Manager {
6 - const DEFAULT_CSS_DIR = 'elementor/css/';
9 + const DEFAULT_CSS_DIR = 'css/';
7 10 const FILE_EXTENSION = '.css';
8 11 // Read and write permissions for the owner
9 12 const PERMISSIONS = 0644;
10 13
11 - public function get( string $handle, string $media, callable $get_css, bool $is_valid_cache ): ?Style_File {
12 - $filesystem = $this->get_filesystem();
14 + private Cache_Validity $cache_validity;
15 +
16 + public function __construct( ?Cache_Validity $cache_validity = null ) {
17 + $this->cache_validity = $cache_validity ?? new Cache_Validity();
18 + }
19 +
20 + /**
21 + * Return the CSS file to enqueue for the given cache path, generating it on demand.
22 + *
23 + * The `should_exist` meta flag distinguishes an intentionally empty file from one
24 + * that went missing after being generated, and cache is regenerated whenever it's
25 + * invalid or the expected file can't be found on disk.
26 + *
27 + * @param string $handle
28 + * @param string $media
29 + * @param callable $get_css
30 + * @param array<string> $cache_path Cache-validity leaf path.
31 + * @return Style_File|null
32 + */
33 + public function get( string $handle, string $media, callable $get_css, array $cache_path ): ?Style_File {
13 34 $path = $this->get_path( $handle );
35 + $is_cache_valid = $this->cache_validity->is_valid( $cache_path );
36 + $should_exist = $this->get_should_exist_meta( $cache_path );
14 37
15 - if ( $is_valid_cache ) {
16 - if ( ! $filesystem->exists( $path ) ) {
38 + if ( $is_cache_valid ) {
39 + if ( false === $should_exist ) {
17 40 return null;
18 41 }
19 42
20 - // Return the existing file
21 - return Style_File::create(
22 - $this->sanitize_handle( $handle ),
23 - $this->get_filesystem_path( $this->get_path( $handle ) ),
24 - $this->get_url( $handle ),
25 - $media,
26 - );
43 + if ( $this->has_non_empty_file( $path ) ) {
44 + return $this->create_style_file( $handle, $path, $media );
45 + }
27 46 }
28 47
29 48 $css = $get_css();
30 49
31 50 if ( empty( $css ) ) {
51 + // Nothing to serve for this path; purge any stale file and record the state
52 + // so we don't try to regenerate on every subsequent request.
53 + $this->delete_if_exists( $path );
54 + $this->cache_validity->validate( $cache_path, [ 'should_exist' => false ] );
55 +
32 56 return null;
33 57 }
34 58
35 59 $filesystem_path = $this->get_filesystem_path( $path );
36 60
37 - $is_created = $filesystem->put_contents( $filesystem_path, $css, self::PERMISSIONS );
61 + if ( ! $this->write_atomically( $filesystem_path, $css ) ) {
62 + // Hard write failure: leave the cache invalid so the next request retries.
63 + return null;
64 + }
38 65
39 - if ( false === $is_created ) {
66 + $this->cache_validity->validate( $cache_path, [ 'should_exist' => true ] );
67 +
68 + return $this->create_style_file( $handle, $path, $media );
69 + }
70 +
71 + public function delete( string $handle ): void {
72 + $this->delete_if_exists( $this->get_path( $handle ) );
73 + }
74 +
75 + private function get_should_exist_meta( array $cache_path ): ?bool {
76 + $meta = $this->cache_validity->get_meta( $cache_path );
77 +
78 + if ( ! is_array( $meta ) || ! array_key_exists( 'should_exist', $meta ) ) {
40 79 return null;
41 80 }
42 81
82 + return (bool) $meta['should_exist'];
83 + }
84 +
85 + private function create_style_file( string $handle, string $path, string $media ): Style_File {
43 86 return Style_File::create(
44 87 $this->sanitize_handle( $handle ),
45 - $filesystem_path,
88 + $this->get_filesystem_path( $path ),
46 89 $this->get_url( $handle ),
47 90 $media
48 91 );
49 92 }
50 93
51 - public function delete( string $handle ): void {
94 + private function has_non_empty_file( string $path ): bool {
52 95 $filesystem = $this->get_filesystem();
53 - $path = $this->get_path( $handle );
54 96
55 97 if ( ! $filesystem->exists( $path ) ) {
98 + return false;
99 + }
100 +
101 + $size = $filesystem->size( $path );
102 +
103 + // `size()` may return false on filesystems that don't support it; treat that as
104 + // "we don't know" and fall back to trusting the existence check.
105 + if ( false === $size ) {
106 + return true;
107 + }
108 +
109 + return $size > 0;
110 + }
111 +
112 + private function delete_if_exists( string $path ): void {
113 + $filesystem = $this->get_filesystem();
114 +
115 + if ( ! $filesystem->exists( $path ) ) {
56 116 return;
57 117 }
58 118
59 119 $filesystem->delete( $path );
@@ -58,8 +118,52 @@
58 118
59 119 $filesystem->delete( $path );
60 120 }
61 121
122 + /**
123 + * Write to a temp file first and then move it into place. The move step is atomic on
124 + * POSIX filesystems, so the public URL cannot serve a partial or zero-byte asset while a
125 + * concurrent render is in progress. If the atomic move is not supported by the current
126 + * filesystem adapter, fall back to a direct write.
127 + */
128 + private function write_atomically( string $filesystem_path, string $css ): bool {
129 + $filesystem = $this->get_filesystem();
130 +
131 + if ( ! $this->ensure_directory_exists( dirname( $filesystem_path ) ) ) {
132 + return false;
133 + }
134 +
135 + $tmp_path = $filesystem_path . '.tmp-' . wp_generate_password( 8, false, false );
136 +
137 + $is_written = $filesystem->put_contents( $tmp_path, $css, self::PERMISSIONS );
138 +
139 + if ( false === $is_written ) {
140 + return false;
141 + }
142 +
143 + if ( ! method_exists( $filesystem, 'move' ) || ! $filesystem->move( $tmp_path, $filesystem_path, true ) ) {
144 + $fallback = $filesystem->put_contents( $filesystem_path, $css, self::PERMISSIONS );
145 +
146 + if ( $filesystem->exists( $tmp_path ) ) {
147 + $filesystem->delete( $tmp_path );
148 + }
149 +
150 + return false !== $fallback;
151 + }
152 +
153 + return true;
154 + }
155 +
156 + private function ensure_directory_exists( string $directory ): bool {
157 + $filesystem = $this->get_filesystem();
158 +
159 + if ( $filesystem->is_dir( $directory ) ) {
160 + return true;
161 + }
162 +
163 + return wp_mkdir_p( $directory );
164 + }
165 +
62 166 private function get_filesystem(): \WP_Filesystem_Base {
63 167 global $wp_filesystem;
64 168
65 169 if ( empty( $wp_filesystem ) ) {
@@ -76,21 +180,19 @@
76 180 return str_replace( ABSPATH, $filesystem->abspath(), $path );
77 181 }
78 182
79 183 private function get_url( string $handle ): string {
80 - $upload_dir = wp_upload_dir();
81 184 $sanitized_handle = $this->sanitize_handle( $handle );
82 185 $handle = $sanitized_handle . self::FILE_EXTENSION;
83 186
84 - return trailingslashit( $upload_dir['baseurl'] ) . self::DEFAULT_CSS_DIR . $handle;
187 + return Base_File::get_base_uploads_url() . self::DEFAULT_CSS_DIR . $handle;
85 188 }
86 189
87 190 private function get_path( string $handle ): string {
88 - $upload_dir = wp_upload_dir();
89 191 $sanitized_handle = $this->sanitize_handle( $handle );
90 192 $handle = $sanitized_handle . self::FILE_EXTENSION;
91 193
92 - return trailingslashit( $upload_dir['basedir'] ) . self::DEFAULT_CSS_DIR . $handle;
194 + return Base_File::get_base_uploads_dir() . self::DEFAULT_CSS_DIR . $handle;
93 195 }
94 196
95 197 private function sanitize_handle( string $handle ): string {
96 198 return preg_replace( '/[^a-zA-Z0-9_-]/', '', $handle );