PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.2.6
ShopBuilder – WooCommerce Builder For Elementor v3.2.6
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
← All changes | app/Models/AssetBundler.php +7 -255 3.4.23.2.6 View file →
@@ -9,10 +9,10 @@
9 9 */
10 10
11 11 namespace RadiusTheme\SB\Models;
12 12
13 -use RadiusTheme\SB\Dependencies\MatthiasMullie\Minify\JS;
14 -use RadiusTheme\SB\Dependencies\MatthiasMullie\Minify\CSS;
13 +use MatthiasMullie\Minify\JS;
14 +use MatthiasMullie\Minify\CSS;
15 15
16 16 // Do not allow directly accessing this file.
17 17 if ( ! defined( 'ABSPATH' ) ) {
18 18 exit( 'This script cannot be accessed directly.' );
@@ -21,21 +21,9 @@
21 21 /**
22 22 * AssetBundler class.
23 23 */
24 24 class AssetBundler {
25 -
26 25 /**
27 - * How long a build lock stays valid, in seconds.
28 - *
29 - * Generous compared with the measured build cost (tens of milliseconds) so a
30 - * slow host is never treated as a crashed one, while still bounding how long
31 - * a lock left by a fatal error can hold up the next attempt.
32 - *
33 - * @var int
34 - */
35 - const BUILD_LOCK_TTL = 60;
36 -
37 - /**
38 26 * Handle for this bundle (used in filename/versioning).
39 27 *
40 28 * @var string
41 29 */
@@ -97,24 +85,8 @@
97 85 protected function maybe_create_cache_dir() {
98 86 if ( ! file_exists( $this->upload_dir ) ) {
99 87 wp_mkdir_p( $this->upload_dir );
100 88 }
101 -
102 - if ( ! wp_is_writable( $this->upload_dir ) ) {
103 - global $wp_filesystem;
104 -
105 - if ( ! function_exists( 'WP_Filesystem' ) ) {
106 - require_once ABSPATH . 'wp-admin/includes/file.php';
107 - }
108 -
109 - if ( ! $wp_filesystem ) {
110 - WP_Filesystem();
111 - }
112 -
113 - if ( $wp_filesystem ) {
114 - $wp_filesystem->chmod( $this->upload_dir, FS_CHMOD_DIR );
115 - }
116 - }
117 89 }
118 90
119 91 /**
120 92 * Generates the full path to the cached JS file based on handle and file hash.
@@ -144,98 +116,15 @@
144 116 public function build() {
145 117 $cache_path = $this->get_cache_file_path();
146 118
147 119 if ( ! $this->is_cached() ) {
148 - if ( ! wp_is_writable( $this->upload_dir ) ) {
149 - return '';
150 - }
120 + $minifier = ( 'css' === $this->type ) ? new CSS() : new JS();
151 121
152 - // Guard against the scoped Minify library not being loadable yet — e.g. the first
153 - // request right after a plugin upgrade, when opcache/the autoload map still points
154 - // at the old (now deleted) files. Instantiating the class then would fatal; instead
155 - // skip bundling for this request and fall back to the unbundled assets. It self-heals
156 - // on the next request once the autoloader is refreshed.
157 - $minify_class = ( 'css' === $this->type ) ? CSS::class : JS::class;
158 -
159 - if ( ! class_exists( $minify_class ) ) {
160 - return '';
122 + foreach ( $this->source_files as $file ) {
123 + $this->safely_add_file_to_minifier( $file, $minifier );
161 124 }
162 125
163 - /*
164 - * One builder per bundle.
165 - *
166 - * Without this every concurrent request that finds a cold bundle
167 - * minifies the whole set independently: measured 8 of 8 and 13 of 16
168 - * processes doing identical work. The loser does not wait - waiting
169 - * would just move the cost rather than remove it - it returns an empty
170 - * source so the caller falls back to the unbundled assets for this one
171 - * request, and picks up the finished bundle on the next.
172 - *
173 - * This is an optimisation, not a correctness mechanism: the atomic
174 - * rename below is what guarantees nobody ever reads a partial file, so
175 - * a lock that occasionally fails to exclude is harmless.
176 - */
177 - if ( ! $this->acquire_build_lock() ) {
178 - return '';
179 - }
180 -
181 - try {
182 - $minifier = ( 'css' === $this->type ) ? new CSS() : new JS();
183 -
184 - foreach ( $this->source_files as $file ) {
185 - $this->safely_add_file_to_minifier( $file, $minifier );
186 - }
187 -
188 - /*
189 - * Minify to a temporary file and move it into place only once it is
190 - * complete.
191 - *
192 - * The minifier opens its target with fopen( $path, 'w' ), which
193 - * truncates immediately, and is_cached() only tests file_exists().
194 - * Writing straight to the final path therefore exposes a window in
195 - * which the bundle exists at zero bytes and is reported as cached, so
196 - * a concurrent request would serve an empty stylesheet or script.
197 - * rename() is atomic on POSIX, so readers see either the previous
198 - * bundle or the finished one, never a partial write.
199 - */
200 - $temp_path = $this->get_temp_file_path();
201 -
202 - try {
203 - $minifier->minify( $temp_path );
204 - } catch ( \Exception $e ) {
205 - $this->delete_temp_file( $temp_path );
206 - error_log( '[ShopBuilder Minify] ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
207 -
208 - return '';
209 - }
210 -
211 - /*
212 - * rename() rather than WP_Filesystem::move(): atomicity is the whole
213 - * point of this fix, and WP_Filesystem only delivers it on the direct
214 - * transport. Over FTP or SSH it copies, which reintroduces the partial
215 - * read this is meant to remove, and initialising WP_Filesystem on a
216 - * frontend request is both expensive and liable to fail without
217 - * credentials. The @ suppresses the warning on a losing race, which is
218 - * handled below rather than surfaced.
219 - */
220 - // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename, WordPress.PHP.NoSilencedErrors.Discouraged
221 - if ( ! @rename( $temp_path, $cache_path ) ) {
222 - $this->delete_temp_file( $temp_path );
223 - error_log( '[ShopBuilder Minify] Could not move the bundle into place: ' . $cache_path ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
224 -
225 - return '';
226 - }
227 -
228 - // Only once the new bundle is in place is it safe to drop the ones it
229 - // replaced. Doing this earlier would leave the site with nothing to
230 - // serve if the build failed.
231 - $this->prune_obsolete_bundles();
232 - } finally {
233 - // Every exit above returns from inside the try, so the release has
234 - // to live here: success, minify exception and rename failure all
235 - // have to hand the lock back.
236 - $this->release_build_lock();
237 - }
126 + $minifier->minify( $cache_path );
238 127 }
239 128
240 129 return $this->get_url();
241 130 }
@@ -240,139 +129,8 @@
240 129 return $this->get_url();
241 130 }
242 131
243 132 /**
244 - * Path of the lock guarding this bundle's build.
245 - *
246 - * Sits beside the bundle it guards, so the lock is per bundle rather than
247 - * global and two different bundles never contend. The `.lock` suffix keeps it
248 - * outside the `handle-*.min.type` glob used by prune_obsolete_bundles(), so
249 - * pruning can never delete a live lock.
250 - *
251 - * @return string
252 - */
253 - protected function get_lock_file_path() {
254 - return $this->get_cache_file_path() . '.lock';
255 - }
256 -
257 - /**
258 - * Try to become the process that builds this bundle.
259 - *
260 - * Opening with the 'x' mode creates the file only if it does not already
261 - * exist, and the check and create are a single atomic operation, so exactly
262 - * one caller can win. A lock older than the TTL is assumed to belong to a
263 - * request that died mid build and is reclaimed.
264 - *
265 - * @return bool Whether this process holds the lock.
266 - */
267 - protected function acquire_build_lock() {
268 - $lock_path = $this->get_lock_file_path();
269 -
270 - clearstatcache( true, $lock_path );
271 -
272 - if ( file_exists( $lock_path ) ) {
273 - $age = time() - (int) filemtime( $lock_path );
274 -
275 - if ( $age < self::BUILD_LOCK_TTL ) {
276 - return false;
277 - }
278 -
279 - // Stale: the builder that created this is gone.
280 - wp_delete_file( $lock_path );
281 - }
282 -
283 - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.PHP.NoSilencedErrors.Discouraged
284 - $handle = @fopen( $lock_path, 'x' );
285 -
286 - if ( false === $handle ) {
287 - // Another process created it between the check above and here.
288 - return false;
289 - }
290 -
291 - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite, WordPress.PHP.NoSilencedErrors.Discouraged
292 - @fwrite( $handle, (string) getmypid() );
293 - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
294 - fclose( $handle );
295 -
296 - return true;
297 - }
298 -
299 - /**
300 - * Hand the build lock back.
301 - *
302 - * @return void
303 - */
304 - protected function release_build_lock() {
305 - $lock_path = $this->get_lock_file_path();
306 -
307 - clearstatcache( true, $lock_path );
308 -
309 - if ( file_exists( $lock_path ) ) {
310 - wp_delete_file( $lock_path );
311 - }
312 - }
313 -
314 - /**
315 - * Path for the in-progress bundle.
316 - *
317 - * Lives in the same directory as the final file so rename() stays on one
318 - * filesystem, and carries a per-process suffix so two concurrent builds never
319 - * share a temporary file. The suffix also keeps it outside the
320 - * `handle-*.min.type` glob used for pruning, so one build cannot delete
321 - * another's work in progress.
322 - *
323 - * @return string
324 - */
325 - protected function get_temp_file_path() {
326 - return $this->get_cache_file_path() . '.tmp-' . getmypid() . '-' . uniqid( '', true );
327 - }
328 -
329 - /**
330 - * Remove a temporary file, ignoring the case where it was never created.
331 - *
332 - * @param string $path Temporary file path.
333 - *
334 - * @return void
335 - */
336 - protected function delete_temp_file( $path ) {
337 - if ( $path && file_exists( $path ) ) {
338 - wp_delete_file( $path );
339 - }
340 - }
341 -
342 - /**
343 - * Delete previous bundles for this handle, keeping the current one.
344 - *
345 - * Bundle filenames embed a hash of the plugin versions and the source file
346 - * list, so a new file appears on every update and on every settings change
347 - * that alters which assets are included. Without this the directory grows
348 - * without bound: nothing else prunes it, because Cache::clear_asset_cache()
349 - * returns early when optimization is disabled or the directory is not
350 - * writable.
351 - *
352 - * @return void
353 - */
354 - protected function prune_obsolete_bundles() {
355 - $current = $this->get_cache_file_path();
356 - $pattern = $this->upload_dir . "$this->handle-*.min.$this->type";
357 - $files = glob( $pattern );
358 -
359 - if ( ! is_array( $files ) ) {
360 - return;
361 - }
362 -
363 - foreach ( $files as $file ) {
364 - if ( $file === $current ) {
365 - continue;
366 - }
367 -
368 - if ( file_exists( $file ) ) {
369 - wp_delete_file( $file );
370 - }
371 - }
372 - }
373 -
374 - /**
375 133 * Returns the URL to the cached JS bundle.
376 134 *
377 135 * @return string
378 136 */
@@ -400,15 +158,9 @@
400 158 *
401 159 * @return string
402 160 */
403 161 protected function generate_filename() {
404 - $version = defined( 'RTSB_VERSION' ) ? RTSB_VERSION : '';
405 -
406 - if ( defined( 'RTSBPRO_VERSION' ) ) {
407 - $version .= '-' . RTSBPRO_VERSION;
408 - }
409 -
410 - $hash = md5( $this->handle . $version . wp_json_encode( $this->source_files ) );
162 + $hash = md5( $this->handle . wp_json_encode( $this->source_files ) );
411 163
412 164 return "$this->handle-$hash.min.$this->type";
413 165 }
414 166