From 5b30e0de52c8849e0026cf0f401d7b62dd033d5e Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Thu, 13 Feb 2025 14:46:25 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feature:=20Add=20update=20from=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- includes/GitHubUpdater.php | 1388 ++++++++++++++++++++++++++++++++++++ includes/updater.php | 410 ----------- resource-filter.php | 7 +- 3 files changed, 1394 insertions(+), 411 deletions(-) create mode 100644 includes/GitHubUpdater.php delete mode 100644 includes/updater.php diff --git a/includes/GitHubUpdater.php b/includes/GitHubUpdater.php new file mode 100644 index 0000000..9d469d6 --- /dev/null +++ b/includes/GitHubUpdater.php @@ -0,0 +1,1388 @@ +file = $file; + + $this->load(); + } + + /** + * Set GitHub access token. + * + * @param string $accessToken github_pat_fU7xGh... + * @return $this + */ + public function setAccessToken(string $accessToken): self + { + $this->gitHubAccessToken = $accessToken; + + return $this; + } + + /** + * Set GitHub branch of plugin. + * + * @param string $branch main + * @return $this + */ + public function setBranch(string $branch): self + { + $this->gitHubBranch = $branch; + + return $this; + } + + /** + * Set relative path to plugin icon from plugin root. + * + * @param string $file assets/icon.png + * @return $this + */ + public function setPluginIcon(string $file): self + { + $this->pluginIcon = ltrim($file, '/'); + + return $this; + } + + /** + * Set relative path to small plugin banner from plugin root. + * + * @param string $file assets/banner-772x250.jpg + * @return $this + */ + public function setPluginBannerSmall(string $file): self + { + $this->pluginBannerSmall = ltrim($file, '/'); + + return $this; + } + + /** + * Set relative path to large plugin banner from plugin root. + * + * @param string $file assets/banner-1544x500.jpg + * @return $this + */ + public function setPluginBannerLarge(string $file): self + { + $this->pluginBannerLarge = ltrim($file, '/'); + + return $this; + } + + /** + * Set changelog to use for plugin detail modal. + * + * @param string $changelog CHANGELOG.md + * @return $this + */ + public function setChangelog(string $changelog): self + { + $this->changelog = $changelog; + + return $this; + } + + /** + * Enable GitHubUpdater debugger. + * + * If this property is set to true, as well as the WP_DEBUG and WP_DEBUG_LOG + * constants within wp-config.php, then GitHubUpdater will log pertinent + * information to wp-content/debug.log. + * + * @return $this + */ + public function enableDebugger(): self + { + $this->enableDebugger = true; + + return $this; + } + + /** + * Add update mechanism to plugin. + * + * @return void + */ + public function add(): void + { + $this->buildPluginDetailsResult(); + //$this->logPluginDetailsResult();; + $this->checkPluginUpdates(); + $this->prepareHttpRequestArgs(); + $this->moveUpdatedPlugin(); + } + + /**************************************************************************/ + + /** + * Load properties with values based on $file. + * + * $gitHubUrl GitHub URL https://github.com/ryansechrest/github-updater-demo + * $gitHubPath GitHub path ryansechrest/github-updater-demo + * $gitHubOrg GitHub organization ryansechrest + * $gitHubRepo GitHub repository github-updater-demo + * $pluginFile Plugin file github-updater-demo/github-updater-demo.php + * $pluginDir Plugin directory github-updater-demo + * $pluginFilename Plugin filename github-updater-demo.php + * $pluginSlug Plugin slug ryansechrest-github-updater-demo + * $pluginUrl Plugin URL https://github.com/ryansechrest/github-updater-demo + * $pluginVersion Plugin version 1.0.0 + * $testedUpTo Tested up to 6.6 + */ + private function load(): void + { + // Fields from plugin header + $pluginData = get_file_data( + $this->file, + [ + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'TestedUpTo' => 'Tested up to', + 'UpdateURI' => 'Update URI', + ] + ); + + // Extract fields from plugin header + $pluginUri = $pluginData['PluginURI'] ?? ''; + $updateUri = $pluginData['UpdateURI'] ?? ''; + $version = $pluginData['Version'] ?? ''; + $testedUpTo = $pluginData['TestedUpTo'] ?? ''; + + // If required fields were not set, exit + if (!$updateUri || !$version) { + $this->addAdminNotice('Plugin %s is missing one or more required header fields: Version and/or Update URI.'); + return; + } + + // e.g. `https://github.com/ryansechrest/github-updater-demo` + $this->gitHubUrl = $updateUri; + + // e.g. `ryansechrest/github-updater-demo` + $this->gitHubPath = trim( + wp_parse_url($updateUri, PHP_URL_PATH), + '/' + ); + + // e.g. `ryansechrest` and `github-updater-demo` + [$this->gitHubOrg, $this->gitHubRepo] = explode( + '/', $this->gitHubPath + ); + + // e.g. `github-updater-demo/github-updater-demo.php` + $this->pluginFile = str_replace( + WP_PLUGIN_DIR . '/', '', $this->file + ); + + // e.g. `github-updater-demo` and `github-updater-demo.php` + [$this->pluginDir, $this->pluginFilename] = explode( + '/', $this->pluginFile + ); + + // e.g. `ryansechrest-github-updater-demo` + $this->pluginSlug = sprintf( + '%s-%s', $this->gitHubOrg, $this->gitHubRepo + ); + + // e.g. `https://github.com/ryansechrest/github-updater-demo` + $this->pluginUrl = $pluginUri; + + // e.g. `1.0.0` + $this->pluginVersion = $version; + + // e.g. `6.6` + $this->testedUpTo = $testedUpTo; + } + + /*------------------------------------------------------------------------*/ + + /** + * Build plugin details result. + * + * When WordPress checks for plugin updates, it queries wordpress.org, + * however this plugin does not exist there. We use this hook to intercept + * the request and manually populate the desired fields so that WordPress + * can render the plugin details modal. + * + * @return void + */ + private function buildPluginDetailsResult(): void + { + add_filter( + 'plugins_api', + [$this, '_buildPluginDetailsResult'], + 10, + 3 + ); + } + + /** + * Hook to build plugin details result. + * + * @param array|false|object $result ['name' => 'GitHub Updater Demo', ...] + * @param string $action plugin_information + * @param object $args ['slug' => 'ryansechrest-github-updater-demo', ...] + * @return array|false|object ['name' => 'GitHub Updater Demo', ...] + */ + public function _buildPluginDetailsResult( + array|false|object $result, string $action, object $args + ): array|false|object + { + // If action is query_plugins, hot_tags, or hot_categories, exit + if ($action !== 'plugin_information') return $result; + + // If not our plugin, exit + if ($args->slug !== $this->pluginSlug) return $result; + + // Get remote plugin file contents to read plugin header + $fileContents = $this->getRemotePluginFileContents( + $this->pluginFilename + ); + + // If remote plugin file could not be retrieved, exit + if (!$fileContents) return $result; + + // Extract plugin version from remote plugin file contents + $fields = $this->extractPluginHeaderFields( + [ + 'Plugin Name' => '', + 'Plugin URI' => '', + 'Version' => 'version', + 'Author' => '', + 'Author URI' => '', + 'Tested up to' => 'version', + 'Requires at least' => 'version', + 'Requires PHP' => 'version', + ], + $fileContents + ); + + // Build plugin detail result + $result = [ + 'name' => $fields['Plugin Name'], + 'slug' => $this->pluginSlug, + 'version' => $fields['Version'], + 'requires' => $fields['Requires at least'], + 'tested' => $fields['Tested up to'], + 'requires_php' => $fields['Requires PHP'], + 'homepage' => $fields['Plugin URI'], + 'sections' => [], + ]; + + // Assume no author + $author = ''; + + // If author name exists, use it + if ($fields['Author']) { + $author = $fields['Author']; + } + + // If author name and URL exist, use them both + if ($fields['Author'] && $fields['Author URI']) { + $author = sprintf( + '%s', + $fields['Author URI'], + $fields['Author'] + ); + } + + // If author exists, set it + if ($author) { + $result['author'] = $author; + } + + // If small plugin banner exists, set it + if ($pluginBannerSmall = $this->getPluginBannerSmall()) { + $result['banners']['low'] = $pluginBannerSmall; + } + + // If large plugin banner exists, set it + if ($pluginBannerLarge = $this->getPluginBannerLarge()) { + $result['banners']['high'] = $pluginBannerLarge; + } + + // If changelog exists, set it + if ($changelog = $this->getChangelog()) { + $result['sections']['changelog'] = $changelog; + } + + $this->logStart('_buildPluginDetailsResult', 'plugins_api'); + $this->logValue('Return $result', $result); + $this->logValue('$action', $action); + $this->logValue('$args', $args); + $this->logFinish('_buildPluginDetailsResult'); + + return (object) $result; + } + + /*------------------------------------------------------------------------*/ + + /** + * Log plugin details for plugins. + * + * Useful for inspecting options on officially-hosted WordPress plugins. + * + * @return void + */ + private function logPluginDetailsResult(): void + { + add_filter( + 'plugins_api_result', + [$this, '_logPluginDetailsResult'], + 10, + 3 + ); + } + + /** + * Hook to log plugin details for plugins. + * + * @param object $res ['name' => 'GitHub Updater Demo', ...] + * @param string $action plugin_information + * @param object $args ['slug' => 'ryansechrest-github-updater-demo', ...] + * @return object ['name' => 'GitHub Updater Demo', ...] + */ + public function _logPluginDetailsResult( + object $res, string $action, object $args + ): object + { + if ($action !== 'plugin_information') return $res; + + $this->logStart('_logPluginDetailsResult', 'plugins_api_result'); + $this->logValue('Return $res', $res); + $this->logValue('$action', $action); + $this->logValue('$args', $args); + $this->logFinish('_logPluginDetailsResult'); + + return $res; + } + + /*------------------------------------------------------------------------*/ + + /** + * Check for plugin updates. + * + * If plugin has an `Update URI` pointing to `github.com`, then check if + * plugin was updated on GitHub, and if so, record a pending update so that + * either WordPress can automatically update it (if enabled), or a user can + * manually update it much like an officially-hosted plugin. + * + * @return void + */ + private function checkPluginUpdates(): void + { + add_filter( + 'update_plugins_github.com', + [$this, '_checkPluginUpdates'], + 10, + 3 + ); + } + + /** + * Hook to check for plugin updates. + * + * $update Plugin update data with the latest details. + * $data Plugin data as defined in plugin header. + * $file Plugin file, e.g. `github-updater-demo/github-updater-demo.php` + * + * @param array|false $update false + * @param array $data ['PluginName' => 'GitHub Updater Demo', ...] + * @param string $file github-updater-demo/github-updater-demo.php + * @return array|false + */ + public function _checkPluginUpdates( + array|false $update, array $data, string $file + ): array|false + { + // If plugin does not match this plugin, exit + if ($file !== $this->pluginFile) return $update; + + $this->logStart( + '_checkPluginUpdates', 'update_plugins_github.com' + ); + + // Get remote plugin file contents to read plugin header + $fileContents = $this->getRemotePluginFileContents( + $this->pluginFilename + ); + + // Extract plugin version from remote plugin file contents + $fields = $this->extractPluginHeaderFields( + ['Version' => 'version'], $fileContents + ); + + $this->log('Does $newVersion (' . $fields['Version'] . ') exist...'); + + // If version wasn't found, exit + if (!$fields['Version']) { + $this->log('No'); + $this->logValue('Return early', $update); + $this->logFinish('_checkPluginUpdates'); + + return $update; + } + + $this->log('Yes'); + + // Build plugin data response for WordPress + $pluginData = [ + 'id' => $this->gitHubUrl, + 'slug' => $this->pluginSlug, + 'plugin' => $this->pluginFile, + 'version' => $fields['Version'], + 'url' => $this->pluginUrl, + 'package' => $this->getRemotePluginZipFile(), + 'tested' => $this->testedUpTo, + ]; + + $pluginIcon = $this->getPluginIcon(); + + $this->log('Does $pluginIcon (' . $pluginIcon . ') exist...'); + + // If no icon was defined, exit with plugin data + if (!$pluginIcon) { + $this->log('No'); + $this->logValue('Return early', $pluginData); + $this->logFinish('_checkPluginUpdates'); + + return $pluginData; + } + + $this->log('Yes'); + + // Otherwise add icon to plugin data + $pluginData['icons'] = ['default' => $pluginIcon]; + + $this->logValue('Return', $pluginData); + $this->logFinish('_checkPluginUpdates'); + + return $pluginData; + } + + /** + * Get remote plugin file contents from GitHub repository. + * + * @param string $filename github-updater-demo.php + * @return string + */ + private function getRemotePluginFileContents(string $filename): string + { + return $this->gitHubAccessToken + ? $this->getPrivateRemotePluginFileContents($filename) + : $this->getPublicRemotePluginFileContents($filename); + } + + /** + * Get remote plugin file contents from public GitHub repository. + * + * @param string $filename github-updater-demo.php + * @return string + */ + private function getPublicRemotePluginFileContents(string $filename): string + { + // Get public remote plugin file containing plugin header, + // e.g. `https://raw.githubusercontent.com/ryansechrest/github-updater-demo/main/github-updater-demo.php` + $remoteFile = $this->getPublicRemotePluginFile($filename); + + return wp_remote_retrieve_body(wp_remote_get($remoteFile)); + } + + /** + * Get public remote plugin file. + * + * @param string $filename github-updater-demo.php + * @return string https://raw.githubusercontent.com/ryansechrest/github-updater-demo/main/github-updater-demo.php + */ + private function getPublicRemotePluginFile(string $filename): string + { + // Generate URL to public remote plugin file. + return sprintf( + 'https://raw.githubusercontent.com/%s/%s/%s', + $this->gitHubPath, + $this->gitHubBranch, + $filename + ); + } + + /** + * Get remote plugin file contents from private GitHub repository. + * + * @param string $filename github-updater-demo.php + * @return string + */ + private function getPrivateRemotePluginFileContents( + string $filename + ): string + { + // Get public remote plugin file containing plugin header, + // e.g. `https://api.github.com/repos/ryansechrest/github-updater-demo/contents/github-updater-demo.php?ref=main` + $remoteFile = $this->getPrivateRemotePluginFile($filename); + + return wp_remote_retrieve_body( + wp_remote_get( + $remoteFile, + [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->gitHubAccessToken, + 'Accept' => 'application/vnd.github.raw+json', + ] + ] + ) + ); + } + + /** + * Get private remote plugin file. + * + * @param string $filename github-updater-demo.php + * @return string https://api.github.com/repos/ryansechrest/github-updater-demo/contents/github-updater-demo.php?ref=main + */ + private function getPrivateRemotePluginFile(string $filename): string + { + // Generate URL to private remote plugin file. + return sprintf( + 'https://api.github.com/repos/%s/contents/%s?ref=%s', + $this->gitHubPath, + $filename, + $this->gitHubBranch + ); + } + + /** + * Get path to remote plugin ZIP file. + * + * @return string https://github.com/ryansechrest/github-updater-demo/archive/refs/heads/main.zip + */ + private function getRemotePluginZipFile(): string + { + return $this->gitHubAccessToken + ? $this->getPrivateRemotePluginZipFile() + : $this->getPublicRemotePluginZipFile(); + } + + /** + * Get path to public remote plugin ZIP file. + * + * @return string https://github.com/ryansechrest/github-updater-demo/archive/refs/heads/main.zip + */ + private function getPublicRemotePluginZipFile(): string + { + return sprintf( + 'https://github.com/%s/archive/refs/heads/%s.zip', + $this->gitHubPath, + $this->gitHubBranch + ); + } + + /** + * Get path to private remote plugin ZIP file. + * + * @return string https://api.github.com/repos/ryansechrest/github-updater-demo/zipball/main + */ + private function getPrivateRemotePluginZipFile(): string + { + return sprintf( + 'https://api.github.com/repos/%s/zipball/%s', + $this->gitHubPath, + $this->gitHubBranch + ); + } + + /*------------------------------------------------------------------------*/ + + /** + * Prepare HTTP request args. + * + * Include GitHub access token in request header when repository is private + * so that WordPress has access to download the remote plugin ZIP file. + * + * @return void + */ + private function prepareHttpRequestArgs(): void + { + add_filter( + 'http_request_args', + [$this, '_prepareHttpRequestArgs'], + 10, + 2 + ); + } + + /** + * Hook to prepare HTTP request args. + * + * $args An array of HTTP request arguments. + * $url The request URL. + * + * @param array $args ['method' => 'GET', 'headers' => [], ...] + * @param string $url https://api.github.com/repos/ryansechrest/github-updater-demo/zipball/main + * @return array ['headers' => ['Authorization => 'Bearer...'], ...] + */ + public function _prepareHttpRequestArgs(array $args, string $url): array + { + // If URL doesn't match ZIP file to private GitHub repo, exit + if ($url !== $this->getPrivateRemotePluginZipFile()) return $args; + + // Include GitHub access token and file type + $args['headers']['Authorization'] = 'Bearer ' . $this->gitHubAccessToken; + $args['headers']['Accept'] = 'application/vnd.github+json'; + + $this->logStart('_prepareHttpRequestArgs', 'http_request_args'); + $this->logValue('Return', $args); + $this->logFinish('_prepareHttpRequestArgs'); + + return $args; + } + + /*------------------------------------------------------------------------*/ + + /** + * Move updated plugin. + * + * The updated plugin will be extracted into a directory containing GitHub's + * branch name (e.g. `github-updater-demo-main`). Since this likely differs from + * the old plugin (e.g. `github-updater-demo`), it will cause WordPress to + * deactivate it. In order to prevent this, we move the new plugin to the + * old plugin's directory. + * + * @return void + */ + private function moveUpdatedPlugin(): void + { + add_filter( + 'upgrader_install_package_result', + [$this, '_moveUpdatedPlugin'], + 10, + 2 + ); + } + + /** + * Hook to move updated plugin. + * + * @param array $result ['destination' => '.../wp-content/plugins/github-updater-demo-main', ...] + * @param array $options ['plugin' => 'github-updater-demo/github-updater-demo.php', ...] + * @return array + */ + public function _moveUpdatedPlugin(array $result, array $options): array + { + // Get plugin being updated + // e.g. `github-updater-demo/github-updater-demo.php` + $pluginFile = $options['plugin'] ?? ''; + + // If plugin does not match this plugin, exit + if ($pluginFile !== $this->pluginFile) return $result; + + $this->logStart( + '_moveUpdatedPlugin', 'upgrader_install_package_result' + ); + + // Save path to new plugin + // e.g. `.../wp-content/plugins/github-updater-demo-main` + $newPluginPath = $result['destination'] ?? ''; + + $this->log( + 'Does $newPluginPath (' . $newPluginPath . ') exist...' + ); + + // If path to new plugin doesn't exist, exit + if (!$newPluginPath) { + $this->log('No'); + $this->logValue('Return early', $result); + $this->logFinish('_moveUpdatedPlugin'); + + return $result; + } + + $this->log('Yes'); + + // Save root path to all plugins, e.g. `.../wp-content/plugins` + $pluginRootPath = $result['local_destination'] ?? WP_PLUGIN_DIR; + + // Piece together path to old plugin, + // e.g. `.../wp-content/plugins/github-updater-demo` + $oldPluginPath = $pluginRootPath . '/' . $this->pluginDir; + + // Move new plugin to old plugin directory + move_dir($newPluginPath, $oldPluginPath); + + // Update result based on changes above + // destination: `.../wp-content/plugins/github-updater-demo` + // destination_name: `github-updater-demo` + // remote_destination: `.../wp-content/plugins/github-updater-demo` + $result['destination'] = $oldPluginPath; + $result['destination_name'] = $this->pluginDir; + $result['remote_destination'] = $oldPluginPath; + + $this->logValue('Return', $result); + $this->logFinish('_moveUpdatedPlugin'); + + return $result; + } + + /**************************************************************************/ + + /** + * Add admin notice that required plugin header fields are missing. + * + * @param string $message Plugin %s is missing one or more required header fields: Plugin URI, Version, and/or Update URI. + * @return void + */ + private function addAdminNotice(string $message): void + { + add_action('admin_notices', function () use ($message) { + $pluginFile = str_replace( + WP_PLUGIN_DIR . '/', '', $this->file + ); + echo '
'; + echo '

'; + echo wp_kses( + sprintf($message, $pluginFile), + ['b' => []] + ); + echo '

'; + echo '
'; + }); + } + + /*------------------------------------------------------------------------*/ + + /** + * Get plugin icon if defined and valid. + * + * @return string https://example.org/wp-content/plugins/github-updater-demo/assets/icon.png + */ + private function getPluginIcon(): string + { + if (!$this->pluginIcon) return ''; + + $pluginIconPath = $this->pluginDir . '/' . $this->pluginIcon; + + return plugins_url($pluginIconPath); + } + + /** + * Get small plugin banner (772x250). + * + * @return string https://example.org/wp-content/plugins/github-updater-demo/assets/banner-772x250.jpg + */ + private function getPluginBannerSmall(): string + { + if (!$this->pluginBannerSmall) return ''; + + return $this->getPluginFile($this->pluginBannerSmall); + } + + /** + * Get large plugin banner (1544x500). + * + * @return string https://example.org/wp-content/plugins/github-updater-demo/assets/banner-1544x500.jpg + */ + private function getPluginBannerLarge(): string + { + if (!$this->pluginBannerLarge) return ''; + + return $this->getPluginFile($this->pluginBannerLarge); + } + + /** + * Get plugin file if exists. + * + * @param string $file assets/icon.png + * @return string https://example.org/wp-content/plugins/github-updater-demo/assets/icon.png + */ + private function getPluginFile(string $file): string + { + $file = sprintf('%s/%s', $this->pluginDir, $file); + + if (!file_exists(WP_PLUGIN_DIR . '/' . $file)) return ''; + + return plugins_url($file); + } + + /** + * Get changelog from GitHub. + * + * @return string + */ + private function getChangelog(): string + { + // If no changelog specified, exit + if (!$this->changelog) return ''; + + // Get changelog contents from GitHub + $changelogContents = $this->getRemotePluginFileContents( + $this->changelog + ); + + // If changelog contents are blank, exit with error + if (!$changelogContents) { + return '
' + . '

ERROR: Changelog could not be retrieved from GitHub repository.

' + . '
'; + } + + // If changelog contents contains 404, exit with error + if (str_contains($changelogContents, '404')) { + return '
' + . '

ERROR: Changelog not found within GitHub repository.

' + . '
'; + } + + return $this->convertMarkdownToHtml($changelogContents); + } + + /*------------------------------------------------------------------------*/ + + /** + * Extract plugin header fields from file contents. + * + * @param array $fields ['Version' => 'version', ...] + * @param string $contents + * @return array ['Version' => '1.0.0.', ...] + */ + private function extractPluginHeaderFields( + array $fields, string $contents + ): array + { + $values = []; + + foreach ($fields as $field => $type) { + + // Select regex based on specified field type + $regex = match ($type) { + 'version' => '\d+(\.\d+){0,2}', + default => '.+', + }; + + // Extract field value using selected regex + preg_match( + '/\s+\*\s+' . $field . ':\s+(' . $regex . ')/', + $contents, + $matches + ); + + // Always return field with a value + $values[$field] = $matches[1] ?? ''; + + // Remove possible leading or trailing whitespace + $values[$field] = trim($values[$field]); + } + + return $values; + } + + /*------------------------------------------------------------------------*/ + + /** + * Convert markdown to HTML. + * + * @param string $markdown # Changelog + * @return string

Changelog

+ */ + private function convertMarkdownToHtml(string $markdown): string + { + $html = []; + $lines = explode(PHP_EOL, $markdown); + $index = 0; + + while (isset($lines[$index])) { + $line = trim($lines[$index]); + $element = match ($this->getMarkdownBlockType($line)) { + 'header' => $this->convertMarkdownHeader($line), + 'list' => $this->convertMarkdownList($index, $lines), + 'blockquote' => $this->convertMarkdownBlockquote($line), + 'code' => $this->convertMarkdownCode($index, $lines), + 'paragraph' => $this->convertMarkdownParagraph($line), + default => [$line], + }; + $html = array_merge($html, $element); + $index++; + } + + return implode(PHP_EOL, $html); + } + + /** + * Get Markdown block type from line. + * + * @param string $line # Foobar + * @return string header + */ + private function getMarkdownBlockType(string $line): string + { + if ($this->isMarkdownHeader($line)) { + return 'header'; + } elseif ($this->isMarkdownList($line)) { + return 'list'; + } elseif ($this->isMarkdownBlockquote($line)) { + return 'blockquote'; + } elseif ($this->isMarkdownCode($line)) { + return 'code'; + } elseif ($this->isMarkdownParagraph($line)) { + return 'paragraph'; + } + + return ''; + } + + /** + * Whether line contains Markdown header. + * + * @param string $line # Foobar + * @return bool true + */ + private function isMarkdownHeader(string $line): bool + { + return str_starts_with($line, '#'); + } + + /** + * Convert Markdown header to HTML. + * + * # Foo ->

Foo

+ * ## Foo ->

Foo

+ * ### Foo ->

Foo

+ * #### Foo ->

Foo

+ * ##### Foo ->
Foo
+ * ###### Foo ->
Foo
+ * + * @param string $line # Foobar + * @return string[] ['

Foobar

'] + */ + private function convertMarkdownHeader(string $line): array + { + $html = preg_replace_callback( + '/(#{1,6}) (.+)/', + function($match) { + $size = strlen($match[1]); + return '' . $match[2] . ''; + }, + $line + ); + + return [$html]; + } + + /** + * Whether line contains Markdown list. + * + * @param string $line - Foobar + * @return bool true + */ + private function isMarkdownList(string $line): bool + { + return str_starts_with($line, '-'); + } + + /** + * Convert unordered lists. + * + * - Foo + * - Bar + * + * + * + * @param int $index 0 + * @param array $lines ['- Foo', '- Bar'] + * @return string[] [''] + */ + private function convertMarkdownList(int &$index, array $lines): array + { + $html[] = ''; + + return $html; + } + + /** + * Whether line contains Markdown blockquote. + * + * @param string $line > Foobar + * @return bool true + */ + private function isMarkdownBlockquote(string $line): bool + { + return str_starts_with($line, '>'); + } + + /** + * Convert Markdown blockquote. + * + * > Foobar + * + *
Foobar
+ * + * @param string $line > Foobar + * @return string[] ['
Foobar
'] + */ + private function convertMarkdownBlockquote(string $line): array + { + $html = preg_replace( + '/> (.+)/', + '
$1
', + $this->convertInlineMarkdown($line) + ); + + return [$html]; + } + + /** + * Whether line contains Markdown code block. + * + * @param string $line ``` + * @return bool true + */ + private function isMarkdownCode(string $line): bool + { + return str_starts_with($line, '```'); + } + + /** + * Convert Markdown code block. + * + * ``` + * + * function foo() { + * echo 'bar'; + * } + * + * + * @param int $index 0 + * @param array $lines ['```', 'Foobar', '```'] + * @return array ['
', 'Foobar', '
'] + */ + private function convertMarkdownCode(int &$index, array $lines): array + { + $html[] = preg_replace_callback( + '/```(.*)/', + function($match) { + $lang = trim($match[1]); + return $lang === '' + ? '
'
+                    : '
';
+            },
+            trim($lines[$index])
+        );
+
+        $index++;
+
+        while (isset($lines[$index]) && !$this->isMarkdownCode($lines[$index])) {
+            $html[] = $lines[$index];
+            $index++;
+        }
+
+        $html[] = '
'; + + return $html; + } + + /** + * Whether line contains Markdown paragraph. + * + * @param string $line Foobar + * @return bool true + */ + private function isMarkdownParagraph(string $line): bool + { + return $line !== ''; + } + + /** + * Convert Markdown paragraph. + * + * @param string $line Foobar + * @return string[] ['

Foobar

'] + */ + private function convertMarkdownParagraph(string $line): array + { + return ['

' . $this->convertInlineMarkdown($line) . '

']; + } + + /** + * Convert inline Markdown. + * + * @param string $line Convert `code`, **bold**, and *italic* text. + * @return string Convert code, bold, and italic text. + */ + private function convertInlineMarkdown(string $line): string + { + /** + * Convert code text. + * + * `Foo` -> Foo + */ + $line = preg_replace( + '/`(.+)`/U', + '$1', + $line + ); + + /** + * Convert bold text. + * + * **Foo** -> Foo + */ + $line = preg_replace( + '/\*\*(.+)\*\*/U', + '$1', + $line + ); + + /** + * Convert italic text. + * + * *Foo* -> Foo + */ + $line = preg_replace( + '/\*(.+)\*/U', + '$1', + $line + ); + + return $line; + } + + /*------------------------------------------------------------------------*/ + + /** + * Log message with optional value. + * + * @param string $message Plugins data + * @return void + */ + private function log(string $message,): void + { + if (!$this->enableDebugger || !WP_DEBUG || !WP_DEBUG_LOG) return; + + error_log('[GitHubUpdater] ' . $message); + } + + /** + * Log when method starts running. + * + * @param string $method _checkPluginUpdates + * @param string $hook update_plugins_github.com + * @return void + */ + private function logStart(string $method, string $hook = ''): void + { + $message = $method . '() '; + + if ($hook) $message = $hook . ' → ' . $message; + + $this->log($message); + $this->log(str_repeat('-', 50)); + } + + /** + * Log label and value through print_r(). + * + * @param string $label $pluginData + * @param mixed $value ['version' => '1.0.0', ...] + * @return void + */ + private function logValue(string $label, mixed $value): void + { + if (!is_string($value)) { + $value = var_export($value, true); + } + + $this->log($label . ': ' . $value); + } + + /** + * Log when method finishes running. + * + * @param string $method _checkPluginUpdates + * @return void + */ + private function logFinish(string $method): void + { + $this->log('/ ' . $method . '()'); + $this->log(''); + } +} diff --git a/includes/updater.php b/includes/updater.php deleted file mode 100644 index 640ae40..0000000 --- a/includes/updater.php +++ /dev/null @@ -1,410 +0,0 @@ - - * @link http://jkudish.com - * @package WP_GitHub_Updater - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @copyright Copyright (c) 2011-2013, Joachim Kudish - * - * GNU General Public License, Free Software Foundation - * - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ -class WpGitHubUpdater { - /** - * GitHub Updater version - */ - const VERSION = 1.6; - - /** - * @var $config the config for the updater - * @access public - */ - public $config; - - /** - * @var $missingConfig any config that is missing from the initialization of this instance - * @access public - */ - public $missingConfig; - - /** - * @var $githubData temporiraly store the data fetched from GitHub, allows us to only load the data once per class instance - * @access private - */ - private $githubData; - - - /** - * Class Constructor - * - * @since 1.0 - * @param array $config the configuration required for the updater to work - * @see hasMinimumConfig() - * @return void - */ - public function __construct($config = array()) { - - $defaults = array( - 'slug' => plugin_basename(__FILE__), - 'proper_folder_name' => dirname(plugin_basename(__FILE__)), - 'sslverify' => true, - 'access_token' => '', - ); - - $this->config = wp_parse_args($config, $defaults); - - // if the minimum config isn't set, issue a warning and bail - if (!$this->hasMinimumConfig()) { - $message = 'The GitHub Updater was initialized without the minimum required configuration, please check the config in your plugin. The following params are missing: '; - $message .= implode(',', $this->missingConfig); - _doing_it_wrong(__CLASS__, $message , self::VERSION); - return; - } - - $this->setDefaults(); - - add_filter('pre_set_site_transient_update_plugins', array($this, 'apiCheck')); - - // Hook into the plugin details screen - add_filter('plugins_api', array($this, 'getPluginInfo'), 10, 3); - add_filter('upgraderPostInstall', array($this, 'upgraderPostInstall'), 10, 3); - - // set timeout - add_filter('httpRequestTimeout', array($this, 'httpRequestTimeout')); - - // set sslverify for zip download - add_filter('http_request_args', array($this, 'httpRequestSslverify'), 10, 2); - } - - public function hasMinimumConfig() { - - $this->missingConfig = array(); - - $required_config_params = array( - 'api_url', - 'raw_url', - 'github_url', - 'zip_url', - 'requires', - 'tested', - 'readme', - ); - - foreach ($required_config_params as $required_param) { - if (empty($this->config[$required_param])) { $this->missingConfig[] = $required_param; } - } - - return empty($this->missingConfig); - } - - - /** - * Check wether or not the transients need to be overruled and API needs to be called for every single page load - * - * @return bool overrule or not - */ - public function overruleTransients() { - return defined('WP_GITHUB_FORCE_UPDATE') && WP_GITHUB_FORCE_UPDATE; - } - - - /** - * Set defaults - * - * @since 1.2 - * @return void - */ - public function setDefaults() { - if (!empty($this->config['access_token'])) { - - // See Downloading a zipball (private repo) https://help.github.com/articles/downloading-files-from-the-command-line - extract(parse_url($this->config['zip_url'])); // $scheme, $host, $path - - $zip_url = $scheme . '://api.github.com/repos' . $path; - $zip_url = add_query_arg(array('access_token' => $this->config['access_token']), $zip_url); - - $this->config['zip_url'] = $zip_url; - } - - - if (!isset($this->config['new_version'])) { $this->config['new_version'] = $this->getNewVersion(); } - - if (!isset($this->config['last_updated'])) { $this->config['last_updated'] = $this->getDate(); } - - if (!isset($this->config['description'])) { $this->config['description'] = $this->getDescription(); } - - $plugin_data = $this->getPluginData(); - if (!isset($this->config['plugin_name'])) { $this->config['plugin_name'] = $plugin_data['Name']; } - - if (!isset($this->config['version'])) { $this->config['version'] = $plugin_data['Version']; } - - if (!isset($this->config['author'])) { $this->config['author'] = $plugin_data['Author']; } - - if (!isset($this->config['homepage'])) { $this->config['homepage'] = $plugin_data['PluginURI']; } - - if (!isset($this->config['readme'])) { $this->config['readme'] = 'README.md'; } - - } - - - /** - * Callback fn for the httpRequestTimeout filter - * - * @since 1.0 - * @return int timeout value - */ - public function httpRequestTimeout() { - return 2; - } - - /** - * Callback fn for the http_request_args filter - * - * @param unknown $args - * @param unknown $url - * - * @return mixed - */ - public function httpRequestSslverify($args, $url) { - if ($this->config[ 'zip_url' ] == $url) { $args[ 'sslverify' ] = $this->config[ 'sslverify' ]; } - - return $args; - } - - - /** - * Get New Version from GitHub - * - * @since 1.0 - * @return int $version the version number - */ - public function getNewVersion() { - $version = get_site_transient(md5($this->config['slug']).'_new_version'); - - if ($this->overruleTransients() || (!isset($version) || !$version || '' == $version)) { - $version = $this->fetchVersionFromGitHub(); - if (false === $version) { - $version = $this->fetchVersionFromReadme(); - } - if (false !== $version) { - set_site_transient(md5($this->config['slug']).'_new_version', $version, 60*60*6); - } - } - - return $version; - } - - private function fetchVersionFromGitHub() { - $raw_response = $this->remoteGet(trailingslashit($this->config['raw_url']) . basename($this->config['slug'])); - if (is_wp_error($raw_response)) { return false; } - - if (is_array($raw_response) && !empty($raw_response['body'])) { - preg_match('/.*Version\:\s*(.*)$/mi', $raw_response['body'], $matches); - } - - return empty($matches[1]) ? false : $matches[1]; - } - - private function fetchVersionFromReadme() { - $raw_response = $this->remoteGet(trailingslashit($this->config['raw_url']) . $this->config['readme']); - if (is_wp_error($raw_response)) { return false; } - - preg_match('#^\s*`*~Current Version\:\s*([^~]*)~#im', $raw_response['body'], $__version); - - return isset($__version[1]) ? $__version[1] : false; - } - - - /** - * Interact with GitHub - * - * @param string $query - * - * @since 1.6 - * @return mixed - */ - public function remoteGet($query) { - if (!empty($this->config['access_token'])) { $query = add_query_arg(array('access_token' => $this->config['access_token']), $query); } - - return wp_remote_get($query, array( - 'sslverify' => $this->config['sslverify'] - )); - } - - - /** - * Get GitHub Data from the specified repository - * - * @since 1.0 - * @return array $githubData the data - */ - public function getGithubData() { - if (isset($this->githubData) && !empty($this->githubData)) { - $githubData = $this->githubData; - } else { - $githubData = get_site_transient(md5($this->config['slug']).'_githubData'); - - if ($this->overruleTransients() || (!isset($githubData) || !$githubData || '' == $githubData)) { - $githubData = $this->remoteGet($this->config['api_url']); - - if (is_wp_error($githubData)) { return false; } - - $githubData = json_decode($githubData['body']); - - // refresh every 6 hours - set_site_transient(md5($this->config['slug']).'_githubData', $githubData, 60*60*6); - } - - // Store the data in this class instance for future calls - $this->githubData = $githubData; - } - - return $githubData; - } - - - /** - * Get update date - * - * @since 1.0 - * @return string $date the date - */ - public function getDate() { - $_date = $this->getGithubData(); - return (!empty($_date->updated_at)) ? date('Y-m-d', strtotime($_date->updated_at)) : false; - } - - - /** - * Get plugin description - * - * @since 1.0 - * @return string $description the description - */ - public function getDescription() { - $_description = $this->getGithubData(); - return (!empty($_description->description)) ? $_description->description : false; - } - - - /** - * Get Plugin data - * - * @since 1.0 - * @return object $data the data - */ - public function getPluginData() { - include_once ABSPATH.'/wp-admin/includes/plugin.php'; - return get_plugin_data(WP_PLUGIN_DIR.'/'.$this->config['slug']); - } - - - /** - * Hook into the plugin update check and connect to GitHub - * - * @since 1.0 - * @param object $transient the plugin data transient - * @return object $transient updated plugin data transient - */ - public function apiCheck($transient) { - - // Check if the transient contains the 'checked' information - // If not, just return its value without hacking it - if (empty($transient->checked)) { return $transient; } - - // check the version and decide if it's new - $update = version_compare($this->config['new_version'], $this->config['version']); - - if (1 === $update) { - $response = new stdClass; - $response->new_version = $this->config['new_version']; - $response->slug = $this->config['proper_folder_name']; - $response->url = add_query_arg(array('access_token' => $this->config['access_token']), $this->config['github_url']); - $response->package = $this->config['zip_url']; - - // If response is false, don't alter the transient - if (false !== $response) { $transient->response[ $this->config['slug'] ] = $response; } - } - - return $transient; - } - - - /** - * Get Plugin info - * - * @since 1.0 - * @param bool $false always false - * @param string $action the API function being performed - * @param object $args plugin arguments - * @return object $response the plugin info - */ - public function getPluginInfo($false, $action, $response) { - - // Check if this call API is for the right plugin - if (!isset($response->slug) || $response->slug != $this->config['slug']) { return false; } - - $response->slug = $this->config['slug']; - $response->plugin_name = $this->config['plugin_name']; - $response->version = $this->config['new_version']; - $response->author = $this->config['author']; - $response->homepage = $this->config['homepage']; - $response->requires = $this->config['requires']; - $response->tested = $this->config['tested']; - $response->downloaded = 0; - $response->last_updated = $this->config['last_updated']; - $response->sections = array('description' => $this->config['description']); - $response->download_link = $this->config['zip_url']; - - return $response; - } - - - /** - * Upgrader/Updater - * Move & activate the plugin, echo the update message - * - * @since 1.0 - * @param boolean $true always true - * @param mixed $hook_extra not used - * @param array $result the result of the move - * @return array $result the result of the move - */ - public function upgraderPostInstall($true, $hook_extra, $result) { - - global $wp_filesystem; - - // Move & Activate - $proper_destination = WP_PLUGIN_DIR.'/'.$this->config['proper_folder_name']; - $wp_filesystem->move($result['destination'], $proper_destination); - $result['destination'] = $proper_destination; - $activate = activate_plugin(WP_PLUGIN_DIR.'/'.$this->config['slug']); - - // Output the update message - $fail = __('The plugin has been updated, but could not be reactivated. Please reactivate it manually.', 'github_plugin_updater'); - $success = __('Plugin reactivated successfully.', 'github_plugin_updater'); - echo is_wp_error($activate) ? $fail : $success; - return $result; - - } -} diff --git a/resource-filter.php b/resource-filter.php index a5ea3ab..4837f48 100644 --- a/resource-filter.php +++ b/resource-filter.php @@ -1,14 +1,17 @@ add();