cache_dir = $basedir . '/master_addons/templates-library/'; $this->cache_expiry = apply_filters('jltma_cache_expiry', 6 * HOUR_IN_SECONDS); // 6 hours default, filterable // The class is instantiated lazily (first get_instance() call), which may // happen before or after `init` has run. Hooking `init` after it already // fired would silently skip bootstrapping, leaving $config empty and the // cache directory missing, so bootstrap immediately in that case. if (did_action('init')) { $this->init_config(); $this->init(); } else { add_action('init', [$this, 'init_config'], 20); add_action('init', [$this, 'init'], 25); } } public function init_config() { if (!empty($this->config)) { return $this->config; } // Initialize config after templates system is ready if (function_exists('MasterAddons\\Inc\\Admin\\Templates\\master_addons_templates')) { $templates_instance = Templates\master_addons_templates(); if ($templates_instance && isset($templates_instance->config)) { $this->config = $templates_instance->config->get('api'); } } return $this->config; } /** * Resolve the API config on demand. * * The templates system may register itself after this class is built, so * retry the lookup instead of assuming init_config() succeeded. */ private function get_api_config() { if (empty($this->config)) { $this->init_config(); } return $this->config; } public function init() { if ($this->initialized) { return; } $this->initialized = true; // Directory cleanup/creation is deferred to the first actual cache access // (see maybe_bootstrap_cache_dir()) so a plain page load does no disk work. // Schedule cache updates add_action('wp', [$this, 'schedule_cache_updates']); add_action('jltma_templates_cache_update', [$this, 'update_templates_cache']); // Admin hooks add_action('admin_init', [$this, 'maybe_clear_cache']); // Performance optimizations add_action('wp_ajax_jltma_preload_cache', [$this, 'preload_cache_ajax']); add_action('jltma_templates_preload_cache', [$this, 'preload_popular_templates']); add_action('jltma_background_preload', [$this, 'do_background_preload']); // Extend existing cache methods add_filter('jltma_templates_cache_enabled', '__return_true'); } /** * Ensure cache directory exists with proper structure */ private function ensure_cache_directory() { // Nothing is written to disk unless the site opted back in, so creating // the tree would leave a client with six empty folders under uploads // that never fill up and that the cleanup routine then has to remove. if (!$this->local_cache_enabled()) { return false; } // Check if uploads directory is writable if (!$this->is_uploads_writable()) { return false; } if (!file_exists($this->cache_dir)) { if (!wp_mkdir_p($this->cache_dir)) { return false; } // Create subdirectories for different template types // Removed 'template-kits' as it should be in its own separate folder $subdirs = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers', 'master_widgets']; foreach ($subdirs as $subdir) { wp_mkdir_p($this->cache_dir . $subdir . '/'); wp_mkdir_p($this->cache_dir . $subdir . '/categories/'); wp_mkdir_p($this->cache_dir . $subdir . '/keywords/'); wp_mkdir_p($this->cache_dir . $subdir . '/templates/'); wp_mkdir_p($this->cache_dir . $subdir . '/images/'); } // Create .htaccess for security $htaccess_content = "Options -Indexes\n\nOrder allow,deny\nAllow from all\n"; $this->fs_put_contents($this->cache_dir . '.htaccess', $htaccess_content); // Create index.php files $index_content = "fs_put_contents($this->cache_dir . 'index.php', $index_content); foreach ($subdirs as $subdir) { $this->fs_put_contents($this->cache_dir . $subdir . '/index.php', $index_content); } } return true; } /** * Clean up incorrect folders created in wrong location */ private function cleanup_incorrect_folders() { // Remove template-kits folder from templates-library if it exists $incorrect_folder = $this->cache_dir . 'template-kits/'; if (file_exists($incorrect_folder)) { $this->delete_directory_recursively($incorrect_folder); } // Also check for any variations that might have been created $variations = ['template_kits', 'templatekits', 'template-kit']; foreach ($variations as $variant) { $incorrect_variant = $this->cache_dir . $variant . '/'; if (file_exists($incorrect_variant)) { $this->delete_directory_recursively($incorrect_variant); } } } /** * Delete a directory and all its contents recursively */ private function delete_directory_recursively($dir) { if (!file_exists($dir)) { return; } if (!is_dir($dir)) { return; } // Normalize the directory path to avoid double slashes $dir = rtrim($dir, '/\\'); // Try to scan the directory, but handle failures gracefully $scan_result = @scandir($dir); if ($scan_result === false) { // If we can't scan it, try to remove it directly @rmdir($dir); return; } $files = array_diff($scan_result, array('.', '..')); foreach ($files as $file) { $path = $dir . DIRECTORY_SEPARATOR . $file; if (is_dir($path)) { $this->delete_directory_recursively($path); } else { wp_delete_file($path); } } @rmdir($dir); } /** * Check if uploads directory is writable */ private function is_uploads_writable() { $upload_dir = wp_upload_dir(); // Check if uploads dir exists and is writable if (!file_exists($upload_dir['basedir'])) { return false; } return is_writable($upload_dir['basedir']); } /** * Schedule cache update events via Background_Task_Manager */ public function schedule_cache_updates() { if (!$this->local_cache_enabled()) { return; } Background_Task_Manager::get_instance()->schedule_recurring( 'jltma_templates_cache_update', 12 * HOUR_IN_SECONDS ); } /** * Get cached templates for specific tab */ public function get_cached_templates($tab, $force_refresh = false, $per_page = 0) { // Try transient cache first if file cache is not available if (!$this->is_file_cache_available()) { return $this->get_transient_cached_templates($tab, $force_refresh); } $cache_file = $this->cache_dir . "{$tab}/templates/templates.json"; $cache_meta_file = $this->cache_dir . "{$tab}/templates/meta.json"; // Check if cache exists and is valid if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) { $cached_data = $this->read_cache_file($cache_file); if ($cached_data !== false) { // Update thumbnail URLs to use cache folder first foreach ($cached_data as &$template) { $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']); if ($cached_thumbnail) { $template['thumbnail'] = $cached_thumbnail; } } return $cached_data; } } // Fetch fresh data from remote API $fresh_data = $this->fetch_remote_templates($tab, $per_page); if ($fresh_data !== false) { // Update thumbnail URLs to use cache folder first foreach ($fresh_data as &$template) { $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']); if ($cached_thumbnail) { $template['thumbnail'] = $cached_thumbnail; } } // Cache the data $this->write_cache_file($cache_file, $fresh_data); $this->write_cache_meta($cache_meta_file); // Cache individual template thumbnails $this->cache_template_images($fresh_data, $tab); return $fresh_data; } // Fallback to expired cache if available $fallback_data = $this->read_cache_file($cache_file); if ($fallback_data !== false) { // Update thumbnail URLs to use cache folder first for fallback data foreach ($fallback_data as &$template) { $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']); if ($cached_thumbnail) { $template['thumbnail'] = $cached_thumbnail; } } } return $fallback_data; } /** * Get cached categories for specific tab */ public function get_cached_categories($tab, $force_refresh = false) { // Try transient cache first if file cache is not available if (!$this->is_file_cache_available()) { return $this->get_transient_cached_categories($tab, $force_refresh); } $cache_file = $this->cache_dir . "{$tab}/categories/categories.json"; $cache_meta_file = $this->cache_dir . "{$tab}/categories/meta.json"; if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) { $cached_data = $this->read_cache_file($cache_file); if ($cached_data !== false) { return $cached_data; } } $fresh_data = $this->fetch_remote_categories($tab); if ($fresh_data !== false) { $this->write_cache_file($cache_file, $fresh_data); $this->write_cache_meta($cache_meta_file); return $fresh_data; } return $this->read_cache_file($cache_file); } /** * Get cached keywords for specific tab */ public function get_cached_keywords($tab, $force_refresh = false) { // Try transient cache first if file cache is not available if (!$this->is_file_cache_available()) { return $this->get_transient_cached_keywords($tab, $force_refresh); } $cache_file = $this->cache_dir . "{$tab}/keywords/keywords.json"; $cache_meta_file = $this->cache_dir . "{$tab}/keywords/meta.json"; if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) { $cached_data = $this->read_cache_file($cache_file); if ($cached_data !== false) { return $cached_data; } } $fresh_data = $this->fetch_remote_keywords($tab); if ($fresh_data !== false) { $this->write_cache_file($cache_file, $fresh_data); $this->write_cache_meta($cache_meta_file); return $fresh_data; } return $this->read_cache_file($cache_file); } /** * Get cached individual template */ public function get_cached_template($template_id, $tab, $force_refresh = false) { $cache_file = $this->cache_dir . "{$tab}/templates/template-{$template_id}.json"; $cache_meta_file = $this->cache_dir . "{$tab}/templates/template-{$template_id}-meta.json"; if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) { $cached_data = $this->read_cache_file($cache_file); if ($cached_data !== false) { return $cached_data; } } // For individual templates, we don't cache them unless they're part of a larger fetch // This prevents excessive API calls for single template requests return false; } /** * Cache individual template data (called after successful API fetch) */ public function cache_template_data($template_id, $tab, $data) { $cache_file = $this->cache_dir . "{$tab}/templates/template-{$template_id}.json"; $cache_meta_file = $this->cache_dir . "{$tab}/templates/template-{$template_id}-meta.json"; $this->write_cache_file($cache_file, $data); $this->write_cache_meta($cache_meta_file); } /** * Get the cached widgets catalog. * * The Widgets Library has a single flat endpoint rather than one per tab, * so it gets its own accessor instead of a $tab argument, but it lands in * the same uploads/master_addons/templates-library tree as the templates * and is cleared by the same clear_cache(). */ public function get_cached_widgets($force_refresh = false) { if (!$this->is_file_cache_available()) { return $this->get_transient_cached_widgets($force_refresh); } $cache_file = $this->cache_dir . 'master_widgets/widgets/widgets.json'; $meta_file = $this->cache_dir . 'master_widgets/widgets/meta.json'; if (!$force_refresh && $this->is_cache_valid($meta_file)) { $cached_data = $this->read_cache_file($cache_file); if ($cached_data !== false) { return $this->localize_widget_images($cached_data); } } $fresh_data = $this->fetch_remote_widgets($force_refresh); if ($fresh_data !== false) { $this->write_cache_file($cache_file, $fresh_data); $this->write_cache_meta($meta_file); $this->cache_widget_images($fresh_data); return $this->localize_widget_images($fresh_data); } // Serve the expired copy rather than an empty grid when the hub is down. $fallback_data = $this->read_cache_file($cache_file); return $fallback_data === false ? false : $this->localize_widget_images($fallback_data); } /** * Transient fallback, for installs whose uploads directory is not writable. */ private function get_transient_cached_widgets($force_refresh = false) { $transient_key = 'jltma_widgets_catalog'; $meta_transient_key = 'jltma_widgets_catalog_meta'; if (!$force_refresh) { $cached_meta = get_transient($meta_transient_key); if ($cached_meta && (time() - $cached_meta['timestamp']) < $this->cache_expiry) { $cached_data = get_transient($transient_key); if ($cached_data !== false) { return $cached_data; } } } $fresh_data = $this->fetch_remote_widgets($force_refresh); if ($fresh_data !== false) { set_transient($transient_key, $fresh_data, $this->cache_expiry); set_transient($meta_transient_key, ['timestamp' => time()], $this->cache_expiry); return $fresh_data; } return get_transient($transient_key); } /** * Fetch the widgets catalog from the remote API */ private function fetch_remote_widgets($force_refresh = false) { $config = $this->get_api_config(); if (empty($config) || empty($config['endpoints']['widgets'])) { return false; } $api_url = $this->remote_url($config['base'] . $config['path'] . $config['endpoints']['widgets'], $force_refresh); $response = wp_remote_get($api_url, [ 'timeout' => 60, 'sslverify' => false, 'headers' => [ 'User-Agent' => 'Master Addons Widgets Cache/' . JLTMA_VER ] ]); if (is_wp_error($response)) { return false; } $data = json_decode(wp_remote_retrieve_body($response), true); if (json_last_error() !== JSON_ERROR_NONE || empty($data['success']) || !isset($data['widgets'])) { return false; } return $data['widgets']; } /** * Pull widget thumbnails down beside the catalog */ private function cache_widget_images($widgets) { if (!is_array($widgets)) { return; } foreach ($widgets as $widget) { if (empty($widget['widget_id'])) { continue; } if (!empty($widget['thumbnail'])) { $this->cache_image($widget['thumbnail'], 'master_widgets', "widget-{$widget['widget_id']}-thumb"); } } } /** * Swap remote thumbnail URLs for local copies where one has been pulled down. */ private function localize_widget_images($widgets) { if (!is_array($widgets)) { return $widgets; } $upload_dir = wp_upload_dir(); foreach ($widgets as &$widget) { if (empty($widget['widget_id']) || empty($widget['thumbnail'])) { continue; } $extension = pathinfo($widget['thumbnail'], PATHINFO_EXTENSION); if (empty($extension)) { $extension = 'jpg'; } $local_file = $this->cache_dir . "master_widgets/images/widget-{$widget['widget_id']}-thumb.{$extension}"; if (!file_exists($local_file)) { continue; } $local_url = $upload_dir['baseurl'] . str_replace($upload_dir['basedir'], '', $local_file); // The hub sends the same image as preview unless the entry has a // laid-out preview page, in which case preview_url carries the // permalink and must stay pointing at the hub. $preview_matches = !empty($widget['preview']) && $widget['preview'] === $widget['thumbnail']; $widget['thumbnail'] = $local_url; if ($preview_matches) { $widget['preview'] = $local_url; } } return $widgets; } /** * Add the cache-busting parameter for an explicit refresh. * * The library API serves its list endpoints from a static file on the * server, rebuilt on a schedule. A plain request is answered from that * file, so clearing the local cache and re-requesting returns the same * stale listing — which is what made "Refresh from server" appear to do * nothing. Any query parameter makes the server answer live, so only an * explicit refresh sends one; routine reads still hit the static file and * leave the server's database alone. * * @param string $url * @param bool $force_refresh * @return string */ private function remote_url($url, $force_refresh) { return $force_refresh ? add_query_arg('force_refresh', '1', $url) : $url; } /** * Fetch templates from remote API */ private function fetch_remote_templates($tab, $per_page = 0) { $config = $this->get_api_config(); pretty_log('$config', $config); if (empty($config) || empty($config['endpoints']['templates'])) { return false; } $api_url = $config['base'] . $config['path'] . $config['endpoints']['templates'] . $tab; // Without this the listing falls back to the API's own default page // size, which is deliberately small — the popup picker was showing six // templates and had nothing left to scroll through. if ($per_page > 0) { $api_url = add_query_arg(['page' => 1, 'per_page' => (int) $per_page], $api_url); } $response = wp_remote_get($api_url, [ 'timeout' => 60, 'sslverify' => false, 'headers' => [ 'User-Agent' => 'Master Addons Templates Cache/' . JLTMA_VER ] ]); pretty_log('$response', $response); if (is_wp_error($response)) { return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE || !isset($data['success']) || !$data['success']) { return false; } return $this->expand_thumbnails( isset($data['templates']) ? $data['templates'] : [], isset($data['thumb_base']) ? $data['thumb_base'] : '' ); } /** * Put the uploads URL back on the front of each thumbnail. * * The paged listing sends thumb_base once and each row's path relative to * it, which keeps the response small. Consumers that read this class * directly — the popup builder's template picker among them — use * 'thumbnail' as an img src, so they were rendering "2025/10/sale-shoes.png" * and showing a broken image. Expanding here covers every caller of this * class rather than each one repeating it. * * @param array $templates * @param string $thumb_base Absolute uploads URL, empty on the legacy shape. * @return array */ private function expand_thumbnails($templates, $thumb_base) { if (empty($thumb_base) || !is_array($templates)) { return $templates; } $base = trailingslashit($thumb_base); foreach ($templates as $i => $template) { if (!is_array($template)) { continue; } foreach (['thumbnail', 'preview'] as $key) { if (empty($template[$key]) || !is_string($template[$key])) { continue; } // Already absolute on the legacy shape; leave those alone. if (preg_match('#^(https?:)?//#', $template[$key])) { continue; } $templates[$i][$key] = $base . ltrim($template[$key], '/'); } // The paged shape omits 'preview'; it is the same image. if (empty($templates[$i]['preview']) && !empty($templates[$i]['thumbnail'])) { $templates[$i]['preview'] = $templates[$i]['thumbnail']; } } return $templates; } /** * Fetch categories from remote API */ private function fetch_remote_categories($tab) { $config = $this->get_api_config(); if (empty($config) || empty($config['endpoints']['categories'])) { return false; } $api_url = $config['base'] . $config['path'] . $config['endpoints']['categories'] . $tab; $response = wp_remote_get($api_url, [ 'timeout' => 60, 'sslverify' => false ]); if (is_wp_error($response)) { return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE || !isset($data['success']) || !$data['success']) { return false; } return isset($data['terms']) ? $data['terms'] : []; } /** * Fetch keywords from remote API */ private function fetch_remote_keywords($tab) { $config = $this->get_api_config(); if (empty($config) || empty($config['endpoints']['keywords'])) { return false; } $api_url = $config['base'] . $config['path'] . $config['endpoints']['keywords'] . $tab; $response = wp_remote_get($api_url, [ 'timeout' => 60, 'sslverify' => false ]); if (is_wp_error($response)) { return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE || !isset($data['success']) || !$data['success']) { return false; } return isset($data['terms']) ? $data['terms'] : []; } /** * Cache template images locally */ private function cache_template_images($templates, $tab) { if (!is_array($templates)) { return; } foreach ($templates as $template) { if (isset($template['thumbnail']) && !empty($template['thumbnail'])) { $template_id = $template['template_id'] ?? uniqid(); $this->cache_image($template['thumbnail'], $tab, "template-{$template_id}-thumb"); } if (isset($template['preview']) && !empty($template['preview'])) { $template_id = $template['template_id'] ?? uniqid(); $this->cache_image($template['preview'], $tab, "template-{$template_id}-preview"); } } } /** * Cache individual image */ private function cache_image($image_url, $tab, $filename) { if (empty($image_url)) { return false; } $extension = pathinfo($image_url, PATHINFO_EXTENSION); if (empty($extension)) { $extension = 'jpg'; } $local_file = $this->cache_dir . "{$tab}/images/{$filename}.{$extension}"; // Skip if already cached and recent if (file_exists($local_file) && (time() - filemtime($local_file)) < DAY_IN_SECONDS) { return $local_file; } // Ensure the directory exists before trying to write $image_dir = dirname($local_file); if (!file_exists($image_dir)) { wp_mkdir_p($image_dir); } $response = wp_remote_get($image_url, [ 'timeout' => 30 ]); if (is_wp_error($response)) { return false; } $image_data = wp_remote_retrieve_body($response); if ($this->fs_put_contents($local_file, $image_data)) { return $local_file; } return false; } /** * Update templates cache (scheduled event) */ public function update_templates_cache() { // This job exists to refresh the local mirror. With no mirror there is // nothing to refresh, and running it anyway means re-downloading every // listing on a schedule — which is what exhausted memory here before. if (!$this->local_cache_enabled()) { return; } $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers']; $btm = Background_Task_Manager::get_instance(); foreach ($template_types as $tab) { try { $btm->execute_with_retry(function () use ($tab) { // Update templates $this->get_cached_templates($tab, true); // Update categories $this->get_cached_categories($tab, true); // Update keywords $this->get_cached_keywords($tab, true); }, 3, "templates_cache_sync_{$tab}"); } catch (\Exception $e) { // Individual tab failure logged by execute_with_retry; continue with others } } // The widgets catalog is a single flat endpoint, so it sits outside the // per-tab loop. try { $btm->execute_with_retry(function () { $this->get_cached_widgets(true); }, 3, 'widgets_cache_sync'); } catch (\Exception $e) { // Logged by execute_with_retry. } // Clean up old cache files $this->cleanup_old_cache(); // Update last cache time set_transient('jltma_templates_last_cache_update', time(), DAY_IN_SECONDS); } /** * Check if cache is valid */ /** * Whether this site may keep a local copy of the remote library on disk. * * Off by default: client sites read everything from the remote API, which * is CDN-cached, so a local mirror only costs them disk and write errors. * el.master-addons.com itself turns this back on (it IS the source), and * any site can opt in with: * * add_filter('jltma_local_template_cache', '__return_true'); */ private function local_cache_enabled() { return (bool) apply_filters('jltma_local_template_cache', false); } private function is_cache_valid($meta_file) { if (!$this->local_cache_enabled()) { return false; } if (!file_exists($meta_file)) { return false; } $meta = json_decode(file_get_contents($meta_file), true); if (!$meta || !isset($meta['timestamp'])) { return false; } return (time() - $meta['timestamp']) < $this->cache_expiry; } /** * Read cache file with priority tracking */ private function read_cache_file($file_path) { if (!file_exists($file_path)) { return false; } // Track access for priority system $this->track_cache_access($file_path); $content = file_get_contents($file_path); if ($content === false) { return false; } $data = json_decode($content, true); return json_last_error() === JSON_ERROR_NONE ? $data : false; } /** * Write a file through the WP_Filesystem API. * * @param string $file * @param string $contents * @return bool */ private function fs_put_contents($file, $contents) { if (!$this->local_cache_enabled()) { return false; } global $wp_filesystem; if (empty($wp_filesystem)) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } if (empty($wp_filesystem)) { return false; } return $wp_filesystem->put_contents($file, $contents, FS_CHMOD_FILE); } /** * Write cache file */ private function write_cache_file($file_path, $data) { $dir = dirname($file_path); if (!file_exists($dir)) { wp_mkdir_p($dir); } $json = wp_json_encode($data, JSON_PRETTY_PRINT); return $this->fs_put_contents($file_path, $json) !== false; } /** * Write cache metadata */ private function write_cache_meta($meta_file) { $meta = [ 'timestamp' => time(), 'version' => JLTMA_VER, 'expiry' => $this->cache_expiry ]; return $this->fs_put_contents($meta_file, wp_json_encode($meta)) !== false; } /** * Clear all template cache */ public function clear_cache() { $cleared = false; // Clear file cache if available if (file_exists($this->cache_dir)) { $this->delete_directory_contents($this->cache_dir); $this->ensure_cache_directory(); $cleared = true; } // Always clear transient cache $this->clear_transient_cache(); // Clear related transients (legacy support) $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers']; foreach ($template_types as $tab) { delete_transient("master_addons_templates_master-api_{$tab}"); delete_transient("master_addons_categories_master-api_{$tab}"); delete_transient("master_addons_keywords_master-api_{$tab}"); } delete_transient('jltma_templates_last_cache_update'); return true; } /** * Refresh cache by clearing and fetching fresh data from API */ public function refresh_cache() { // Clear all existing cache $this->clear_cache(); // Force fetch fresh data from API for all template types $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers']; $refreshed_data = []; foreach ($template_types as $tab) { // Force refresh templates $templates = $this->get_cached_templates($tab, true); // Force refresh categories $categories = $this->get_cached_categories($tab, true); // Force refresh keywords $keywords = $this->get_cached_keywords($tab, true); $refreshed_data[$tab] = [ 'templates' => is_array($templates) ? count($templates) : 0, 'categories' => is_array($categories) ? count($categories) : 0, 'keywords' => is_array($keywords) ? count($keywords) : 0 ]; } // Update last cache refresh time set_transient('jltma_templates_last_cache_update', time(), DAY_IN_SECONDS); // Log successful refresh return $refreshed_data; } /** * Clean up old cache files with priority system */ private function cleanup_old_cache() { $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers']; // Cache priority ages (high priority files are kept longer) $priority_ages = [ 'high' => 14 * DAY_IN_SECONDS, // 14 days for high priority 'medium' => 7 * DAY_IN_SECONDS, // 7 days for medium priority 'low' => 3 * DAY_IN_SECONDS // 3 days for low priority ]; foreach ($template_types as $tab) { $tab_dir = $this->cache_dir . $tab . '/'; if (!file_exists($tab_dir)) { continue; } $subdirs = ['categories', 'keywords', 'templates', 'images']; foreach ($subdirs as $subdir) { $full_dir = $tab_dir . $subdir . '/'; if (!file_exists($full_dir)) { continue; } $files = glob($full_dir . '*'); foreach ($files as $file) { if (is_file($file)) { $file_age = time() - filemtime($file); $priority = $this->get_file_cache_priority($file, $tab); $max_age = $priority_ages[$priority] ?? $priority_ages['low']; if ($file_age > $max_age) { wp_delete_file($file); } } } } } } /** * Determine cache file priority based on usage patterns */ private function get_file_cache_priority($file_path, $tab) { $filename = basename($file_path); $access_count = $this->get_file_access_count($file_path); $recent_access = $this->get_recent_access_time($file_path); // High priority: Frequently accessed files (>10 times) or recently accessed (within 2 days) if ($access_count > 10 || (time() - $recent_access) < (2 * DAY_IN_SECONDS)) { return 'high'; } // Medium priority: Moderately accessed files (3-10 times) or accessed within a week if ($access_count >= 3 || (time() - $recent_access) < (7 * DAY_IN_SECONDS)) { return 'medium'; } // Low priority: Everything else return 'low'; } /** * Get file access count from usage tracking */ private function get_file_access_count($file_path) { $access_data = get_transient('jltma_cache_access_' . md5($file_path)); return $access_data ? (int) $access_data['count'] : 0; } /** * Get recent access time for file */ private function get_recent_access_time($file_path) { $access_data = get_transient('jltma_cache_access_' . md5($file_path)); return $access_data ? (int) $access_data['last_access'] : filemtime($file_path); } /** * Track cache file access for priority system */ private function track_cache_access($file_path) { $access_key = 'jltma_cache_access_' . md5($file_path); $access_data = get_transient($access_key) ?: ['count' => 0, 'last_access' => 0]; $access_data['count']++; $access_data['last_access'] = time(); set_transient($access_key, $access_data, 30 * DAY_IN_SECONDS); } /** * Delete directory contents recursively */ private function delete_directory_contents($dir) { if (!file_exists($dir)) { return; } // glob() skips dotfiles, so the cache directories' own .htaccess (and a // stray .DS_Store) used to survive and every rmdir below then failed // with "Directory not empty", filling debug.log with warnings and // leaving the tree behind. Match dot entries too, minus . and .. $files = array_merge( glob($dir . '*', GLOB_MARK) ?: array(), array_diff(glob($dir . '.*', GLOB_MARK) ?: array(), array($dir . './', $dir . '../')) ); foreach ($files as $file) { if (is_dir($file)) { $this->delete_directory_contents($file); // Suppressed: a directory the caller cannot remove is not worth // a warning, and the sweep should continue regardless. @rmdir($file); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } else { wp_delete_file($file); } } } /** * Handle cache clearing from admin */ public function maybe_clear_cache() { if (isset($_GET['jltma_clear_templates_cache']) && isset($_GET['_wpnonce']) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'jltma_clear_templates_cache') && current_user_can('manage_options')) { $this->clear_cache(); wp_safe_redirect(add_query_arg([ 'jltma_templates_cache_cleared' => '1' ], remove_query_arg(['jltma_clear_templates_cache', '_wpnonce']))); exit; } // Handle cache refresh from admin if (isset($_GET['jltma_refresh_templates_cache']) && isset($_GET['_wpnonce']) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'jltma_refresh_templates_cache') && current_user_can('manage_options')) { $this->refresh_cache(); wp_safe_redirect(add_query_arg([ 'jltma_templates_cache_refreshed' => '1' ], remove_query_arg(['jltma_refresh_templates_cache', '_wpnonce']))); exit; } } /** * Get cached image URL */ public function get_cached_image_url($original_url, $tab, $filename) { $extension = pathinfo($original_url, PATHINFO_EXTENSION) ?: 'jpg'; $local_file = $this->cache_dir . "{$tab}/images/{$filename}.{$extension}"; if (file_exists($local_file)) { $upload_dir = wp_upload_dir(); $relative_path = str_replace($upload_dir['basedir'], '', $local_file); return $upload_dir['baseurl'] . $relative_path; } return $original_url; } /** * Get template kit thumbnail from cache or generate fallback URL */ public function get_kit_thumbnail_url($kit_name, $template_name = 'home', $original_url = null) { // Normalize kit name for filename $kit_slug = sanitize_title($kit_name); $template_slug = sanitize_title($template_name); // Without a local mirror there is nothing on disk to look for, and the // stat calls below would just cost I/O on every template in the grid. if ($this->local_cache_enabled()) { $cache_image_dir = $this->cache_dir . 'master_section/images/'; $cached_file_patterns = [ "{$kit_slug}-{$template_slug}.jpg", "{$kit_slug}-{$template_slug}.png", "{$kit_slug}.jpg", "{$kit_slug}.png" ]; foreach ($cached_file_patterns as $pattern) { $cached_file = $cache_image_dir . $pattern; if (file_exists($cached_file)) { $upload_dir = wp_upload_dir(); $relative_path = str_replace($upload_dir['basedir'], '', $cached_file); return $upload_dir['baseurl'] . $relative_path; } } } // If original URL provided, return it if ($original_url) { return $original_url; } // Last resort: guess the published thumbnail URL from the kit name. // Callers that only know the template title pass an empty $kit_name, // which used to produce ".../templates-kit/-v1/.jpg" — a URL // that cannot exist and 404s for every template on the screen. With no // kit to build a path from there is nothing to guess, so say so. if ('' === $kit_slug) { return ''; } $kit_version = $this->get_kit_version($kit_name); return "https://master-addons.com/templates-kit/{$kit_slug}{$kit_version}/{$template_slug}.jpg"; } /** * Get kit version suffix for URL generation */ private function get_kit_version($kit_name) { // Common version patterns for kits $version_patterns = [ 'business-agency' => '-v1', 'restaurant' => '-v2', 'portfolio' => '-v1', 'ecommerce' => '-v3' ]; $kit_slug = sanitize_title($kit_name); return $version_patterns[$kit_slug] ?? '-v1'; } /** * Get only the cached templates count. * * Lightweight counterpart of get_cache_stats(): skips directory size * calculation and kit manifest parsing, so it is safe to call on demand * (e.g. when the templates modal is opened). */ /** * Get cache statistics */ public function get_cache_stats() { $stats = [ 'cache_dir_exists' => file_exists($this->cache_dir), 'cache_size' => $this->get_directory_size($this->cache_dir), 'last_update' => get_transient('jltma_templates_last_cache_update'), 'template_types' => [], 'next_scheduled_update' => wp_next_scheduled('jltma_templates_cache_update'), 'total_kits' => 0, 'total_templates' => 0 ]; $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers']; $total_templates = 0; foreach ($template_types as $type) { $template_count = 0; $image_count = 0; // Count file cache if available if ($this->is_file_cache_available()) { $template_count = count(glob($this->cache_dir . "{$type}/templates/template-*.json")); $image_count = count(glob($this->cache_dir . "{$type}/images/*")); } else { // Count transient cache $cached_templates = get_transient("jltma_templates_{$type}"); if ($cached_templates && is_array($cached_templates)) { $template_count = count($cached_templates); } } $stats['template_types'][$type] = [ 'templates' => $template_count, 'images' => $image_count ]; $total_templates += $template_count; } // For template kits, we'll count unique kits from cached data $stats['total_kits'] = $this->count_cached_kits(); $stats['total_templates'] = $total_templates; return $stats; } /** * Count cached template kits */ private function count_cached_kits() { $kit_count = 0; // If using file cache, look for kit manifest files if ($this->is_file_cache_available()) { $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers']; foreach ($template_types as $type) { $templates_file = $this->cache_dir . "{$type}/templates/templates.json"; if (file_exists($templates_file)) { $templates_data = $this->read_cache_file($templates_file); if ($templates_data && is_array($templates_data)) { $kit_count += count($templates_data); } } } } else { // Count from transients $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers']; foreach ($template_types as $type) { $cached_templates = get_transient("jltma_templates_{$type}"); if ($cached_templates && is_array($cached_templates)) { $kit_count += count($cached_templates); } } } return $kit_count; } /** * Get directory size in bytes */ private function get_directory_size($dir) { $size = 0; if (file_exists($dir)) { foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)) as $file) { if ($file->isFile()) { $size += $file->getSize(); } } } return $size; } /** * Check if file cache is available */ private function is_file_cache_available() { $this->maybe_bootstrap_cache_dir(); return file_exists($this->cache_dir) && is_writable($this->cache_dir); } /** * Create/clean the cache directory the first time the cache is touched. * * Runs once per request, and only when a cache read/write actually happens. */ private function maybe_bootstrap_cache_dir() { if ($this->cache_dir_ready) { return; } $this->cache_dir_ready = true; if (!$this->local_cache_enabled()) { return; } // Clean up old incorrect template-kits folders if they exist $this->cleanup_incorrect_folders(); // Ensure cache directory exists $this->ensure_cache_directory(); } /** * Get cached templates using transients (fallback method) */ private function get_transient_cached_templates($tab, $force_refresh = false) { $transient_key = "jltma_templates_{$tab}"; $meta_transient_key = "jltma_templates_{$tab}_meta"; // Check if cache exists and is valid if (!$force_refresh) { $cached_meta = get_transient($meta_transient_key); if ($cached_meta && (time() - $cached_meta['timestamp']) < $this->cache_expiry) { $cached_data = get_transient($transient_key); if ($cached_data !== false) { // Update thumbnail URLs to use cache folder first for transient cached templates foreach ($cached_data as &$template) { $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']); if ($cached_thumbnail) { $template['thumbnail'] = $cached_thumbnail; } } return $cached_data; } } } // Fetch fresh data from remote API $fresh_data = $this->fetch_remote_templates($tab); if ($fresh_data !== false) { // Update thumbnail URLs to use cache folder first for fresh transient templates foreach ($fresh_data as &$template) { $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']); if ($cached_thumbnail) { $template['thumbnail'] = $cached_thumbnail; } } // Cache the data using transients set_transient($transient_key, $fresh_data, $this->cache_expiry); set_transient($meta_transient_key, ['timestamp' => time()], $this->cache_expiry); return $fresh_data; } // Return cached data even if expired $fallback_transient_data = get_transient($transient_key); if ($fallback_transient_data !== false) { // Update thumbnail URLs to use cache folder first for expired transient templates foreach ($fallback_transient_data as &$template) { $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']); if ($cached_thumbnail) { $template['thumbnail'] = $cached_thumbnail; } } } return $fallback_transient_data; } /** * Get cached categories using transients (fallback method) */ private function get_transient_cached_categories($tab, $force_refresh = false) { $transient_key = "jltma_categories_{$tab}"; $meta_transient_key = "jltma_categories_{$tab}_meta"; if (!$force_refresh) { $cached_meta = get_transient($meta_transient_key); if ($cached_meta && (time() - $cached_meta['timestamp']) < $this->cache_expiry) { $cached_data = get_transient($transient_key); if ($cached_data !== false) { return $cached_data; } } } $fresh_data = $this->fetch_remote_categories($tab); if ($fresh_data !== false) { set_transient($transient_key, $fresh_data, $this->cache_expiry); set_transient($meta_transient_key, ['timestamp' => time()], $this->cache_expiry); return $fresh_data; } return get_transient($transient_key); } /** * Get cached keywords using transients (fallback method) */ private function get_transient_cached_keywords($tab, $force_refresh = false) { $transient_key = "jltma_keywords_{$tab}"; $meta_transient_key = "jltma_keywords_{$tab}_meta"; if (!$force_refresh) { $cached_meta = get_transient($meta_transient_key); if ($cached_meta && (time() - $cached_meta['timestamp']) < $this->cache_expiry) { $cached_data = get_transient($transient_key); if ($cached_data !== false) { return $cached_data; } } } $fresh_data = $this->fetch_remote_keywords($tab); if ($fresh_data !== false) { set_transient($transient_key, $fresh_data, $this->cache_expiry); set_transient($meta_transient_key, ['timestamp' => time()], $this->cache_expiry); return $fresh_data; } return get_transient($transient_key); } /** * Clear transient cache (fallback method) */ private function clear_transient_cache() { $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers']; foreach ($template_types as $tab) { delete_transient("jltma_templates_{$tab}"); delete_transient("jltma_templates_{$tab}_meta"); delete_transient("jltma_categories_{$tab}"); delete_transient("jltma_categories_{$tab}_meta"); delete_transient("jltma_keywords_{$tab}"); delete_transient("jltma_keywords_{$tab}_meta"); } } /** * Preload popular templates in background */ public function preload_popular_templates() { $popular_tabs = ['master_section', 'master_headers']; foreach ($popular_tabs as $tab) { if (!get_transient("jltma_preload_{$tab}")) { wp_schedule_single_event(time() + 60, 'jltma_background_preload', [$tab]); set_transient("jltma_preload_{$tab}", true, HOUR_IN_SECONDS); } } } /** * Background preload handler (callback for jltma_background_preload action) * * @param string $tab Template tab to preload */ public function do_background_preload($tab) { if (!empty($tab)) { $this->get_cached_templates($tab, true); } } /** * AJAX handler for cache preloading */ public function preload_cache_ajax() { if (!current_user_can('manage_options')) { wp_die(-1); } if (!check_ajax_referer('jltma_preload_cache_nonce', 'security', false)) { wp_die(-1); } $tab = sanitize_text_field( wp_unslash( $_POST['tab'] ?? '' ) ); if (empty($tab)) { wp_send_json_error('Invalid tab'); } // Preload in background $this->get_cached_templates($tab, true); wp_send_json_success('Cache preloaded for ' . $tab); } /** * Get singleton instance */ public static function get_instance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } } // Initialize templates cache manager Template_Library_Cache::get_instance();