Edit file File name : Help.php Content :<?php namespace Duplicator\Utils\Help; use DUP_LITE_Plugin_Upgrade; use DUP_Log; use DUP_Settings; use Duplicator\Controllers\HelpPageController; use Duplicator\Libs\Snap\SnapIO; use Duplicator\Libs\Snap\SnapJson; use Duplicator\Core\Controllers\ControllersManager; use Duplicator\Utils\ExpireOptions; /* * Dynamic Help from site documentation */ class Help { /** @var string The doc article endpoint */ const ARTICLE_ENDPOINT = 'https://www.duplicator.com/wp-json/wp/v2/ht-kb'; /** @var string The doc categories endpoint */ const CATEGORY_ENDPOINT = 'https://www.duplicator.com/wp-json/wp/v2/ht-kb-category'; /** @var string The doc tags endpoint */ const TAGS_ENDPOINT = 'https://www.duplicator.com/wp-json/wp/v2/ht-kb-tag'; /** @var int Maximum number of articles to load */ const MAX_ARTICLES = 500; /** @var int Maximum number of categories to load */ const MAX_CATEGORY = 20; /** @var int Maximum number of tags to load */ const MAX_TAGS = 100; /** @var int Per page limit */ const PER_PAGE = 100; /** @var string Cron hook */ const DOCS_EXPIRE_OPT_KEY = 'duplicator_help_docs_expire'; /** @var Article[] The articles */ private $articles = []; /** @var Category[] The categories */ private $categories = []; /** @var array<int, string> The tags ID => slug */ private $tags = []; /** @var self The instance */ private static $instance = null; /** * Init * * @return void */ private function __construct() { // Update data from API if cache is expired or does not exist if ( !ExpireOptions::getUpdate(self::DOCS_EXPIRE_OPT_KEY, true, WEEK_IN_SECONDS) || !$this->loadData() ) { $this->updateData(); } } /** * Get the instance * * @return self The instance */ public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Get the help page URL with tag * * @return string The URL with tag */ public static function getHelpPageUrl() { return HelpPageController::getHelpLink() . '&tag=' . self::getCurrentPageTag(); } /** * Get articles by category * * @param int $categoryId The category ID * * @return Article[] The articles */ public function getArticlesByCategory($categoryId) { return array_filter($this->articles, function (Article $article) use ($categoryId) { return in_array($categoryId, $article->getCategories()); }); } /** * Get articles by tag * * @param string $tag The tag * * @return Article[] The articles */ public function getArticlesByTag($tag) { if ($tag === '') { return []; } return array_filter($this->articles, function (Article $article) use ($tag) { return in_array($tag, $article->getTags()); }); } /** * Get top level categories. * E.g. categories without parents & with children or articles * * @return Category[] The categories */ public function getTopLevelCategories() { return array_filter($this->categories, function (Category $category) { return $category->getParent() === null && (count($category->getChildren()) > 0 || $category->getArticleCount() > 0); }); } /** * Load data from API * * @return array{articles: mixed[], categories: mixed[], tags: mixed[]}|array<mixed> The data */ private function getDataFromApi() { $categories = $this->fetchDataFromEndpoint( self::CATEGORY_ENDPOINT, self::MAX_CATEGORY, [ 'id', 'name', 'count', 'parent', ] ); $articles = $this->fetchDataFromEndpoint( self::ARTICLE_ENDPOINT, self::MAX_ARTICLES, [ 'id', 'title', 'link', 'ht-kb-category', 'ht-kb-tag', ] ); $tags = $this->fetchDataFromEndpoint( self::TAGS_ENDPOINT, self::MAX_TAGS, [ 'id', 'slug', ] ); if ($categories === [] || $articles === [] || $tags === []) { DUP_Log::Trace('Failed to load from API. No data.'); return []; } return [ 'articles' => $articles, 'categories' => $categories, 'tags' => $tags, ]; } /** * Load from API * * @param string $endpoint The endpoint * @param int $limit Maximum number of items to load * @param string[] $fields The fields to load * * @return array<mixed> The data */ private function fetchDataFromEndpoint($endpoint, $limit, $fields = []) { $result = []; $endpointUrl = $endpoint . '?per_page=' . self::PER_PAGE; if (count($fields) > 0) { $endpointUrl .= '&_fields[]=' . implode('&_fields[]=', $fields); } $maxPages = ceil($limit / self::PER_PAGE); for ($i = 1; $i <= $maxPages; $i++) { $endpointUrl .= '&page=' . $i; $response = wp_remote_get( $endpointUrl, ['timeout' => 15] ); if (is_wp_error($response)) { DUP_Log::Trace("Failed to load from API: {$endpointUrl}"); DUP_Log::Trace($response->get_error_message()); return []; } $code = wp_remote_retrieve_response_code($response); if ($code !== 200) { DUP_Log::Trace("Failed to load from API: {$endpointUrl}, code: {$code}"); return []; } $body = wp_remote_retrieve_body($response); if (($data = json_decode($body, true)) === null) { DUP_Log::Trace("Failed to decode response: {$body}"); return []; } $result = array_merge($result, $data); $totalPages = wp_remote_retrieve_header($response, 'x-wp-totalpages'); if ($totalPages === '' || $i >= (int) $totalPages) { break; } } $result = array_combine(array_column($result, 'id'), $result); return $result; } /** * Get the current page tag * * @return string The tag */ private static function getCurrentPageTag() { if (!isset($_GET['page'])) { return ''; } $page = $_GET['page']; $tab = isset($_GET['tab']) ? $_GET['tab'] : ''; $innerPage = isset($_GET['inner_page']) ? $_GET['inner_page'] : ''; switch ($page) { case ControllersManager::PACKAGES_SUBMENU_SLUG: if ($innerPage === 'new1') { return 'backup_step_1'; } elseif ($tab === 'new2') { return 'backup_step_2'; } elseif ($tab === 'new3') { return 'backup_step_3'; } return 'backups'; case ControllersManager::IMPORT_SUBMENU_SLUG: return 'import'; case ControllersManager::SCHEDULES_SUBMENU_SLUG: return 'schedules'; case ControllersManager::STORAGE_SUBMENU_SLUG: return 'storages'; case ControllersManager::TOOLS_SUBMENU_SLUG: if ($tab === 'templates') { return 'templates'; } elseif ($tab === 'recovery') { return 'recovery'; } return 'tools'; case ControllersManager::SETTINGS_SUBMENU_SLUG: return 'settings'; default: DUP_Log::Trace("No tag for page."); } return ''; } /** * Get the cache path * * @return string The cache path */ private static function getCacheFilePath() { $installInfo = DUP_LITE_Plugin_Upgrade::getInstallInfo(); return DUP_Settings::getSsdirPath() . '/cache_' . md5($installInfo['time']) . '/duplicator_help_cache.json'; } /** * Set from cache data * * @param array{articles: mixed[], categories: mixed[], tags: mixed[]} $data The data * * @return bool True if set */ private function setFromArray($data) { if (!isset($data['articles']) || !isset($data['categories']) || !isset($data['tags'])) { DUP_Log::Trace("Invalid data."); return false; } foreach ($data['tags'] as $tag) { $this->tags[$tag['id']] = $tag['slug']; } foreach ($data['categories'] as $category) { $this->categories[$category['id']] = new Category( $category['id'], $category['name'], $category['count'] ); } foreach ($this->categories as $category) { if ( ($parentId = $data['categories'][$category->getId()]['parent']) === 0 || !isset($this->categories[$parentId]) ) { continue; } $this->categories[$parentId]->addChild($category); $category->setParent($this->categories[$parentId]); } foreach ($data['articles'] as $article) { $this->articles[$article['id']] = new Article( $article['id'], $article['title']['rendered'], $article['link'], $article['ht-kb-category'], array_map(function ($tagId) { return $this->tags[$tagId]; }, $article['ht-kb-tag']) ); } return true; } /** * Get data from cache * * @return bool True if loaded */ private function loadData() { if (!file_exists(self::getCacheFilePath())) { DUP_Log::Trace("Cache file does not exist: " . self::getCacheFilePath()); return false; } if (($contents = file_get_contents(self::getCacheFilePath())) === false) { DUP_Log::Trace("Failed to read cache file: " . self::getCacheFilePath()); return false; } if (($data = json_decode($contents, true)) === null) { DUP_Log::Trace("Failed to decode cache file: " . self::getCacheFilePath()); return false; } return $this->setFromArray($data); } /** * Save to cache * * @return bool True if saved */ public function updateData() { if (($data = $this->getDataFromApi()) === []) { DUP_Log::Trace("Failed to load data from API."); return false; } $cachePath = self::getCacheFilePath(); $cacheDir = dirname($cachePath); if (!file_exists($cacheDir) && !SnapIO::mkdir($cacheDir, 0755, true)) { DUP_Log::Trace("Failed to create cache directory: {$cacheDir}"); return false; } if (($encoded = SnapJson::jsonEncode($data)) === false) { DUP_Log::Trace("Failed to encode cache data."); return false; } if (file_put_contents(self::getCacheFilePath(), $encoded) === false) { DUP_Log::Trace("Failed to write cache file: {$cachePath}"); return false; } return $this->setFromArray($data); } } Save