| @@ -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 | |