version = sanitize_text_field((string) $version); } /** * Returns the shared manager for this plugin. * * @param string $plugin_name Plugin identifier. * @param string $version Plugin version. * @return AdUnblocker_Admin_Assets */ public static function instance($plugin_name, $version) { $key = sanitize_key($plugin_name); if (! isset(self::$instances[$key])) { self::$instances[$key] = new self($version); } return self::$instances[$key]; } /** * Returns an opaque public URL for an admin asset. * * The original URL is returned when the source cannot be copied, allowing * WordPress to retain its normal asset-loading behaviour on read-only hosts. * * @param string $logical_name Internal asset identifier. * @param string $source_path Absolute source file path. * @param string $fallback_url Original plugin asset URL. * @return string */ public function get_asset_url($logical_name, $source_path, $fallback_url) { $logical_name = sanitize_key($logical_name); $source_path = (string) $source_path; $extension = strtolower(pathinfo($source_path, PATHINFO_EXTENSION)); if ( '' === $logical_name || ! in_array($extension, array('css', 'js'), true) || ! is_file($source_path) || ! is_readable($source_path) ) { return $fallback_url; } $state = $this->get_state(); if (! $this->ensure_bundle_directory($state['key'])) { return $fallback_url; } $signature = hash_file('sha256', $source_path); if (! is_string($signature) || '' === $signature) { return $fallback_url; } $entry = isset($state['files'][$logical_name]) && is_array($state['files'][$logical_name]) ? $state['files'][$logical_name] : array(); $file_name = isset($entry['file']) ? sanitize_file_name((string) $entry['file']) : ''; $target_path = $this->get_bundle_path($state['key'], $file_name); $is_current = ( isset($entry['signature']) && hash_equals((string) $entry['signature'], $signature) && $this->is_safe_asset_file_name($file_name, $extension) && is_file($target_path) ); if (! $is_current) { $new_file_name = $this->generate_available_file_name($state['key'], $extension); if ('' === $new_file_name) { return $fallback_url; } $new_target_path = $this->get_bundle_path($state['key'], $new_file_name); if (! $this->copy_atomically($source_path, $new_target_path)) { return $fallback_url; } $old_file_name = $file_name; $state['files'][$logical_name] = array( 'file' => $new_file_name, 'signature' => $signature, ); $this->state = $state; $this->persist_state($state); if ($this->is_safe_asset_file_name($old_file_name, $extension)) { $old_target_path = AdUnblocker_Asset_Cache::get_path($state['key'], $old_file_name); if ($old_target_path !== $new_target_path && is_file($old_target_path)) { AdUnblocker_Asset_Cache::delete_file($state['key'], self::BUNDLE_OWNER, $old_file_name); } } $file_name = $new_file_name; AdUnblocker_Asset_Cache::delete_legacy_marked_bundle( $state['key'], self::MARKER_FILE, self::MARKER_CONTENT ); } $asset_url = AdUnblocker_Asset_Cache::get_url($state['key'], $file_name); return '' !== $asset_url ? $asset_url : $fallback_url; } /** * Removes the generated admin bundle during plugin deactivation. * * @return void */ public static function clear_bundle() { $state = get_option(self::OPTION_NAME, array()); $key = isset($state['key']) ? sanitize_file_name((string) $state['key']) : ''; AdUnblocker_Asset_Cache::delete_bundle($key, self::BUNDLE_OWNER); AdUnblocker_Asset_Cache::delete_legacy_marked_bundle( $key, self::MARKER_FILE, self::MARKER_CONTENT ); delete_option(self::OPTION_NAME); self::$instances = array(); } private function get_state() { if (is_array($this->state)) { return $this->state; } $state = get_option(self::OPTION_NAME, array()); $is_valid = ( is_array($state) && isset($state['key'], $state['version'], $state['files']) && $this->is_safe_bundle_key($state['key']) && is_array($state['files']) ); if ($is_valid && (string) $state['version'] === $this->version) { $this->state = $state; return $this->state; } if ($is_valid) { AdUnblocker_Asset_Cache::delete_bundle($state['key'], self::BUNDLE_OWNER); AdUnblocker_Asset_Cache::delete_legacy_marked_bundle( $state['key'], self::MARKER_FILE, self::MARKER_CONTENT ); } $this->state = array( 'key' => $this->generate_available_bundle_key(), 'version' => $this->version, 'files' => array(), ); $this->persist_state($this->state); return $this->state; } private function persist_state($state) { if (false === get_option(self::OPTION_NAME, false)) { return add_option(self::OPTION_NAME, $state, '', 'no'); } return update_option(self::OPTION_NAME, $state, false); } private function ensure_bundle_directory($key) { return AdUnblocker_Asset_Cache::ensure_bundle($key, self::BUNDLE_OWNER); } private function copy_atomically($source_path, $target_path) { $file_name = wp_basename($target_path); $bundle_key = wp_basename(dirname($target_path)); return AdUnblocker_Asset_Cache::copy( $bundle_key, self::BUNDLE_OWNER, $file_name, $source_path ); } private function generate_available_bundle_key() { for ($attempt = 0; $attempt < 10; $attempt++) { $key = $this->generate_opaque_name(16); if (! file_exists(AdUnblocker_Asset_Cache::get_path($key))) { return $key; } } return ''; } private function generate_available_file_name($key, $extension) { for ($attempt = 0; $attempt < 10; $attempt++) { $file_name = $this->generate_opaque_name(16) . '.' . $extension; if (! file_exists(AdUnblocker_Asset_Cache::get_path($key, $file_name))) { return $file_name; } } return ''; } private function generate_opaque_name($length) { $characters = 'cefghijklmnpqruvwxyz'; $name = ''; $max_index = strlen($characters) - 1; for ($index = 0; $index < $length; $index++) { $name .= $characters[random_int(0, $max_index)]; } return $name; } private function get_bundle_path($key, $file_name = '') { return AdUnblocker_Asset_Cache::get_path($key, $file_name); } private function is_safe_bundle_key($key) { return is_string($key) && 1 === preg_match('/^[cefghijklmnpqruvwxyz]{16}$/', $key); } private function is_safe_asset_file_name($file_name, $extension) { return is_string($file_name) && 1 === preg_match( '/^[cefghijklmnpqruvwxyz]{16}\.' . preg_quote($extension, '/') . '$/', $file_name ); } }